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 ---
|
||||
|
||||
playbackModel.setupPlayback(requireContext())
|
||||
|
||||
// Change CompactPlaybackFragment's visibility here so that an animation occurs.
|
||||
binding.mainPlayback.isVisible = playbackModel.song.value != null
|
||||
|
||||
|
|
|
@ -78,23 +78,28 @@ class DetailViewModel : ViewModel() {
|
|||
|
||||
private var currentMenuContext: DisplayMode? = null
|
||||
|
||||
private val musicStore = MusicStore.getInstance()
|
||||
private val settingsManager = SettingsManager.getInstance()
|
||||
|
||||
fun setGenre(id: Long, context: Context) {
|
||||
if (mCurGenre.value?.id == id) return
|
||||
|
||||
val musicStore = MusicStore.requireInstance()
|
||||
mCurGenre.value = musicStore.genres.find { it.id == id }
|
||||
refreshGenreData(context)
|
||||
}
|
||||
|
||||
fun setArtist(id: Long, context: Context) {
|
||||
if (mCurArtist.value?.id == id) return
|
||||
|
||||
val musicStore = MusicStore.requireInstance()
|
||||
mCurArtist.value = musicStore.artists.find { it.id == id }
|
||||
refreshArtistData(context)
|
||||
}
|
||||
|
||||
fun setAlbum(id: Long, context: Context) {
|
||||
if (mCurAlbum.value?.id == id) return
|
||||
|
||||
val musicStore = MusicStore.requireInstance()
|
||||
mCurAlbum.value = musicStore.albums.find { it.id == id }
|
||||
refreshAlbumData(context)
|
||||
}
|
||||
|
|
|
@ -18,12 +18,16 @@
|
|||
|
||||
package org.oxycblt.auxio.home
|
||||
|
||||
import android.Manifest
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.view.iterator
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.core.view.updatePaddingRelative
|
||||
|
@ -34,6 +38,7 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import org.oxycblt.auxio.MainFragmentDirections
|
||||
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.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.MusicStore
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.ui.DisplayMode
|
||||
|
@ -73,6 +79,13 @@ class HomeFragment : Fragment() {
|
|||
var bottomPadding = 0
|
||||
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 ---
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
@ -193,22 +206,96 @@ class HomeFragment : Fragment() {
|
|||
registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) = homeModel.updateCurrentTab(position)
|
||||
})
|
||||
|
||||
TabLayoutMediator(binding.homeTabs, this) { tab, pos ->
|
||||
tab.setText(homeModel.tabs[pos].string)
|
||||
}.attach()
|
||||
}
|
||||
|
||||
binding.homeFab.setOnClickListener {
|
||||
playbackModel.shuffleAll()
|
||||
}
|
||||
|
||||
TabLayoutMediator(binding.homeTabs, binding.homePager) { tab, pos ->
|
||||
tab.setText(homeModel.tabs[pos].string)
|
||||
}.attach()
|
||||
|
||||
// --- 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.
|
||||
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 ->
|
||||
// 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) {
|
||||
binding.homeFab.hide()
|
||||
} else {
|
||||
|
|
|
@ -18,9 +18,12 @@
|
|||
|
||||
package org.oxycblt.auxio.home
|
||||
|
||||
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.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
|
@ -36,13 +39,8 @@ import org.oxycblt.auxio.ui.SortMode
|
|||
* @author OxygenCobalt
|
||||
*/
|
||||
class HomeViewModel : ViewModel(), SettingsManager.Callback {
|
||||
private val musicStore = MusicStore.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>())
|
||||
val songs: LiveData<List<Song>> get() = mSongs
|
||||
|
||||
|
@ -58,12 +56,13 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback {
|
|||
var tabs: List<DisplayMode> = visibleTabs
|
||||
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])
|
||||
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.
|
||||
* 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)
|
||||
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 {
|
||||
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)
|
||||
mAlbums.value = settingsManager.libAlbumSort.sortAlbums(musicStore.albums)
|
||||
mArtists.value = settingsManager.libArtistSort.sortModels(musicStore.artists)
|
||||
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.RecyclerView
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.util.canScroll
|
||||
import org.oxycblt.auxio.util.resolveAttr
|
||||
import org.oxycblt.auxio.util.resolveDrawable
|
||||
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.
|
||||
*
|
||||
* 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]
|
||||
* MODIFIER: OxygenCobalt [https://github.com/]
|
||||
*
|
||||
* !!! MODIFICATIONS !!!:
|
||||
* - Scroller will no longer show itself on startup, which looked unpleasent with multiple
|
||||
* views
|
||||
* - Scroller will no longer show itself on startup or relayouts, which looked unpleasant
|
||||
* with multiple views
|
||||
* - DefaultAnimationHelper and RecyclerViewHelper were merged into the class
|
||||
* - FastScroller overlay was merged into RecyclerView instance
|
||||
* - Removed FastScrollerBuilder
|
||||
|
@ -65,8 +66,6 @@ import kotlin.math.abs
|
|||
* - Added drag listener
|
||||
* - TODO: Added documentation
|
||||
* - TODO: Popup will center itself to the thumb when possible
|
||||
*
|
||||
* TODO: Debug this
|
||||
*/
|
||||
class FastScrollRecyclerView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
|
@ -193,10 +192,6 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
|||
})
|
||||
}
|
||||
|
||||
fun addPopupProvider(provider: (Int) -> String) {
|
||||
popupProvider = provider
|
||||
}
|
||||
|
||||
// --- RECYCLERVIEW EVENT MANAGEMENT ---
|
||||
|
||||
private fun onPreDraw() {
|
||||
|
@ -316,6 +311,10 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
|||
}
|
||||
|
||||
private fun updateScrollbarState() {
|
||||
if (!canScroll() || childCount == 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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.
|
||||
*
|
||||
* 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]
|
||||
* MODIFIER: OxygenCobalt [https://github.com/]
|
||||
*
|
||||
|
@ -47,7 +47,6 @@ import kotlin.math.sqrt
|
|||
* - Use modified Auxio resources instead of AFS resources
|
||||
* - Variable names are no longer prefixed with m
|
||||
* - Suppressed deprecation warning when dealing with convexness
|
||||
* - TODO: Popup will center itself to the thumb when possible
|
||||
*/
|
||||
class Md2PopupBackground(context: Context) : Drawable() {
|
||||
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.newMenu
|
||||
import org.oxycblt.auxio.ui.sliceArticle
|
||||
import org.oxycblt.auxio.util.applySpans
|
||||
|
||||
class SongListFragment : HomeListFragment() {
|
||||
override fun onCreateView(
|
||||
|
@ -47,7 +46,6 @@ class SongListFragment : HomeListFragment() {
|
|||
)
|
||||
|
||||
setupRecycler(R.id.home_song_list, adapter, homeModel.songs)
|
||||
binding.homeRecycler.applySpans { it == 0 }
|
||||
|
||||
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
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import androidx.core.content.ContextCompat
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
@ -46,25 +49,20 @@ class MusicStore private constructor() {
|
|||
private var mSongs = listOf<Song>()
|
||||
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.
|
||||
*/
|
||||
suspend fun load(context: Context): Response {
|
||||
return withContext(Dispatchers.IO) {
|
||||
loadMusicInternal(context)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Do the actual music loading process internally.
|
||||
*/
|
||||
private fun loadMusicInternal(context: Context): Response {
|
||||
private fun load(context: Context): Response {
|
||||
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 {
|
||||
val start = System.currentTimeMillis()
|
||||
|
||||
|
@ -72,7 +70,7 @@ class MusicStore private constructor() {
|
|||
loader.load()
|
||||
|
||||
if (loader.songs.isEmpty()) {
|
||||
return Response.NO_MUSIC
|
||||
return Response.Err(ErrorKind.NO_MUSIC)
|
||||
}
|
||||
|
||||
mSongs = loader.songs
|
||||
|
@ -85,12 +83,10 @@ class MusicStore private constructor() {
|
|||
logE("Something went horribly wrong.")
|
||||
logE(e.stackTraceToString())
|
||||
|
||||
return Response.FAILED
|
||||
return Response.Err(ErrorKind.FAILED)
|
||||
}
|
||||
|
||||
loaded = true
|
||||
|
||||
return Response.SUCCESS
|
||||
return Response.Ok(this)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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 {
|
||||
NO_MUSIC, NO_PERMS, FAILED, SUCCESS
|
||||
sealed class Response {
|
||||
class Ok(val musicStore: MusicStore) : Response()
|
||||
class Err(val kind: ErrorKind) : Response()
|
||||
}
|
||||
|
||||
enum class ErrorKind {
|
||||
NO_PERMS, NO_MUSIC, FAILED
|
||||
}
|
||||
|
||||
companion object {
|
||||
@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
|
||||
|
||||
if (currentInstance != null) {
|
||||
if (currentInstance is Response.Ok) {
|
||||
return currentInstance
|
||||
}
|
||||
|
||||
return withContext(Dispatchers.IO) {
|
||||
val result = MusicStore().load(context)
|
||||
|
||||
synchronized(this) {
|
||||
val newInstance = MusicStore()
|
||||
INSTANCE = newInstance
|
||||
return newInstance
|
||||
INSTANCE = result
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
private val playbackManager = PlaybackStateManager.getInstance()
|
||||
private val playbackManager = PlaybackStateManager.maybeGetInstance()
|
||||
private val settingsManager = SettingsManager.getInstance()
|
||||
private val musicStore = MusicStore.getInstance()
|
||||
|
||||
init {
|
||||
playbackManager.addCallback(this)
|
||||
|
@ -173,7 +172,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
|||
*/
|
||||
fun playWithUri(uri: Uri, context: Context) {
|
||||
// Check if everything is already running to run the URI play
|
||||
if (playbackManager.isRestored && musicStore.loaded) {
|
||||
if (playbackManager.isRestored && MusicStore.loaded()) {
|
||||
playWithUriInternal(uri, context)
|
||||
} else {
|
||||
logD("Cant play this URI right now, waiting...")
|
||||
|
@ -189,18 +188,13 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
|||
private fun playWithUriInternal(uri: Uri, context: Context) {
|
||||
logD("Playing with uri $uri")
|
||||
|
||||
val musicStore = MusicStore.requireInstance()
|
||||
|
||||
musicStore.findSongForUri(uri, context.contentResolver)?.let { song ->
|
||||
playSong(song)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Play all songs
|
||||
*/
|
||||
fun playAll() {
|
||||
playbackManager.playAll()
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuffle all songs
|
||||
*/
|
||||
|
@ -370,6 +364,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
|||
* Restore playback on startup. This can do one of two things:
|
||||
* - Play a file intent that was given by MainActivity in [playWithUri]
|
||||
* - 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) {
|
||||
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 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
|
||||
*/
|
||||
class PlaybackStateManager private constructor() {
|
||||
|
@ -132,7 +132,6 @@ class PlaybackStateManager private constructor() {
|
|||
val hasPlayed: Boolean get() = mHasPlayed
|
||||
|
||||
private val settingsManager = SettingsManager.getInstance()
|
||||
private val musicStore = MusicStore.getInstance()
|
||||
|
||||
// --- CALLBACKS ---
|
||||
|
||||
|
@ -164,6 +163,8 @@ class PlaybackStateManager private constructor() {
|
|||
|
||||
when (mode) {
|
||||
PlaybackMode.ALL_SONGS -> {
|
||||
val musicStore = MusicStore.requireInstance()
|
||||
|
||||
mParent = null
|
||||
mQueue = musicStore.songs.toMutableList()
|
||||
}
|
||||
|
@ -231,22 +232,12 @@ class PlaybackStateManager private constructor() {
|
|||
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.
|
||||
*/
|
||||
fun shuffleAll() {
|
||||
val musicStore = MusicStore.maybeGetInstance() ?: return
|
||||
|
||||
mMode = PlaybackMode.ALL_SONGS
|
||||
mQueue = musicStore.songs.toMutableList()
|
||||
mParent = null
|
||||
|
@ -478,11 +469,17 @@ class PlaybackStateManager private constructor() {
|
|||
private fun resetShuffle(keepSong: Boolean, useLastSong: Boolean) {
|
||||
val lastSong = if (useLastSong) mQueue[mIndex] else mSong
|
||||
|
||||
val musicStore = MusicStore.requireInstance()
|
||||
|
||||
mQueue = when (mMode) {
|
||||
PlaybackMode.IN_ARTIST -> orderSongsInArtist(mParent as Artist)
|
||||
PlaybackMode.IN_ALBUM -> orderSongsInAlbum(mParent as Album)
|
||||
PlaybackMode.IN_GENRE -> orderSongsInGenre(mParent as Genre)
|
||||
PlaybackMode.ALL_SONGS -> orderSongs()
|
||||
PlaybackMode.ALL_SONGS ->
|
||||
settingsManager.libSongSort.sortSongs(musicStore.songs).toMutableList()
|
||||
PlaybackMode.IN_ALBUM ->
|
||||
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) {
|
||||
|
@ -610,8 +607,10 @@ class PlaybackStateManager private constructor() {
|
|||
if (playbackState != null) {
|
||||
logD("Found playback state $playbackState with queue size ${queueItems.size}")
|
||||
|
||||
unpackFromPlaybackState(playbackState)
|
||||
unpackQueue(queueItems)
|
||||
val musicStore = MusicStore.requireInstance()
|
||||
|
||||
unpackFromPlaybackState(playbackState, musicStore)
|
||||
unpackQueue(queueItems, musicStore)
|
||||
doParentSanityCheck()
|
||||
}
|
||||
|
||||
|
@ -640,12 +639,12 @@ class PlaybackStateManager private constructor() {
|
|||
/**
|
||||
* 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.
|
||||
|
||||
// Do queue setup first
|
||||
mMode = PlaybackMode.fromInt(playbackState.mode) ?: PlaybackMode.ALL_SONGS
|
||||
mParent = findParent(playbackState.parentHash, mMode)
|
||||
mParent = findParent(playbackState.parentHash, mMode, musicStore)
|
||||
mIndex = playbackState.index
|
||||
|
||||
// Then set up the current state
|
||||
|
@ -663,7 +662,6 @@ class PlaybackStateManager private constructor() {
|
|||
*/
|
||||
private fun packQueue(): List<DatabaseQueueItem> {
|
||||
val unified = mutableListOf<DatabaseQueueItem>()
|
||||
|
||||
var queueItemId = 0L
|
||||
|
||||
mUserQueue.forEach { song ->
|
||||
|
@ -683,7 +681,7 @@ class PlaybackStateManager private constructor() {
|
|||
* Unpack a list of queue items into a queue & user queue.
|
||||
* @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) {
|
||||
musicStore.findSongFast(item.songHash, item.albumHash)?.let { song ->
|
||||
if (item.isUserQueue) {
|
||||
|
@ -711,7 +709,7 @@ class PlaybackStateManager private constructor() {
|
|||
/**
|
||||
* 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) {
|
||||
PlaybackMode.IN_GENRE -> musicStore.genres.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].
|
||||
* Add the callback to [PlaybackStateManager] using [addCallback],
|
||||
|
@ -796,7 +764,7 @@ class PlaybackStateManager private constructor() {
|
|||
/**
|
||||
* Get/Instantiate the single instance of [PlaybackStateManager].
|
||||
*/
|
||||
fun getInstance(): PlaybackStateManager {
|
||||
fun maybeGetInstance(): PlaybackStateManager {
|
||||
val currentInstance = INSTANCE
|
||||
|
||||
if (currentInstance != null) {
|
||||
|
|
|
@ -37,7 +37,7 @@ class AudioReactor(
|
|||
context: Context,
|
||||
private val player: SimpleExoPlayer
|
||||
) : AudioManager.OnAudioFocusChangeListener {
|
||||
private val playbackManager = PlaybackStateManager.getInstance()
|
||||
private val playbackManager = PlaybackStateManager.maybeGetInstance()
|
||||
private val settingsManager = SettingsManager.getInstance()
|
||||
private val audioManager = context.getSystemServiceSafe(AudioManager::class)
|
||||
|
||||
|
|
|
@ -92,7 +92,7 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
|
|||
private val systemReceiver = SystemEventReceiver()
|
||||
|
||||
// Managers
|
||||
private val playbackManager = PlaybackStateManager.getInstance()
|
||||
private val playbackManager = PlaybackStateManager.maybeGetInstance()
|
||||
private val settingsManager = SettingsManager.getInstance()
|
||||
|
||||
// State
|
||||
|
|
|
@ -39,7 +39,7 @@ class PlaybackSessionConnector(
|
|||
private val player: Player,
|
||||
private val mediaSession: MediaSessionCompat
|
||||
) : PlaybackStateManager.Callback, Player.Listener, MediaSessionCompat.Callback() {
|
||||
private val playbackManager = PlaybackStateManager.getInstance()
|
||||
private val playbackManager = PlaybackStateManager.maybeGetInstance()
|
||||
private val emptyMetadata = MediaMetadataCompat.Builder().build()
|
||||
|
||||
init {
|
||||
|
|
|
@ -48,7 +48,6 @@ class SearchViewModel : ViewModel() {
|
|||
val isNavigating: Boolean get() = mIsNavigating
|
||||
val filterMode: DisplayMode? get() = mFilterMode
|
||||
|
||||
private val musicStore = MusicStore.getInstance()
|
||||
private val settingsManager = SettingsManager.getInstance()
|
||||
|
||||
init {
|
||||
|
@ -60,9 +59,10 @@ class SearchViewModel : ViewModel() {
|
|||
* Will push results to [searchResults].
|
||||
*/
|
||||
fun doSearch(query: String, context: Context) {
|
||||
val musicStore = MusicStore.maybeGetInstance()
|
||||
mLastQuery = query
|
||||
|
||||
if (query.isEmpty()) {
|
||||
if (query.isEmpty() || musicStore == null) {
|
||||
mSearchResults.value = listOf()
|
||||
|
||||
return
|
||||
|
|
|
@ -29,11 +29,13 @@ import android.view.ViewGroup
|
|||
import androidx.core.net.toUri
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
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.MusicStore
|
||||
import org.oxycblt.auxio.util.applyEdge
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
@ -44,13 +46,14 @@ import org.oxycblt.auxio.util.showToast
|
|||
* @author OxygenCobalt
|
||||
*/
|
||||
class AboutFragment : Fragment() {
|
||||
private val homeModel: HomeViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
val binding = FragmentAboutBinding.inflate(layoutInflater)
|
||||
val musicStore = MusicStore.getInstance()
|
||||
|
||||
binding.applyEdge { bars ->
|
||||
binding.aboutAppbar.updatePadding(top = bars.top)
|
||||
|
@ -65,9 +68,17 @@ class AboutFragment : Fragment() {
|
|||
binding.aboutCode.setOnClickListener { openLinkInBrowser(LINK_CODEBASE) }
|
||||
binding.aboutFaq.setOnClickListener { openLinkInBrowser(LINK_FAQ) }
|
||||
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(
|
||||
R.string.fmt_songs_loaded, musicStore.songs.size
|
||||
R.string.fmt_songs_loaded, count
|
||||
)
|
||||
}
|
||||
|
||||
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.
|
||||
* 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
|
||||
* and removing the padding if it isnt available, which works okayish. I think Material Files has
|
||||
* a better implementation of the same fix however, so once I'm able to hack that layout into
|
||||
* Auxio things should be better.
|
||||
* TODO: Get rid of this get rid of this get rid of this
|
||||
* and removing the padding if there is one, which is a stupidly fragile band-aid but it
|
||||
* works.
|
||||
*
|
||||
* 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(
|
||||
playbackModel: PlaybackViewModel,
|
||||
|
|
|
@ -33,7 +33,7 @@ import org.oxycblt.auxio.settings.SettingsManager
|
|||
class WidgetController(private val context: Context) :
|
||||
PlaybackStateManager.Callback,
|
||||
SettingsManager.Callback {
|
||||
private val playbackManager = PlaybackStateManager.getInstance()
|
||||
private val playbackManager = PlaybackStateManager.maybeGetInstance()
|
||||
private val settingsManager = SettingsManager.getInstance()
|
||||
private val widget = WidgetProvider()
|
||||
|
||||
|
|
|
@ -55,6 +55,7 @@
|
|||
android:layout_margin="@dimen/spacing_medium"
|
||||
android:contentDescription="@string/desc_shuffle_all"
|
||||
app:layout_anchor="@id/home_pager"
|
||||
app:tint="?attr/colorControlNormal"
|
||||
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior"
|
||||
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"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
app:startDestination="@id/loading_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>
|
||||
app:startDestination="@id/main_fragment">
|
||||
<fragment
|
||||
android:id="@+id/main_fragment"
|
||||
android:name="org.oxycblt.auxio.MainFragment"
|
||||
|
|
Loading…
Reference in a new issue