all: switch to stateflow
Switch from LiveData to StateFlow. While LiveData is a pretty good data storage/observer mechanism, it has a few flaws: - Values are always nullable in LiveData, even if you make them non-null. - LiveData can only be mutated on Dispatchers.Main, which frustrates possible additions like a more fine-grained music status system. - LiveData's perks are exclusive to ViewModels, which made coupling with shared objects somewhat cumbersome. StateFlow solves all of these by being a native coroutine solution with proper android bindings. Use it instead.
This commit is contained in:
parent
402a290db7
commit
a65d37c421
32 changed files with 268 additions and 232 deletions
|
@ -2,6 +2,9 @@
|
||||||
|
|
||||||
## dev [v2.3.1, v2.4.0, or v3.0.0]
|
## dev [v2.3.1, v2.4.0, or v3.0.0]
|
||||||
|
|
||||||
|
#### What's Improved
|
||||||
|
- Loading UI is now more clear and easy-to-use
|
||||||
|
|
||||||
#### What's Fixed
|
#### What's Fixed
|
||||||
- Fixed crash when seeking to the end of a track as the track changed to a track with a lower duration
|
- Fixed crash when seeking to the end of a track as the track changed to a track with a lower duration
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ android {
|
||||||
compileSdkVersion 32
|
compileSdkVersion 32
|
||||||
buildToolsVersion "32.0.0"
|
buildToolsVersion "32.0.0"
|
||||||
|
|
||||||
// ExoPlayer needs Java 8 to compile.
|
// ExoPlayer, AndroidX, and Material Components all need Java 8 to compile.
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
@ -76,6 +76,7 @@ dependencies {
|
||||||
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
|
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
|
||||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
|
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
|
||||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
|
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
|
||||||
|
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
implementation "androidx.navigation:navigation-ui-ktx:$navigation_version"
|
implementation "androidx.navigation:navigation-ui-ktx:$navigation_version"
|
||||||
|
@ -91,7 +92,7 @@ dependencies {
|
||||||
// --- THIRD PARTY ---
|
// --- THIRD PARTY ---
|
||||||
|
|
||||||
// Exoplayer
|
// Exoplayer
|
||||||
// WARNING: THE EXOPLAYER VERSION MUST BE KEPT IN LOCK-STEP WITH THE FLAC EXTENSION.
|
// WARNING: THE EXOPLAYER VERSION MUST BE KEPT IN LOCK-STEP WITH THE PRE-BUILD SCRIPT.
|
||||||
// IF NOT, VERY UNFRIENDLY BUILD FAILURES AND CRASHES MAY ENSUE.
|
// IF NOT, VERY UNFRIENDLY BUILD FAILURES AND CRASHES MAY ENSUE.
|
||||||
implementation "com.google.android.exoplayer:exoplayer-core:2.17.1"
|
implementation "com.google.android.exoplayer:exoplayer-core:2.17.1"
|
||||||
implementation fileTree(dir: "libs", include: ["extension-*.aar"])
|
implementation fileTree(dir: "libs", include: ["extension-*.aar"])
|
||||||
|
@ -109,7 +110,7 @@ dependencies {
|
||||||
spotless {
|
spotless {
|
||||||
kotlin {
|
kotlin {
|
||||||
target "src/**/*.kt"
|
target "src/**/*.kt"
|
||||||
ktfmt('0.37').dropboxStyle()
|
ktfmt("0.37").dropboxStyle()
|
||||||
licenseHeaderFile("NOTICE")
|
licenseHeaderFile("NOTICE")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,10 +22,10 @@ import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.navigation.findNavController
|
import androidx.navigation.findNavController
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
import org.oxycblt.auxio.databinding.FragmentMainBinding
|
import org.oxycblt.auxio.databinding.FragmentMainBinding
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicViewModel
|
import org.oxycblt.auxio.music.MusicViewModel
|
||||||
|
@ -34,6 +34,7 @@ import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.ui.MainNavigationAction
|
import org.oxycblt.auxio.ui.MainNavigationAction
|
||||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||||
import org.oxycblt.auxio.ui.ViewBindingFragment
|
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||||
|
import org.oxycblt.auxio.util.launch
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A wrapper around the home fragment that shows the playback fragment and controls the more
|
* A wrapper around the home fragment that shows the playback fragment and controls the more
|
||||||
|
@ -50,12 +51,6 @@ class MainFragment : ViewBindingFragment<FragmentMainBinding>() {
|
||||||
|
|
||||||
override fun onBindingCreated(binding: FragmentMainBinding, savedInstanceState: Bundle?) {
|
override fun onBindingCreated(binding: FragmentMainBinding, savedInstanceState: Bundle?) {
|
||||||
// --- UI SETUP ---
|
// --- UI SETUP ---
|
||||||
// Build the permission launcher here as you can only do it in onCreateView/onCreate
|
|
||||||
val permLauncher =
|
|
||||||
registerForActivityResult(ActivityResultContracts.RequestPermission()) {
|
|
||||||
musicModel.reloadMusic(requireContext())
|
|
||||||
}
|
|
||||||
|
|
||||||
requireActivity()
|
requireActivity()
|
||||||
.onBackPressedDispatcher.addCallback(
|
.onBackPressedDispatcher.addCallback(
|
||||||
viewLifecycleOwner, DynamicBackPressedCallback().also { callback = it })
|
viewLifecycleOwner, DynamicBackPressedCallback().also { callback = it })
|
||||||
|
@ -81,10 +76,9 @@ class MainFragment : ViewBindingFragment<FragmentMainBinding>() {
|
||||||
// TODO: Move this to a service [automatic rescanning]
|
// TODO: Move this to a service [automatic rescanning]
|
||||||
musicModel.loadMusic(requireContext())
|
musicModel.loadMusic(requireContext())
|
||||||
|
|
||||||
navModel.mainNavigationAction.observe(viewLifecycleOwner, ::handleMainNavigation)
|
launch { navModel.mainNavigationAction.collect(::handleMainNavigation) }
|
||||||
navModel.exploreNavigationItem.observe(viewLifecycleOwner, ::handleExploreNavigation)
|
launch { navModel.exploreNavigationItem.collect(::handleExploreNavigation) }
|
||||||
|
launch { playbackModel.song.collect(::updateSong) }
|
||||||
playbackModel.song.observe(viewLifecycleOwner, ::updateSong)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
|
|
@ -25,6 +25,7 @@ import androidx.core.view.children
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
import androidx.recyclerview.widget.LinearSmoothScroller
|
import androidx.recyclerview.widget.LinearSmoothScroller
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
||||||
import org.oxycblt.auxio.detail.recycler.AlbumDetailAdapter
|
import org.oxycblt.auxio.detail.recycler.AlbumDetailAdapter
|
||||||
|
@ -39,6 +40,7 @@ import org.oxycblt.auxio.ui.Item
|
||||||
import org.oxycblt.auxio.ui.newMenu
|
import org.oxycblt.auxio.ui.newMenu
|
||||||
import org.oxycblt.auxio.util.applySpans
|
import org.oxycblt.auxio.util.applySpans
|
||||||
import org.oxycblt.auxio.util.canScroll
|
import org.oxycblt.auxio.util.canScroll
|
||||||
|
import org.oxycblt.auxio.util.launch
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.logW
|
import org.oxycblt.auxio.util.logW
|
||||||
import org.oxycblt.auxio.util.showToast
|
import org.oxycblt.auxio.util.showToast
|
||||||
|
@ -66,9 +68,9 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
|
||||||
|
|
||||||
// -- VIEWMODEL SETUP ---
|
// -- VIEWMODEL SETUP ---
|
||||||
|
|
||||||
detailModel.albumData.observe(viewLifecycleOwner, detailAdapter.data::submitList)
|
launch { detailModel.albumData.collect(detailAdapter.data::submitList) }
|
||||||
navModel.exploreNavigationItem.observe(viewLifecycleOwner, ::handleNavigation)
|
launch { navModel.exploreNavigationItem.collect(::handleNavigation) }
|
||||||
playbackModel.song.observe(viewLifecycleOwner, ::updateSong)
|
launch { playbackModel.song.collect(::updateSong) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||||
|
|
|
@ -22,6 +22,7 @@ import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
||||||
import org.oxycblt.auxio.detail.recycler.ArtistDetailAdapter
|
import org.oxycblt.auxio.detail.recycler.ArtistDetailAdapter
|
||||||
|
@ -37,6 +38,7 @@ import org.oxycblt.auxio.ui.Header
|
||||||
import org.oxycblt.auxio.ui.Item
|
import org.oxycblt.auxio.ui.Item
|
||||||
import org.oxycblt.auxio.ui.newMenu
|
import org.oxycblt.auxio.ui.newMenu
|
||||||
import org.oxycblt.auxio.util.applySpans
|
import org.oxycblt.auxio.util.applySpans
|
||||||
|
import org.oxycblt.auxio.util.launch
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.logW
|
import org.oxycblt.auxio.util.logW
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
@ -64,10 +66,10 @@ class ArtistDetailFragment : DetailFragment(), DetailAdapter.Listener {
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ---
|
// --- VIEWMODEL SETUP ---
|
||||||
|
|
||||||
detailModel.artistData.observe(viewLifecycleOwner, detailAdapter.data::submitList)
|
launch { detailModel.artistData.collect(detailAdapter.data::submitList) }
|
||||||
navModel.exploreNavigationItem.observe(viewLifecycleOwner, ::handleNavigation)
|
launch { navModel.exploreNavigationItem.collect(::handleNavigation) }
|
||||||
playbackModel.song.observe(viewLifecycleOwner, ::updateSong)
|
launch { playbackModel.song.collect(::updateSong) }
|
||||||
playbackModel.parent.observe(viewLifecycleOwner, ::updateParent)
|
launch { playbackModel.parent.collect(::updateParent) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMenuItemClick(item: MenuItem): Boolean = false
|
override fun onMenuItemClick(item: MenuItem): Boolean = false
|
||||||
|
|
|
@ -17,9 +17,9 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.detail
|
package org.oxycblt.auxio.detail
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.detail.recycler.DiscHeader
|
import org.oxycblt.auxio.detail.recycler.DiscHeader
|
||||||
import org.oxycblt.auxio.detail.recycler.SortHeader
|
import org.oxycblt.auxio.detail.recycler.SortHeader
|
||||||
|
@ -45,12 +45,12 @@ class DetailViewModel : ViewModel() {
|
||||||
private val musicStore = MusicStore.getInstance()
|
private val musicStore = MusicStore.getInstance()
|
||||||
private val settingsManager = SettingsManager.getInstance()
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
|
|
||||||
private val _currentAlbum = MutableLiveData<Album?>()
|
private val _currentAlbum = MutableStateFlow<Album?>(null)
|
||||||
val currentAlbum: LiveData<Album?>
|
val currentAlbum: StateFlow<Album?>
|
||||||
get() = _currentAlbum
|
get() = _currentAlbum
|
||||||
|
|
||||||
private val _albumData = MutableLiveData(listOf<Item>())
|
private val _albumData = MutableStateFlow(listOf<Item>())
|
||||||
val albumData: LiveData<List<Item>>
|
val albumData: StateFlow<List<Item>>
|
||||||
get() = _albumData
|
get() = _albumData
|
||||||
|
|
||||||
var albumSort: Sort
|
var albumSort: Sort
|
||||||
|
@ -60,12 +60,12 @@ class DetailViewModel : ViewModel() {
|
||||||
currentAlbum.value?.let(::refreshAlbumData)
|
currentAlbum.value?.let(::refreshAlbumData)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val _currentArtist = MutableLiveData<Artist?>()
|
private val _currentArtist = MutableStateFlow<Artist?>(null)
|
||||||
val currentArtist: LiveData<Artist?>
|
val currentArtist: StateFlow<Artist?>
|
||||||
get() = _currentArtist
|
get() = _currentArtist
|
||||||
|
|
||||||
private val _artistData = MutableLiveData(listOf<Item>())
|
private val _artistData = MutableStateFlow(listOf<Item>())
|
||||||
val artistData: LiveData<List<Item>> = _artistData
|
val artistData: StateFlow<List<Item>> = _artistData
|
||||||
|
|
||||||
var artistSort: Sort
|
var artistSort: Sort
|
||||||
get() = settingsManager.detailArtistSort
|
get() = settingsManager.detailArtistSort
|
||||||
|
@ -74,12 +74,12 @@ class DetailViewModel : ViewModel() {
|
||||||
currentArtist.value?.let(::refreshArtistData)
|
currentArtist.value?.let(::refreshArtistData)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val _currentGenre = MutableLiveData<Genre?>()
|
private val _currentGenre = MutableStateFlow<Genre?>(null)
|
||||||
val currentGenre: LiveData<Genre?>
|
val currentGenre: StateFlow<Genre?>
|
||||||
get() = _currentGenre
|
get() = _currentGenre
|
||||||
|
|
||||||
private val _genreData = MutableLiveData(listOf<Item>())
|
private val _genreData = MutableStateFlow(listOf<Item>())
|
||||||
val genreData: LiveData<List<Item>> = _genreData
|
val genreData: StateFlow<List<Item>> = _genreData
|
||||||
|
|
||||||
var genreSort: Sort
|
var genreSort: Sort
|
||||||
get() = settingsManager.detailGenreSort
|
get() = settingsManager.detailGenreSort
|
||||||
|
|
|
@ -22,6 +22,7 @@ import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
||||||
import org.oxycblt.auxio.detail.recycler.DetailAdapter
|
import org.oxycblt.auxio.detail.recycler.DetailAdapter
|
||||||
|
@ -37,6 +38,7 @@ import org.oxycblt.auxio.ui.Header
|
||||||
import org.oxycblt.auxio.ui.Item
|
import org.oxycblt.auxio.ui.Item
|
||||||
import org.oxycblt.auxio.ui.newMenu
|
import org.oxycblt.auxio.ui.newMenu
|
||||||
import org.oxycblt.auxio.util.applySpans
|
import org.oxycblt.auxio.util.applySpans
|
||||||
|
import org.oxycblt.auxio.util.launch
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
|
@ -62,9 +64,9 @@ class GenreDetailFragment : DetailFragment(), DetailAdapter.Listener {
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ---
|
// --- VIEWMODEL SETUP ---
|
||||||
|
|
||||||
detailModel.genreData.observe(viewLifecycleOwner, detailAdapter.data::submitList)
|
launch { detailModel.genreData.collect(detailAdapter.data::submitList) }
|
||||||
navModel.exploreNavigationItem.observe(viewLifecycleOwner, ::handleNavigation)
|
launch { navModel.exploreNavigationItem.collect(::handleNavigation) }
|
||||||
playbackModel.song.observe(viewLifecycleOwner, ::updateSong)
|
launch { playbackModel.song.collect(::updateSong) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMenuItemClick(item: MenuItem): Boolean = false
|
override fun onMenuItemClick(item: MenuItem): Boolean = false
|
||||||
|
|
|
@ -35,6 +35,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.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentHomeBinding
|
import org.oxycblt.auxio.databinding.FragmentHomeBinding
|
||||||
import org.oxycblt.auxio.home.list.AlbumListFragment
|
import org.oxycblt.auxio.home.list.AlbumListFragment
|
||||||
|
@ -53,6 +54,7 @@ import org.oxycblt.auxio.ui.DisplayMode
|
||||||
import org.oxycblt.auxio.ui.MainNavigationAction
|
import org.oxycblt.auxio.ui.MainNavigationAction
|
||||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||||
import org.oxycblt.auxio.ui.ViewBindingFragment
|
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||||
|
import org.oxycblt.auxio.util.launch
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.logE
|
import org.oxycblt.auxio.util.logE
|
||||||
import org.oxycblt.auxio.util.logTraceOrThrow
|
import org.oxycblt.auxio.util.logTraceOrThrow
|
||||||
|
@ -71,13 +73,14 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
||||||
private val homeModel: HomeViewModel by activityViewModels()
|
private val homeModel: HomeViewModel by activityViewModels()
|
||||||
private val musicModel: MusicViewModel by activityViewModels()
|
private val musicModel: MusicViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private var storagePermissionLauncher: ActivityResultLauncher<String>? = null
|
||||||
|
private var sortItem: MenuItem? = null
|
||||||
|
|
||||||
override fun onCreateBinding(inflater: LayoutInflater) = FragmentHomeBinding.inflate(inflater)
|
override fun onCreateBinding(inflater: LayoutInflater) = FragmentHomeBinding.inflate(inflater)
|
||||||
|
|
||||||
override fun onBindingCreated(binding: FragmentHomeBinding, savedInstanceState: Bundle?) {
|
override fun onBindingCreated(binding: FragmentHomeBinding, savedInstanceState: Bundle?) {
|
||||||
val sortItem: MenuItem
|
|
||||||
|
|
||||||
// Build the permission launcher here as you can only do it in onCreateView/onCreate
|
// Build the permission launcher here as you can only do it in onCreateView/onCreate
|
||||||
val permLauncher =
|
storagePermissionLauncher =
|
||||||
registerForActivityResult(ActivityResultContracts.RequestPermission()) {
|
registerForActivityResult(ActivityResultContracts.RequestPermission()) {
|
||||||
musicModel.reloadMusic(requireContext())
|
musicModel.reloadMusic(requireContext())
|
||||||
}
|
}
|
||||||
|
@ -89,8 +92,8 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
||||||
|
|
||||||
updateTabConfiguration()
|
updateTabConfiguration()
|
||||||
|
|
||||||
binding.homeLoadingContainer.setOnApplyWindowInsetsListener { v, insets ->
|
binding.homeLoadingContainer.setOnApplyWindowInsetsListener { view, insets ->
|
||||||
v.updatePadding(bottom = insets.systemBarInsetsCompat.bottom)
|
view.updatePadding(bottom = insets.systemBarInsetsCompat.bottom)
|
||||||
insets
|
insets
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,20 +121,18 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ---
|
// --- VIEWMODEL SETUP ---
|
||||||
|
|
||||||
homeModel.isFastScrolling.observe(viewLifecycleOwner, ::updateFastScrolling)
|
launch { homeModel.isFastScrolling.collect(::updateFastScrolling) }
|
||||||
homeModel.currentTab.observe(viewLifecycleOwner) { tab -> updateCurrentTab(sortItem, tab) }
|
launch { homeModel.currentTab.collect(::updateCurrentTab) }
|
||||||
homeModel.recreateTabs.observe(viewLifecycleOwner, ::handleRecreateTabs)
|
launch { homeModel.recreateTabs.collect(::handleRecreateTabs) }
|
||||||
|
launch { musicModel.response.collect(::handleLoaderResponse) }
|
||||||
musicModel.loaderResponse.observe(viewLifecycleOwner) { response ->
|
launch { navModel.exploreNavigationItem.collect(::handleNavigation) }
|
||||||
handleLoaderResponse(response, permLauncher)
|
|
||||||
}
|
|
||||||
|
|
||||||
navModel.exploreNavigationItem.observe(viewLifecycleOwner, ::handleNavigation)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyBinding(binding: FragmentHomeBinding) {
|
override fun onDestroyBinding(binding: FragmentHomeBinding) {
|
||||||
super.onDestroyBinding(binding)
|
super.onDestroyBinding(binding)
|
||||||
binding.homeToolbar.setOnMenuItemClickListener(null)
|
binding.homeToolbar.setOnMenuItemClickListener(null)
|
||||||
|
storagePermissionLauncher = null
|
||||||
|
sortItem = null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||||
|
@ -178,7 +179,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
||||||
|
|
||||||
// Make sure an update here doesn't mess up the FAB state when it comes to the
|
// Make sure an update here doesn't mess up the FAB state when it comes to the
|
||||||
// loader response.
|
// loader response.
|
||||||
if (musicModel.loaderResponse.value !is MusicStore.Response.Ok) {
|
if (musicModel.response.value !is MusicStore.Response.Ok) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,21 +190,21 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateCurrentTab(sortItem: MenuItem, tab: DisplayMode) {
|
private fun updateCurrentTab(tab: DisplayMode) {
|
||||||
// Make sure that we update the scrolling view and allowed menu items whenever
|
// Make sure that we update the scrolling view and allowed menu items whenever
|
||||||
// the tab changes.
|
// the tab changes.
|
||||||
val binding = requireBinding()
|
val binding = requireBinding()
|
||||||
when (tab) {
|
when (tab) {
|
||||||
DisplayMode.SHOW_SONGS -> {
|
DisplayMode.SHOW_SONGS -> {
|
||||||
updateSortMenu(sortItem, tab) { id -> id != R.id.option_sort_count }
|
updateSortMenu(tab) { id -> id != R.id.option_sort_count }
|
||||||
binding.homeAppbar.liftOnScrollTargetViewId = R.id.home_song_list
|
binding.homeAppbar.liftOnScrollTargetViewId = R.id.home_song_list
|
||||||
}
|
}
|
||||||
DisplayMode.SHOW_ALBUMS -> {
|
DisplayMode.SHOW_ALBUMS -> {
|
||||||
updateSortMenu(sortItem, tab) { id -> id != R.id.option_sort_album }
|
updateSortMenu(tab) { id -> id != R.id.option_sort_album }
|
||||||
binding.homeAppbar.liftOnScrollTargetViewId = R.id.home_album_list
|
binding.homeAppbar.liftOnScrollTargetViewId = R.id.home_album_list
|
||||||
}
|
}
|
||||||
DisplayMode.SHOW_ARTISTS -> {
|
DisplayMode.SHOW_ARTISTS -> {
|
||||||
updateSortMenu(sortItem, tab) { id ->
|
updateSortMenu(tab) { id ->
|
||||||
id == R.id.option_sort_asc ||
|
id == R.id.option_sort_asc ||
|
||||||
id == R.id.option_sort_name ||
|
id == R.id.option_sort_name ||
|
||||||
id == R.id.option_sort_count ||
|
id == R.id.option_sort_count ||
|
||||||
|
@ -212,7 +213,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
||||||
binding.homeAppbar.liftOnScrollTargetViewId = R.id.home_artist_list
|
binding.homeAppbar.liftOnScrollTargetViewId = R.id.home_artist_list
|
||||||
}
|
}
|
||||||
DisplayMode.SHOW_GENRES -> {
|
DisplayMode.SHOW_GENRES -> {
|
||||||
updateSortMenu(sortItem, tab) { id ->
|
updateSortMenu(tab) { id ->
|
||||||
id == R.id.option_sort_asc ||
|
id == R.id.option_sort_asc ||
|
||||||
id == R.id.option_sort_name ||
|
id == R.id.option_sort_name ||
|
||||||
id == R.id.option_sort_count ||
|
id == R.id.option_sort_count ||
|
||||||
|
@ -223,14 +224,13 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSortMenu(
|
private fun updateSortMenu(displayMode: DisplayMode, isVisible: (Int) -> Boolean) {
|
||||||
item: MenuItem,
|
val sortItem =
|
||||||
displayMode: DisplayMode,
|
requireNotNull(sortItem) { "Cannot update sort menu when view does not exist" }
|
||||||
isVisible: (Int) -> Boolean
|
|
||||||
) {
|
|
||||||
val toHighlight = homeModel.getSortForDisplay(displayMode)
|
val toHighlight = homeModel.getSortForDisplay(displayMode)
|
||||||
|
|
||||||
for (option in item.subMenu) {
|
for (option in sortItem.subMenu) {
|
||||||
if (option.itemId == toHighlight.itemId) {
|
if (option.itemId == toHighlight.itemId) {
|
||||||
option.isChecked = true
|
option.isChecked = true
|
||||||
}
|
}
|
||||||
|
@ -257,10 +257,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleLoaderResponse(
|
private fun handleLoaderResponse(response: MusicStore.Response?) {
|
||||||
response: MusicStore.Response?,
|
|
||||||
permLauncher: ActivityResultLauncher<String>
|
|
||||||
) {
|
|
||||||
val binding = requireBinding()
|
val binding = requireBinding()
|
||||||
|
|
||||||
if (response is MusicStore.Response.Ok) {
|
if (response is MusicStore.Response.Ok) {
|
||||||
|
@ -296,13 +293,18 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is MusicStore.Response.NoPerms -> {
|
is MusicStore.Response.NoPerms -> {
|
||||||
|
val launcher =
|
||||||
|
requireNotNull(storagePermissionLauncher) {
|
||||||
|
"Cannot access permission launcher while in non-view state"
|
||||||
|
}
|
||||||
|
|
||||||
binding.homeLoadingProgress.visibility = View.INVISIBLE
|
binding.homeLoadingProgress.visibility = View.INVISIBLE
|
||||||
binding.homeLoadingStatus.textSafe = getString(R.string.err_no_perms)
|
binding.homeLoadingStatus.textSafe = getString(R.string.err_no_perms)
|
||||||
binding.homeLoadingAction.apply {
|
binding.homeLoadingAction.apply {
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
text = getString(R.string.lbl_grant)
|
text = getString(R.string.lbl_grant)
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
permLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
|
launcher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,9 +17,9 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.home
|
package org.oxycblt.auxio.home
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import org.oxycblt.auxio.home.tabs.Tab
|
import org.oxycblt.auxio.home.tabs.Tab
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
|
@ -40,20 +40,20 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.Callback
|
||||||
private val musicStore = MusicStore.getInstance()
|
private val musicStore = MusicStore.getInstance()
|
||||||
private val settingsManager = SettingsManager.getInstance()
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
|
|
||||||
private val _songs = MutableLiveData(listOf<Song>())
|
private val _songs = MutableStateFlow(listOf<Song>())
|
||||||
val songs: LiveData<List<Song>>
|
val songs: StateFlow<List<Song>>
|
||||||
get() = _songs
|
get() = _songs
|
||||||
|
|
||||||
private val _albums = MutableLiveData(listOf<Album>())
|
private val _albums = MutableStateFlow(listOf<Album>())
|
||||||
val albums: LiveData<List<Album>>
|
val albums: StateFlow<List<Album>>
|
||||||
get() = _albums
|
get() = _albums
|
||||||
|
|
||||||
private val _artists = MutableLiveData(listOf<Artist>())
|
private val _artists = MutableStateFlow(listOf<Artist>())
|
||||||
val artists: LiveData<List<Artist>>
|
val artists: MutableStateFlow<List<Artist>>
|
||||||
get() = _artists
|
get() = _artists
|
||||||
|
|
||||||
private val _genres = MutableLiveData(listOf<Genre>())
|
private val _genres = MutableStateFlow(listOf<Genre>())
|
||||||
val genres: LiveData<List<Genre>>
|
val genres: StateFlow<List<Genre>>
|
||||||
get() = _genres
|
get() = _genres
|
||||||
|
|
||||||
var tabs: List<DisplayMode> = visibleTabs
|
var tabs: List<DisplayMode> = visibleTabs
|
||||||
|
@ -63,18 +63,18 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.Callback
|
||||||
private val visibleTabs: List<DisplayMode>
|
private val visibleTabs: List<DisplayMode>
|
||||||
get() = settingsManager.libTabs.filterIsInstance<Tab.Visible>().map { it.mode }
|
get() = settingsManager.libTabs.filterIsInstance<Tab.Visible>().map { it.mode }
|
||||||
|
|
||||||
private val _currentTab = MutableLiveData(tabs[0])
|
private val _currentTab = MutableStateFlow(tabs[0])
|
||||||
val currentTab: LiveData<DisplayMode> = _currentTab
|
val currentTab: StateFlow<DisplayMode> = _currentTab
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marker to recreate all library tabs, usually initiated by a settings change. When this flag
|
* Marker to recreate all library tabs, usually initiated by a settings change. When this flag
|
||||||
* is set, all tabs (and their respective viewpager fragments) will be recreated from scratch.
|
* is set, all tabs (and their respective viewpager fragments) will be recreated from scratch.
|
||||||
*/
|
*/
|
||||||
private val _shouldRecreateTabs = MutableLiveData(false)
|
private val _shouldRecreateTabs = MutableStateFlow(false)
|
||||||
val recreateTabs: LiveData<Boolean> = _shouldRecreateTabs
|
val recreateTabs: StateFlow<Boolean> = _shouldRecreateTabs
|
||||||
|
|
||||||
private val _isFastScrolling = MutableLiveData(false)
|
private val _isFastScrolling = MutableStateFlow(false)
|
||||||
val isFastScrolling: LiveData<Boolean> = _isFastScrolling
|
val isFastScrolling: StateFlow<Boolean> = _isFastScrolling
|
||||||
|
|
||||||
init {
|
init {
|
||||||
musicStore.addCallback(this)
|
musicStore.addCallback(this)
|
||||||
|
@ -121,7 +121,6 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.Callback
|
||||||
settingsManager.libGenreSort = sort
|
settingsManager.libGenreSort = sort
|
||||||
_genres.value = sort.genres(unlikelyToBeNull(_genres.value))
|
_genres.value = sort.genres(unlikelyToBeNull(_genres.value))
|
||||||
}
|
}
|
||||||
else -> {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,9 +17,11 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.home.list
|
package org.oxycblt.auxio.home.list
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import kotlinx.coroutines.launch
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
|
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.ui.AlbumViewHolder
|
import org.oxycblt.auxio.ui.AlbumViewHolder
|
||||||
|
@ -31,6 +33,7 @@ import org.oxycblt.auxio.ui.PrimitiveBackingData
|
||||||
import org.oxycblt.auxio.ui.Sort
|
import org.oxycblt.auxio.ui.Sort
|
||||||
import org.oxycblt.auxio.ui.newMenu
|
import org.oxycblt.auxio.ui.newMenu
|
||||||
import org.oxycblt.auxio.util.formatDuration
|
import org.oxycblt.auxio.util.formatDuration
|
||||||
|
import org.oxycblt.auxio.util.launch
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -40,13 +43,15 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
class AlbumListFragment : HomeListFragment<Album>() {
|
class AlbumListFragment : HomeListFragment<Album>() {
|
||||||
private val homeAdapter = AlbumAdapter(this)
|
private val homeAdapter = AlbumAdapter(this)
|
||||||
|
|
||||||
override fun setupRecycler(recycler: RecyclerView) {
|
override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
|
||||||
recycler.apply {
|
super.onBindingCreated(binding, savedInstanceState)
|
||||||
|
|
||||||
|
binding.homeRecycler.apply {
|
||||||
id = R.id.home_album_list
|
id = R.id.home_album_list
|
||||||
adapter = homeAdapter
|
adapter = homeAdapter
|
||||||
}
|
}
|
||||||
|
|
||||||
homeModel.albums.observe(viewLifecycleOwner) { list -> homeAdapter.data.submitList(list) }
|
launch { homeModel.albums.collect(homeAdapter.data::submitList) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPopup(pos: Int): String? {
|
override fun getPopup(pos: Int): String? {
|
||||||
|
|
|
@ -17,9 +17,11 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.home.list
|
package org.oxycblt.auxio.home.list
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import kotlinx.coroutines.launch
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
|
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.ui.ArtistViewHolder
|
import org.oxycblt.auxio.ui.ArtistViewHolder
|
||||||
|
@ -31,6 +33,7 @@ import org.oxycblt.auxio.ui.PrimitiveBackingData
|
||||||
import org.oxycblt.auxio.ui.Sort
|
import org.oxycblt.auxio.ui.Sort
|
||||||
import org.oxycblt.auxio.ui.newMenu
|
import org.oxycblt.auxio.ui.newMenu
|
||||||
import org.oxycblt.auxio.util.formatDuration
|
import org.oxycblt.auxio.util.formatDuration
|
||||||
|
import org.oxycblt.auxio.util.launch
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -40,13 +43,15 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
class ArtistListFragment : HomeListFragment<Artist>() {
|
class ArtistListFragment : HomeListFragment<Artist>() {
|
||||||
private val homeAdapter = ArtistAdapter(this)
|
private val homeAdapter = ArtistAdapter(this)
|
||||||
|
|
||||||
override fun setupRecycler(recycler: RecyclerView) {
|
override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
|
||||||
recycler.apply {
|
super.onBindingCreated(binding, savedInstanceState)
|
||||||
|
|
||||||
|
binding.homeRecycler.apply {
|
||||||
id = R.id.home_artist_list
|
id = R.id.home_artist_list
|
||||||
adapter = homeAdapter
|
adapter = homeAdapter
|
||||||
}
|
}
|
||||||
|
|
||||||
homeModel.artists.observe(viewLifecycleOwner) { list -> homeAdapter.data.submitList(list) }
|
launch { homeModel.artists.collect(homeAdapter.data::submitList) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPopup(pos: Int): String? {
|
override fun getPopup(pos: Int): String? {
|
||||||
|
|
|
@ -17,9 +17,11 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.home.list
|
package org.oxycblt.auxio.home.list
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import kotlinx.coroutines.launch
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
|
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.ui.DisplayMode
|
import org.oxycblt.auxio.ui.DisplayMode
|
||||||
|
@ -31,6 +33,7 @@ import org.oxycblt.auxio.ui.PrimitiveBackingData
|
||||||
import org.oxycblt.auxio.ui.Sort
|
import org.oxycblt.auxio.ui.Sort
|
||||||
import org.oxycblt.auxio.ui.newMenu
|
import org.oxycblt.auxio.ui.newMenu
|
||||||
import org.oxycblt.auxio.util.formatDuration
|
import org.oxycblt.auxio.util.formatDuration
|
||||||
|
import org.oxycblt.auxio.util.launch
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -40,13 +43,15 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
class GenreListFragment : HomeListFragment<Genre>() {
|
class GenreListFragment : HomeListFragment<Genre>() {
|
||||||
private val homeAdapter = GenreAdapter(this)
|
private val homeAdapter = GenreAdapter(this)
|
||||||
|
|
||||||
override fun setupRecycler(recycler: RecyclerView) {
|
override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
|
||||||
recycler.apply {
|
super.onBindingCreated(binding, savedInstanceState)
|
||||||
|
|
||||||
|
binding.homeRecycler.apply {
|
||||||
id = R.id.home_genre_list
|
id = R.id.home_genre_list
|
||||||
adapter = homeAdapter
|
adapter = homeAdapter
|
||||||
}
|
}
|
||||||
|
|
||||||
homeModel.genres.observe(viewLifecycleOwner) { list -> homeAdapter.data.submitList(list) }
|
launch { homeModel.genres.collect(homeAdapter.data::submitList) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPopup(pos: Int): String? {
|
override fun getPopup(pos: Int): String? {
|
||||||
|
|
|
@ -21,7 +21,6 @@ import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||||
import org.oxycblt.auxio.home.HomeViewModel
|
import org.oxycblt.auxio.home.HomeViewModel
|
||||||
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
|
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
|
||||||
|
@ -40,8 +39,6 @@ abstract class HomeListFragment<T : Item> :
|
||||||
MenuItemListener,
|
MenuItemListener,
|
||||||
FastScrollRecyclerView.PopupProvider,
|
FastScrollRecyclerView.PopupProvider,
|
||||||
FastScrollRecyclerView.OnFastScrollListener {
|
FastScrollRecyclerView.OnFastScrollListener {
|
||||||
abstract fun setupRecycler(recycler: RecyclerView)
|
|
||||||
|
|
||||||
protected val playbackModel: PlaybackViewModel by activityViewModels()
|
protected val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
protected val navModel: NavigationViewModel by activityViewModels()
|
protected val navModel: NavigationViewModel by activityViewModels()
|
||||||
protected val homeModel: HomeViewModel by activityViewModels()
|
protected val homeModel: HomeViewModel by activityViewModels()
|
||||||
|
@ -50,7 +47,6 @@ abstract class HomeListFragment<T : Item> :
|
||||||
FragmentHomeListBinding.inflate(inflater)
|
FragmentHomeListBinding.inflate(inflater)
|
||||||
|
|
||||||
override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
|
override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
|
||||||
setupRecycler(binding.homeRecycler)
|
|
||||||
binding.homeRecycler.popupProvider = this
|
binding.homeRecycler.popupProvider = this
|
||||||
binding.homeRecycler.listener = this
|
binding.homeRecycler.listener = this
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,9 +17,11 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.home.list
|
package org.oxycblt.auxio.home.list
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import kotlinx.coroutines.launch
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
|
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.ui.DisplayMode
|
import org.oxycblt.auxio.ui.DisplayMode
|
||||||
import org.oxycblt.auxio.ui.Item
|
import org.oxycblt.auxio.ui.Item
|
||||||
|
@ -30,6 +32,7 @@ import org.oxycblt.auxio.ui.SongViewHolder
|
||||||
import org.oxycblt.auxio.ui.Sort
|
import org.oxycblt.auxio.ui.Sort
|
||||||
import org.oxycblt.auxio.ui.newMenu
|
import org.oxycblt.auxio.ui.newMenu
|
||||||
import org.oxycblt.auxio.util.formatDuration
|
import org.oxycblt.auxio.util.formatDuration
|
||||||
|
import org.oxycblt.auxio.util.launch
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -39,20 +42,22 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
class SongListFragment : HomeListFragment<Song>() {
|
class SongListFragment : HomeListFragment<Song>() {
|
||||||
private val homeAdapter = SongsAdapter(this)
|
private val homeAdapter = SongsAdapter(this)
|
||||||
|
|
||||||
override fun setupRecycler(recycler: RecyclerView) {
|
override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
|
||||||
recycler.apply {
|
super.onBindingCreated(binding, savedInstanceState)
|
||||||
|
|
||||||
|
binding.homeRecycler.apply {
|
||||||
id = R.id.home_song_list
|
id = R.id.home_song_list
|
||||||
adapter = homeAdapter
|
adapter = homeAdapter
|
||||||
}
|
}
|
||||||
|
|
||||||
homeModel.songs.observe(viewLifecycleOwner) { list -> homeAdapter.data.submitList(list) }
|
launch { homeModel.songs.collect(homeAdapter.data::submitList) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPopup(pos: Int): String? {
|
override fun getPopup(pos: Int): String? {
|
||||||
val song = unlikelyToBeNull(homeModel.songs.value)[pos]
|
val song = unlikelyToBeNull(homeModel.songs.value)[pos]
|
||||||
|
|
||||||
// Change how we display the popup depending on the mode.
|
// Change how we display the popup depending on the mode.
|
||||||
// We don't use the more correct resolve(Model)Name here, as sorts are largely
|
// Note: We don't use the more correct individual artist name here, as sorts are largely
|
||||||
// based off the names of the parent objects and not the child objects.
|
// based off the names of the parent objects and not the child objects.
|
||||||
return when (homeModel.getSortForDisplay(DisplayMode.SHOW_SONGS)) {
|
return when (homeModel.getSortForDisplay(DisplayMode.SHOW_SONGS)) {
|
||||||
// Name -> Use name
|
// Name -> Use name
|
||||||
|
|
|
@ -137,6 +137,10 @@ data class Song(
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Internal field. Do not use. */
|
||||||
|
val _genreGroupingId: Long
|
||||||
|
get() = (_genreName ?: MediaStore.UNKNOWN_STRING).hashCode().toLong()
|
||||||
|
|
||||||
/** Internal field. Do not use. */
|
/** Internal field. Do not use. */
|
||||||
val _artistGroupingName: String?
|
val _artistGroupingName: String?
|
||||||
get() = _albumArtistName ?: _artistName
|
get() = _albumArtistName ?: _artistName
|
||||||
|
|
|
@ -74,7 +74,7 @@ class MusicStore private constructor() {
|
||||||
return newResponse
|
return newResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun loadImpl(context: Context): Response {
|
private fun loadImpl(context: Context): Response {
|
||||||
val notGranted =
|
val notGranted =
|
||||||
ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) ==
|
ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) ==
|
||||||
PackageManager.PERMISSION_DENIED
|
PackageManager.PERMISSION_DENIED
|
||||||
|
@ -110,19 +110,18 @@ class MusicStore private constructor() {
|
||||||
val albums: List<Album>,
|
val albums: List<Album>,
|
||||||
val songs: List<Song>
|
val songs: List<Song>
|
||||||
) {
|
) {
|
||||||
/** Find a song in a faster manner using an ID for its album as well. */
|
/** Find a song in a faster manner by using the album ID as well.. */
|
||||||
fun findSongFast(songId: Long, albumId: Long): Song? {
|
fun findSongFast(songId: Long, albumId: Long) =
|
||||||
return albums.find { it.id == albumId }?.songs?.find { it.id == songId }
|
albums.find { it.id == albumId }.run { songs.find { it.id == songId } }
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find a song for a [uri], this is similar to [findSongFast], but with some kind of content
|
* Find a song for a [uri], this is similar to [findSongFast], but with some kind of content
|
||||||
* uri.
|
* uri.
|
||||||
* @return The corresponding [Song] for this [uri], null if there isn't one.
|
* @return The corresponding [Song] for this [uri], null if there isn't one.
|
||||||
*/
|
*/
|
||||||
fun findSongForUri(context: Context, uri: Uri): Song? {
|
fun findSongForUri(context: Context, uri: Uri) =
|
||||||
return context.contentResolverSafe.useQuery(
|
context.contentResolverSafe.useQuery(uri, arrayOf(OpenableColumns.DISPLAY_NAME)) {
|
||||||
uri, arrayOf(OpenableColumns.DISPLAY_NAME)) { cursor ->
|
cursor ->
|
||||||
cursor.moveToFirst()
|
cursor.moveToFirst()
|
||||||
|
|
||||||
val displayName =
|
val displayName =
|
||||||
|
@ -130,16 +129,11 @@ class MusicStore private constructor() {
|
||||||
|
|
||||||
songs.find { it.fileName == displayName }
|
songs.find { it.fileName == displayName }
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A response that [MusicStore] returns when loading music. And before you ask, yes, I do like
|
|
||||||
* rust.
|
|
||||||
*/
|
|
||||||
sealed class Response {
|
sealed class Response {
|
||||||
class Ok(val library: Library) : Response()
|
data class Ok(val library: Library) : Response()
|
||||||
class Err(throwable: Throwable) : Response()
|
data class Err(val throwable: Throwable) : Response()
|
||||||
object NoMusic : Response()
|
object NoMusic : Response()
|
||||||
object NoPerms : Response()
|
object NoPerms : Response()
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,42 +18,38 @@
|
||||||
package org.oxycblt.auxio.music
|
package org.oxycblt.auxio.music
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
/** A [ViewModel] that represents the current music indexing state. */
|
/** A [ViewModel] that represents the current music indexing state. */
|
||||||
class MusicViewModel : ViewModel(), MusicStore.Callback {
|
class MusicViewModel : ViewModel() {
|
||||||
private val musicStore = MusicStore.getInstance()
|
private val musicStore = MusicStore.getInstance()
|
||||||
|
|
||||||
private val _loaderResponse = MutableLiveData<MusicStore.Response?>(null)
|
private val _response = MutableStateFlow<MusicStore.Response?>(null)
|
||||||
val loaderResponse: LiveData<MusicStore.Response?> = _loaderResponse
|
val response: StateFlow<MusicStore.Response?> = _response
|
||||||
|
|
||||||
private var isBusy = false
|
private var isBusy = false
|
||||||
|
|
||||||
init {
|
|
||||||
musicStore.addCallback(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initiate the loading process. This is done here since HomeFragment will be the first fragment
|
* 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.
|
* navigated to and because SnackBars will have the best UX here.
|
||||||
*/
|
*/
|
||||||
fun loadMusic(context: Context) {
|
fun loadMusic(context: Context) {
|
||||||
if (_loaderResponse.value != null || isBusy) {
|
if (_response.value != null || isBusy) {
|
||||||
logD("Loader is busy/already completed, not reloading")
|
logD("Loader is busy/already completed, not reloading")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
isBusy = true
|
isBusy = true
|
||||||
_loaderResponse.value = null
|
_response.value = null
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val result = musicStore.load(context)
|
val result = musicStore.load(context)
|
||||||
_loaderResponse.value = result
|
_response.value = result
|
||||||
isBusy = false
|
isBusy = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -64,16 +60,7 @@ class MusicViewModel : ViewModel(), MusicStore.Callback {
|
||||||
*/
|
*/
|
||||||
fun reloadMusic(context: Context) {
|
fun reloadMusic(context: Context) {
|
||||||
logD("Reloading music library")
|
logD("Reloading music library")
|
||||||
_loaderResponse.value = null
|
_response.value = null
|
||||||
loadMusic(context)
|
loadMusic(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMusicUpdate(response: MusicStore.Response) {
|
|
||||||
_loaderResponse.value = response
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCleared() {
|
|
||||||
super.onCleared()
|
|
||||||
musicStore.removeCallback(this)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ import org.oxycblt.auxio.databinding.DialogExcludedBinding
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
|
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
|
||||||
import org.oxycblt.auxio.util.hardRestart
|
import org.oxycblt.auxio.util.hardRestart
|
||||||
|
import org.oxycblt.auxio.util.launch
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.logW
|
import org.oxycblt.auxio.util.logW
|
||||||
import org.oxycblt.auxio.util.showToast
|
import org.oxycblt.auxio.util.showToast
|
||||||
|
@ -92,7 +93,7 @@ class ExcludedDialog :
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ---
|
// --- VIEWMODEL SETUP ---
|
||||||
|
|
||||||
excludedModel.paths.observe(viewLifecycleOwner, ::updatePaths)
|
launch { excludedModel.paths.collect(::updatePaths) }
|
||||||
|
|
||||||
logD("Dialog created")
|
logD("Dialog created")
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,12 +18,12 @@
|
||||||
package org.oxycblt.auxio.music.excluded
|
package org.oxycblt.auxio.music.excluded
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
@ -37,8 +37,8 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
* TODO: Unify with MusicViewModel
|
* TODO: Unify with MusicViewModel
|
||||||
*/
|
*/
|
||||||
class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewModel() {
|
class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewModel() {
|
||||||
private val _paths = MutableLiveData(mutableListOf<String>())
|
private val _paths = MutableStateFlow(mutableListOf<String>())
|
||||||
val paths: LiveData<MutableList<String>>
|
val paths: StateFlow<MutableList<String>>
|
||||||
get() = _paths
|
get() = _paths
|
||||||
|
|
||||||
var isModified: Boolean = false
|
var isModified: Boolean = false
|
||||||
|
|
|
@ -91,6 +91,8 @@ class ExoPlayerBackend(private val inner: MediaStoreBackend) : Indexer.Backend {
|
||||||
// Spin until all tasks are complete
|
// Spin until all tasks are complete
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Stabilize sorting order
|
||||||
|
|
||||||
return songs
|
return songs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,9 +126,8 @@ class ExoPlayerBackend(private val inner: MediaStoreBackend) : Indexer.Backend {
|
||||||
// We only support two formats as it stands:
|
// We only support two formats as it stands:
|
||||||
// - ID3v2 text frames
|
// - ID3v2 text frames
|
||||||
// - Vorbis comments
|
// - Vorbis comments
|
||||||
// This should be enough to cover the vast, vast majority of audio formats.
|
// TODO: Formats like flac can have both ID3v2 and OGG tags, so we might want to split
|
||||||
// It is also assumed that a file only has either ID3v2 text frames or vorbis
|
// up this logic.
|
||||||
// comments.
|
|
||||||
when (val tag = metadata.get(i)) {
|
when (val tag = metadata.get(i)) {
|
||||||
is TextInformationFrame ->
|
is TextInformationFrame ->
|
||||||
if (tag.value.isNotEmpty()) {
|
if (tag.value.isNotEmpty()) {
|
||||||
|
|
|
@ -191,12 +191,16 @@ object Indexer {
|
||||||
return artists
|
return artists
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Build genres and link them to their particular songs. */
|
/**
|
||||||
|
* Group up songs into genres. This is a relatively simple step compared to the other library
|
||||||
|
* steps, as there is no demand to deduplicate genres by a lowercase name.
|
||||||
|
*/
|
||||||
private fun buildGenres(songs: List<Song>): List<Genre> {
|
private fun buildGenres(songs: List<Song>): List<Genre> {
|
||||||
val genres = mutableListOf<Genre>()
|
val genres = mutableListOf<Genre>()
|
||||||
val songsByGenre = songs.groupBy { it._genreName?.hashCode() }
|
val songsByGenre = songs.groupBy { it._genreGroupingId }
|
||||||
|
|
||||||
for (entry in songsByGenre) {
|
for (entry in songsByGenre) {
|
||||||
|
// The first song fill suffice for template metadata.
|
||||||
val templateSong = entry.value[0]
|
val templateSong = entry.value[0]
|
||||||
genres.add(Genre(rawName = templateSong._genreName, songs = entry.value))
|
genres.add(Genre(rawName = templateSong._genreName, songs = entry.value))
|
||||||
}
|
}
|
||||||
|
@ -214,10 +218,4 @@ object Indexer {
|
||||||
/** Create a list of songs from the [Cursor] queried in [query]. */
|
/** Create a list of songs from the [Cursor] queried in [query]. */
|
||||||
fun loadSongs(context: Context, cursor: Cursor): Collection<Song>
|
fun loadSongs(context: Context, cursor: Cursor): Collection<Song>
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class Event {
|
|
||||||
object Query : Event()
|
|
||||||
object LoadSongs : Event()
|
|
||||||
object BuildLibrary : Event()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@ import org.oxycblt.auxio.ui.MainNavigationAction
|
||||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||||
import org.oxycblt.auxio.ui.ViewBindingFragment
|
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||||
import org.oxycblt.auxio.util.getColorStateListSafe
|
import org.oxycblt.auxio.util.getColorStateListSafe
|
||||||
|
import org.oxycblt.auxio.util.launch
|
||||||
import org.oxycblt.auxio.util.systemGestureInsetsCompat
|
import org.oxycblt.auxio.util.systemGestureInsetsCompat
|
||||||
import org.oxycblt.auxio.util.textSafe
|
import org.oxycblt.auxio.util.textSafe
|
||||||
|
|
||||||
|
@ -78,10 +79,9 @@ class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() {
|
||||||
|
|
||||||
// -- VIEWMODEL SETUP ---
|
// -- VIEWMODEL SETUP ---
|
||||||
|
|
||||||
playbackModel.song.observe(viewLifecycleOwner, ::updateSong)
|
launch { playbackModel.song.collect(::updateSong) }
|
||||||
playbackModel.isPlaying.observe(viewLifecycleOwner, ::updateIsPlaying)
|
launch { playbackModel.isPlaying.collect(::updateIsPlaying) }
|
||||||
|
launch { playbackModel.positionSecs.collect(::updatePosition) }
|
||||||
playbackModel.positionSecs.observe(viewLifecycleOwner, ::updatePosition)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSong(song: Song?) {
|
private fun updateSong(song: Song?) {
|
||||||
|
|
|
@ -32,6 +32,7 @@ import org.oxycblt.auxio.playback.state.RepeatMode
|
||||||
import org.oxycblt.auxio.ui.MainNavigationAction
|
import org.oxycblt.auxio.ui.MainNavigationAction
|
||||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||||
import org.oxycblt.auxio.ui.ViewBindingFragment
|
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||||
|
import org.oxycblt.auxio.util.launch
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
import org.oxycblt.auxio.util.textSafe
|
import org.oxycblt.auxio.util.textSafe
|
||||||
|
@ -52,6 +53,8 @@ class PlaybackPanelFragment :
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
private val navModel: NavigationViewModel by activityViewModels()
|
private val navModel: NavigationViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private var queueItem: MenuItem? = null
|
||||||
|
|
||||||
override fun onCreateBinding(inflater: LayoutInflater) =
|
override fun onCreateBinding(inflater: LayoutInflater) =
|
||||||
FragmentPlaybackPanelBinding.inflate(inflater)
|
FragmentPlaybackPanelBinding.inflate(inflater)
|
||||||
|
|
||||||
|
@ -67,8 +70,6 @@ class PlaybackPanelFragment :
|
||||||
insets
|
insets
|
||||||
}
|
}
|
||||||
|
|
||||||
val queueItem: MenuItem
|
|
||||||
|
|
||||||
binding.playbackToolbar.apply {
|
binding.playbackToolbar.apply {
|
||||||
setNavigationOnClickListener { navModel.mainNavigateTo(MainNavigationAction.COLLAPSE) }
|
setNavigationOnClickListener { navModel.mainNavigateTo(MainNavigationAction.COLLAPSE) }
|
||||||
setOnMenuItemClickListener(this@PlaybackPanelFragment)
|
setOnMenuItemClickListener(this@PlaybackPanelFragment)
|
||||||
|
@ -105,18 +106,13 @@ class PlaybackPanelFragment :
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP --
|
// --- VIEWMODEL SETUP --
|
||||||
|
|
||||||
playbackModel.song.observe(viewLifecycleOwner, ::updateSong)
|
launch { playbackModel.song.collect(::updateSong) }
|
||||||
playbackModel.parent.observe(viewLifecycleOwner, ::updateParent)
|
launch { playbackModel.parent.collect(::updateParent) }
|
||||||
playbackModel.positionSecs.observe(viewLifecycleOwner, ::updatePosition)
|
launch { playbackModel.positionSecs.collect(::updatePosition) }
|
||||||
playbackModel.repeatMode.observe(viewLifecycleOwner, ::updateRepeat)
|
launch { playbackModel.repeatMode.collect(::updateRepeat) }
|
||||||
playbackModel.isPlaying.observe(viewLifecycleOwner, ::updatePlaying)
|
launch { playbackModel.isPlaying.collect(::updatePlaying) }
|
||||||
playbackModel.isShuffled.observe(viewLifecycleOwner, ::updateShuffled)
|
launch { playbackModel.isShuffled.collect(::updateShuffled) }
|
||||||
|
launch { playbackModel.nextUp.collect(::updateNextUp) }
|
||||||
playbackModel.nextUp.observe(viewLifecycleOwner) { nextUp ->
|
|
||||||
// The queue icon uses a selector that will automatically tint the icon as active or
|
|
||||||
// inactive. We just need to set the flag.
|
|
||||||
queueItem.isEnabled = nextUp.isNotEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
logD("Fragment Created")
|
logD("Fragment Created")
|
||||||
}
|
}
|
||||||
|
@ -176,4 +172,9 @@ class PlaybackPanelFragment :
|
||||||
private fun updateShuffled(isShuffled: Boolean) {
|
private fun updateShuffled(isShuffled: Boolean) {
|
||||||
requireBinding().playbackShuffle.isActivated = isShuffled
|
requireBinding().playbackShuffle.isActivated = isShuffled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateNextUp(nextUp: List<Song>) {
|
||||||
|
requireNotNull(queueItem) { "Cannot update next up in non-view state" }.isEnabled =
|
||||||
|
nextUp.isNotEmpty()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,10 +19,10 @@ package org.oxycblt.auxio.playback
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
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
|
||||||
|
@ -52,32 +52,32 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback, MusicStore
|
||||||
|
|
||||||
private var pendingDelayedAction: DelayedActionImpl? = null
|
private var pendingDelayedAction: DelayedActionImpl? = null
|
||||||
|
|
||||||
private val _song = MutableLiveData<Song?>()
|
private val _song = MutableStateFlow<Song?>(null)
|
||||||
/** The current song. */
|
/** The current song. */
|
||||||
val song: LiveData<Song?>
|
val song: StateFlow<Song?>
|
||||||
get() = _song
|
get() = _song
|
||||||
private val _parent = MutableLiveData<MusicParent?>()
|
private val _parent = MutableStateFlow<MusicParent?>(null)
|
||||||
/** The current model that is being played from, such as an [Album] or [Artist] */
|
/** The current model that is being played from, such as an [Album] or [Artist] */
|
||||||
val parent: LiveData<MusicParent?>
|
val parent: StateFlow<MusicParent?>
|
||||||
get() = _parent
|
get() = _parent
|
||||||
private val _isPlaying = MutableLiveData(false)
|
private val _isPlaying = MutableStateFlow(false)
|
||||||
val isPlaying: LiveData<Boolean>
|
val isPlaying: StateFlow<Boolean>
|
||||||
get() = _isPlaying
|
get() = _isPlaying
|
||||||
private val _positionSecs = MutableLiveData(0L)
|
private val _positionSecs = MutableStateFlow(0L)
|
||||||
/** The current playback position, in seconds */
|
/** The current playback position, in seconds */
|
||||||
val positionSecs: LiveData<Long>
|
val positionSecs: StateFlow<Long>
|
||||||
get() = _positionSecs
|
get() = _positionSecs
|
||||||
private val _repeatMode = MutableLiveData(RepeatMode.NONE)
|
private val _repeatMode = MutableStateFlow(RepeatMode.NONE)
|
||||||
/** The current repeat mode, see [RepeatMode] for more information */
|
/** The current repeat mode, see [RepeatMode] for more information */
|
||||||
val repeatMode: LiveData<RepeatMode>
|
val repeatMode: StateFlow<RepeatMode>
|
||||||
get() = _repeatMode
|
get() = _repeatMode
|
||||||
private val _isShuffled = MutableLiveData(false)
|
private val _isShuffled = MutableStateFlow(false)
|
||||||
val isShuffled: LiveData<Boolean>
|
val isShuffled: StateFlow<Boolean>
|
||||||
get() = _isShuffled
|
get() = _isShuffled
|
||||||
|
|
||||||
private val _nextUp = MutableLiveData(listOf<Song>())
|
private val _nextUp = MutableStateFlow(listOf<Song>())
|
||||||
/** The queue, without the previous items. */
|
/** The queue, without the previous items. */
|
||||||
val nextUp: LiveData<List<Song>>
|
val nextUp: StateFlow<List<Song>>
|
||||||
get() = _nextUp
|
get() = _nextUp
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
|
@ -28,6 +28,7 @@ import org.oxycblt.auxio.databinding.FragmentQueueBinding
|
||||||
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.ViewBindingFragment
|
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||||
|
import org.oxycblt.auxio.util.launch
|
||||||
import org.oxycblt.auxio.util.requireAttached
|
import org.oxycblt.auxio.util.requireAttached
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -52,7 +53,7 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemList
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ----
|
// --- VIEWMODEL SETUP ----
|
||||||
|
|
||||||
playbackModel.nextUp.observe(viewLifecycleOwner, ::updateQueue)
|
launch { playbackModel.nextUp.collect(::updateQueue) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyBinding(binding: FragmentQueueBinding) {
|
override fun onDestroyBinding(binding: FragmentQueueBinding) {
|
||||||
|
|
|
@ -331,8 +331,6 @@ class PlaybackStateManager private constructor() {
|
||||||
|
|
||||||
logD("State read completed successfully in ${System.currentTimeMillis() - start}ms")
|
logD("State read completed successfully in ${System.currentTimeMillis() - start}ms")
|
||||||
|
|
||||||
// Get off the IO coroutine since it will cause LiveData updates to throw an exception
|
|
||||||
|
|
||||||
if (state != null) {
|
if (state != null) {
|
||||||
index = state.index
|
index = state.index
|
||||||
parent = state.parent
|
parent = state.parent
|
||||||
|
|
|
@ -30,6 +30,7 @@ import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentSearchBinding
|
import org.oxycblt.auxio.databinding.FragmentSearchBinding
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
|
@ -49,6 +50,7 @@ import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||||
import org.oxycblt.auxio.ui.newMenu
|
import org.oxycblt.auxio.ui.newMenu
|
||||||
import org.oxycblt.auxio.util.applySpans
|
import org.oxycblt.auxio.util.applySpans
|
||||||
import org.oxycblt.auxio.util.getSystemServiceSafe
|
import org.oxycblt.auxio.util.getSystemServiceSafe
|
||||||
|
import org.oxycblt.auxio.util.launch
|
||||||
import org.oxycblt.auxio.util.requireAttached
|
import org.oxycblt.auxio.util.requireAttached
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -107,9 +109,9 @@ class SearchFragment :
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ---
|
// --- VIEWMODEL SETUP ---
|
||||||
|
|
||||||
searchModel.searchResults.observe(viewLifecycleOwner, ::updateResults)
|
launch { searchModel.searchResults.collect(::updateResults) }
|
||||||
navModel.exploreNavigationItem.observe(viewLifecycleOwner, ::handleNavigation)
|
launch { musicModel.response.collect(::handleLoaderResponse) }
|
||||||
musicModel.loaderResponse.observe(viewLifecycleOwner, ::handleLoaderResponse)
|
launch { navModel.exploreNavigationItem.collect(::handleNavigation) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyBinding(binding: FragmentSearchBinding) {
|
override fun onDestroyBinding(binding: FragmentSearchBinding) {
|
||||||
|
@ -144,10 +146,6 @@ class SearchFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateResults(results: List<Item>) {
|
private fun updateResults(results: List<Item>) {
|
||||||
if (isDetached) {
|
|
||||||
error("Fragment not attached to activity")
|
|
||||||
}
|
|
||||||
|
|
||||||
val binding = requireBinding()
|
val binding = requireBinding()
|
||||||
|
|
||||||
searchAdapter.data.submitList(results.toMutableList()) {
|
searchAdapter.data.submitList(results.toMutableList()) {
|
||||||
|
|
|
@ -19,11 +19,11 @@ package org.oxycblt.auxio.search
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.annotation.IdRes
|
import androidx.annotation.IdRes
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import java.text.Normalizer
|
import java.text.Normalizer
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
|
@ -43,16 +43,17 @@ class SearchViewModel : ViewModel() {
|
||||||
private val musicStore = MusicStore.getInstance()
|
private val musicStore = MusicStore.getInstance()
|
||||||
private val settingsManager = SettingsManager.getInstance()
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
|
|
||||||
private val _searchResults = MutableLiveData(listOf<Item>())
|
private val _searchResults = MutableStateFlow(listOf<Item>())
|
||||||
private var _filterMode: DisplayMode? = null
|
|
||||||
private var lastQuery: String? = null
|
|
||||||
|
|
||||||
/** Current search results from the last [search] call. */
|
/** Current search results from the last [search] call. */
|
||||||
val searchResults: LiveData<List<Item>>
|
val searchResults: StateFlow<List<Item>>
|
||||||
get() = _searchResults
|
get() = _searchResults
|
||||||
|
|
||||||
|
private var _filterMode: DisplayMode? = null
|
||||||
val filterMode: DisplayMode?
|
val filterMode: DisplayMode?
|
||||||
get() = _filterMode
|
get() = _filterMode
|
||||||
|
|
||||||
|
private var lastQuery: String? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
_filterMode = settingsManager.searchFilterMode
|
_filterMode = settingsManager.searchFilterMode
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,12 +28,14 @@ import androidx.core.view.updatePadding
|
||||||
import androidx.fragment.app.activityViewModels
|
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 kotlinx.coroutines.flow.collect
|
||||||
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.home.HomeViewModel
|
||||||
import org.oxycblt.auxio.ui.ViewBindingFragment
|
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||||
import org.oxycblt.auxio.util.formatDuration
|
import org.oxycblt.auxio.util.formatDuration
|
||||||
|
import org.oxycblt.auxio.util.launch
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.showToast
|
import org.oxycblt.auxio.util.showToast
|
||||||
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
|
@ -61,24 +63,37 @@ class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
|
||||||
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.songs.observe(viewLifecycleOwner) { songs ->
|
launch {
|
||||||
binding.aboutSongCount.textSafe = getString(R.string.fmt_songs_loaded, songs.size)
|
homeModel.songs.collect { songs ->
|
||||||
binding.aboutTotalDuration.textSafe =
|
binding.aboutSongCount.textSafe = getString(R.string.fmt_songs_loaded, songs.size)
|
||||||
getString(
|
binding.aboutTotalDuration.textSafe =
|
||||||
R.string.fmt_total_duration,
|
getString(
|
||||||
songs.sumOf { it.durationSecs }.formatDuration(false))
|
R.string.fmt_total_duration,
|
||||||
|
getString(
|
||||||
|
R.string.fmt_total_duration,
|
||||||
|
songs.sumOf { it.durationSecs }.formatDuration(false)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
homeModel.albums.observe(viewLifecycleOwner) { albums ->
|
launch {
|
||||||
binding.aboutAlbumCount.textSafe = getString(R.string.fmt_albums_loaded, albums.size)
|
homeModel.albums.collect { albums ->
|
||||||
|
binding.aboutAlbumCount.textSafe =
|
||||||
|
getString(R.string.fmt_albums_loaded, albums.size)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
homeModel.artists.observe(viewLifecycleOwner) { artists ->
|
launch {
|
||||||
binding.aboutArtistCount.textSafe = getString(R.string.fmt_artists_loaded, artists.size)
|
homeModel.artists.collect { artists ->
|
||||||
|
binding.aboutArtistCount.textSafe =
|
||||||
|
getString(R.string.fmt_artists_loaded, artists.size)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
homeModel.genres.observe(viewLifecycleOwner) { genres ->
|
launch {
|
||||||
binding.aboutGenreCount.textSafe = getString(R.string.fmt_genres_loaded, genres.size)
|
homeModel.genres.collect { genres ->
|
||||||
|
binding.aboutGenreCount.textSafe =
|
||||||
|
getString(R.string.fmt_genres_loaded, genres.size)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,9 +17,9 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.ui
|
package org.oxycblt.auxio.ui
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -27,17 +27,17 @@ import org.oxycblt.auxio.music.Music
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class NavigationViewModel : ViewModel() {
|
class NavigationViewModel : ViewModel() {
|
||||||
private val _mainNavigationAction = MutableLiveData<MainNavigationAction?>()
|
private val _mainNavigationAction = MutableStateFlow<MainNavigationAction?>(null)
|
||||||
/** Flag for main fragment navigation. Intended for MainFragment use only. */
|
/** Flag for main fragment navigation. Intended for MainFragment use only. */
|
||||||
val mainNavigationAction: LiveData<MainNavigationAction?>
|
val mainNavigationAction: StateFlow<MainNavigationAction?>
|
||||||
get() = _mainNavigationAction
|
get() = _mainNavigationAction
|
||||||
|
|
||||||
private val _exploreNavigationItem = MutableLiveData<Music?>()
|
private val _exploreNavigationItem = MutableStateFlow<Music?>(null)
|
||||||
/**
|
/**
|
||||||
* Flag for navigation within the explore fragments. Observe this to coordinate navigation to an
|
* Flag for navigation within the explore fragments. Observe this to coordinate navigation to an
|
||||||
* item's UI.
|
* item's UI.
|
||||||
*/
|
*/
|
||||||
val exploreNavigationItem: LiveData<Music?>
|
val exploreNavigationItem: StateFlow<Music?>
|
||||||
get() = _exploreNavigationItem
|
get() = _exploreNavigationItem
|
||||||
|
|
||||||
/** Notify MainFragment to navigate to the location outlined in [MainNavigationAction]. */
|
/** Notify MainFragment to navigate to the location outlined in [MainNavigationAction]. */
|
||||||
|
|
|
@ -31,9 +31,14 @@ import android.widget.TextView
|
||||||
import androidx.annotation.ColorRes
|
import androidx.annotation.ColorRes
|
||||||
import androidx.core.graphics.drawable.DrawableCompat
|
import androidx.core.graphics.drawable.DrawableCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -147,6 +152,18 @@ val @receiver:ColorRes Int.stateList
|
||||||
/** Require the fragment is attached to an activity. */
|
/** Require the fragment is attached to an activity. */
|
||||||
fun Fragment.requireAttached() = check(!isDetached) { "Fragment is detached from activity" }
|
fun Fragment.requireAttached() = check(!isDetached) { "Fragment is detached from activity" }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launches [block] in a lifecycle-aware coroutine once [state] is reached. This is primarily a
|
||||||
|
* shortcut intended to correctly launch a co-routine on a fragment in a way that won't cause
|
||||||
|
* miscellaneous coroutine insanity.
|
||||||
|
*/
|
||||||
|
fun Fragment.launch(
|
||||||
|
state: Lifecycle.State = Lifecycle.State.STARTED,
|
||||||
|
block: suspend CoroutineScope.() -> Unit
|
||||||
|
) {
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(state, block) }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shortcut for querying all items in a database and running [block] with the cursor returned. Will
|
* Shortcut for querying all items in a database and running [block] with the cursor returned. Will
|
||||||
* not run if the cursor is null.
|
* not run if the cursor is null.
|
||||||
|
|
|
@ -31,7 +31,6 @@
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:animateLayoutChanges="true"
|
|
||||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
|
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
@ -40,8 +39,7 @@
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:animateLayoutChanges="true"
|
android:animateLayoutChanges="true"
|
||||||
android:paddingStart="@dimen/spacing_medium"
|
android:paddingStart="@dimen/spacing_medium"
|
||||||
android:paddingEnd="@dimen/spacing_medium"
|
android:paddingEnd="@dimen/spacing_medium">
|
||||||
tools:visibility="invisible">
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/home_loading_status"
|
android:id="@+id/home_loading_status"
|
||||||
|
@ -70,6 +68,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/lbl_retry"
|
android:text="@string/lbl_retry"
|
||||||
|
android:visibility="invisible"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/home_loading_status" />
|
app:layout_constraintTop_toBottomOf="@+id/home_loading_status" />
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue