home: reflect no music state in tabs

This commit is contained in:
Alexander Capehart 2024-11-26 14:52:37 -07:00
parent 4618996fc5
commit 38ed432555
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
11 changed files with 165 additions and 25 deletions

View file

@ -175,6 +175,7 @@ class HomeFragment :
// --- VIEWMODEL SETUP --- // --- VIEWMODEL SETUP ---
collect(homeModel.recreateTabs.flow, ::handleRecreate) collect(homeModel.recreateTabs.flow, ::handleRecreate)
collect(homeModel.chooseMusicLocations.flow, ::handleChooseFolders)
collectImmediately(homeModel.currentTabType, ::updateCurrentTab) collectImmediately(homeModel.currentTabType, ::updateCurrentTab)
collect(detailModel.toShow.flow, ::handleShow) collect(detailModel.toShow.flow, ::handleShow)
collect(listModel.menu.flow, ::handleMenu) collect(listModel.menu.flow, ::handleMenu)
@ -301,6 +302,16 @@ class HomeFragment :
homeModel.recreateTabs.consume() homeModel.recreateTabs.consume()
} }
private fun handleChooseFolders(unit: Unit?) {
if (unit == null) {
return
}
findNavController().navigateSafe(
HomeFragmentDirections.chooseLocations()
)
homeModel.chooseMusicLocations.consume()
}
private fun updateIndexerState(state: IndexingState?) { private fun updateIndexerState(state: IndexingState?) {
// TODO: Make music loading experience a bit more pleasant // TODO: Make music loading experience a bit more pleasant
// 1. Loading placeholder for item lists // 1. Loading placeholder for item lists

View file

@ -159,6 +159,10 @@ constructor(
val showOuter: Event<Outer> val showOuter: Event<Outer>
get() = _showOuter get() = _showOuter
private val _chooseMusicLocations = MutableEvent<Unit>()
val chooseMusicLocations: Event<Unit>
get() = _chooseMusicLocations
init { init {
homeGenerator.attach() homeGenerator.attach()
} }
@ -263,6 +267,10 @@ constructor(
_isFastScrolling.value = isFastScrolling _isFastScrolling.value = isFastScrolling
} }
fun startChooseMusicLocations() {
_chooseMusicLocations.put(Unit)
}
fun showSettings() { fun showSettings() {
_showOuter.put(Outer.Settings) _showOuter.put(Outer.Settings)
} }

View file

@ -22,6 +22,7 @@ import android.os.Bundle
import android.text.format.DateUtils import android.text.format.DateUtils
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isInvisible
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import java.util.Formatter import java.util.Formatter
@ -37,6 +38,7 @@ import org.oxycblt.auxio.list.recycler.AlbumViewHolder
import org.oxycblt.auxio.list.recycler.FastScrollRecyclerView import org.oxycblt.auxio.list.recycler.FastScrollRecyclerView
import org.oxycblt.auxio.list.sort.Sort import org.oxycblt.auxio.list.sort.Sort
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.IndexingState
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.MusicViewModel
@ -79,7 +81,13 @@ class AlbumListFragment :
listener = this@AlbumListFragment listener = this@AlbumListFragment
} }
collectImmediately(homeModel.albumList, ::updateAlbums) binding.homeNoMusicMsg.text = getString(R.string.lng_no_albums)
binding.homeChooseMusicSources.setOnClickListener {
homeModel.startChooseMusicLocations()
}
collectImmediately(homeModel.albumList, musicModel.indexingState, ::updateAlbums)
collectImmediately(listModel.selected, ::updateSelection) collectImmediately(listModel.selected, ::updateSelection)
collectImmediately( collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
@ -143,7 +151,11 @@ class AlbumListFragment :
listModel.openMenu(R.menu.album, item) listModel.openMenu(R.menu.album, item)
} }
private fun updateAlbums(albums: List<Album>) { private fun updateAlbums(albums: List<Album>, indexingState: IndexingState?) {
requireBinding().apply {
homeRecycler.isInvisible = indexingState is IndexingState.Indexing || albums.isEmpty()
homeNoMusic.isInvisible = albums.isEmpty()
}
albumAdapter.update(albums, homeModel.albumInstructions.consume()) albumAdapter.update(albums, homeModel.albumInstructions.consume())
} }

View file

@ -21,6 +21,7 @@ package org.oxycblt.auxio.home.list
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isInvisible
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
@ -35,6 +36,7 @@ import org.oxycblt.auxio.list.recycler.ArtistViewHolder
import org.oxycblt.auxio.list.recycler.FastScrollRecyclerView import org.oxycblt.auxio.list.recycler.FastScrollRecyclerView
import org.oxycblt.auxio.list.sort.Sort import org.oxycblt.auxio.list.sort.Sort
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.IndexingState
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.MusicViewModel
@ -74,7 +76,13 @@ class ArtistListFragment :
listener = this@ArtistListFragment listener = this@ArtistListFragment
} }
collectImmediately(homeModel.artistList, ::updateArtists) binding.homeNoMusicMsg.text = getString(R.string.lng_no_artists)
binding.homeChooseMusicSources.setOnClickListener {
homeModel.startChooseMusicLocations()
}
collectImmediately(homeModel.artistList, musicModel.indexingState, ::updateArtists)
collectImmediately(listModel.selected, ::updateSelection) collectImmediately(listModel.selected, ::updateSelection)
collectImmediately( collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
@ -119,7 +127,11 @@ class ArtistListFragment :
listModel.openMenu(R.menu.parent, item) listModel.openMenu(R.menu.parent, item)
} }
private fun updateArtists(artists: List<Artist>) { private fun updateArtists(artists: List<Artist>, indexingState: IndexingState?) {
requireBinding().apply {
homeRecycler.isInvisible = indexingState is IndexingState.Indexing || artists.isEmpty()
homeNoMusic.isInvisible = artists.isEmpty()
}
artistAdapter.update(artists, homeModel.artistInstructions.consume()) artistAdapter.update(artists, homeModel.artistInstructions.consume())
} }

View file

@ -21,6 +21,7 @@ package org.oxycblt.auxio.home.list
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isInvisible
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
@ -35,6 +36,7 @@ import org.oxycblt.auxio.list.recycler.FastScrollRecyclerView
import org.oxycblt.auxio.list.recycler.GenreViewHolder import org.oxycblt.auxio.list.recycler.GenreViewHolder
import org.oxycblt.auxio.list.sort.Sort import org.oxycblt.auxio.list.sort.Sort
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.IndexingState
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.MusicViewModel
@ -73,7 +75,13 @@ class GenreListFragment :
listener = this@GenreListFragment listener = this@GenreListFragment
} }
collectImmediately(homeModel.genreList, ::updateGenres) binding.homeNoMusicMsg.text = getString(R.string.lng_no_genres)
binding.homeChooseMusicSources.setOnClickListener {
homeModel.startChooseMusicLocations()
}
collectImmediately(homeModel.genreList, musicModel.indexingState, ::updateGenres)
collectImmediately(listModel.selected, ::updateSelection) collectImmediately(listModel.selected, ::updateSelection)
collectImmediately( collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
@ -118,7 +126,11 @@ class GenreListFragment :
listModel.openMenu(R.menu.parent, item) listModel.openMenu(R.menu.parent, item)
} }
private fun updateGenres(genres: List<Genre>) { private fun updateGenres(genres: List<Genre>, indexingState: IndexingState?) {
requireBinding().apply {
homeRecycler.isInvisible = indexingState is IndexingState.Indexing || genres.isEmpty()
homeNoMusic.isInvisible = genres.isEmpty()
}
genreAdapter.update(genres, homeModel.genreInstructions.consume()) genreAdapter.update(genres, homeModel.genreInstructions.consume())
} }

View file

@ -21,6 +21,7 @@ package org.oxycblt.auxio.home.list
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isInvisible
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding import org.oxycblt.auxio.databinding.FragmentHomeListBinding
@ -33,6 +34,7 @@ import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.recycler.FastScrollRecyclerView import org.oxycblt.auxio.list.recycler.FastScrollRecyclerView
import org.oxycblt.auxio.list.recycler.PlaylistViewHolder import org.oxycblt.auxio.list.recycler.PlaylistViewHolder
import org.oxycblt.auxio.list.sort.Sort import org.oxycblt.auxio.list.sort.Sort
import org.oxycblt.auxio.music.IndexingState
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.MusicViewModel
@ -71,7 +73,13 @@ class PlaylistListFragment :
listener = this@PlaylistListFragment listener = this@PlaylistListFragment
} }
collectImmediately(homeModel.playlistList, ::updatePlaylists) binding.homeNoMusicMsg.text = getString(R.string.lng_no_playlists)
binding.homeChooseMusicSources.setOnClickListener {
homeModel.startChooseMusicLocations()
}
collectImmediately(homeModel.playlistList, musicModel.indexingState, ::updatePlaylists)
collectImmediately(listModel.selected, ::updateSelection) collectImmediately(listModel.selected, ::updateSelection)
collectImmediately( collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
@ -116,7 +124,11 @@ class PlaylistListFragment :
listModel.openMenu(R.menu.playlist, item) listModel.openMenu(R.menu.playlist, item)
} }
private fun updatePlaylists(playlists: List<Playlist>) { private fun updatePlaylists(playlists: List<Playlist>, indexingState: IndexingState?) {
requireBinding().apply {
homeRecycler.isInvisible = indexingState is IndexingState.Indexing || playlists.isEmpty()
homeNoMusic.isInvisible = playlists.isEmpty()
}
playlistAdapter.update(playlists, homeModel.playlistInstructions.consume()) playlistAdapter.update(playlists, homeModel.playlistInstructions.consume())
} }

View file

@ -15,13 +15,14 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.home.list package org.oxycblt.auxio.home.list
import android.os.Bundle import android.os.Bundle
import android.text.format.DateUtils import android.text.format.DateUtils
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isInvisible
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import java.util.Formatter import java.util.Formatter
@ -35,6 +36,7 @@ import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.recycler.FastScrollRecyclerView import org.oxycblt.auxio.list.recycler.FastScrollRecyclerView
import org.oxycblt.auxio.list.recycler.SongViewHolder import org.oxycblt.auxio.list.recycler.SongViewHolder
import org.oxycblt.auxio.list.sort.Sort import org.oxycblt.auxio.list.sort.Sort
import org.oxycblt.auxio.music.IndexingState
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.MusicViewModel
@ -59,6 +61,7 @@ class SongListFragment :
override val musicModel: MusicViewModel by activityViewModels() override val musicModel: MusicViewModel by activityViewModels()
override val playbackModel: PlaybackViewModel by activityViewModels() override val playbackModel: PlaybackViewModel by activityViewModels()
private val songAdapter = SongAdapter(this) private val songAdapter = SongAdapter(this)
// Save memory by re-using the same formatter and string builder when creating popup text // Save memory by re-using the same formatter and string builder when creating popup text
private val formatterSb = StringBuilder(64) private val formatterSb = StringBuilder(64)
private val formatter = Formatter(formatterSb) private val formatter = Formatter(formatterSb)
@ -76,10 +79,17 @@ class SongListFragment :
listener = this@SongListFragment listener = this@SongListFragment
} }
collectImmediately(homeModel.songList, ::updateSongs) binding.homeNoMusicMsg.text = getString(R.string.lng_no_songs)
binding.homeChooseMusicSources.setOnClickListener {
homeModel.startChooseMusicLocations()
}
collectImmediately(homeModel.songList, musicModel.indexingState, ::updateSongs)
collectImmediately(listModel.selected, ::updateSelection) collectImmediately(listModel.selected, ::updateSelection)
collectImmediately( collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback
)
} }
override fun onDestroyBinding(binding: FragmentHomeListBinding) { override fun onDestroyBinding(binding: FragmentHomeListBinding) {
@ -117,11 +127,12 @@ class SongListFragment :
val dateAddedMillis = song.dateAdded.secsToMs() val dateAddedMillis = song.dateAdded.secsToMs()
formatterSb.setLength(0) formatterSb.setLength(0)
DateUtils.formatDateRange( DateUtils.formatDateRange(
context, context,
formatter, formatter,
dateAddedMillis, dateAddedMillis,
dateAddedMillis, dateAddedMillis,
DateUtils.FORMAT_ABBREV_ALL) DateUtils.FORMAT_ABBREV_ALL
)
.toString() .toString()
} }
@ -142,8 +153,13 @@ class SongListFragment :
listModel.openMenu(R.menu.song, item, homeModel.playWith) listModel.openMenu(R.menu.song, item, homeModel.playWith)
} }
private fun updateSongs(songs: List<Song>) { private fun updateSongs(songs: List<Song>, indexingState: IndexingState?) {
requireBinding().apply {
homeRecycler.isInvisible = indexingState is IndexingState.Indexing || songs.isEmpty()
homeNoMusic.isInvisible = songs.isEmpty()
}
songAdapter.update(songs, homeModel.songInstructions.consume()) songAdapter.update(songs, homeModel.songInstructions.consume())
} }
private fun updateSelection(selection: List<Music>) { private fun updateSelection(selection: List<Music>) {

View file

@ -120,9 +120,7 @@ constructor(
// TODO: Differentiate "hard reloads" (Need the cache) and "Soft reloads" // TODO: Differentiate "hard reloads" (Need the cache) and "Soft reloads"
// (just need to manipulate data) // (just need to manipulate data)
when (key) { when (key) {
getString(R.string.set_key_exclude_non_music), getString(R.string.set_key_music_locations),
getString(R.string.set_key_music_dirs),
getString(R.string.set_key_music_dirs_include),
getString(R.string.set_key_separators), getString(R.string.set_key_separators),
getString(R.string.set_key_auto_sort_names) -> { getString(R.string.set_key_auto_sort_names) -> {
L.d("Dispatching indexing setting change for $key") L.d("Dispatching indexing setting change for $key")

View file

@ -1,8 +1,52 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<org.oxycblt.auxio.list.recycler.FastScrollRecyclerView xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout 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" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/home_recycler" android:animateLayoutChanges="true"
style="@style/Widget.Auxio.RecyclerView.Grid.WithAdaptiveFab"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
tools:listitem="@layout/item_song" />
<org.oxycblt.auxio.list.recycler.FastScrollRecyclerView
android:id="@+id/home_recycler"
style="@style/Widget.Auxio.RecyclerView.Grid.WithAdaptiveFab"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/item_song" />
<LinearLayout
android:id="@+id/home_no_music"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center_horizontal"
android:visibility="invisible"
android:orientation="vertical"
android:padding="@dimen/spacing_medium">
<ImageView
android:id="@+id/home_no_music_placeholder"
android:layout_width="@dimen/size_icon_huge"
android:layout_height="@dimen/size_icon_huge"
android:layout_marginBottom="@dimen/spacing_small"
android:src="@drawable/ic_song_24"
app:tint="?attr/colorOnSurface" />
<TextView
android:id="@+id/home_no_music_msg"
android:layout_width="256dp"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/spacing_small"
tools:text="@string/lng_no_songs"
android:textAlignment="center"
android:textAppearance="?attr/textAppearanceBodyLarge" />
<org.oxycblt.auxio.ui.RippleFixMaterialButton
android:id="@+id/home_choose_music_sources"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/lbl_music_sources" />
</LinearLayout>
</FrameLayout>

View file

@ -87,6 +87,9 @@
<action <action
android:id="@+id/report_error" android:id="@+id/report_error"
app:destination="@id/error_details_dialog" /> app:destination="@id/error_details_dialog" />
<action
android:id="@+id/choose_locations"
app:destination="@id/music_dirs_dialog" />
</fragment> </fragment>
<dialog <dialog
@ -564,4 +567,10 @@
android:name="parcel" android:name="parcel"
app:argType="org.oxycblt.auxio.list.menu.Menu$ForSelection$Parcel" /> app:argType="org.oxycblt.auxio.list.menu.Menu$ForSelection$Parcel" />
</dialog> </dialog>
<dialog
android:id="@+id/music_dirs_dialog"
android:name="org.oxycblt.auxio.music.dirs.MusicDirsDialog"
android:label="music_dirs_dialog"
tools:layout="@layout/dialog_music_dirs" />
</navigation> </navigation>

View file

@ -16,6 +16,7 @@
<string name="lbl_observing">Monitoring music library</string> <string name="lbl_observing">Monitoring music library</string>
<!-- As in to retry loading music --> <!-- As in to retry loading music -->
<string name="lbl_retry">Retry</string> <string name="lbl_retry">Retry</string>
<string name="lbl_music_sources">Pick folders</string>
<!-- As in to show additional information about a music loading error --> <!-- As in to show additional information about a music loading error -->
<string name="lbl_show_error_info">More</string> <string name="lbl_show_error_info">More</string>
<!-- As in grant permission --> <!-- As in grant permission -->
@ -214,6 +215,11 @@
Starts Auxio using the previously saved state. If no saved state is available, all songs will be shuffled. Playback will start immediately. Starts Auxio using the previously saved state. If no saved state is available, all songs will be shuffled. Playback will start immediately.
\n\nWARNING: Be careful controlling this service, if you close it and then try to use it again, you will probably crash the app. \n\nWARNING: Be careful controlling this service, if you close it and then try to use it again, you will probably crash the app.
</string> </string>
<string name="lng_no_songs">Your songs will show up here once you add some music.</string>
<string name="lng_no_albums">Your albums will show up here once you add some music.</string>
<string name="lng_no_artists">Your artists will show up here once you add some music.</string>
<string name="lng_no_genres">Your genres will show up here once you add some music.</string>
<string name="lng_no_playlists">Your playlists will show up here once you add some music.</string>
<!-- Settings namespace | Settings-related labels --> <!-- Settings namespace | Settings-related labels -->
<eat-comment /> <eat-comment />