mirror of
https://gitlab.com/ultrasonic/ultrasonic.git
synced 2025-06-05 01:53:05 +03:00
Make SearchResults expandable,
finish music folder support, change Service interface of AlbumList to return listOf(Album)
This commit is contained in:
parent
aa33d7c882
commit
bdac092eff
@ -51,6 +51,7 @@ class MusicDirectory : ArrayList<MusicDirectory.Child>() {
|
|||||||
abstract var starred: Boolean
|
abstract var starred: Boolean
|
||||||
abstract var path: String?
|
abstract var path: String?
|
||||||
abstract var closeness: Int
|
abstract var closeness: Int
|
||||||
|
abstract var isVideo: Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Rename to Track
|
// TODO: Rename to Track
|
||||||
@ -77,7 +78,7 @@ class MusicDirectory : ArrayList<MusicDirectory.Child>() {
|
|||||||
override var duration: Int? = null,
|
override var duration: Int? = null,
|
||||||
var bitRate: Int? = null,
|
var bitRate: Int? = null,
|
||||||
override var path: String? = null,
|
override var path: String? = null,
|
||||||
var isVideo: Boolean = false,
|
override var isVideo: Boolean = false,
|
||||||
override var starred: Boolean = false,
|
override var starred: Boolean = false,
|
||||||
override var discNumber: Int? = null,
|
override var discNumber: Int? = null,
|
||||||
var type: String? = null,
|
var type: String? = null,
|
||||||
@ -133,5 +134,6 @@ class MusicDirectory : ArrayList<MusicDirectory.Child>() {
|
|||||||
override var closeness: Int = 0,
|
override var closeness: Int = 0,
|
||||||
) : Child() {
|
) : Child() {
|
||||||
override var isDirectory = true
|
override var isDirectory = true
|
||||||
|
override var isVideo = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ package org.moire.ultrasonic.api.subsonic.response
|
|||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions
|
import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions
|
||||||
import org.moire.ultrasonic.api.subsonic.SubsonicError
|
import org.moire.ultrasonic.api.subsonic.SubsonicError
|
||||||
import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild
|
import org.moire.ultrasonic.api.subsonic.models.Album
|
||||||
|
|
||||||
class GetAlbumListResponse(
|
class GetAlbumListResponse(
|
||||||
status: Status,
|
status: Status,
|
||||||
@ -12,10 +12,10 @@ class GetAlbumListResponse(
|
|||||||
) : SubsonicResponse(status, version, error) {
|
) : SubsonicResponse(status, version, error) {
|
||||||
@JsonProperty("albumList") private val albumWrapper = AlbumWrapper()
|
@JsonProperty("albumList") private val albumWrapper = AlbumWrapper()
|
||||||
|
|
||||||
val albumList: List<MusicDirectoryChild>
|
val albumList: List<Album>
|
||||||
get() = albumWrapper.albumList
|
get() = albumWrapper.albumList
|
||||||
}
|
}
|
||||||
|
|
||||||
private class AlbumWrapper(
|
private class AlbumWrapper(
|
||||||
@JsonProperty("album") val albumList: List<MusicDirectoryChild> = emptyList()
|
@JsonProperty("album") val albumList: List<Album> = emptyList()
|
||||||
)
|
)
|
||||||
|
@ -1,72 +0,0 @@
|
|||||||
package org.moire.ultrasonic.util;
|
|
||||||
|
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
import java.util.Comparator;
|
|
||||||
|
|
||||||
public class EntryByDiscAndTrackComparator implements Comparator<MusicDirectory.Entry>, Serializable
|
|
||||||
{
|
|
||||||
private static final long serialVersionUID = 5540441864560835223L;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int compare(MusicDirectory.Entry x, MusicDirectory.Entry y)
|
|
||||||
{
|
|
||||||
Integer discX = x.getDiscNumber();
|
|
||||||
Integer discY = y.getDiscNumber();
|
|
||||||
Integer trackX = x.getTrack();
|
|
||||||
Integer trackY = y.getTrack();
|
|
||||||
String albumX = x.getAlbum();
|
|
||||||
String albumY = y.getAlbum();
|
|
||||||
String pathX = x.getPath();
|
|
||||||
String pathY = y.getPath();
|
|
||||||
|
|
||||||
int albumComparison = compare(albumX, albumY);
|
|
||||||
|
|
||||||
if (albumComparison != 0)
|
|
||||||
{
|
|
||||||
return albumComparison;
|
|
||||||
}
|
|
||||||
|
|
||||||
int discComparison = compare(discX == null ? 0 : discX, discY == null ? 0 : discY);
|
|
||||||
|
|
||||||
if (discComparison != 0)
|
|
||||||
{
|
|
||||||
return discComparison;
|
|
||||||
}
|
|
||||||
|
|
||||||
int trackComparison = compare(trackX == null ? 0 : trackX, trackY == null ? 0 : trackY);
|
|
||||||
|
|
||||||
if (trackComparison != 0)
|
|
||||||
{
|
|
||||||
return trackComparison;
|
|
||||||
}
|
|
||||||
|
|
||||||
return compare(pathX == null ? "" : pathX, pathY == null ? "" : pathY);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int compare(long a, long b)
|
|
||||||
{
|
|
||||||
return Long.compare(a, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int compare(String a, String b)
|
|
||||||
{
|
|
||||||
if (a == null && b == null)
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (a == null)
|
|
||||||
{
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (b == null)
|
|
||||||
{
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return a.compareTo(b);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +1,4 @@
|
|||||||
package org.moire.ultrasonic.util
|
package org.moire.ultrasonic.adapters
|
||||||
|
|
||||||
import java.util.HashSet
|
import java.util.HashSet
|
||||||
import org.moire.ultrasonic.domain.Identifiable
|
import org.moire.ultrasonic.domain.Identifiable
|
||||||
@ -7,7 +7,7 @@ import org.moire.ultrasonic.util.Settings.shouldUseFolderForArtistName
|
|||||||
import org.moire.ultrasonic.util.Util.getGrandparent
|
import org.moire.ultrasonic.util.Util.getGrandparent
|
||||||
|
|
||||||
class AlbumHeader(
|
class AlbumHeader(
|
||||||
var entries: List<MusicDirectory.Entry>,
|
var entries: List<MusicDirectory.Child>,
|
||||||
var name: String?
|
var name: String?
|
||||||
) : Identifiable {
|
) : Identifiable {
|
||||||
var isAllVideo: Boolean
|
var isAllVideo: Boolean
|
||||||
@ -35,7 +35,7 @@ class AlbumHeader(
|
|||||||
val years: Set<Int>
|
val years: Set<Int>
|
||||||
get() = _years
|
get() = _years
|
||||||
|
|
||||||
private fun processGrandParents(entry: MusicDirectory.Entry) {
|
private fun processGrandParents(entry: MusicDirectory.Child) {
|
||||||
val grandParent = getGrandparent(entry.path)
|
val grandParent = getGrandparent(entry.path)
|
||||||
if (grandParent != null) {
|
if (grandParent != null) {
|
||||||
_grandParents.add(grandParent)
|
_grandParents.add(grandParent)
|
||||||
@ -43,7 +43,7 @@ class AlbumHeader(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("NestedBlockDepth")
|
@Suppress("NestedBlockDepth")
|
||||||
private fun processEntries(list: List<MusicDirectory.Entry>) {
|
private fun processEntries(list: List<MusicDirectory.Child>) {
|
||||||
entries = list
|
entries = list
|
||||||
childCount = entries.size
|
childCount = entries.size
|
||||||
for (entry in entries) {
|
for (entry in entries) {
|
@ -16,6 +16,7 @@ class DividerBinder : ItemViewBinder<DividerBinder.Divider, DividerBinder.ViewHo
|
|||||||
|
|
||||||
// Set our layout files
|
// Set our layout files
|
||||||
val layout = R.layout.list_item_divider
|
val layout = R.layout.list_item_divider
|
||||||
|
val more_button = R.layout.list_item_more_button
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, item: Divider) {
|
override fun onBindViewHolder(holder: ViewHolder, item: Divider) {
|
||||||
// Set text
|
// Set text
|
||||||
|
@ -15,7 +15,6 @@ import org.koin.core.component.KoinComponent
|
|||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
|
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
|
||||||
import org.moire.ultrasonic.util.AlbumHeader
|
|
||||||
import org.moire.ultrasonic.util.Util
|
import org.moire.ultrasonic.util.Util
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -0,0 +1,50 @@
|
|||||||
|
package org.moire.ultrasonic.adapters
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.drakeet.multitype.ItemViewBinder
|
||||||
|
import org.moire.ultrasonic.R
|
||||||
|
import org.moire.ultrasonic.domain.Identifiable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a row in a RecyclerView which can be used as a divide between different sections
|
||||||
|
*/
|
||||||
|
class MoreButtonBinder : ItemViewBinder<MoreButtonBinder.MoreButton, RecyclerView.ViewHolder>() {
|
||||||
|
|
||||||
|
// Set our layout files
|
||||||
|
val layout = R.layout.list_item_more_button
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, item: MoreButton) {
|
||||||
|
holder.itemView.setOnClickListener {
|
||||||
|
item.onClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
parent: ViewGroup
|
||||||
|
): RecyclerView.ViewHolder {
|
||||||
|
return ViewHolder(inflater.inflate(layout, parent, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ViewHolder class
|
||||||
|
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
|
||||||
|
|
||||||
|
// Class to store our data into
|
||||||
|
data class MoreButton(
|
||||||
|
val stringId: Int,
|
||||||
|
val onClick: (() -> Unit)
|
||||||
|
): Identifiable {
|
||||||
|
|
||||||
|
override val id: String
|
||||||
|
get() = stringId.toString()
|
||||||
|
override val longId: Long
|
||||||
|
get() = stringId.toLong()
|
||||||
|
|
||||||
|
override fun compareTo(other: Identifiable): Int = longId.compareTo(other.longId)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -7,7 +7,8 @@ import org.moire.ultrasonic.api.subsonic.models.Album
|
|||||||
|
|
||||||
fun Album.toDomainEntity(): MusicDirectory.Album = MusicDirectory.Album(
|
fun Album.toDomainEntity(): MusicDirectory.Album = MusicDirectory.Album(
|
||||||
id = this@toDomainEntity.id,
|
id = this@toDomainEntity.id,
|
||||||
title = this@toDomainEntity.name,
|
title = this@toDomainEntity.title,
|
||||||
|
album = this@toDomainEntity.album,
|
||||||
coverArt = this@toDomainEntity.coverArt,
|
coverArt = this@toDomainEntity.coverArt,
|
||||||
artist = this@toDomainEntity.artist,
|
artist = this@toDomainEntity.artist,
|
||||||
artistId = this@toDomainEntity.artistId,
|
artistId = this@toDomainEntity.artistId,
|
||||||
|
@ -24,7 +24,6 @@ import org.moire.ultrasonic.util.Constants
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays a list of Albums from the media library
|
* Displays a list of Albums from the media library
|
||||||
* FIXME: Add music folder support
|
|
||||||
*/
|
*/
|
||||||
class AlbumListFragment : EntryListFragment<MusicDirectory.Album>() {
|
class AlbumListFragment : EntryListFragment<MusicDirectory.Album>() {
|
||||||
|
|
||||||
@ -41,10 +40,10 @@ class AlbumListFragment : EntryListFragment<MusicDirectory.Album>() {
|
|||||||
/**
|
/**
|
||||||
* 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
|
||||||
*/
|
*/
|
||||||
override fun getLiveData(args: Bundle?): LiveData<List<MusicDirectory.Album>> {
|
override fun getLiveData(args: Bundle?, refresh: Boolean): LiveData<List<MusicDirectory.Album>> {
|
||||||
if (args == null) throw IllegalArgumentException("Required arguments are missing")
|
if (args == null) throw IllegalArgumentException("Required arguments are missing")
|
||||||
|
|
||||||
val refresh = args.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH)
|
val refresh = args.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH) || refresh
|
||||||
val append = args.getBoolean(Constants.INTENT_EXTRA_NAME_APPEND)
|
val append = args.getBoolean(Constants.INTENT_EXTRA_NAME_APPEND)
|
||||||
|
|
||||||
return listModel.getAlbumList(refresh or append, refreshListView!!, args)
|
return listModel.getAlbumList(refresh or append, refreshListView!!, args)
|
||||||
@ -87,39 +86,4 @@ class AlbumListFragment : EntryListFragment<MusicDirectory.Album>() {
|
|||||||
bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, item.parent)
|
bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, item.parent)
|
||||||
findNavController().navigate(R.id.trackCollectionFragment, bundle)
|
findNavController().navigate(R.id.trackCollectionFragment, bundle)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* What to do when the list has changed
|
|
||||||
*/
|
|
||||||
override val defaultObserver: (List<MusicDirectory.Album>) -> Unit = {
|
|
||||||
emptyView.isVisible = it.isEmpty()
|
|
||||||
|
|
||||||
if (showFolderHeader()) {
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val list = it as MutableList<Identifiable>
|
|
||||||
list.add(0, folderHeader)
|
|
||||||
} else {
|
|
||||||
viewAdapter.submitList(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a folder header and update it on changes
|
|
||||||
*/
|
|
||||||
private val folderHeader: FolderSelectorBinder.FolderHeader by lazy {
|
|
||||||
val header = FolderSelectorBinder.FolderHeader(
|
|
||||||
listModel.musicFolders.value!!,
|
|
||||||
listModel.activeServer.musicFolderId
|
|
||||||
)
|
|
||||||
|
|
||||||
listModel.musicFolders.observe(
|
|
||||||
viewLifecycleOwner,
|
|
||||||
{
|
|
||||||
header.folders = it
|
|
||||||
viewAdapter.notifyItemChanged(0)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
header
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -2,19 +2,25 @@ package org.moire.ultrasonic.fragment
|
|||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
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 androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.adapters.ArtistRowBinder
|
import org.moire.ultrasonic.adapters.ArtistRowBinder
|
||||||
|
import org.moire.ultrasonic.adapters.FolderSelectorBinder
|
||||||
import org.moire.ultrasonic.domain.Artist
|
import org.moire.ultrasonic.domain.Artist
|
||||||
import org.moire.ultrasonic.domain.ArtistOrIndex
|
import org.moire.ultrasonic.domain.ArtistOrIndex
|
||||||
|
import org.moire.ultrasonic.domain.Identifiable
|
||||||
import org.moire.ultrasonic.domain.Index
|
import org.moire.ultrasonic.domain.Index
|
||||||
|
import org.moire.ultrasonic.domain.MusicDirectory
|
||||||
import org.moire.ultrasonic.model.ArtistListModel
|
import org.moire.ultrasonic.model.ArtistListModel
|
||||||
import org.moire.ultrasonic.util.Constants
|
import org.moire.ultrasonic.util.Constants
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays the list of Artists from the media library
|
* Displays the list of Artists from the media library
|
||||||
|
*
|
||||||
|
* FIXME: FOLDER HEADER NOT POPULATED ON FIST LOAD
|
||||||
*/
|
*/
|
||||||
class ArtistListFragment : EntryListFragment<ArtistOrIndex>() {
|
class ArtistListFragment : EntryListFragment<ArtistOrIndex>() {
|
||||||
|
|
||||||
@ -31,8 +37,8 @@ class ArtistListFragment : EntryListFragment<ArtistOrIndex>() {
|
|||||||
/**
|
/**
|
||||||
* 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
|
||||||
*/
|
*/
|
||||||
override fun getLiveData(args: Bundle?): LiveData<List<ArtistOrIndex>> {
|
override fun getLiveData(args: Bundle?, refresh: Boolean): LiveData<List<ArtistOrIndex>> {
|
||||||
val refresh = args?.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH) ?: false
|
val refresh = args?.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH) ?: false || refresh
|
||||||
return listModel.getItems(refresh, refreshListView!!)
|
return listModel.getItems(refresh, refreshListView!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,7 +24,6 @@ import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
|
|||||||
* audio books etc.
|
* audio books etc.
|
||||||
*
|
*
|
||||||
* Therefore this fragment allows only for singular selection and playback.
|
* Therefore this fragment allows only for singular selection and playback.
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
class BookmarksFragment : TrackCollectionFragment() {
|
class BookmarksFragment : TrackCollectionFragment() {
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
@ -35,7 +34,7 @@ class BookmarksFragment : TrackCollectionFragment() {
|
|||||||
viewAdapter.selectionType = BaseAdapter.SelectionType.SINGLE
|
viewAdapter.selectionType = BaseAdapter.SelectionType.SINGLE
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLiveData(args: Bundle?): LiveData<List<MusicDirectory.Entry>> {
|
override fun getLiveData(args: Bundle?, refresh: Boolean): LiveData<List<MusicDirectory.Child>> {
|
||||||
listModel.viewModelScope.launch(handler) {
|
listModel.viewModelScope.launch(handler) {
|
||||||
refreshListView?.isRefreshing = true
|
refreshListView?.isRefreshing = true
|
||||||
listModel.getBookmarks()
|
listModel.getBookmarks()
|
||||||
|
@ -41,7 +41,7 @@ class DownloadsFragment : MultiListFragment<DownloadFile>() {
|
|||||||
/**
|
/**
|
||||||
* 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
|
||||||
*/
|
*/
|
||||||
override fun getLiveData(args: Bundle?): LiveData<List<DownloadFile>> {
|
override fun getLiveData(args: Bundle?, refresh: Boolean): LiveData<List<DownloadFile>> {
|
||||||
return listModel.getList()
|
return listModel.getList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,11 +3,13 @@ package org.moire.ultrasonic.fragment
|
|||||||
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.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.adapters.FolderSelectorBinder
|
import org.moire.ultrasonic.adapters.FolderSelectorBinder
|
||||||
import org.moire.ultrasonic.domain.Artist
|
import org.moire.ultrasonic.domain.Artist
|
||||||
|
import org.moire.ultrasonic.domain.ArtistOrIndex
|
||||||
import org.moire.ultrasonic.domain.GenericEntry
|
import org.moire.ultrasonic.domain.GenericEntry
|
||||||
import org.moire.ultrasonic.domain.Identifiable
|
import org.moire.ultrasonic.domain.Identifiable
|
||||||
import org.moire.ultrasonic.service.RxBus
|
import org.moire.ultrasonic.service.RxBus
|
||||||
@ -48,15 +50,12 @@ abstract class EntryListFragment<T : GenericEntry> : MultiListFragment<T>() {
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
// FIXME: What to do when the user has modified the folder filter
|
|
||||||
RxBus.musicFolderChangedEventObservable.subscribe {
|
RxBus.musicFolderChangedEventObservable.subscribe {
|
||||||
if (!listModel.isOffline()) {
|
if (!listModel.isOffline()) {
|
||||||
val currentSetting = listModel.activeServer
|
val currentSetting = listModel.activeServer
|
||||||
currentSetting.musicFolderId = it
|
currentSetting.musicFolderId = it
|
||||||
serverSettingsModel.updateItem(currentSetting)
|
serverSettingsModel.updateItem(currentSetting)
|
||||||
}
|
}
|
||||||
// FIXME: Needed?
|
|
||||||
viewAdapter.notifyDataSetChanged()
|
|
||||||
listModel.refresh(refreshListView!!, arguments)
|
listModel.refresh(refreshListView!!, arguments)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,6 +64,41 @@ abstract class EntryListFragment<T : GenericEntry> : MultiListFragment<T>() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What to do when the list has changed
|
||||||
|
*/
|
||||||
|
override val defaultObserver: (List<T>) -> Unit = {
|
||||||
|
emptyView.isVisible = it.isEmpty()
|
||||||
|
|
||||||
|
if (showFolderHeader()) {
|
||||||
|
val list = mutableListOf<Identifiable>(folderHeader)
|
||||||
|
list.addAll(it)
|
||||||
|
viewAdapter.submitList(list)
|
||||||
|
} else {
|
||||||
|
viewAdapter.submitList(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a folder header and update it on changes
|
||||||
|
*/
|
||||||
|
private val folderHeader: FolderSelectorBinder.FolderHeader by lazy {
|
||||||
|
val header = FolderSelectorBinder.FolderHeader(
|
||||||
|
listModel.musicFolders.value!!,
|
||||||
|
listModel.activeServer.musicFolderId
|
||||||
|
)
|
||||||
|
|
||||||
|
listModel.musicFolders.observe(
|
||||||
|
viewLifecycleOwner,
|
||||||
|
{
|
||||||
|
header.folders = it
|
||||||
|
viewAdapter.notifyItemChanged(0)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
header
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@Suppress("LongMethod")
|
@Suppress("LongMethod")
|
||||||
internal fun handleContextMenu(
|
internal fun handleContextMenu(
|
||||||
|
@ -72,7 +72,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, refresh: Boolean = false): LiveData<List<T>> {
|
||||||
return MutableLiveData()
|
return MutableLiveData()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,7 +123,7 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Populate the LiveData. This starts an API request in most cases
|
// Populate the LiveData. This starts an API request in most cases
|
||||||
liveDataItems = getLiveData(arguments)
|
liveDataItems = getLiveData(arguments, true)
|
||||||
|
|
||||||
// Link view to display text if the list is empty
|
// Link view to display text if the list is empty
|
||||||
emptyView = view.findViewById(emptyViewId)
|
emptyView = view.findViewById(emptyViewId)
|
||||||
|
@ -22,6 +22,8 @@ import org.moire.ultrasonic.R
|
|||||||
import org.moire.ultrasonic.adapters.AlbumRowBinder
|
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.MoreButtonBinder
|
||||||
|
import org.moire.ultrasonic.adapters.MoreButtonBinder.MoreButton
|
||||||
import org.moire.ultrasonic.adapters.TrackViewBinder
|
import org.moire.ultrasonic.adapters.TrackViewBinder
|
||||||
import org.moire.ultrasonic.domain.Artist
|
import org.moire.ultrasonic.domain.Artist
|
||||||
import org.moire.ultrasonic.domain.Identifiable
|
import org.moire.ultrasonic.domain.Identifiable
|
||||||
@ -44,18 +46,10 @@ import timber.log.Timber
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Initiates a search on the media library and displays the results
|
* Initiates a search on the media library and displays the results
|
||||||
*
|
* FIXME: Artist click, display
|
||||||
* FIXME: Handle context click on song
|
|
||||||
*/
|
*/
|
||||||
class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
||||||
private var moreArtistsButton: View? = null
|
|
||||||
private var moreAlbumsButton: View? = null
|
|
||||||
private var moreSongsButton: View? = null
|
|
||||||
private var searchResult: SearchResult? = null
|
private var searchResult: SearchResult? = null
|
||||||
private var artistAdapter: ArtistAdapter? = null
|
|
||||||
private var moreArtistsAdapter: ListAdapter? = null
|
|
||||||
private var moreAlbumsAdapter: ListAdapter? = null
|
|
||||||
private var moreSongsAdapter: ListAdapter? = null
|
|
||||||
private var searchRefresh: SwipeRefreshLayout? = null
|
private var searchRefresh: SwipeRefreshLayout? = null
|
||||||
|
|
||||||
private val mediaPlayerController: MediaPlayerController by inject()
|
private val mediaPlayerController: MediaPlayerController by inject()
|
||||||
@ -75,40 +69,20 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
|||||||
setTitle(this, R.string.search_title)
|
setTitle(this, R.string.search_title)
|
||||||
setHasOptionsMenu(true)
|
setHasOptionsMenu(true)
|
||||||
|
|
||||||
val buttons = LayoutInflater.from(context).inflate(
|
|
||||||
R.layout.search_buttons,
|
|
||||||
listView, false
|
|
||||||
)
|
|
||||||
|
|
||||||
if (buttons != null) {
|
|
||||||
moreArtistsButton = buttons.findViewById(R.id.search_more_artists)
|
|
||||||
moreAlbumsButton = buttons.findViewById(R.id.search_more_albums)
|
|
||||||
moreSongsButton = buttons.findViewById(R.id.search_more_songs)
|
|
||||||
}
|
|
||||||
|
|
||||||
listModel.searchResult.observe(
|
listModel.searchResult.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner,
|
||||||
{
|
{
|
||||||
if (it != null) populateList(it)
|
if (it != null) {
|
||||||
|
// Shorten the display initially
|
||||||
|
searchResult = it
|
||||||
|
populateList(listModel.trimResultLength(it))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
searchRefresh = view.findViewById(R.id.swipe_refresh_view)
|
searchRefresh = view.findViewById(R.id.swipe_refresh_view)
|
||||||
searchRefresh!!.isEnabled = false
|
searchRefresh!!.isEnabled = false
|
||||||
|
|
||||||
// list.setOnItemClickListener(OnItemClickListener { parent: AdapterView<*>, view1: View, position: Int, id: Long ->
|
|
||||||
// if (view1 === moreArtistsButton) {
|
|
||||||
// expandArtists()
|
|
||||||
// } else if (view1 === moreAlbumsButton) {
|
|
||||||
// expandAlbums()
|
|
||||||
// } else if (view1 === moreSongsButton) {
|
|
||||||
// expandSongs()
|
|
||||||
// } else {
|
|
||||||
// val item = parent.getItemAtPosition(position)
|
|
||||||
//
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
|
|
||||||
registerForContextMenu(listView!!)
|
registerForContextMenu(listView!!)
|
||||||
|
|
||||||
// Register our data binders
|
// Register our data binders
|
||||||
@ -147,6 +121,10 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
|||||||
DividerBinder()
|
DividerBinder()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
viewAdapter.register(
|
||||||
|
MoreButtonBinder()
|
||||||
|
)
|
||||||
|
|
||||||
// Fragment was started with a query (e.g. from voice search), try to execute search right away
|
// Fragment was started with a query (e.g. from voice search), try to execute search right away
|
||||||
val arguments = arguments
|
val arguments = arguments
|
||||||
if (arguments != null) {
|
if (arguments != null) {
|
||||||
@ -229,45 +207,44 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun search(query: String, autoplay: Boolean) {
|
private fun search(query: String, autoplay: Boolean) {
|
||||||
// FIXME support autoplay
|
|
||||||
listModel.viewModelScope.launch(CommunicationError.getHandler(context)) {
|
listModel.viewModelScope.launch(CommunicationError.getHandler(context)) {
|
||||||
refreshListView?.isRefreshing = true
|
refreshListView?.isRefreshing = true
|
||||||
listModel.search(query)
|
listModel.search(query)
|
||||||
refreshListView?.isRefreshing = false
|
refreshListView?.isRefreshing = false
|
||||||
|
}.invokeOnCompletion {
|
||||||
|
if (it == null && autoplay) {
|
||||||
|
autoplay()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun populateList(result: SearchResult) {
|
private fun populateList(result: SearchResult) {
|
||||||
val searchResult = listModel.trimResultLength(result)
|
|
||||||
|
|
||||||
val list = mutableListOf<Identifiable>()
|
val list = mutableListOf<Identifiable>()
|
||||||
|
|
||||||
val artists = searchResult.artists
|
val artists = result.artists
|
||||||
if (artists.isNotEmpty()) {
|
if (artists.isNotEmpty()) {
|
||||||
|
|
||||||
list.add(DividerBinder.Divider(R.string.search_artists))
|
list.add(DividerBinder.Divider(R.string.search_artists))
|
||||||
list.addAll(artists)
|
list.addAll(artists)
|
||||||
if (artists.size > DEFAULT_ARTISTS) {
|
if (searchResult!!.artists.size > artists.size) {
|
||||||
// FIXME
|
list.add(MoreButton(0, ::expandArtists))
|
||||||
// list.add((moreArtistsButton, true)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val albums = searchResult.albums
|
val albums = result.albums
|
||||||
if (albums.isNotEmpty()) {
|
if (albums.isNotEmpty()) {
|
||||||
list.add(DividerBinder.Divider(R.string.search_albums))
|
list.add(DividerBinder.Divider(R.string.search_albums))
|
||||||
list.addAll(albums)
|
list.addAll(albums)
|
||||||
// mergeAdapter!!.addAdapter(albumAdapter)
|
if (searchResult!!.albums.size > albums.size) {
|
||||||
// if (albums.size > DEFAULT_ALBUMS) {
|
list.add(MoreButton(1, ::expandAlbums))
|
||||||
// moreAlbumsAdapter = mergeAdapter!!.addView(moreAlbumsButton, true)
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
val songs = searchResult.songs
|
}
|
||||||
|
val songs = result.songs
|
||||||
if (songs.isNotEmpty()) {
|
if (songs.isNotEmpty()) {
|
||||||
list.add(DividerBinder.Divider(R.string.search_songs))
|
list.add(DividerBinder.Divider(R.string.search_songs))
|
||||||
list.addAll(songs)
|
list.addAll(songs)
|
||||||
// if (songs.size > DEFAULT_SONGS) {
|
if (searchResult!!.songs.size > songs.size) {
|
||||||
// moreSongsAdapter = mergeAdapter!!.addView(moreSongsButton, true)
|
list.add(MoreButton(2, ::expandSongs))
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show/hide the empty text view
|
// Show/hide the empty text view
|
||||||
@ -276,35 +253,17 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
|||||||
viewAdapter.submitList(list)
|
viewAdapter.submitList(list)
|
||||||
}
|
}
|
||||||
|
|
||||||
// private fun expandArtists() {
|
private fun expandArtists() {
|
||||||
// artistAdapter!!.clear()
|
populateList(listModel.trimResultLength(searchResult!!, maxArtists = Int.MAX_VALUE))
|
||||||
// for (artist in searchResult!!.artists) {
|
}
|
||||||
// artistAdapter!!.add(artist)
|
|
||||||
// }
|
private fun expandAlbums() {
|
||||||
// artistAdapter!!.notifyDataSetChanged()
|
populateList(listModel.trimResultLength(searchResult!!, maxAlbums = Int.MAX_VALUE))
|
||||||
// mergeAdapter!!.removeAdapter(moreArtistsAdapter)
|
}
|
||||||
// mergeAdapter!!.notifyDataSetChanged()
|
|
||||||
// }
|
private fun expandSongs() {
|
||||||
//
|
populateList(listModel.trimResultLength(searchResult!!, maxSongs = Int.MAX_VALUE))
|
||||||
// private fun expandAlbums() {
|
}
|
||||||
// albumAdapter!!.clear()
|
|
||||||
// for (album in searchResult!!.albums) {
|
|
||||||
// albumAdapter!!.add(album)
|
|
||||||
// }
|
|
||||||
// albumAdapter!!.notifyDataSetChanged()
|
|
||||||
// mergeAdapter!!.removeAdapter(moreAlbumsAdapter)
|
|
||||||
// mergeAdapter!!.notifyDataSetChanged()
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private fun expandSongs() {
|
|
||||||
// songAdapter!!.clear()
|
|
||||||
// for (song in searchResult!!.songs) {
|
|
||||||
// songAdapter!!.add(song)
|
|
||||||
// }
|
|
||||||
// songAdapter!!.notifyDataSetChanged()
|
|
||||||
// mergeAdapter!!.removeAdapter(moreSongsAdapter)
|
|
||||||
// mergeAdapter!!.notifyDataSetChanged()
|
|
||||||
// }
|
|
||||||
|
|
||||||
private fun onArtistSelected(artist: Artist) {
|
private fun onArtistSelected(artist: Artist) {
|
||||||
val bundle = Bundle()
|
val bundle = Bundle()
|
||||||
@ -343,12 +302,6 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
var DEFAULT_ARTISTS = Settings.defaultArtists
|
|
||||||
var DEFAULT_ALBUMS = Settings.defaultAlbums
|
|
||||||
var DEFAULT_SONGS = Settings.defaultSongs
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemClick(item: Identifiable) {
|
override fun onItemClick(item: Identifiable) {
|
||||||
when (item) {
|
when (item) {
|
||||||
is Artist -> {
|
is Artist -> {
|
||||||
@ -464,4 +417,10 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
|||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
var DEFAULT_ARTISTS = Settings.defaultArtists
|
||||||
|
var DEFAULT_ALBUMS = Settings.defaultAlbums
|
||||||
|
var DEFAULT_SONGS = Settings.defaultSongs
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,8 @@ import kotlinx.coroutines.CoroutineExceptionHandler
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
|
import org.moire.ultrasonic.adapters.AlbumHeader
|
||||||
|
import org.moire.ultrasonic.adapters.AlbumRowBinder
|
||||||
import org.moire.ultrasonic.adapters.HeaderViewBinder
|
import org.moire.ultrasonic.adapters.HeaderViewBinder
|
||||||
import org.moire.ultrasonic.adapters.TrackViewBinder
|
import org.moire.ultrasonic.adapters.TrackViewBinder
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
||||||
@ -39,7 +41,6 @@ import org.moire.ultrasonic.service.MediaPlayerController
|
|||||||
import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker
|
import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker
|
||||||
import org.moire.ultrasonic.subsonic.ShareHandler
|
import org.moire.ultrasonic.subsonic.ShareHandler
|
||||||
import org.moire.ultrasonic.subsonic.VideoPlayer
|
import org.moire.ultrasonic.subsonic.VideoPlayer
|
||||||
import org.moire.ultrasonic.util.AlbumHeader
|
|
||||||
import org.moire.ultrasonic.util.CancellationToken
|
import org.moire.ultrasonic.util.CancellationToken
|
||||||
import org.moire.ultrasonic.util.CommunicationError
|
import org.moire.ultrasonic.util.CommunicationError
|
||||||
import org.moire.ultrasonic.util.Constants
|
import org.moire.ultrasonic.util.Constants
|
||||||
@ -48,11 +49,16 @@ import org.moire.ultrasonic.util.Settings
|
|||||||
import org.moire.ultrasonic.util.Util
|
import org.moire.ultrasonic.util.Util
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
*
|
||||||
* Displays a group of tracks, eg. the songs of an album, of a playlist etc.
|
* Displays a group of tracks, eg. the songs of an album, of a playlist etc.
|
||||||
* FIXME: Mixed lists are not handled correctly
|
*
|
||||||
|
* In most cases the data should be just a list of Entries, but there are some cases
|
||||||
|
* where the list can contain Albums as well. This happens especially when having ID3 tags disabled,
|
||||||
|
* or using Offline mode, both in which Indexes instead of Artists are being used.
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
@Suppress("TooManyFunctions")
|
@Suppress("TooManyFunctions")
|
||||||
open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Entry>() {
|
open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
|
||||||
|
|
||||||
private var albumButtons: View? = null
|
private var albumButtons: View? = null
|
||||||
internal var selectButton: ImageView? = null
|
internal var selectButton: ImageView? = null
|
||||||
@ -128,6 +134,15 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Entry>() {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
viewAdapter.register(
|
||||||
|
AlbumRowBinder(
|
||||||
|
{ entry -> onItemClick(entry) },
|
||||||
|
{ menuItem, entry -> onContextMenuItemSelected(menuItem, entry) },
|
||||||
|
imageLoaderProvider.getImageLoader(),
|
||||||
|
context = requireContext()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
enableButtons()
|
enableButtons()
|
||||||
|
|
||||||
// Update the buttons when the selection has changed
|
// Update the buttons when the selection has changed
|
||||||
@ -447,9 +462,9 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Entry>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override val defaultObserver: (List<MusicDirectory.Entry>) -> Unit = {
|
override val defaultObserver: (List<MusicDirectory.Child>) -> Unit = {
|
||||||
|
|
||||||
val entryList: MutableList<MusicDirectory.Entry> = it.toMutableList()
|
val entryList: MutableList<MusicDirectory.Child> = it.toMutableList()
|
||||||
|
|
||||||
if (listModel.currentListIsSortable && Settings.shouldSortByDisc) {
|
if (listModel.currentListIsSortable && Settings.shouldSortByDisc) {
|
||||||
Collections.sort(entryList, EntryByDiscAndTrackComparator())
|
Collections.sort(entryList, EntryByDiscAndTrackComparator())
|
||||||
@ -470,7 +485,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Entry>() {
|
|||||||
val listSize = arguments?.getInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0) ?: 0
|
val listSize = arguments?.getInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0) ?: 0
|
||||||
|
|
||||||
// Hide select button for video lists and singular selection lists
|
// Hide select button for video lists and singular selection lists
|
||||||
selectButton!!.isVisible = (!allVideos && viewAdapter.hasMultipleSelection())
|
selectButton!!.isVisible = !allVideos && viewAdapter.hasMultipleSelection() && songCount > 0
|
||||||
|
|
||||||
if (songCount > 0) {
|
if (songCount > 0) {
|
||||||
if (listSize == 0 || songCount < listSize) {
|
if (listSize == 0 || songCount < listSize) {
|
||||||
@ -550,12 +565,11 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Entry>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("LongMethod")
|
@Suppress("LongMethod")
|
||||||
override fun getLiveData(args: Bundle?): LiveData<List<MusicDirectory.Entry>> {
|
override fun getLiveData(args: Bundle?, refresh: Boolean): LiveData<List<MusicDirectory.Child>> {
|
||||||
if (args == null) return listModel.currentList
|
if (args == null) return listModel.currentList
|
||||||
val id = args.getString(Constants.INTENT_EXTRA_NAME_ID)
|
val id = args.getString(Constants.INTENT_EXTRA_NAME_ID)
|
||||||
val isAlbum = args.getBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, false)
|
val isAlbum = args.getBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, false)
|
||||||
val name = args.getString(Constants.INTENT_EXTRA_NAME_NAME)
|
val name = args.getString(Constants.INTENT_EXTRA_NAME_NAME)
|
||||||
val parentId = args.getString(Constants.INTENT_EXTRA_NAME_PARENT_ID)
|
|
||||||
val playlistId = args.getString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID)
|
val playlistId = args.getString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID)
|
||||||
val podcastChannelId = args.getString(
|
val podcastChannelId = args.getString(
|
||||||
Constants.INTENT_EXTRA_NAME_PODCAST_CHANNEL_ID
|
Constants.INTENT_EXTRA_NAME_PODCAST_CHANNEL_ID
|
||||||
@ -574,7 +588,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Entry>() {
|
|||||||
val albumListOffset = args.getInt(
|
val albumListOffset = args.getInt(
|
||||||
Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0
|
Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0
|
||||||
)
|
)
|
||||||
val refresh = args.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH, true)
|
val refresh = args.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH, true) || refresh
|
||||||
|
|
||||||
listModel.viewModelScope.launch(handler) {
|
listModel.viewModelScope.launch(handler) {
|
||||||
refreshListView?.isRefreshing = true
|
refreshListView?.isRefreshing = true
|
||||||
@ -621,7 +635,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Entry>() {
|
|||||||
@Suppress("LongMethod")
|
@Suppress("LongMethod")
|
||||||
override fun onContextMenuItemSelected(
|
override fun onContextMenuItemSelected(
|
||||||
menuItem: MenuItem,
|
menuItem: MenuItem,
|
||||||
item: MusicDirectory.Entry
|
item: MusicDirectory.Child
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val entryId = item.id
|
val entryId = item.id
|
||||||
|
|
||||||
@ -673,13 +687,12 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Entry>() {
|
|||||||
playAll()
|
playAll()
|
||||||
}
|
}
|
||||||
R.id.menu_item_share -> {
|
R.id.menu_item_share -> {
|
||||||
val entries: MutableList<MusicDirectory.Entry?> = ArrayList(1)
|
if (item is MusicDirectory.Entry) {
|
||||||
entries.add(item)
|
|
||||||
shareHandler.createShare(
|
shareHandler.createShare(
|
||||||
this, entries, refreshListView,
|
this, listOf(item), refreshListView,
|
||||||
cancellationToken!!
|
cancellationToken!!
|
||||||
)
|
)
|
||||||
return true
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
return super.onContextItemSelected(menuItem)
|
return super.onContextItemSelected(menuItem)
|
||||||
@ -688,7 +701,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Entry>() {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemClick(item: MusicDirectory.Entry) {
|
override fun onItemClick(item: MusicDirectory.Child) {
|
||||||
when {
|
when {
|
||||||
item.isDirectory -> {
|
item.isDirectory -> {
|
||||||
val bundle = Bundle()
|
val bundle = Bundle()
|
||||||
@ -701,7 +714,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Entry>() {
|
|||||||
bundle
|
bundle
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
item.isVideo -> {
|
item is MusicDirectory.Entry && item.isVideo -> {
|
||||||
VideoPlayer.playVideo(requireContext(), item)
|
VideoPlayer.playVideo(requireContext(), item)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
|
@ -33,7 +33,12 @@ class AlbumListModel(application: Application) : GenericListModel(application) {
|
|||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAlbumsOfArtist(musicService: MusicService, refresh: Boolean, id: String, name: String?) {
|
private fun getAlbumsOfArtist(
|
||||||
|
musicService: MusicService,
|
||||||
|
refresh: Boolean,
|
||||||
|
id: String,
|
||||||
|
name: String?
|
||||||
|
) {
|
||||||
list.postValue(musicService.getArtist(id, name, refresh))
|
list.postValue(musicService.getArtist(id, name, refresh))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,7 +56,7 @@ class AlbumListModel(application: Application) : GenericListModel(application) {
|
|||||||
var offset = args.getInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0)
|
var offset = args.getInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0)
|
||||||
val append = args.getBoolean(Constants.INTENT_EXTRA_NAME_APPEND, false)
|
val append = args.getBoolean(Constants.INTENT_EXTRA_NAME_APPEND, false)
|
||||||
|
|
||||||
val musicDirectory: MusicDirectory
|
val musicDirectory: List<MusicDirectory.Album>
|
||||||
val musicFolderId = if (showSelectFolderHeader(args)) {
|
val musicFolderId = if (showSelectFolderHeader(args)) {
|
||||||
activeServerProvider.getActiveServer().musicFolderId
|
activeServerProvider.getActiveServer().musicFolderId
|
||||||
} else {
|
} else {
|
||||||
@ -72,7 +77,8 @@ class AlbumListModel(application: Application) : GenericListModel(application) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (useId3Tags) {
|
if (useId3Tags) {
|
||||||
musicDirectory = musicService.getAlbumList2(
|
musicDirectory =
|
||||||
|
musicService.getAlbumList2(
|
||||||
albumListType, size,
|
albumListType, size,
|
||||||
offset, musicFolderId
|
offset, musicFolderId
|
||||||
)
|
)
|
||||||
@ -85,15 +91,13 @@ class AlbumListModel(application: Application) : GenericListModel(application) {
|
|||||||
|
|
||||||
currentListIsSortable = isCollectionSortable(albumListType)
|
currentListIsSortable = isCollectionSortable(albumListType)
|
||||||
|
|
||||||
// TODO: Change signature of musicService.getAlbumList to return a List
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
if (append && list.value != null) {
|
if (append && list.value != null) {
|
||||||
val list = ArrayList<MusicDirectory.Child>()
|
val newList = ArrayList<MusicDirectory.Album>()
|
||||||
list.addAll(this.list.value!!)
|
newList.addAll(list.value!!)
|
||||||
list.addAll(musicDirectory.getChildren())
|
newList.addAll(musicDirectory)
|
||||||
this.list.postValue(list as List<MusicDirectory.Album>)
|
this.list.postValue(newList)
|
||||||
} else {
|
} else {
|
||||||
list.postValue(musicDirectory.getChildren() as List<MusicDirectory.Album>)
|
list.postValue(musicDirectory)
|
||||||
}
|
}
|
||||||
|
|
||||||
loadedUntil = offset
|
loadedUntil = offset
|
||||||
|
@ -16,7 +16,6 @@ import org.koin.core.component.KoinComponent
|
|||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||||
import org.moire.ultrasonic.data.ServerSetting
|
import org.moire.ultrasonic.data.ServerSetting
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory
|
|
||||||
import org.moire.ultrasonic.domain.MusicFolder
|
import org.moire.ultrasonic.domain.MusicFolder
|
||||||
import org.moire.ultrasonic.service.MusicService
|
import org.moire.ultrasonic.service.MusicService
|
||||||
import org.moire.ultrasonic.service.MusicServiceFactory
|
import org.moire.ultrasonic.service.MusicServiceFactory
|
||||||
@ -44,7 +43,7 @@ open class GenericListModel(application: Application) :
|
|||||||
|
|
||||||
@Suppress("UNUSED_PARAMETER")
|
@Suppress("UNUSED_PARAMETER")
|
||||||
open fun showSelectFolderHeader(args: Bundle?): Boolean {
|
open fun showSelectFolderHeader(args: Bundle?): Boolean {
|
||||||
return true
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -109,20 +108,11 @@ open class GenericListModel(application: Application) :
|
|||||||
args: Bundle
|
args: Bundle
|
||||||
) {
|
) {
|
||||||
// Update the list of available folders if enabled
|
// Update the list of available folders if enabled
|
||||||
// FIXME && refresh ?
|
@Suppress("ComplexCondition")
|
||||||
if (showSelectFolderHeader(args) && !isOffline && !useId3Tags) {
|
if (showSelectFolderHeader(args) && !isOffline && !useId3Tags && refresh) {
|
||||||
musicFolders.postValue(
|
musicFolders.postValue(
|
||||||
musicService.getMusicFolders(refresh)
|
musicService.getMusicFolders(refresh)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Some shared helper functions
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Returns true if the directory contains only folders
|
|
||||||
internal fun hasOnlyFolders(musicDirectory: MusicDirectory) =
|
|
||||||
musicDirectory.getChildren(includeDirs = true, includeFiles = false).size ==
|
|
||||||
musicDirectory.getChildren(includeDirs = true, includeFiles = true).size
|
|
||||||
}
|
}
|
||||||
|
@ -40,11 +40,16 @@ class SearchListModel(application: Application) : GenericListModel(application)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun trimResultLength(result: SearchResult): SearchResult {
|
fun trimResultLength(
|
||||||
|
result: SearchResult,
|
||||||
|
maxArtists: Int = SearchFragment.DEFAULT_ARTISTS,
|
||||||
|
maxAlbums: Int = SearchFragment.DEFAULT_ALBUMS,
|
||||||
|
maxSongs: Int = SearchFragment.DEFAULT_SONGS
|
||||||
|
): SearchResult {
|
||||||
return SearchResult(
|
return SearchResult(
|
||||||
artists = result.artists.take(SearchFragment.DEFAULT_ARTISTS),
|
artists = result.artists.take(maxArtists),
|
||||||
albums = result.albums.take(SearchFragment.DEFAULT_ALBUMS),
|
albums = result.albums.take(maxAlbums),
|
||||||
songs = result.songs.take(SearchFragment.DEFAULT_SONGS)
|
songs = result.songs.take(maxSongs)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ import org.moire.ultrasonic.util.Util
|
|||||||
*/
|
*/
|
||||||
class TrackCollectionModel(application: Application) : GenericListModel(application) {
|
class TrackCollectionModel(application: Application) : GenericListModel(application) {
|
||||||
|
|
||||||
val currentList: MutableLiveData<List<MusicDirectory.Entry>> = MutableLiveData()
|
val currentList: MutableLiveData<List<MusicDirectory.Child>> = MutableLiveData()
|
||||||
val songsForGenre: MutableLiveData<MusicDirectory> = MutableLiveData()
|
val songsForGenre: MutableLiveData<MusicDirectory> = MutableLiveData()
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -155,6 +155,6 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateList(root: MusicDirectory) {
|
private fun updateList(root: MusicDirectory) {
|
||||||
currentList.postValue(root.getTracks())
|
currentList.postValue(root.getChildren())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -575,7 +575,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
albums?.getChildren()?.map { album ->
|
albums?.map { album ->
|
||||||
mediaItems.add(
|
mediaItems.add(
|
||||||
album.title ?: "",
|
album.title ?: "",
|
||||||
listOf(MEDIA_ALBUM_ITEM, album.id, album.name)
|
listOf(MEDIA_ALBUM_ITEM, album.id, album.name)
|
||||||
|
@ -249,7 +249,7 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
|
|||||||
size: Int,
|
size: Int,
|
||||||
offset: Int,
|
offset: Int,
|
||||||
musicFolderId: String?
|
musicFolderId: String?
|
||||||
): MusicDirectory {
|
): List<MusicDirectory.Album> {
|
||||||
return musicService.getAlbumList(type, size, offset, musicFolderId)
|
return musicService.getAlbumList(type, size, offset, musicFolderId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -259,7 +259,7 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
|
|||||||
size: Int,
|
size: Int,
|
||||||
offset: Int,
|
offset: Int,
|
||||||
musicFolderId: String?
|
musicFolderId: String?
|
||||||
): MusicDirectory {
|
): List<MusicDirectory.Album> {
|
||||||
return musicService.getAlbumList2(type, size, offset, musicFolderId)
|
return musicService.getAlbumList2(type, size, offset, musicFolderId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,7 +90,12 @@ interface MusicService {
|
|||||||
fun scrobble(id: String, submission: Boolean)
|
fun scrobble(id: String, submission: Boolean)
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun getAlbumList(type: String, size: Int, offset: Int, musicFolderId: String?): MusicDirectory
|
fun getAlbumList(
|
||||||
|
type: String,
|
||||||
|
size: Int,
|
||||||
|
offset: Int,
|
||||||
|
musicFolderId: String?
|
||||||
|
): List<MusicDirectory.Album>
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun getAlbumList2(
|
fun getAlbumList2(
|
||||||
@ -98,7 +103,7 @@ interface MusicService {
|
|||||||
size: Int,
|
size: Int,
|
||||||
offset: Int,
|
offset: Int,
|
||||||
musicFolderId: String?
|
musicFolderId: String?
|
||||||
): MusicDirectory
|
): List<MusicDirectory.Album>
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun getRandomSongs(size: Int): MusicDirectory
|
fun getRandomSongs(size: Int): MusicDirectory
|
||||||
|
@ -296,10 +296,20 @@ class OfflineMusicService : MusicService, KoinComponent {
|
|||||||
size: Int,
|
size: Int,
|
||||||
offset: Int,
|
offset: Int,
|
||||||
musicFolderId: String?
|
musicFolderId: String?
|
||||||
): MusicDirectory {
|
): List<MusicDirectory.Album> {
|
||||||
throw OfflineException("Album lists not available in offline mode")
|
throw OfflineException("Album lists not available in offline mode")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Throws(OfflineException::class)
|
||||||
|
override fun getAlbumList2(
|
||||||
|
type: String,
|
||||||
|
size: Int,
|
||||||
|
offset: Int,
|
||||||
|
musicFolderId: String?
|
||||||
|
): List<MusicDirectory.Album> {
|
||||||
|
throw OfflineException("getAlbumList2 isn't available in offline mode")
|
||||||
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun updateJukeboxPlaylist(ids: List<String>?): JukeboxStatus {
|
override fun updateJukeboxPlaylist(ids: List<String>?): JukeboxStatus {
|
||||||
throw OfflineException("Jukebox not available in offline mode")
|
throw OfflineException("Jukebox not available in offline mode")
|
||||||
@ -389,16 +399,6 @@ class OfflineMusicService : MusicService, KoinComponent {
|
|||||||
throw OfflineException("Music folders not available in offline mode")
|
throw OfflineException("Music folders not available in offline mode")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(OfflineException::class)
|
|
||||||
override fun getAlbumList2(
|
|
||||||
type: String,
|
|
||||||
size: Int,
|
|
||||||
offset: Int,
|
|
||||||
musicFolderId: String?
|
|
||||||
): MusicDirectory {
|
|
||||||
throw OfflineException("getAlbumList2 isn't available in offline mode")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(OfflineException::class)
|
@Throws(OfflineException::class)
|
||||||
override fun getVideoUrl(id: String): String? {
|
override fun getVideoUrl(id: String): String? {
|
||||||
throw OfflineException("getVideoUrl isn't available in offline mode")
|
throw OfflineException("getVideoUrl isn't available in offline mode")
|
||||||
@ -512,7 +512,6 @@ class OfflineMusicService : MusicService, KoinComponent {
|
|||||||
return album
|
return album
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Extracts some basic data from a File object and applies it to an Album or Entry
|
* Extracts some basic data from a File object and applies it to an Album or Entry
|
||||||
*/
|
*/
|
||||||
@ -531,7 +530,6 @@ class OfflineMusicService : MusicService, KoinComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* More extensive variant of Child.populateWithDataFrom(), which also parses the ID3 tags of
|
* More extensive variant of Child.populateWithDataFrom(), which also parses the ID3 tags of
|
||||||
* a given track file.
|
* a given track file.
|
||||||
@ -559,7 +557,7 @@ class OfflineMusicService : MusicService, KoinComponent {
|
|||||||
|
|
||||||
artist = meta.artist ?: file.parentFile!!.parentFile!!.name
|
artist = meta.artist ?: file.parentFile!!.parentFile!!.name
|
||||||
album = meta.album ?: file.parentFile!!.name
|
album = meta.album ?: file.parentFile!!.name
|
||||||
title = meta.title?: title
|
title = meta.title ?: title
|
||||||
isVideo = meta.hasVideo != null
|
isVideo = meta.hasVideo != null
|
||||||
track = parseSlashedNumber(meta.track)
|
track = parseSlashedNumber(meta.track)
|
||||||
discNumber = parseSlashedNumber(meta.disc)
|
discNumber = parseSlashedNumber(meta.disc)
|
||||||
@ -660,7 +658,6 @@ class OfflineMusicService : MusicService, KoinComponent {
|
|||||||
return closeness
|
return closeness
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun listFilesRecursively(parent: File, children: MutableList<File>) {
|
private fun listFilesRecursively(parent: File, children: MutableList<File>) {
|
||||||
for (file in FileUtil.listMediaFiles(parent)) {
|
for (file in FileUtil.listMediaFiles(parent)) {
|
||||||
if (file.isFile) {
|
if (file.isFile) {
|
||||||
|
@ -350,7 +350,7 @@ open class RESTMusicService(
|
|||||||
size: Int,
|
size: Int,
|
||||||
offset: Int,
|
offset: Int,
|
||||||
musicFolderId: String?
|
musicFolderId: String?
|
||||||
): MusicDirectory {
|
): List<MusicDirectory.Album> {
|
||||||
val response = API.getAlbumList(
|
val response = API.getAlbumList(
|
||||||
fromName(type),
|
fromName(type),
|
||||||
size,
|
size,
|
||||||
@ -361,11 +361,8 @@ open class RESTMusicService(
|
|||||||
musicFolderId
|
musicFolderId
|
||||||
).execute().throwOnFailure()
|
).execute().throwOnFailure()
|
||||||
|
|
||||||
val childList = response.body()!!.albumList.toDomainEntityList()
|
|
||||||
val result = MusicDirectory()
|
|
||||||
result.addAll(childList)
|
|
||||||
|
|
||||||
return result
|
return response.body()!!.albumList.toDomainEntityList()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
@ -374,7 +371,7 @@ open class RESTMusicService(
|
|||||||
size: Int,
|
size: Int,
|
||||||
offset: Int,
|
offset: Int,
|
||||||
musicFolderId: String?
|
musicFolderId: String?
|
||||||
): MusicDirectory {
|
): List<MusicDirectory.Album> {
|
||||||
val response = API.getAlbumList2(
|
val response = API.getAlbumList2(
|
||||||
fromName(type),
|
fromName(type),
|
||||||
size,
|
size,
|
||||||
@ -385,10 +382,7 @@ open class RESTMusicService(
|
|||||||
musicFolderId
|
musicFolderId
|
||||||
).execute().throwOnFailure()
|
).execute().throwOnFailure()
|
||||||
|
|
||||||
val result = MusicDirectory()
|
return response.body()!!.albumList.toDomainEntityList()
|
||||||
result.addAll(response.body()!!.albumList.toDomainEntityList())
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
|
@ -0,0 +1,50 @@
|
|||||||
|
package org.moire.ultrasonic.util
|
||||||
|
|
||||||
|
import java.util.Comparator
|
||||||
|
import org.moire.ultrasonic.domain.MusicDirectory
|
||||||
|
|
||||||
|
class EntryByDiscAndTrackComparator : Comparator<MusicDirectory.Child> {
|
||||||
|
override fun compare(x: MusicDirectory.Child, y: MusicDirectory.Child): Int {
|
||||||
|
val discX = x.discNumber
|
||||||
|
val discY = y.discNumber
|
||||||
|
val trackX = if (x is MusicDirectory.Entry) x.track else null
|
||||||
|
val trackY = if (y is MusicDirectory.Entry) y.track else null
|
||||||
|
val albumX = x.album
|
||||||
|
val albumY = y.album
|
||||||
|
val pathX = x.path
|
||||||
|
val pathY = y.path
|
||||||
|
val albumComparison = compare(albumX, albumY)
|
||||||
|
if (albumComparison != 0) {
|
||||||
|
return albumComparison
|
||||||
|
}
|
||||||
|
val discComparison = compare(discX ?: 0, discY ?: 0)
|
||||||
|
if (discComparison != 0) {
|
||||||
|
return discComparison
|
||||||
|
}
|
||||||
|
val trackComparison = compare(trackX ?: 0, trackY ?: 0)
|
||||||
|
return if (trackComparison != 0) {
|
||||||
|
trackComparison
|
||||||
|
} else compare(
|
||||||
|
pathX ?: "",
|
||||||
|
pathY ?: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private fun compare(a: Int, b: Int): Int {
|
||||||
|
return a.compareTo(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun compare(a: String?, b: String?): Int {
|
||||||
|
if (a == null && b == null) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if (a == null) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return if (b == null) {
|
||||||
|
1
|
||||||
|
} else a.compareTo(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
ultrasonic/src/main/res/layout/list_item_more_button.xml
Normal file
16
ultrasonic/src/main/res/layout/list_item_more_button.xml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/search_more"
|
||||||
|
android:text="@string/search.more"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||||
|
android:gravity="center"
|
||||||
|
android:paddingTop="8dp"
|
||||||
|
android:paddingBottom="8dp"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
@ -1,39 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
|
|
||||||
a:orientation="vertical"
|
|
||||||
a:layout_width="fill_parent"
|
|
||||||
a:layout_height="wrap_content">
|
|
||||||
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
a:id="@+id/search_more_artists"
|
|
||||||
a:text="@string/search.more"
|
|
||||||
a:layout_width="fill_parent"
|
|
||||||
a:layout_height="wrap_content"
|
|
||||||
a:textAppearance="?android:attr/textAppearanceSmall"
|
|
||||||
a:gravity="center"
|
|
||||||
a:paddingTop="8dp"
|
|
||||||
a:paddingBottom="8dp"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
a:id="@+id/search_more_albums"
|
|
||||||
a:text="@string/search.more"
|
|
||||||
a:layout_width="fill_parent"
|
|
||||||
a:layout_height="wrap_content"
|
|
||||||
a:textAppearance="?android:attr/textAppearanceSmall"
|
|
||||||
a:gravity="center"
|
|
||||||
a:paddingTop="8dp"
|
|
||||||
a:paddingBottom="8dp"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
a:id="@+id/search_more_songs"
|
|
||||||
a:text="@string/search.more"
|
|
||||||
a:layout_width="fill_parent"
|
|
||||||
a:layout_height="wrap_content"
|
|
||||||
a:textAppearance="?android:attr/textAppearanceSmall"
|
|
||||||
a:gravity="center"
|
|
||||||
a:paddingTop="8dp"
|
|
||||||
a:paddingBottom="8dp"/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user