music: make musicloader async
Make MusicLoader instantiation fully asynchronous. This implementation changes a lot about Auxio. For one, the loading screen is now gone. However, many parts of the app now run under the fact that MusicStore might not be available. However, I don't think there will be too much bugs from it. Some more changes will be made to improve this implementation.
This commit is contained in:
parent
926fef4218
commit
fe0c2761c7
22 changed files with 286 additions and 503 deletions
|
@ -58,8 +58,6 @@ class MainFragment : Fragment() {
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ---
|
// --- VIEWMODEL SETUP ---
|
||||||
|
|
||||||
playbackModel.setupPlayback(requireContext())
|
|
||||||
|
|
||||||
// Change CompactPlaybackFragment's visibility here so that an animation occurs.
|
// Change CompactPlaybackFragment's visibility here so that an animation occurs.
|
||||||
binding.mainPlayback.isVisible = playbackModel.song.value != null
|
binding.mainPlayback.isVisible = playbackModel.song.value != null
|
||||||
|
|
||||||
|
|
|
@ -78,23 +78,28 @@ class DetailViewModel : ViewModel() {
|
||||||
|
|
||||||
private var currentMenuContext: DisplayMode? = null
|
private var currentMenuContext: DisplayMode? = null
|
||||||
|
|
||||||
private val musicStore = MusicStore.getInstance()
|
|
||||||
private val settingsManager = SettingsManager.getInstance()
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
|
|
||||||
fun setGenre(id: Long, context: Context) {
|
fun setGenre(id: Long, context: Context) {
|
||||||
if (mCurGenre.value?.id == id) return
|
if (mCurGenre.value?.id == id) return
|
||||||
|
|
||||||
|
val musicStore = MusicStore.requireInstance()
|
||||||
mCurGenre.value = musicStore.genres.find { it.id == id }
|
mCurGenre.value = musicStore.genres.find { it.id == id }
|
||||||
refreshGenreData(context)
|
refreshGenreData(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setArtist(id: Long, context: Context) {
|
fun setArtist(id: Long, context: Context) {
|
||||||
if (mCurArtist.value?.id == id) return
|
if (mCurArtist.value?.id == id) return
|
||||||
|
|
||||||
|
val musicStore = MusicStore.requireInstance()
|
||||||
mCurArtist.value = musicStore.artists.find { it.id == id }
|
mCurArtist.value = musicStore.artists.find { it.id == id }
|
||||||
refreshArtistData(context)
|
refreshArtistData(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setAlbum(id: Long, context: Context) {
|
fun setAlbum(id: Long, context: Context) {
|
||||||
if (mCurAlbum.value?.id == id) return
|
if (mCurAlbum.value?.id == id) return
|
||||||
|
|
||||||
|
val musicStore = MusicStore.requireInstance()
|
||||||
mCurAlbum.value = musicStore.albums.find { it.id == id }
|
mCurAlbum.value = musicStore.albums.find { it.id == id }
|
||||||
refreshAlbumData(context)
|
refreshAlbumData(context)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,12 +18,16 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.home
|
package org.oxycblt.auxio.home
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Button
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.core.content.res.ResourcesCompat
|
||||||
import androidx.core.view.iterator
|
import androidx.core.view.iterator
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.core.view.updatePaddingRelative
|
import androidx.core.view.updatePaddingRelative
|
||||||
|
@ -34,6 +38,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
import org.oxycblt.auxio.MainFragmentDirections
|
import org.oxycblt.auxio.MainFragmentDirections
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
|
@ -46,6 +51,7 @@ import org.oxycblt.auxio.home.list.SongListFragment
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
|
import org.oxycblt.auxio.music.MusicStore
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.ui.DisplayMode
|
import org.oxycblt.auxio.ui.DisplayMode
|
||||||
|
@ -73,6 +79,13 @@ class HomeFragment : Fragment() {
|
||||||
var bottomPadding = 0
|
var bottomPadding = 0
|
||||||
val sortItem: MenuItem
|
val sortItem: MenuItem
|
||||||
|
|
||||||
|
// Build the permission launcher here as you can only do it in onCreateView/onCreate
|
||||||
|
val permLauncher = registerForActivityResult(
|
||||||
|
ActivityResultContracts.RequestPermission()
|
||||||
|
) {
|
||||||
|
homeModel.reloadMusic(requireContext())
|
||||||
|
}
|
||||||
|
|
||||||
// --- UI SETUP ---
|
// --- UI SETUP ---
|
||||||
|
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
binding.lifecycleOwner = viewLifecycleOwner
|
||||||
|
@ -193,22 +206,96 @@ class HomeFragment : Fragment() {
|
||||||
registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
||||||
override fun onPageSelected(position: Int) = homeModel.updateCurrentTab(position)
|
override fun onPageSelected(position: Int) = homeModel.updateCurrentTab(position)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
TabLayoutMediator(binding.homeTabs, this) { tab, pos ->
|
||||||
|
tab.setText(homeModel.tabs[pos].string)
|
||||||
|
}.attach()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.homeFab.setOnClickListener {
|
binding.homeFab.setOnClickListener {
|
||||||
playbackModel.shuffleAll()
|
playbackModel.shuffleAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
TabLayoutMediator(binding.homeTabs, binding.homePager) { tab, pos ->
|
|
||||||
tab.setText(homeModel.tabs[pos].string)
|
|
||||||
}.attach()
|
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ---
|
// --- VIEWMODEL SETUP ---
|
||||||
|
|
||||||
|
// Initialize music loading. Unlike MainFragment, we can not only do this here on startup
|
||||||
|
// but also show a SnackBar in a reasonable place in this fragment.
|
||||||
|
homeModel.loadMusic(requireContext())
|
||||||
|
|
||||||
// There is no way a fast scrolling event can continue across a re-create. Reset it.
|
// There is no way a fast scrolling event can continue across a re-create. Reset it.
|
||||||
homeModel.updateFastScrolling(false)
|
homeModel.updateFastScrolling(false)
|
||||||
|
|
||||||
|
homeModel.loaderResponse.observe(viewLifecycleOwner) { response ->
|
||||||
|
// Handle the loader response.
|
||||||
|
when (response) {
|
||||||
|
is MusicStore.Response.Ok -> {
|
||||||
|
logD("Received Ok")
|
||||||
|
|
||||||
|
binding.homeFab.show()
|
||||||
|
playbackModel.setupPlayback(requireContext())
|
||||||
|
}
|
||||||
|
|
||||||
|
is MusicStore.Response.Err -> {
|
||||||
|
logD("Received Error")
|
||||||
|
|
||||||
|
// We received an error. Hide the FAB and show a Snackbar with the error
|
||||||
|
// message and a corresponding action
|
||||||
|
binding.homeFab.hide()
|
||||||
|
|
||||||
|
val errorRes = when (response.kind) {
|
||||||
|
MusicStore.ErrorKind.NO_MUSIC -> R.string.err_no_music
|
||||||
|
MusicStore.ErrorKind.NO_PERMS -> R.string.err_no_perms
|
||||||
|
MusicStore.ErrorKind.FAILED -> R.string.err_load_failed
|
||||||
|
}
|
||||||
|
|
||||||
|
val snackbar = Snackbar.make(
|
||||||
|
binding.root, getString(errorRes), Snackbar.LENGTH_INDEFINITE
|
||||||
|
)
|
||||||
|
|
||||||
|
snackbar.view.apply {
|
||||||
|
// Change the font family to our semibold color
|
||||||
|
findViewById<Button>(
|
||||||
|
com.google.android.material.R.id.snackbar_action
|
||||||
|
).typeface = ResourcesCompat.getFont(requireContext(), R.font.inter_semibold)
|
||||||
|
|
||||||
|
fitsSystemWindows = false
|
||||||
|
|
||||||
|
// Prevent fitsSystemWindows margins from being applied to this view
|
||||||
|
// [We already do it]
|
||||||
|
setOnApplyWindowInsetsListener { v, insets -> insets }
|
||||||
|
}
|
||||||
|
|
||||||
|
when (response.kind) {
|
||||||
|
MusicStore.ErrorKind.FAILED, MusicStore.ErrorKind.NO_MUSIC -> {
|
||||||
|
snackbar.setAction(R.string.lbl_retry) {
|
||||||
|
homeModel.reloadMusic(requireContext())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MusicStore.ErrorKind.NO_PERMS -> {
|
||||||
|
snackbar.setAction(R.string.lbl_grant) {
|
||||||
|
permLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
snackbar.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
// While loading or during an error, make sure we keep the shuffle fab hidden so
|
||||||
|
// that any kind of loading is impossible. PlaybackStateManager also relies on this
|
||||||
|
// invariant, so please don't change it.
|
||||||
|
null -> binding.homeFab.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
homeModel.fastScrolling.observe(viewLifecycleOwner) { scrolling ->
|
homeModel.fastScrolling.observe(viewLifecycleOwner) { scrolling ->
|
||||||
|
// Make sure an update here doesn't mess up the FAB state when it comes to the
|
||||||
|
// loader response.
|
||||||
|
if (homeModel.loaderResponse.value !is MusicStore.Response.Ok) {
|
||||||
|
return@observe
|
||||||
|
}
|
||||||
|
|
||||||
if (scrolling) {
|
if (scrolling) {
|
||||||
binding.homeFab.hide()
|
binding.homeFab.hide()
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -18,9 +18,12 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.home
|
package org.oxycblt.auxio.home
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
|
@ -36,13 +39,8 @@ import org.oxycblt.auxio.ui.SortMode
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class HomeViewModel : ViewModel(), SettingsManager.Callback {
|
class HomeViewModel : ViewModel(), SettingsManager.Callback {
|
||||||
private val musicStore = MusicStore.getInstance()
|
|
||||||
private val settingsManager = SettingsManager.getInstance()
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
|
|
||||||
/** Internal getter for getting the visible library tabs */
|
|
||||||
private val visibleTabs: List<DisplayMode> get() = settingsManager.libTabs
|
|
||||||
.filterIsInstance<Tab.Visible>().map { it.mode }
|
|
||||||
|
|
||||||
private val mSongs = MutableLiveData(listOf<Song>())
|
private val mSongs = MutableLiveData(listOf<Song>())
|
||||||
val songs: LiveData<List<Song>> get() = mSongs
|
val songs: LiveData<List<Song>> get() = mSongs
|
||||||
|
|
||||||
|
@ -58,12 +56,13 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback {
|
||||||
var tabs: List<DisplayMode> = visibleTabs
|
var tabs: List<DisplayMode> = visibleTabs
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
/** Internal getter for getting the visible library tabs */
|
||||||
|
private val visibleTabs: List<DisplayMode> get() = settingsManager.libTabs
|
||||||
|
.filterIsInstance<Tab.Visible>().map { it.mode }
|
||||||
|
|
||||||
private val mCurTab = MutableLiveData(tabs[0])
|
private val mCurTab = MutableLiveData(tabs[0])
|
||||||
val curTab: LiveData<DisplayMode> = mCurTab
|
val curTab: LiveData<DisplayMode> = mCurTab
|
||||||
|
|
||||||
private val mFastScrolling = MutableLiveData(false)
|
|
||||||
val fastScrolling: LiveData<Boolean> = mFastScrolling
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marker to recreate all library tabs, usually initiated by a settings change.
|
* 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
|
* When this flag is set, all tabs (and their respective viewpager fragments) will be
|
||||||
|
@ -72,13 +71,51 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback {
|
||||||
private val mRecreateTabs = MutableLiveData(false)
|
private val mRecreateTabs = MutableLiveData(false)
|
||||||
val recreateTabs: LiveData<Boolean> = mRecreateTabs
|
val recreateTabs: LiveData<Boolean> = mRecreateTabs
|
||||||
|
|
||||||
|
private val mFastScrolling = MutableLiveData(false)
|
||||||
|
val fastScrolling: LiveData<Boolean> = mFastScrolling
|
||||||
|
|
||||||
|
private val mLoaderResponse = MutableLiveData<MusicStore.Response?>(null)
|
||||||
|
val loaderResponse: LiveData<MusicStore.Response?> = mLoaderResponse
|
||||||
|
|
||||||
|
private var isBusy = false
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
settingsManager.addCallback(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiate the loading process. This is done here since HomeFragment will be the first
|
||||||
|
* fragment navigated to and because SnackBars will have the best UX here.
|
||||||
|
*/
|
||||||
|
fun loadMusic(context: Context) {
|
||||||
|
if (mLoaderResponse.value != null || isBusy) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isBusy = true
|
||||||
|
mLoaderResponse.value = null
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
val result = MusicStore.initInstance(context)
|
||||||
|
|
||||||
|
isBusy = false
|
||||||
|
mLoaderResponse.value = result
|
||||||
|
|
||||||
|
if (result is MusicStore.Response.Ok) {
|
||||||
|
val musicStore = result.musicStore
|
||||||
|
|
||||||
mSongs.value = settingsManager.libSongSort.sortSongs(musicStore.songs)
|
mSongs.value = settingsManager.libSongSort.sortSongs(musicStore.songs)
|
||||||
mAlbums.value = settingsManager.libAlbumSort.sortAlbums(musicStore.albums)
|
mAlbums.value = settingsManager.libAlbumSort.sortAlbums(musicStore.albums)
|
||||||
mArtists.value = settingsManager.libArtistSort.sortModels(musicStore.artists)
|
mArtists.value = settingsManager.libArtistSort.sortModels(musicStore.artists)
|
||||||
mGenres.value = settingsManager.libGenreSort.sortModels(musicStore.genres)
|
mGenres.value = settingsManager.libGenreSort.sortModels(musicStore.genres)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
settingsManager.addCallback(this)
|
fun reloadMusic(context: Context) {
|
||||||
|
mLoaderResponse.value = null
|
||||||
|
|
||||||
|
loadMusic(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -38,6 +38,7 @@ import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
|
import org.oxycblt.auxio.util.canScroll
|
||||||
import org.oxycblt.auxio.util.resolveAttr
|
import org.oxycblt.auxio.util.resolveAttr
|
||||||
import org.oxycblt.auxio.util.resolveDrawable
|
import org.oxycblt.auxio.util.resolveDrawable
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
@ -47,13 +48,13 @@ import kotlin.math.abs
|
||||||
* Zhanghi's AndroidFastScroll but slimmed down for Auxio and with a couple of enhancements.
|
* Zhanghi's AndroidFastScroll but slimmed down for Auxio and with a couple of enhancements.
|
||||||
*
|
*
|
||||||
* Attributions as per the Apache 2.0 license:
|
* Attributions as per the Apache 2.0 license:
|
||||||
* ORIGINAL AUTHOR: Zhanghai [https://github.com/zhanghai]
|
* ORIGINAL AUTHOR: Hai Zhang [https://github.com/zhanghai]
|
||||||
* PROJECT: Android Fast Scroll [https://github.com/zhanghai/AndroidFastScroll]
|
* PROJECT: Android Fast Scroll [https://github.com/zhanghai/AndroidFastScroll]
|
||||||
* MODIFIER: OxygenCobalt [https://github.com/]
|
* MODIFIER: OxygenCobalt [https://github.com/]
|
||||||
*
|
*
|
||||||
* !!! MODIFICATIONS !!!:
|
* !!! MODIFICATIONS !!!:
|
||||||
* - Scroller will no longer show itself on startup, which looked unpleasent with multiple
|
* - Scroller will no longer show itself on startup or relayouts, which looked unpleasant
|
||||||
* views
|
* with multiple views
|
||||||
* - DefaultAnimationHelper and RecyclerViewHelper were merged into the class
|
* - DefaultAnimationHelper and RecyclerViewHelper were merged into the class
|
||||||
* - FastScroller overlay was merged into RecyclerView instance
|
* - FastScroller overlay was merged into RecyclerView instance
|
||||||
* - Removed FastScrollerBuilder
|
* - Removed FastScrollerBuilder
|
||||||
|
@ -65,8 +66,6 @@ import kotlin.math.abs
|
||||||
* - Added drag listener
|
* - Added drag listener
|
||||||
* - TODO: Added documentation
|
* - TODO: Added documentation
|
||||||
* - TODO: Popup will center itself to the thumb when possible
|
* - TODO: Popup will center itself to the thumb when possible
|
||||||
*
|
|
||||||
* TODO: Debug this
|
|
||||||
*/
|
*/
|
||||||
class FastScrollRecyclerView @JvmOverloads constructor(
|
class FastScrollRecyclerView @JvmOverloads constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
|
@ -193,10 +192,6 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addPopupProvider(provider: (Int) -> String) {
|
|
||||||
popupProvider = provider
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- RECYCLERVIEW EVENT MANAGEMENT ---
|
// --- RECYCLERVIEW EVENT MANAGEMENT ---
|
||||||
|
|
||||||
private fun onPreDraw() {
|
private fun onPreDraw() {
|
||||||
|
@ -316,6 +311,10 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateScrollbarState() {
|
private fun updateScrollbarState() {
|
||||||
|
if (!canScroll() || childCount == 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Getting a pixel-perfect scroll position from a recyclerview is a bit of an involved
|
// Getting a pixel-perfect scroll position from a recyclerview is a bit of an involved
|
||||||
// process. It's kind of expected given how RecyclerView well...recycles views, but it's
|
// process. It's kind of expected given how RecyclerView well...recycles views, but it's
|
||||||
// still very annoying how many hoops one has to jump through.
|
// still very annoying how many hoops one has to jump through.
|
||||||
|
|
|
@ -39,7 +39,7 @@ import kotlin.math.sqrt
|
||||||
* This is an adaptation from AndroidFastScroll's MD2 theme.
|
* This is an adaptation from AndroidFastScroll's MD2 theme.
|
||||||
*
|
*
|
||||||
* Attributions as per the Apache 2.0 license:
|
* Attributions as per the Apache 2.0 license:
|
||||||
* ORIGINAL AUTHOR: Zhanghai [https://github.com/zhanghai]
|
* ORIGINAL AUTHOR: Hai Zhang [https://github.com/zhanghai]
|
||||||
* PROJECT: Android Fast Scroll [https://github.com/zhanghai/AndroidFastScroll]
|
* PROJECT: Android Fast Scroll [https://github.com/zhanghai/AndroidFastScroll]
|
||||||
* MODIFIER: OxygenCobalt [https://github.com/]
|
* MODIFIER: OxygenCobalt [https://github.com/]
|
||||||
*
|
*
|
||||||
|
@ -47,7 +47,6 @@ import kotlin.math.sqrt
|
||||||
* - Use modified Auxio resources instead of AFS resources
|
* - Use modified Auxio resources instead of AFS resources
|
||||||
* - Variable names are no longer prefixed with m
|
* - Variable names are no longer prefixed with m
|
||||||
* - Suppressed deprecation warning when dealing with convexness
|
* - Suppressed deprecation warning when dealing with convexness
|
||||||
* - TODO: Popup will center itself to the thumb when possible
|
|
||||||
*/
|
*/
|
||||||
class Md2PopupBackground(context: Context) : Drawable() {
|
class Md2PopupBackground(context: Context) : Drawable() {
|
||||||
private val paint: Paint = Paint()
|
private val paint: Paint = Paint()
|
||||||
|
|
|
@ -29,7 +29,6 @@ import org.oxycblt.auxio.ui.SongViewHolder
|
||||||
import org.oxycblt.auxio.ui.SortMode
|
import org.oxycblt.auxio.ui.SortMode
|
||||||
import org.oxycblt.auxio.ui.newMenu
|
import org.oxycblt.auxio.ui.newMenu
|
||||||
import org.oxycblt.auxio.ui.sliceArticle
|
import org.oxycblt.auxio.ui.sliceArticle
|
||||||
import org.oxycblt.auxio.util.applySpans
|
|
||||||
|
|
||||||
class SongListFragment : HomeListFragment() {
|
class SongListFragment : HomeListFragment() {
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
|
@ -47,7 +46,6 @@ class SongListFragment : HomeListFragment() {
|
||||||
)
|
)
|
||||||
|
|
||||||
setupRecycler(R.id.home_song_list, adapter, homeModel.songs)
|
setupRecycler(R.id.home_song_list, adapter, homeModel.songs)
|
||||||
binding.homeRecycler.applySpans { it == 0 }
|
|
||||||
|
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,197 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2021 Auxio Project
|
|
||||||
* LoadingFragment.kt is part of Auxio.
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.oxycblt.auxio.loading
|
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.viewModels
|
|
||||||
import androidx.navigation.fragment.findNavController
|
|
||||||
import org.oxycblt.auxio.R
|
|
||||||
import org.oxycblt.auxio.databinding.FragmentLoadingBinding
|
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
|
||||||
import org.oxycblt.auxio.util.logD
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fragment that handles what to display during the loading process.
|
|
||||||
* TODO: Figure out how to phase out the loading screen since
|
|
||||||
* Android 12 is annoyingly stubborn about having one splash
|
|
||||||
* screen and one splash screen only.
|
|
||||||
* @author OxygenCobalt
|
|
||||||
*/
|
|
||||||
class LoadingFragment : Fragment() {
|
|
||||||
private val loadingModel: LoadingViewModel by viewModels()
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
val binding = FragmentLoadingBinding.inflate(inflater)
|
|
||||||
|
|
||||||
// Build the permission launcher here as you can only do it in onCreateView/onCreate
|
|
||||||
val permLauncher = registerForActivityResult(
|
|
||||||
ActivityResultContracts.RequestPermission(), ::onPermResult
|
|
||||||
)
|
|
||||||
|
|
||||||
// --- UI SETUP ---
|
|
||||||
|
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
|
||||||
binding.loadingModel = loadingModel
|
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ---
|
|
||||||
|
|
||||||
loadingModel.doGrant.observe(viewLifecycleOwner) { doGrant ->
|
|
||||||
if (doGrant) {
|
|
||||||
permLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
|
|
||||||
loadingModel.doneWithGrant()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadingModel.response.observe(viewLifecycleOwner) { response ->
|
|
||||||
when (response) {
|
|
||||||
// Success should lead to navigation to the main fragment
|
|
||||||
MusicStore.Response.SUCCESS -> findNavController().navigate(
|
|
||||||
LoadingFragmentDirections.actionToMain()
|
|
||||||
)
|
|
||||||
|
|
||||||
// Null means that the loading process is going on
|
|
||||||
null -> showLoading(binding)
|
|
||||||
|
|
||||||
// Anything else is an error
|
|
||||||
else -> showError(binding, response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasNoPermissions()) {
|
|
||||||
// MusicStore.Response.NO_PERMS isnt actually returned by MusicStore, its just
|
|
||||||
// a way to keep the current permission state across device changes
|
|
||||||
loadingModel.notifyNoPermissions()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loadingModel.response.value == null) {
|
|
||||||
loadingModel.load(requireContext())
|
|
||||||
}
|
|
||||||
|
|
||||||
logD("Fragment created")
|
|
||||||
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
|
|
||||||
// Navigate away if the music has already been loaded.
|
|
||||||
// This causes a memory leak, but there's nothing I can do about it.
|
|
||||||
if (loadingModel.loaded) {
|
|
||||||
findNavController().navigate(
|
|
||||||
LoadingFragmentDirections.actionToMain()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- PERMISSIONS ---
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if Auxio has the permissions to load music
|
|
||||||
*/
|
|
||||||
private fun hasNoPermissions(): Boolean {
|
|
||||||
val needRationale = shouldShowRequestPermissionRationale(
|
|
||||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
|
||||||
)
|
|
||||||
|
|
||||||
val notGranted = ContextCompat.checkSelfPermission(
|
|
||||||
requireContext(), Manifest.permission.READ_EXTERNAL_STORAGE
|
|
||||||
) == PackageManager.PERMISSION_DENIED
|
|
||||||
|
|
||||||
return needRationale || notGranted
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onPermResult(granted: Boolean) {
|
|
||||||
if (granted) {
|
|
||||||
// If granted, its now safe to load, which will clear the NO_PERMS response
|
|
||||||
// we applied earlier.
|
|
||||||
loadingModel.load(requireContext())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- UI DISPLAY ---
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hide all error elements and return to the loading view
|
|
||||||
*/
|
|
||||||
private fun showLoading(binding: FragmentLoadingBinding) {
|
|
||||||
binding.apply {
|
|
||||||
loadingErrorText.visibility = View.INVISIBLE
|
|
||||||
loadingActionButton.visibility = View.INVISIBLE
|
|
||||||
loadingCircle.visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show an error prompt.
|
|
||||||
* @param error The [MusicStore.Response] that this error corresponds to. Ignores
|
|
||||||
* [MusicStore.Response.SUCCESS]
|
|
||||||
*/
|
|
||||||
private fun showError(binding: FragmentLoadingBinding, error: MusicStore.Response) {
|
|
||||||
binding.loadingCircle.visibility = View.GONE
|
|
||||||
binding.loadingErrorText.visibility = View.VISIBLE
|
|
||||||
binding.loadingActionButton.visibility = View.VISIBLE
|
|
||||||
|
|
||||||
when (error) {
|
|
||||||
MusicStore.Response.NO_MUSIC -> {
|
|
||||||
binding.loadingErrorText.text = getString(R.string.err_no_music)
|
|
||||||
binding.loadingActionButton.apply {
|
|
||||||
setText(R.string.lbl_retry)
|
|
||||||
setOnClickListener {
|
|
||||||
loadingModel.load(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MusicStore.Response.FAILED -> {
|
|
||||||
binding.loadingErrorText.text = getString(R.string.err_load_failed)
|
|
||||||
binding.loadingActionButton.apply {
|
|
||||||
setText(R.string.lbl_retry)
|
|
||||||
setOnClickListener {
|
|
||||||
loadingModel.load(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MusicStore.Response.NO_PERMS -> {
|
|
||||||
binding.loadingErrorText.text = getString(R.string.err_no_perms)
|
|
||||||
binding.loadingActionButton.apply {
|
|
||||||
setText(R.string.lbl_grant)
|
|
||||||
setOnClickListener {
|
|
||||||
loadingModel.grant()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,82 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2021 Auxio Project
|
|
||||||
* LoadingViewModel.kt is part of Auxio.
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.oxycblt.auxio.loading
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ViewModel responsible for the loading UI and beginning the loading process overall.
|
|
||||||
* @author OxygenCobalt
|
|
||||||
*/
|
|
||||||
class LoadingViewModel : ViewModel() {
|
|
||||||
private val mResponse = MutableLiveData<MusicStore.Response?>(null)
|
|
||||||
private val mDoGrant = MutableLiveData(false)
|
|
||||||
|
|
||||||
private var isBusy = false
|
|
||||||
|
|
||||||
/** The last response from [MusicStore]. Null if the loading process is occurring. */
|
|
||||||
val response: LiveData<MusicStore.Response?> = mResponse
|
|
||||||
val doGrant: LiveData<Boolean> = mDoGrant
|
|
||||||
val loaded: Boolean get() = musicStore.loaded
|
|
||||||
|
|
||||||
private val musicStore = MusicStore.getInstance()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Begin the music loading process. The response from MusicStore is pushed to [response]
|
|
||||||
*/
|
|
||||||
fun load(context: Context) {
|
|
||||||
// Dont start a new load if the last one hasnt finished
|
|
||||||
if (isBusy) return
|
|
||||||
|
|
||||||
isBusy = true
|
|
||||||
mResponse.value = null
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
|
||||||
mResponse.value = musicStore.load(context)
|
|
||||||
isBusy = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the grant prompt.
|
|
||||||
*/
|
|
||||||
fun grant() {
|
|
||||||
mDoGrant.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mark that the grant prompt is now shown.
|
|
||||||
*/
|
|
||||||
fun doneWithGrant() {
|
|
||||||
mDoGrant.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notify this viewmodel that there are no permissions
|
|
||||||
*/
|
|
||||||
fun notifyNoPermissions() {
|
|
||||||
mResponse.value = MusicStore.Response.NO_PERMS
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -18,11 +18,14 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.music
|
package org.oxycblt.auxio.music
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
@ -46,25 +49,20 @@ class MusicStore private constructor() {
|
||||||
private var mSongs = listOf<Song>()
|
private var mSongs = listOf<Song>()
|
||||||
val songs: List<Song> get() = mSongs
|
val songs: List<Song> get() = mSongs
|
||||||
|
|
||||||
/** Marker for whether the music loading process has successfully completed. */
|
|
||||||
var loaded = false
|
|
||||||
private set
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 load(context: Context): Response {
|
private fun load(context: Context): Response {
|
||||||
return withContext(Dispatchers.IO) {
|
|
||||||
loadMusicInternal(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Do the actual music loading process internally.
|
|
||||||
*/
|
|
||||||
private fun loadMusicInternal(context: Context): Response {
|
|
||||||
logD("Starting initial music load...")
|
logD("Starting initial music load...")
|
||||||
|
|
||||||
|
val notGranted = ContextCompat.checkSelfPermission(
|
||||||
|
context, Manifest.permission.READ_EXTERNAL_STORAGE
|
||||||
|
) == PackageManager.PERMISSION_DENIED
|
||||||
|
|
||||||
|
if (notGranted) {
|
||||||
|
return Response.Err(ErrorKind.NO_PERMS)
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val start = System.currentTimeMillis()
|
val start = System.currentTimeMillis()
|
||||||
|
|
||||||
|
@ -72,7 +70,7 @@ class MusicStore private constructor() {
|
||||||
loader.load()
|
loader.load()
|
||||||
|
|
||||||
if (loader.songs.isEmpty()) {
|
if (loader.songs.isEmpty()) {
|
||||||
return Response.NO_MUSIC
|
return Response.Err(ErrorKind.NO_MUSIC)
|
||||||
}
|
}
|
||||||
|
|
||||||
mSongs = loader.songs
|
mSongs = loader.songs
|
||||||
|
@ -85,12 +83,10 @@ class MusicStore private constructor() {
|
||||||
logE("Something went horribly wrong.")
|
logE("Something went horribly wrong.")
|
||||||
logE(e.stackTraceToString())
|
logE(e.stackTraceToString())
|
||||||
|
|
||||||
return Response.FAILED
|
return Response.Err(ErrorKind.FAILED)
|
||||||
}
|
}
|
||||||
|
|
||||||
loaded = true
|
return Response.Ok(this)
|
||||||
|
|
||||||
return Response.SUCCESS
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -121,31 +117,77 @@ class MusicStore private constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Responses that [MusicStore] sends back when a [load] call completes.
|
* A response that [MusicStore] returns when loading music.
|
||||||
|
* And before you ask, yes, I do like rust.
|
||||||
*/
|
*/
|
||||||
enum class Response {
|
sealed class Response {
|
||||||
NO_MUSIC, NO_PERMS, FAILED, SUCCESS
|
class Ok(val musicStore: MusicStore) : Response()
|
||||||
|
class Err(val kind: ErrorKind) : Response()
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class ErrorKind {
|
||||||
|
NO_PERMS, NO_MUSIC, FAILED
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@Volatile
|
@Volatile
|
||||||
private var INSTANCE: MusicStore? = null
|
private var INSTANCE: Response? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get/Instantiate the single instance of [MusicStore].
|
* Initialize the loading process for this instance. This must be ran on a background
|
||||||
|
* thread. If the instance has already been loaded successfully, then it will be returned
|
||||||
|
* immediately.
|
||||||
*/
|
*/
|
||||||
fun getInstance(): MusicStore {
|
suspend fun initInstance(context: Context): Response {
|
||||||
val currentInstance = INSTANCE
|
val currentInstance = INSTANCE
|
||||||
|
|
||||||
if (currentInstance != null) {
|
if (currentInstance is Response.Ok) {
|
||||||
return currentInstance
|
return currentInstance
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
val result = MusicStore().load(context)
|
||||||
|
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
val newInstance = MusicStore()
|
INSTANCE = result
|
||||||
INSTANCE = newInstance
|
|
||||||
return newInstance
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maybe get a MusicStore instance.
|
||||||
|
*
|
||||||
|
* @return null if the music store instance is still loading or if the loading process has
|
||||||
|
* encountered an error. An instance is returned otherwise.
|
||||||
|
*/
|
||||||
|
fun maybeGetInstance(): MusicStore? {
|
||||||
|
val currentInstance = INSTANCE
|
||||||
|
|
||||||
|
return if (currentInstance is Response.Ok) {
|
||||||
|
currentInstance.musicStore
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Require a MusicStore instance. This function is dangerous and should only be used if
|
||||||
|
* it's guaranteed that the caller's code will only be called after the initial loading
|
||||||
|
* process.
|
||||||
|
*/
|
||||||
|
fun requireInstance(): MusicStore {
|
||||||
|
return requireNotNull(maybeGetInstance()) {
|
||||||
|
"MusicStore instance was not loaded or loading failed."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this instance has successfully loaded or not.
|
||||||
|
*/
|
||||||
|
fun loaded(): Boolean {
|
||||||
|
return maybeGetInstance() != null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -99,9 +99,8 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
it.slice((mIndex.value!! + 1) until it.size)
|
it.slice((mIndex.value!! + 1) until it.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val playbackManager = PlaybackStateManager.getInstance()
|
private val playbackManager = PlaybackStateManager.maybeGetInstance()
|
||||||
private val settingsManager = SettingsManager.getInstance()
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
private val musicStore = MusicStore.getInstance()
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
playbackManager.addCallback(this)
|
playbackManager.addCallback(this)
|
||||||
|
@ -173,7 +172,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
*/
|
*/
|
||||||
fun playWithUri(uri: Uri, context: Context) {
|
fun playWithUri(uri: Uri, context: Context) {
|
||||||
// Check if everything is already running to run the URI play
|
// Check if everything is already running to run the URI play
|
||||||
if (playbackManager.isRestored && musicStore.loaded) {
|
if (playbackManager.isRestored && MusicStore.loaded()) {
|
||||||
playWithUriInternal(uri, context)
|
playWithUriInternal(uri, context)
|
||||||
} else {
|
} else {
|
||||||
logD("Cant play this URI right now, waiting...")
|
logD("Cant play this URI right now, waiting...")
|
||||||
|
@ -189,18 +188,13 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
private fun playWithUriInternal(uri: Uri, context: Context) {
|
private fun playWithUriInternal(uri: Uri, context: Context) {
|
||||||
logD("Playing with uri $uri")
|
logD("Playing with uri $uri")
|
||||||
|
|
||||||
|
val musicStore = MusicStore.requireInstance()
|
||||||
|
|
||||||
musicStore.findSongForUri(uri, context.contentResolver)?.let { song ->
|
musicStore.findSongForUri(uri, context.contentResolver)?.let { song ->
|
||||||
playSong(song)
|
playSong(song)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Play all songs
|
|
||||||
*/
|
|
||||||
fun playAll() {
|
|
||||||
playbackManager.playAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shuffle all songs
|
* Shuffle all songs
|
||||||
*/
|
*/
|
||||||
|
@ -370,6 +364,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
* Restore playback on startup. This can do one of two things:
|
* Restore playback on startup. This can do one of two things:
|
||||||
* - Play a file intent that was given by MainActivity in [playWithUri]
|
* - Play a file intent that was given by MainActivity in [playWithUri]
|
||||||
* - Restore the last playback state if there is no active file intent.
|
* - Restore the last playback state if there is no active file intent.
|
||||||
|
* TODO: Re-add this to HomeFragment once state can be restored
|
||||||
*/
|
*/
|
||||||
fun setupPlayback(context: Context) {
|
fun setupPlayback(context: Context) {
|
||||||
val intentUri = mIntentUri
|
val intentUri = mIntentUri
|
||||||
|
|
|
@ -38,7 +38,7 @@ import org.oxycblt.auxio.util.logE
|
||||||
* - If you want to use the playback state in the UI, use [org.oxycblt.auxio.playback.PlaybackViewModel] as it can withstand volatile UIs.
|
* - If you want to use the playback state in the UI, use [org.oxycblt.auxio.playback.PlaybackViewModel] as it can withstand volatile UIs.
|
||||||
* - If you want to use the playback state with the ExoPlayer instance or system-side things, use [org.oxycblt.auxio.playback.system.PlaybackService].
|
* - If you want to use the playback state with the ExoPlayer instance or system-side things, use [org.oxycblt.auxio.playback.system.PlaybackService].
|
||||||
*
|
*
|
||||||
* All access should be done with [PlaybackStateManager.getInstance].
|
* All access should be done with [PlaybackStateManager.maybeGetInstance].
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class PlaybackStateManager private constructor() {
|
class PlaybackStateManager private constructor() {
|
||||||
|
@ -132,7 +132,6 @@ class PlaybackStateManager private constructor() {
|
||||||
val hasPlayed: Boolean get() = mHasPlayed
|
val hasPlayed: Boolean get() = mHasPlayed
|
||||||
|
|
||||||
private val settingsManager = SettingsManager.getInstance()
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
private val musicStore = MusicStore.getInstance()
|
|
||||||
|
|
||||||
// --- CALLBACKS ---
|
// --- CALLBACKS ---
|
||||||
|
|
||||||
|
@ -164,6 +163,8 @@ class PlaybackStateManager private constructor() {
|
||||||
|
|
||||||
when (mode) {
|
when (mode) {
|
||||||
PlaybackMode.ALL_SONGS -> {
|
PlaybackMode.ALL_SONGS -> {
|
||||||
|
val musicStore = MusicStore.requireInstance()
|
||||||
|
|
||||||
mParent = null
|
mParent = null
|
||||||
mQueue = musicStore.songs.toMutableList()
|
mQueue = musicStore.songs.toMutableList()
|
||||||
}
|
}
|
||||||
|
@ -231,22 +232,12 @@ class PlaybackStateManager private constructor() {
|
||||||
updatePlayback(mQueue[0])
|
updatePlayback(mQueue[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Play all songs.
|
|
||||||
*/
|
|
||||||
fun playAll() {
|
|
||||||
mMode = PlaybackMode.ALL_SONGS
|
|
||||||
mQueue = musicStore.songs.toMutableList()
|
|
||||||
mParent = null
|
|
||||||
|
|
||||||
setShuffling(false, keepSong = false)
|
|
||||||
updatePlayback(mQueue[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shuffle all songs.
|
* Shuffle all songs.
|
||||||
*/
|
*/
|
||||||
fun shuffleAll() {
|
fun shuffleAll() {
|
||||||
|
val musicStore = MusicStore.maybeGetInstance() ?: return
|
||||||
|
|
||||||
mMode = PlaybackMode.ALL_SONGS
|
mMode = PlaybackMode.ALL_SONGS
|
||||||
mQueue = musicStore.songs.toMutableList()
|
mQueue = musicStore.songs.toMutableList()
|
||||||
mParent = null
|
mParent = null
|
||||||
|
@ -478,11 +469,17 @@ class PlaybackStateManager private constructor() {
|
||||||
private fun resetShuffle(keepSong: Boolean, useLastSong: Boolean) {
|
private fun resetShuffle(keepSong: Boolean, useLastSong: Boolean) {
|
||||||
val lastSong = if (useLastSong) mQueue[mIndex] else mSong
|
val lastSong = if (useLastSong) mQueue[mIndex] else mSong
|
||||||
|
|
||||||
|
val musicStore = MusicStore.requireInstance()
|
||||||
|
|
||||||
mQueue = when (mMode) {
|
mQueue = when (mMode) {
|
||||||
PlaybackMode.IN_ARTIST -> orderSongsInArtist(mParent as Artist)
|
PlaybackMode.ALL_SONGS ->
|
||||||
PlaybackMode.IN_ALBUM -> orderSongsInAlbum(mParent as Album)
|
settingsManager.libSongSort.sortSongs(musicStore.songs).toMutableList()
|
||||||
PlaybackMode.IN_GENRE -> orderSongsInGenre(mParent as Genre)
|
PlaybackMode.IN_ALBUM ->
|
||||||
PlaybackMode.ALL_SONGS -> orderSongs()
|
settingsManager.detailAlbumSort.sortAlbum(mParent as Album).toMutableList()
|
||||||
|
PlaybackMode.IN_ARTIST ->
|
||||||
|
settingsManager.detailArtistSort.sortArtist(mParent as Artist).toMutableList()
|
||||||
|
PlaybackMode.IN_GENRE ->
|
||||||
|
settingsManager.detailGenreSort.sortGenre(mParent as Genre).toMutableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keepSong) {
|
if (keepSong) {
|
||||||
|
@ -610,8 +607,10 @@ class PlaybackStateManager private constructor() {
|
||||||
if (playbackState != null) {
|
if (playbackState != null) {
|
||||||
logD("Found playback state $playbackState with queue size ${queueItems.size}")
|
logD("Found playback state $playbackState with queue size ${queueItems.size}")
|
||||||
|
|
||||||
unpackFromPlaybackState(playbackState)
|
val musicStore = MusicStore.requireInstance()
|
||||||
unpackQueue(queueItems)
|
|
||||||
|
unpackFromPlaybackState(playbackState, musicStore)
|
||||||
|
unpackQueue(queueItems, musicStore)
|
||||||
doParentSanityCheck()
|
doParentSanityCheck()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -640,12 +639,12 @@ class PlaybackStateManager private constructor() {
|
||||||
/**
|
/**
|
||||||
* Unpack a [playbackState] into this instance.
|
* Unpack a [playbackState] into this instance.
|
||||||
*/
|
*/
|
||||||
private fun unpackFromPlaybackState(playbackState: DatabaseState) {
|
private fun unpackFromPlaybackState(playbackState: DatabaseState, musicStore: MusicStore) {
|
||||||
// Turn the simplified information from PlaybackState into usable data.
|
// Turn the simplified information from PlaybackState into usable data.
|
||||||
|
|
||||||
// Do queue setup first
|
// Do queue setup first
|
||||||
mMode = PlaybackMode.fromInt(playbackState.mode) ?: PlaybackMode.ALL_SONGS
|
mMode = PlaybackMode.fromInt(playbackState.mode) ?: PlaybackMode.ALL_SONGS
|
||||||
mParent = findParent(playbackState.parentHash, mMode)
|
mParent = findParent(playbackState.parentHash, mMode, musicStore)
|
||||||
mIndex = playbackState.index
|
mIndex = playbackState.index
|
||||||
|
|
||||||
// Then set up the current state
|
// Then set up the current state
|
||||||
|
@ -663,7 +662,6 @@ class PlaybackStateManager private constructor() {
|
||||||
*/
|
*/
|
||||||
private fun packQueue(): List<DatabaseQueueItem> {
|
private fun packQueue(): List<DatabaseQueueItem> {
|
||||||
val unified = mutableListOf<DatabaseQueueItem>()
|
val unified = mutableListOf<DatabaseQueueItem>()
|
||||||
|
|
||||||
var queueItemId = 0L
|
var queueItemId = 0L
|
||||||
|
|
||||||
mUserQueue.forEach { song ->
|
mUserQueue.forEach { song ->
|
||||||
|
@ -683,7 +681,7 @@ class PlaybackStateManager private constructor() {
|
||||||
* Unpack a list of queue items into a queue & user queue.
|
* Unpack a list of queue items into a queue & user queue.
|
||||||
* @param queueItems The list of [DatabaseQueueItem]s to unpack.
|
* @param queueItems The list of [DatabaseQueueItem]s to unpack.
|
||||||
*/
|
*/
|
||||||
private fun unpackQueue(queueItems: List<DatabaseQueueItem>) {
|
private fun unpackQueue(queueItems: List<DatabaseQueueItem>, musicStore: MusicStore) {
|
||||||
for (item in queueItems) {
|
for (item in queueItems) {
|
||||||
musicStore.findSongFast(item.songHash, item.albumHash)?.let { song ->
|
musicStore.findSongFast(item.songHash, item.albumHash)?.let { song ->
|
||||||
if (item.isUserQueue) {
|
if (item.isUserQueue) {
|
||||||
|
@ -711,7 +709,7 @@ class PlaybackStateManager private constructor() {
|
||||||
/**
|
/**
|
||||||
* Get a [Parent] from music store given a [hash] and PlaybackMode [mode].
|
* Get a [Parent] from music store given a [hash] and PlaybackMode [mode].
|
||||||
*/
|
*/
|
||||||
private fun findParent(hash: Int, mode: PlaybackMode): Parent? {
|
private fun findParent(hash: Int, mode: PlaybackMode, musicStore: MusicStore): Parent? {
|
||||||
return when (mode) {
|
return when (mode) {
|
||||||
PlaybackMode.IN_GENRE -> musicStore.genres.find { it.hash == hash }
|
PlaybackMode.IN_GENRE -> musicStore.genres.find { it.hash == hash }
|
||||||
PlaybackMode.IN_ARTIST -> musicStore.artists.find { it.hash == hash }
|
PlaybackMode.IN_ARTIST -> musicStore.artists.find { it.hash == hash }
|
||||||
|
@ -737,36 +735,6 @@ class PlaybackStateManager private constructor() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- ORDERING FUNCTIONS ---
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an ordered queue based on the main list of songs
|
|
||||||
*/
|
|
||||||
private fun orderSongs(): MutableList<Song> {
|
|
||||||
return settingsManager.libSongSort.sortSongs(musicStore.songs).toMutableList()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an ordered queue based on an [Album].
|
|
||||||
*/
|
|
||||||
private fun orderSongsInAlbum(album: Album): MutableList<Song> {
|
|
||||||
return settingsManager.detailAlbumSort.sortAlbum(album).toMutableList()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an ordered queue based on an [Artist].
|
|
||||||
*/
|
|
||||||
private fun orderSongsInArtist(artist: Artist): MutableList<Song> {
|
|
||||||
return settingsManager.detailArtistSort.sortArtist(artist).toMutableList()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an ordered queue based on a [Genre].
|
|
||||||
*/
|
|
||||||
private fun orderSongsInGenre(genre: Genre): MutableList<Song> {
|
|
||||||
return settingsManager.detailGenreSort.sortGenre(genre).toMutableList()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The interface for receiving updates from [PlaybackStateManager].
|
* The interface for receiving updates from [PlaybackStateManager].
|
||||||
* Add the callback to [PlaybackStateManager] using [addCallback],
|
* Add the callback to [PlaybackStateManager] using [addCallback],
|
||||||
|
@ -796,7 +764,7 @@ class PlaybackStateManager private constructor() {
|
||||||
/**
|
/**
|
||||||
* Get/Instantiate the single instance of [PlaybackStateManager].
|
* Get/Instantiate the single instance of [PlaybackStateManager].
|
||||||
*/
|
*/
|
||||||
fun getInstance(): PlaybackStateManager {
|
fun maybeGetInstance(): PlaybackStateManager {
|
||||||
val currentInstance = INSTANCE
|
val currentInstance = INSTANCE
|
||||||
|
|
||||||
if (currentInstance != null) {
|
if (currentInstance != null) {
|
||||||
|
|
|
@ -37,7 +37,7 @@ class AudioReactor(
|
||||||
context: Context,
|
context: Context,
|
||||||
private val player: SimpleExoPlayer
|
private val player: SimpleExoPlayer
|
||||||
) : AudioManager.OnAudioFocusChangeListener {
|
) : AudioManager.OnAudioFocusChangeListener {
|
||||||
private val playbackManager = PlaybackStateManager.getInstance()
|
private val playbackManager = PlaybackStateManager.maybeGetInstance()
|
||||||
private val settingsManager = SettingsManager.getInstance()
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
private val audioManager = context.getSystemServiceSafe(AudioManager::class)
|
private val audioManager = context.getSystemServiceSafe(AudioManager::class)
|
||||||
|
|
||||||
|
|
|
@ -92,7 +92,7 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
|
||||||
private val systemReceiver = SystemEventReceiver()
|
private val systemReceiver = SystemEventReceiver()
|
||||||
|
|
||||||
// Managers
|
// Managers
|
||||||
private val playbackManager = PlaybackStateManager.getInstance()
|
private val playbackManager = PlaybackStateManager.maybeGetInstance()
|
||||||
private val settingsManager = SettingsManager.getInstance()
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
|
|
||||||
// State
|
// State
|
||||||
|
|
|
@ -39,7 +39,7 @@ class PlaybackSessionConnector(
|
||||||
private val player: Player,
|
private val player: Player,
|
||||||
private val mediaSession: MediaSessionCompat
|
private val mediaSession: MediaSessionCompat
|
||||||
) : PlaybackStateManager.Callback, Player.Listener, MediaSessionCompat.Callback() {
|
) : PlaybackStateManager.Callback, Player.Listener, MediaSessionCompat.Callback() {
|
||||||
private val playbackManager = PlaybackStateManager.getInstance()
|
private val playbackManager = PlaybackStateManager.maybeGetInstance()
|
||||||
private val emptyMetadata = MediaMetadataCompat.Builder().build()
|
private val emptyMetadata = MediaMetadataCompat.Builder().build()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
|
@ -48,7 +48,6 @@ class SearchViewModel : ViewModel() {
|
||||||
val isNavigating: Boolean get() = mIsNavigating
|
val isNavigating: Boolean get() = mIsNavigating
|
||||||
val filterMode: DisplayMode? get() = mFilterMode
|
val filterMode: DisplayMode? get() = mFilterMode
|
||||||
|
|
||||||
private val musicStore = MusicStore.getInstance()
|
|
||||||
private val settingsManager = SettingsManager.getInstance()
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
@ -60,9 +59,10 @@ class SearchViewModel : ViewModel() {
|
||||||
* Will push results to [searchResults].
|
* Will push results to [searchResults].
|
||||||
*/
|
*/
|
||||||
fun doSearch(query: String, context: Context) {
|
fun doSearch(query: String, context: Context) {
|
||||||
|
val musicStore = MusicStore.maybeGetInstance()
|
||||||
mLastQuery = query
|
mLastQuery = query
|
||||||
|
|
||||||
if (query.isEmpty()) {
|
if (query.isEmpty() || musicStore == null) {
|
||||||
mSearchResults.value = listOf()
|
mSearchResults.value = listOf()
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
|
@ -29,11 +29,13 @@ import android.view.ViewGroup
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
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.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
import org.oxycblt.auxio.BuildConfig
|
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.music.MusicStore
|
import org.oxycblt.auxio.music.MusicStore
|
||||||
import org.oxycblt.auxio.util.applyEdge
|
import org.oxycblt.auxio.util.applyEdge
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
@ -44,13 +46,14 @@ import org.oxycblt.auxio.util.showToast
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class AboutFragment : Fragment() {
|
class AboutFragment : Fragment() {
|
||||||
|
private val homeModel: HomeViewModel by activityViewModels()
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View {
|
): View {
|
||||||
val binding = FragmentAboutBinding.inflate(layoutInflater)
|
val binding = FragmentAboutBinding.inflate(layoutInflater)
|
||||||
val musicStore = MusicStore.getInstance()
|
|
||||||
|
|
||||||
binding.applyEdge { bars ->
|
binding.applyEdge { bars ->
|
||||||
binding.aboutAppbar.updatePadding(top = bars.top)
|
binding.aboutAppbar.updatePadding(top = bars.top)
|
||||||
|
@ -65,9 +68,17 @@ class AboutFragment : Fragment() {
|
||||||
binding.aboutCode.setOnClickListener { openLinkInBrowser(LINK_CODEBASE) }
|
binding.aboutCode.setOnClickListener { openLinkInBrowser(LINK_CODEBASE) }
|
||||||
binding.aboutFaq.setOnClickListener { openLinkInBrowser(LINK_FAQ) }
|
binding.aboutFaq.setOnClickListener { openLinkInBrowser(LINK_FAQ) }
|
||||||
binding.aboutLicenses.setOnClickListener { openLinkInBrowser(LINK_LICENSES) }
|
binding.aboutLicenses.setOnClickListener { openLinkInBrowser(LINK_LICENSES) }
|
||||||
|
|
||||||
|
homeModel.loaderResponse.observe(viewLifecycleOwner) { response ->
|
||||||
|
val count = when (response) {
|
||||||
|
is MusicStore.Response.Ok -> response.musicStore.songs.size
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
|
||||||
binding.aboutSongCount.text = getString(
|
binding.aboutSongCount.text = getString(
|
||||||
R.string.fmt_songs_loaded, musicStore.songs.size
|
R.string.fmt_songs_loaded, count
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
logD("Dialog created.")
|
logD("Dialog created.")
|
||||||
|
|
||||||
|
|
|
@ -191,10 +191,12 @@ fun View.applyEdge(onApply: (Rect) -> Unit) {
|
||||||
* Stopgap measure to make edge-to-edge work on views that also have a playback bar.
|
* Stopgap measure to make edge-to-edge work on views that also have a playback bar.
|
||||||
* The issue is that while we can apply padding initially, the padding will still be applied
|
* The issue is that while we can apply padding initially, the padding will still be applied
|
||||||
* when the bar is shown, which is very ungood. We mitigate this by just checking the song state
|
* when the bar is shown, which is very ungood. We mitigate this by just checking the song state
|
||||||
* and removing the padding if it isnt available, which works okayish. I think Material Files has
|
* and removing the padding if there is one, which is a stupidly fragile band-aid but it
|
||||||
* a better implementation of the same fix however, so once I'm able to hack that layout into
|
* works.
|
||||||
* Auxio things should be better.
|
*
|
||||||
* TODO: Get rid of this get rid of this get rid of this
|
* TODO: Dumpster this and replace it with a dedicated layout. Only issue with that is how
|
||||||
|
* nested our layouts are, which basically forces us to do recursion magic. Hai Zhang's Material
|
||||||
|
* Files layout may help in this task.
|
||||||
*/
|
*/
|
||||||
fun View.applyEdgeRespectingBar(
|
fun View.applyEdgeRespectingBar(
|
||||||
playbackModel: PlaybackViewModel,
|
playbackModel: PlaybackViewModel,
|
||||||
|
|
|
@ -33,7 +33,7 @@ import org.oxycblt.auxio.settings.SettingsManager
|
||||||
class WidgetController(private val context: Context) :
|
class WidgetController(private val context: Context) :
|
||||||
PlaybackStateManager.Callback,
|
PlaybackStateManager.Callback,
|
||||||
SettingsManager.Callback {
|
SettingsManager.Callback {
|
||||||
private val playbackManager = PlaybackStateManager.getInstance()
|
private val playbackManager = PlaybackStateManager.maybeGetInstance()
|
||||||
private val settingsManager = SettingsManager.getInstance()
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
private val widget = WidgetProvider()
|
private val widget = WidgetProvider()
|
||||||
|
|
||||||
|
|
|
@ -55,6 +55,7 @@
|
||||||
android:layout_margin="@dimen/spacing_medium"
|
android:layout_margin="@dimen/spacing_medium"
|
||||||
android:contentDescription="@string/desc_shuffle_all"
|
android:contentDescription="@string/desc_shuffle_all"
|
||||||
app:layout_anchor="@id/home_pager"
|
app:layout_anchor="@id/home_pager"
|
||||||
|
app:tint="?attr/colorControlNormal"
|
||||||
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior"
|
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior"
|
||||||
app:layout_anchorGravity="bottom|end" />
|
app:layout_anchorGravity="bottom|end" />
|
||||||
|
|
||||||
|
|
|
@ -1,64 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
|
||||||
|
|
||||||
<data>
|
|
||||||
|
|
||||||
<variable
|
|
||||||
name="loadingModel"
|
|
||||||
type="org.oxycblt.auxio.loading.LoadingViewModel" />
|
|
||||||
</data>
|
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:gravity="center"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
android:id="@+id/loading_panel"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_margin="@dimen/spacing_large"
|
|
||||||
android:animateLayoutChanges="true"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:gravity="center"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<ProgressBar
|
|
||||||
android:id="@+id/loading_circle"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/loading_action_button"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/loading_error_text"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginBottom="@dimen/spacing_small"
|
|
||||||
android:fontFamily="@font/inter_semibold"
|
|
||||||
android:textAlignment="center"
|
|
||||||
android:textColor="?android:attr/textColorPrimary"
|
|
||||||
android:textSize="@dimen/text_size_medium"
|
|
||||||
android:visibility="invisible"
|
|
||||||
app:layout_constraintBottom_toTopOf="@+id/loading_action_button"
|
|
||||||
tools:text="No Music Found" />
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/loading_action_button"
|
|
||||||
style="@style/Widget.Auxio.Button.Primary"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:paddingStart="@dimen/spacing_insane"
|
|
||||||
android:paddingEnd="@dimen/spacing_insane"
|
|
||||||
android:text="@string/lbl_retry"
|
|
||||||
android:visibility="invisible"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
</FrameLayout>
|
|
||||||
</layout>
|
|
|
@ -2,23 +2,7 @@
|
||||||
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
app:startDestination="@id/loading_fragment">
|
app:startDestination="@id/main_fragment">
|
||||||
<fragment
|
|
||||||
android:id="@+id/loading_fragment"
|
|
||||||
android:name="org.oxycblt.auxio.loading.LoadingFragment"
|
|
||||||
android:label="LoadingFragment"
|
|
||||||
tools:layout="@layout/fragment_loading">
|
|
||||||
<action
|
|
||||||
android:id="@+id/action_to_main"
|
|
||||||
app:destination="@id/main_fragment"
|
|
||||||
app:enterAnim="@anim/nav_default_enter_anim"
|
|
||||||
app:exitAnim="@anim/nav_default_exit_anim"
|
|
||||||
app:launchSingleTop="true"
|
|
||||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
|
||||||
app:popExitAnim="@anim/nav_default_pop_exit_anim"
|
|
||||||
app:popUpTo="@id/loading_fragment"
|
|
||||||
app:popUpToInclusive="true" />
|
|
||||||
</fragment>
|
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/main_fragment"
|
android:id="@+id/main_fragment"
|
||||||
android:name="org.oxycblt.auxio.MainFragment"
|
android:name="org.oxycblt.auxio.MainFragment"
|
||||||
|
|
Loading…
Reference in a new issue