Add nice looking empty list view

Also fix shouldRetry() in the Downloader
This commit is contained in:
tzugen 2021-11-26 19:01:14 +01:00
parent 4e37a2483c
commit b33fe2d451
No known key found for this signature in database
GPG Key ID: 61E9C34BC10EC930
18 changed files with 281 additions and 150 deletions

View File

@ -59,15 +59,10 @@ class ArtistListFragment : EntryListFragment<ArtistOrIndex>() {
bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, item.name) bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, item.name)
bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, item.id) bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, item.id)
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, (item is Artist)) bundle.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, (item is Artist))
bundle.putString( bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, Constants.ALBUMS_OF_ARTIST)
Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE,
Constants.ALPHABETICAL_BY_NAME
)
bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, item.name) bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, item.name)
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 1000) bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 1000)
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0) bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0)
findNavController().navigate(itemClickTarget, bundle) findNavController().navigate(itemClickTarget, bundle)
} }
// Constants.ALPHABETICAL_BY_NAME
} }

View File

@ -11,6 +11,7 @@ import android.app.Application
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import androidx.core.view.isVisible
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import org.koin.core.component.inject import org.koin.core.component.inject
@ -76,6 +77,9 @@ class DownloadsFragment : MultiListFragment<DownloadFile>() {
val liveDataList = listModel.getList() val liveDataList = listModel.getList()
emptyTextView.setText(R.string.download_empty)
emptyView.isVisible = liveDataList.value?.isEmpty() ?: true
viewAdapter.submitList(liveDataList.value) viewAdapter.submitList(liveDataList.value)
} }
} }

View File

@ -7,8 +7,11 @@ import androidx.navigation.fragment.findNavController
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.domain.Artist import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.domain.GenericEntry import org.moire.ultrasonic.domain.GenericEntry
import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.service.RxBus
import org.moire.ultrasonic.subsonic.DownloadHandler
import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Constants
import androidx.fragment.app.Fragment
import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Settings
/** /**
@ -27,91 +30,11 @@ abstract class EntryListFragment<T : GenericEntry> : MultiListFragment<T>() {
!listModel.isOffline() && !Settings.shouldUseId3Tags !listModel.isOffline() && !Settings.shouldUseId3Tags
} }
@Suppress("LongMethod")
override fun onContextMenuItemSelected(menuItem: MenuItem, item: T): Boolean { override fun onContextMenuItemSelected(menuItem: MenuItem, item: T): Boolean {
val isArtist = (item is Artist) val isArtist = (item is Artist)
when (menuItem.itemId) { return handleContextMenu(menuItem, item, isArtist, downloadHandler, this)
R.id.menu_play_now ->
downloadHandler.downloadRecursively(
this,
item.id,
save = false,
append = false,
autoPlay = true,
shuffle = false,
background = false,
playNext = false,
unpin = false,
isArtist = isArtist
)
R.id.menu_play_next ->
downloadHandler.downloadRecursively(
this,
item.id,
save = false,
append = false,
autoPlay = true,
shuffle = true,
background = false,
playNext = true,
unpin = false,
isArtist = isArtist
)
R.id.menu_play_last ->
downloadHandler.downloadRecursively(
this,
item.id,
save = false,
append = true,
autoPlay = false,
shuffle = false,
background = false,
playNext = false,
unpin = false,
isArtist = isArtist
)
R.id.menu_pin ->
downloadHandler.downloadRecursively(
this,
item.id,
save = true,
append = true,
autoPlay = false,
shuffle = false,
background = false,
playNext = false,
unpin = false,
isArtist = isArtist
)
R.id.menu_unpin ->
downloadHandler.downloadRecursively(
this,
item.id,
save = false,
append = false,
autoPlay = false,
shuffle = false,
background = false,
playNext = false,
unpin = true,
isArtist = isArtist
)
R.id.menu_download ->
downloadHandler.downloadRecursively(
this,
item.id,
save = false,
append = false,
autoPlay = false,
shuffle = false,
background = true,
playNext = false,
unpin = false,
isArtist = isArtist
)
}
return true
} }
override fun onItemClick(item: T) { override fun onItemClick(item: T) {
@ -137,4 +60,97 @@ abstract class EntryListFragment<T : GenericEntry> : MultiListFragment<T>() {
listModel.refresh(refreshListView!!, arguments) listModel.refresh(refreshListView!!, arguments)
} }
} }
companion object {
@Suppress("LongMethod")
internal fun handleContextMenu(
menuItem: MenuItem,
item: Identifiable,
isArtist: Boolean,
downloadHandler: DownloadHandler,
fragment: Fragment
): Boolean {
when (menuItem.itemId) {
R.id.menu_play_now ->
downloadHandler.downloadRecursively(
fragment,
item.id,
save = false,
append = false,
autoPlay = true,
shuffle = false,
background = false,
playNext = false,
unpin = false,
isArtist = isArtist
)
R.id.menu_play_next ->
downloadHandler.downloadRecursively(
fragment,
item.id,
save = false,
append = false,
autoPlay = true,
shuffle = true,
background = false,
playNext = true,
unpin = false,
isArtist = isArtist
)
R.id.menu_play_last ->
downloadHandler.downloadRecursively(
fragment,
item.id,
save = false,
append = true,
autoPlay = false,
shuffle = false,
background = false,
playNext = false,
unpin = false,
isArtist = isArtist
)
R.id.menu_pin ->
downloadHandler.downloadRecursively(
fragment,
item.id,
save = true,
append = true,
autoPlay = false,
shuffle = false,
background = false,
playNext = false,
unpin = false,
isArtist = isArtist
)
R.id.menu_unpin ->
downloadHandler.downloadRecursively(
fragment,
item.id,
save = false,
append = false,
autoPlay = false,
shuffle = false,
background = false,
playNext = false,
unpin = true,
isArtist = isArtist
)
R.id.menu_download ->
downloadHandler.downloadRecursively(
fragment,
item.id,
save = false,
append = false,
autoPlay = false,
shuffle = false,
background = true,
playNext = false,
unpin = false,
isArtist = isArtist
)
}
return true
}
}
} }

View File

@ -13,6 +13,7 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
@ -46,6 +47,7 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
protected var refreshListView: SwipeRefreshLayout? = null protected var refreshListView: SwipeRefreshLayout? = null
internal var listView: RecyclerView? = null internal var listView: RecyclerView? = null
internal lateinit var viewManager: LinearLayoutManager internal lateinit var viewManager: LinearLayoutManager
internal lateinit var emptyView: ConstraintLayout
internal lateinit var emptyTextView: TextView internal lateinit var emptyTextView: TextView
/** /**
@ -71,7 +73,7 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
* The central function to pass a query to the model and return a LiveData object * The central function to pass a query to the model and return a LiveData object
*/ */
open fun getLiveData(args: Bundle? = null): LiveData<List<T>> { open fun getLiveData(args: Bundle? = null): LiveData<List<T>> {
return MutableLiveData(listOf()) return MutableLiveData()
} }
/** /**
@ -90,7 +92,9 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
*/ */
open val refreshListId = R.id.swipe_refresh_view open val refreshListId = R.id.swipe_refresh_view
open val recyclerViewId = R.id.recycler_view open val recyclerViewId = R.id.recycler_view
open val emptyTextViewId = R.id.empty_list_text open val emptyViewId = R.id.empty_list_view
open val emptyTextId = R.id.empty_list_text
open fun setTitle(title: String?) { open fun setTitle(title: String?) {
if (title == null) { if (title == null) {
@ -121,14 +125,14 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
liveDataItems = getLiveData(arguments) liveDataItems = getLiveData(arguments)
// Link view to display text if the list is empty // Link view to display text if the list is empty
// FIXME: Hook this up globally. emptyView = view.findViewById(emptyViewId)
emptyTextView = view.findViewById(emptyTextViewId) emptyTextView = view.findViewById(emptyTextId)
// Register an observer to update our UI when the data changes // Register an observer to update our UI when the data changes
liveDataItems.observe( liveDataItems.observe(
viewLifecycleOwner, viewLifecycleOwner,
{ newItems -> { newItems ->
emptyTextView.isVisible = newItems.isEmpty() emptyView.isVisible = newItems.isEmpty()
viewAdapter.submitList(newItems) viewAdapter.submitList(newItems)
} }
) )

View File

@ -28,6 +28,7 @@ import org.moire.ultrasonic.adapters.AlbumRowBinder
import org.moire.ultrasonic.adapters.ArtistRowBinder import org.moire.ultrasonic.adapters.ArtistRowBinder
import org.moire.ultrasonic.adapters.DividerBinder import org.moire.ultrasonic.adapters.DividerBinder
import org.moire.ultrasonic.adapters.TrackViewBinder import org.moire.ultrasonic.adapters.TrackViewBinder
import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.domain.SearchResult import org.moire.ultrasonic.domain.SearchResult
@ -176,11 +177,11 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
return search(query, autoPlay) return search(query, autoPlay)
} }
} }
// Fragment was started from the Menu, create empty list
// populateList(SearchResult())
} }
/**
* This method create the search bar above the recycler view
*/
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
val activity = activity ?: return val activity = activity ?: return
val searchManager = activity.getSystemService(Context.SEARCH_SERVICE) as SearchManager val searchManager = activity.getSystemService(Context.SEARCH_SERVICE) as SearchManager
@ -191,8 +192,8 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
searchView.setSearchableInfo(searchableInfo) searchView.setSearchableInfo(searchableInfo)
val arguments = arguments val arguments = arguments
val autoPlay = val autoPlay = arguments != null &&
arguments != null && arguments.getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false) arguments.getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false)
val query = arguments?.getString(Constants.INTENT_EXTRA_NAME_QUERY) val query = arguments?.getString(Constants.INTENT_EXTRA_NAME_QUERY)
// If started with a query, enter it to the searchView // If started with a query, enter it to the searchView
@ -211,13 +212,13 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
val cursor = searchView.suggestionsAdapter.cursor val cursor = searchView.suggestionsAdapter.cursor
cursor.moveToPosition(position) cursor.moveToPosition(position)
// TODO: Try to do something with this magic const: // 2 is the index of col containing suggestion name.
// 2 is the index of col containing suggestion name.
val suggestion = cursor.getString(2) val suggestion = cursor.getString(2)
searchView.setQuery(suggestion, true) searchView.setQuery(suggestion, true)
return true return true
} }
}) })
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean { override fun onQueryTextSubmit(query: String): Boolean {
Timber.d("onQueryTextSubmit: %s", query) Timber.d("onQueryTextSubmit: %s", query)
@ -230,6 +231,7 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
return true return true
} }
}) })
searchView.setIconifiedByDefault(false) searchView.setIconifiedByDefault(false)
searchItem.expandActionView() searchItem.expandActionView()
} }
@ -479,7 +481,7 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
} }
// Show/hide the empty text view // Show/hide the empty text view
emptyTextView.isVisible = list.isEmpty() emptyView.isVisible = list.isEmpty()
viewAdapter.submitList(list) viewAdapter.submitList(list)
} }
@ -557,20 +559,15 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
var DEFAULT_SONGS = Settings.defaultSongs var DEFAULT_SONGS = Settings.defaultSongs
} }
// FIXME!!
override fun getLiveData(args: Bundle?): LiveData<List<Identifiable>> {
return MutableLiveData(listOf())
}
// FIXME // FIXME
override val itemClickTarget: Int = 0 override val itemClickTarget: Int = 0
// FIXME
override fun onContextMenuItemSelected(menuItem: MenuItem, item: Identifiable): Boolean {
return true
}
// FIXME // FIXME
override fun onItemClick(item: Identifiable) { override fun onItemClick(item: Identifiable) {
} }
override fun onContextMenuItemSelected(menuItem: MenuItem, item: Identifiable): Boolean {
val isArtist = (item is Artist)
return EntryListFragment.handleContextMenu(menuItem, item, isArtist, downloadHandler, this)
}
} }

View File

@ -57,7 +57,6 @@ import timber.log.Timber
open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Entry>() { open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Entry>() {
private var albumButtons: View? = null private var albumButtons: View? = null
private var emptyView: TextView? = null
internal var selectButton: ImageView? = null internal var selectButton: ImageView? = null
internal var playNowButton: ImageView? = null internal var playNowButton: ImageView? = null
internal var playNextButton: ImageView? = null internal var playNextButton: ImageView? = null
@ -107,8 +106,6 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Entry>() {
setupButtons(view) setupButtons(view)
emptyView = view.findViewById(R.id.empty_list_text)
registerForContextMenu(listView!!) registerForContextMenu(listView!!)
setHasOptionsMenu(true) setHasOptionsMenu(true)
@ -579,7 +576,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Entry>() {
} }
// Show a text if we have no entries // Show a text if we have no entries
emptyView?.isVisible = entryList.isEmpty() emptyView.isVisible = entryList.isEmpty()
enableButtons() enableButtons()
@ -599,10 +596,8 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Entry>() {
val albumHeader = AlbumHeader(it, name ?: intentAlbumName) val albumHeader = AlbumHeader(it, name ?: intentAlbumName)
val mixedList: MutableList<Identifiable> = mutableListOf(albumHeader) val mixedList: MutableList<Identifiable> = mutableListOf(albumHeader)
mixedList.addAll(entryList) mixedList.addAll(entryList)
Timber.e("SUBMITTING MIXED LIST")
viewAdapter.submitList(mixedList) viewAdapter.submitList(mixedList)
} else { } else {
Timber.e("SUBMITTING ENTRY LIST")
viewAdapter.submitList(entryList) viewAdapter.submitList(entryList)
} }

View File

@ -13,7 +13,7 @@ import org.moire.ultrasonic.util.Settings
class AlbumListModel(application: Application) : GenericListModel(application) { class AlbumListModel(application: Application) : GenericListModel(application) {
val list: MutableLiveData<List<MusicDirectory.Album>> = MutableLiveData(listOf()) val list: MutableLiveData<List<MusicDirectory.Album>> = MutableLiveData()
var lastType: String? = null var lastType: String? = null
private var loadedUntil: Int = 0 private var loadedUntil: Int = 0
@ -26,7 +26,7 @@ class AlbumListModel(application: Application) : GenericListModel(application) {
// This way, we keep the scroll position // This way, we keep the scroll position
val albumListType = args.getString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE)!! val albumListType = args.getString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE)!!
if (refresh || list.value!!.isEmpty() || albumListType != lastType) { if (refresh || list.value?.isEmpty() != false || albumListType != lastType) {
lastType = albumListType lastType = albumListType
backgroundLoadFromServer(refresh, swipe, args) backgroundLoadFromServer(refresh, swipe, args)
} }

View File

@ -31,7 +31,7 @@ import org.moire.ultrasonic.service.MusicService
* Provides ViewModel which contains the list of available Artists * Provides ViewModel which contains the list of available Artists
*/ */
class ArtistListModel(application: Application) : GenericListModel(application) { class ArtistListModel(application: Application) : GenericListModel(application) {
private val artists: MutableLiveData<List<ArtistOrIndex>> = MutableLiveData(listOf()) private val artists: MutableLiveData<List<ArtistOrIndex>> = MutableLiveData()
/** /**
* Retrieves all available Artists in a LiveData * Retrieves all available Artists in a LiveData
@ -39,7 +39,7 @@ class ArtistListModel(application: Application) : GenericListModel(application)
fun getItems(refresh: Boolean, swipe: SwipeRefreshLayout): LiveData<List<ArtistOrIndex>> { fun getItems(refresh: Boolean, swipe: SwipeRefreshLayout): LiveData<List<ArtistOrIndex>> {
// Don't reload the data if navigating back to the view that was active before. // Don't reload the data if navigating back to the view that was active before.
// This way, we keep the scroll position // This way, we keep the scroll position
if (artists.value!!.isEmpty() || refresh) { if (artists.value?.isEmpty() != false || refresh) {
backgroundLoadFromServer(refresh, swipe) backgroundLoadFromServer(refresh, swipe)
} }
return artists return artists

View File

@ -14,7 +14,7 @@ import org.moire.ultrasonic.util.Settings
class SearchListModel(application: Application) : GenericListModel(application) { class SearchListModel(application: Application) : GenericListModel(application) {
var searchResult: MutableLiveData<SearchResult?> = MutableLiveData(null) var searchResult: MutableLiveData<SearchResult?> = MutableLiveData()
override fun load( override fun load(
isOffline: Boolean, isOffline: Boolean,

View File

@ -19,6 +19,9 @@ import org.moire.ultrasonic.util.Util
/* /*
* Model for retrieving different collections of tracks from the API * Model for retrieving different collections of tracks from the API
*
* TODO: Remove double data keeping in currentList/currentDirectory and use the base model liveData
* For this refactor MusicService to replace MusicDirectories with List<Album> or List<Track>
*/ */
class TrackCollectionModel(application: Application) : GenericListModel(application) { class TrackCollectionModel(application: Application) : GenericListModel(application) {

View File

@ -157,7 +157,8 @@ class Downloader(
// Add file to queue if not in one of the queues already. // Add file to queue if not in one of the queues already.
if (!download.isWorkDone && if (!download.isWorkDone &&
!activelyDownloading.contains(download) && !activelyDownloading.contains(download) &&
!downloadQueue.contains(download) !downloadQueue.contains(download) &&
download.shouldRetry()
) { ) {
listChanged = true listChanged = true
downloadQueue.add(download) downloadQueue.add(download)
@ -281,14 +282,18 @@ class Downloader(
fun clearPlaylist() { fun clearPlaylist() {
playlist.clear() playlist.clear()
val toRemove = mutableListOf<DownloadFile>()
// Cancel all active downloads with a high priority // Cancel all active downloads with a high priority
for (download in activelyDownloading) { for (download in activelyDownloading) {
if (download.priority < 100) { if (download.priority < 100) {
download.cancelDownload() download.cancelDownload()
activelyDownloading.remove(download) toRemove.add(download)
} }
} }
activelyDownloading.removeAll(toRemove)
playlistUpdateRevision++ playlistUpdateRevision++
updateLiveData() updateLiveData()
} }

View File

@ -0,0 +1,91 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="#000000"
android:pathData="M90.365,48.085l-3.83,0l-3.859,0l-3.83,0l-3.86,0l-3.83,0l-3.86,0l0,3.86l3.86,0l3.83,0l3.86,0l3.83,0l3.859,0l3.83,0l3.861,0l0,-3.86z" />
<path
android:fillColor="#000000"
android:pathData="M82.676,82.705h3.859v3.83h-3.859z" />
<path
android:fillColor="#000000"
android:pathData="M82.676,13.465h3.859v3.86h-3.859z" />
<path
android:fillColor="#000000"
android:pathData="M78.846,78.846h3.83v3.859h-3.83z" />
<path
android:fillColor="#000000"
android:pathData="M78.846,17.325h3.83v3.83h-3.83z" />
<path
android:fillColor="#000000"
android:pathData="M74.986,75.016h3.859v3.83h-3.859z" />
<path
android:fillColor="#000000"
android:pathData="M74.986,21.155h3.859v3.86h-3.859z" />
<path
android:fillColor="#000000"
android:pathData="M71.156,71.154h3.83v3.861h-3.83z" />
<path
android:fillColor="#000000"
android:pathData="M71.156,25.015h3.83v3.83h-3.83z" />
<path
android:fillColor="#000000"
android:pathData="M67.296,67.324h3.86v3.83h-3.86z" />
<path
android:fillColor="#000000"
android:pathData="M67.296,28.845h3.86v3.86h-3.86z" />
<path
android:fillColor="#000000"
android:pathData="M63.466,63.465h3.83v3.859h-3.83z" />
<path
android:fillColor="#000000"
android:pathData="M63.466,32.705h3.83v3.83h-3.83z" />
<path
android:fillColor="#000000"
android:pathData="M48.056,71.154l0,3.862l0,3.83l0,3.859l0,3.83l0,3.86l0,3.83l3.86,0l0,-3.83l0,-3.86l0,-3.83l0,-3.859l0,-3.83l0,-3.862l0,-3.83l-3.86,0z" />
<path
android:fillColor="#000000"
android:pathData="M48.056,9.635l0,3.83l0,3.86l0,3.83l0,3.86l0,3.83l0,3.86l3.86,0l0,-3.86l0,-3.83l0,-3.86l0,-3.83l0,-3.86l0,-3.83l0,-3.86l-3.86,0z" />
<path
android:fillColor="#000000"
android:pathData="M32.676,63.465h3.859v3.859h-3.859z" />
<path
android:fillColor="#000000"
android:pathData="M32.676,32.705h3.859v3.83h-3.859z" />
<path
android:fillColor="#000000"
android:pathData="M28.846,67.324h3.83v3.83h-3.83z" />
<path
android:fillColor="#000000"
android:pathData="M28.846,28.845h3.83v3.86h-3.83z" />
<path
android:fillColor="#000000"
android:pathData="M24.986,71.154h3.859v3.861h-3.859z" />
<path
android:fillColor="#000000"
android:pathData="M24.986,25.015h3.859v3.83h-3.859z" />
<path
android:fillColor="#000000"
android:pathData="M21.156,75.016h3.83v3.83h-3.83z" />
<path
android:fillColor="#000000"
android:pathData="M21.156,21.155h3.83v3.86h-3.83z" />
<path
android:fillColor="#000000"
android:pathData="M17.296,78.846h3.86v3.859h-3.86z" />
<path
android:fillColor="#000000"
android:pathData="M17.296,17.325h3.86v3.83h-3.86z" />
<path
android:fillColor="#000000"
android:pathData="M13.466,82.705h3.83v3.83h-3.83z" />
<path
android:fillColor="#000000"
android:pathData="M17.296,51.945l3.86,0l3.83,0l3.86,0l3.83,0l0,-3.86l-3.83,0l-3.86,0l-3.83,0l-3.86,0l-3.83,0l-3.861,0l-3.83,0l0,3.86l3.83,0l3.861,0z" />
<path
android:fillColor="#000000"
android:pathData="M13.466,13.465h3.83v3.86h-3.83z" />
</vector>

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:a="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
a:layout_width="match_parent"
a:layout_height="match_parent"
a:visibility="gone"
a:id="@+id/empty_list_view"
>
<ImageView
a:layout_width="100dip"
a:layout_height="100dip"
a:layout_marginStart="50dp"
a:importantForAccessibility="no"
a:layout_marginEnd="50dp"
a:layout_marginBottom="8dp"
a:src="@drawable/ic_empty"
app:layout_constraintBottom_toTopOf="@+id/empty_list_text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<TextView
a:id="@+id/empty_list_text"
a:layout_width="match_parent"
a:layout_height="wrap_content"
a:layout_marginTop="80dp"
a:layout_marginBottom="20dp"
a:drawablePadding="0dp"
a:gravity="center"
a:padding="12dp"
a:text="@string/search.no_match"
a:textAppearance="?android:attr/textAppearanceMedium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -4,6 +4,7 @@
a:layout_height="fill_parent" a:layout_height="fill_parent"
a:orientation="vertical"> a:orientation="vertical">
<include layout="@layout/empty_view" />
<include layout="@layout/recycler_view" /> <include layout="@layout/recycler_view" />
</LinearLayout> </LinearLayout>

View File

@ -1,12 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<merge xmlns:a="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <merge xmlns:a="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
<TextView
a:id="@+id/empty_list_text"
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:padding="10dip"
a:text="@string/select_album.empty"
a:visibility="gone" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
a:id="@+id/swipe_refresh_view" a:id="@+id/swipe_refresh_view"

View File

@ -1,29 +1,21 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
a:layout_width="fill_parent" xmlns:app="http://schemas.android.com/apk/res-auto"
a:layout_height="fill_parent" a:layout_width="match_parent"
a:layout_height="match_parent"
a:orientation="vertical"> a:orientation="vertical">
<TextView <include layout="@layout/empty_view" />
a:id="@+id/empty_list_text"
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:drawablePadding="0dp"
a:gravity="center"
a:padding="12dp"
a:text="@string/search.no_match"
a:textAppearance="?android:attr/textAppearanceMedium"
a:visibility="gone" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
a:id="@+id/swipe_refresh_view" a:id="@+id/swipe_refresh_view"
a:layout_width="fill_parent" a:layout_width="match_parent"
a:layout_height="0dip" a:layout_height="0dip"
a:layout_weight="1.0"> a:layout_weight="1.0">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
a:id="@+id/recycler_view" a:id="@+id/recycler_view"
a:layout_width="fill_parent" a:layout_width="match_parent"
a:layout_height="0dip" a:layout_height="0dip"
a:layout_weight="1.0" /> a:layout_weight="1.0" />

View File

@ -4,11 +4,7 @@
a:layout_height="fill_parent" a:layout_height="fill_parent"
a:orientation="vertical" > a:orientation="vertical" >
<View <include layout="@layout/empty_view" />
a:layout_width="fill_parent"
a:layout_height="1dp"
a:background="@color/dividerColor" />
<include layout="@layout/recycler_view" /> <include layout="@layout/recycler_view" />
<include layout="@layout/album_buttons" /> <include layout="@layout/album_buttons" />

View File

@ -57,6 +57,7 @@
<string name="delete_playlist">Do you want to delete %1$s</string> <string name="delete_playlist">Do you want to delete %1$s</string>
<string name="download.bookmark_removed" formatted="false">Bookmark removed.</string> <string name="download.bookmark_removed" formatted="false">Bookmark removed.</string>
<string name="download.bookmark_set_at_position" formatted="false">Bookmark set at %s.</string> <string name="download.bookmark_set_at_position" formatted="false">Bookmark set at %s.</string>
<string name="download.empty">Nothing is downloading</string>
<string name="playlist.empty">Playlist is empty</string> <string name="playlist.empty">Playlist is empty</string>
<string name="download.jukebox_not_authorized">Remote control is not allowed. Please enable jukebox mode in <b>Users &gt; Settings</b> on your Subsonic server.</string> <string name="download.jukebox_not_authorized">Remote control is not allowed. Please enable jukebox mode in <b>Users &gt; Settings</b> on your Subsonic server.</string>
<string name="download.jukebox_off">Turned off remote control. Music is played on phone.</string> <string name="download.jukebox_off">Turned off remote control. Music is played on phone.</string>