diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt index c8c319d7..b409acab 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt @@ -51,6 +51,7 @@ class MusicDirectory : ArrayList() { abstract var starred: Boolean abstract var path: String? abstract var closeness: Int + abstract var isVideo: Boolean } // TODO: Rename to Track @@ -77,7 +78,7 @@ class MusicDirectory : ArrayList() { override var duration: Int? = null, var bitRate: Int? = null, override var path: String? = null, - var isVideo: Boolean = false, + override var isVideo: Boolean = false, override var starred: Boolean = false, override var discNumber: Int? = null, var type: String? = null, @@ -133,5 +134,6 @@ class MusicDirectory : ArrayList() { override var closeness: Int = 0, ) : Child() { override var isDirectory = true + override var isVideo = false } } diff --git a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/GetAlbumListResponse.kt b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/GetAlbumListResponse.kt index 8e3ca708..81c6be5b 100644 --- a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/GetAlbumListResponse.kt +++ b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/GetAlbumListResponse.kt @@ -3,7 +3,7 @@ package org.moire.ultrasonic.api.subsonic.response import com.fasterxml.jackson.annotation.JsonProperty import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions 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( status: Status, @@ -12,10 +12,10 @@ class GetAlbumListResponse( ) : SubsonicResponse(status, version, error) { @JsonProperty("albumList") private val albumWrapper = AlbumWrapper() - val albumList: List + val albumList: List get() = albumWrapper.albumList } private class AlbumWrapper( - @JsonProperty("album") val albumList: List = emptyList() + @JsonProperty("album") val albumList: List = emptyList() ) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/EntryByDiscAndTrackComparator.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/EntryByDiscAndTrackComparator.java deleted file mode 100644 index cbf91c91..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/EntryByDiscAndTrackComparator.java +++ /dev/null @@ -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, 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); - } -} \ No newline at end of file diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/AlbumHeader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumHeader.kt similarity index 91% rename from ultrasonic/src/main/java/org/moire/ultrasonic/util/AlbumHeader.kt rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumHeader.kt index c049d49c..978ead6f 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/AlbumHeader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumHeader.kt @@ -1,4 +1,4 @@ -package org.moire.ultrasonic.util +package org.moire.ultrasonic.adapters import java.util.HashSet import org.moire.ultrasonic.domain.Identifiable @@ -7,7 +7,7 @@ import org.moire.ultrasonic.util.Settings.shouldUseFolderForArtistName import org.moire.ultrasonic.util.Util.getGrandparent class AlbumHeader( - var entries: List, + var entries: List, var name: String? ) : Identifiable { var isAllVideo: Boolean @@ -35,7 +35,7 @@ class AlbumHeader( val years: Set get() = _years - private fun processGrandParents(entry: MusicDirectory.Entry) { + private fun processGrandParents(entry: MusicDirectory.Child) { val grandParent = getGrandparent(entry.path) if (grandParent != null) { _grandParents.add(grandParent) @@ -43,7 +43,7 @@ class AlbumHeader( } @Suppress("NestedBlockDepth") - private fun processEntries(list: List) { + private fun processEntries(list: List) { entries = list childCount = entries.size for (entry in entries) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/DividerBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/DividerBinder.kt index 679839a7..b4f4627c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/DividerBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/DividerBinder.kt @@ -16,6 +16,7 @@ class DividerBinder : ItemViewBinder() { + + // 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) + } + +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIAlbumConverter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIAlbumConverter.kt index 6fc540a2..acccda31 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIAlbumConverter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIAlbumConverter.kt @@ -7,7 +7,8 @@ import org.moire.ultrasonic.api.subsonic.models.Album fun Album.toDomainEntity(): MusicDirectory.Album = MusicDirectory.Album( id = this@toDomainEntity.id, - title = this@toDomainEntity.name, + title = this@toDomainEntity.title, + album = this@toDomainEntity.album, coverArt = this@toDomainEntity.coverArt, artist = this@toDomainEntity.artist, artistId = this@toDomainEntity.artistId, diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt index 30a0a601..e719e8f8 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt @@ -24,7 +24,6 @@ import org.moire.ultrasonic.util.Constants /** * Displays a list of Albums from the media library - * FIXME: Add music folder support */ class AlbumListFragment : EntryListFragment() { @@ -41,10 +40,10 @@ class AlbumListFragment : EntryListFragment() { /** * The central function to pass a query to the model and return a LiveData object */ - override fun getLiveData(args: Bundle?): LiveData> { + override fun getLiveData(args: Bundle?, refresh: Boolean): LiveData> { 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) return listModel.getAlbumList(refresh or append, refreshListView!!, args) @@ -87,39 +86,4 @@ class AlbumListFragment : EntryListFragment() { bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, item.parent) findNavController().navigate(R.id.trackCollectionFragment, bundle) } - - /** - * What to do when the list has changed - */ - override val defaultObserver: (List) -> Unit = { - emptyView.isVisible = it.isEmpty() - - if (showFolderHeader()) { - @Suppress("UNCHECKED_CAST") - val list = it as MutableList - 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 - } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt index 1ec67ac3..73346de6 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt @@ -2,19 +2,25 @@ package org.moire.ultrasonic.fragment import android.os.Bundle import android.view.View +import androidx.core.view.isVisible import androidx.fragment.app.viewModels import androidx.lifecycle.LiveData import androidx.navigation.fragment.findNavController import org.moire.ultrasonic.R import org.moire.ultrasonic.adapters.ArtistRowBinder +import org.moire.ultrasonic.adapters.FolderSelectorBinder import org.moire.ultrasonic.domain.Artist import org.moire.ultrasonic.domain.ArtistOrIndex +import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.domain.Index +import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.model.ArtistListModel import org.moire.ultrasonic.util.Constants /** * Displays the list of Artists from the media library + * + * FIXME: FOLDER HEADER NOT POPULATED ON FIST LOAD */ class ArtistListFragment : EntryListFragment() { @@ -31,8 +37,8 @@ class ArtistListFragment : EntryListFragment() { /** * The central function to pass a query to the model and return a LiveData object */ - override fun getLiveData(args: Bundle?): LiveData> { - val refresh = args?.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH) ?: false + override fun getLiveData(args: Bundle?, refresh: Boolean): LiveData> { + val refresh = args?.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH) ?: false || refresh return listModel.getItems(refresh, refreshListView!!) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt index 9fdcfa5c..8f575ce7 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt @@ -24,7 +24,6 @@ import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle * audio books etc. * * Therefore this fragment allows only for singular selection and playback. - * */ class BookmarksFragment : TrackCollectionFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -35,7 +34,7 @@ class BookmarksFragment : TrackCollectionFragment() { viewAdapter.selectionType = BaseAdapter.SelectionType.SINGLE } - override fun getLiveData(args: Bundle?): LiveData> { + override fun getLiveData(args: Bundle?, refresh: Boolean): LiveData> { listModel.viewModelScope.launch(handler) { refreshListView?.isRefreshing = true listModel.getBookmarks() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt index 1ad3c658..86847435 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt @@ -41,7 +41,7 @@ class DownloadsFragment : MultiListFragment() { /** * The central function to pass a query to the model and return a LiveData object */ - override fun getLiveData(args: Bundle?): LiveData> { + override fun getLiveData(args: Bundle?, refresh: Boolean): LiveData> { return listModel.getList() } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt index 44dc1aef..00726af2 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt @@ -3,11 +3,13 @@ package org.moire.ultrasonic.fragment import android.os.Bundle import android.view.MenuItem import android.view.View +import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import org.moire.ultrasonic.R import org.moire.ultrasonic.adapters.FolderSelectorBinder import org.moire.ultrasonic.domain.Artist +import org.moire.ultrasonic.domain.ArtistOrIndex import org.moire.ultrasonic.domain.GenericEntry import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.service.RxBus @@ -48,15 +50,12 @@ abstract class EntryListFragment : MultiListFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - // FIXME: What to do when the user has modified the folder filter RxBus.musicFolderChangedEventObservable.subscribe { if (!listModel.isOffline()) { val currentSetting = listModel.activeServer currentSetting.musicFolderId = it serverSettingsModel.updateItem(currentSetting) } - // FIXME: Needed? - viewAdapter.notifyDataSetChanged() listModel.refresh(refreshListView!!, arguments) } @@ -65,6 +64,41 @@ abstract class EntryListFragment : MultiListFragment() { ) } + /** + * What to do when the list has changed + */ + override val defaultObserver: (List) -> Unit = { + emptyView.isVisible = it.isEmpty() + + if (showFolderHeader()) { + val list = mutableListOf(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 { @Suppress("LongMethod") internal fun handleContextMenu( diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt index 5588cebb..d0230a65 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt @@ -72,7 +72,7 @@ abstract class MultiListFragment : Fragment() { /** * The central function to pass a query to the model and return a LiveData object */ - open fun getLiveData(args: Bundle? = null): LiveData> { + open fun getLiveData(args: Bundle? = null, refresh: Boolean = false): LiveData> { return MutableLiveData() } @@ -123,7 +123,7 @@ abstract class MultiListFragment : Fragment() { } // 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 emptyView = view.findViewById(emptyViewId) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt index a9377cda..ded120e4 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt @@ -22,6 +22,8 @@ import org.moire.ultrasonic.R import org.moire.ultrasonic.adapters.AlbumRowBinder import org.moire.ultrasonic.adapters.ArtistRowBinder 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.domain.Artist 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 - * - * FIXME: Handle context click on song + * FIXME: Artist click, display */ class SearchFragment : MultiListFragment(), KoinComponent { - private var moreArtistsButton: View? = null - private var moreAlbumsButton: View? = null - private var moreSongsButton: View? = 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 val mediaPlayerController: MediaPlayerController by inject() @@ -75,40 +69,20 @@ class SearchFragment : MultiListFragment(), KoinComponent { setTitle(this, R.string.search_title) 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( 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!!.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!!) // Register our data binders @@ -147,6 +121,10 @@ class SearchFragment : MultiListFragment(), KoinComponent { DividerBinder() ) + viewAdapter.register( + MoreButtonBinder() + ) + // Fragment was started with a query (e.g. from voice search), try to execute search right away val arguments = arguments if (arguments != null) { @@ -229,45 +207,44 @@ class SearchFragment : MultiListFragment(), KoinComponent { } private fun search(query: String, autoplay: Boolean) { - // FIXME support autoplay listModel.viewModelScope.launch(CommunicationError.getHandler(context)) { refreshListView?.isRefreshing = true listModel.search(query) refreshListView?.isRefreshing = false + }.invokeOnCompletion { + if (it == null && autoplay) { + autoplay() + } } } private fun populateList(result: SearchResult) { - val searchResult = listModel.trimResultLength(result) - val list = mutableListOf() - val artists = searchResult.artists + val artists = result.artists if (artists.isNotEmpty()) { list.add(DividerBinder.Divider(R.string.search_artists)) list.addAll(artists) - if (artists.size > DEFAULT_ARTISTS) { - // FIXME - // list.add((moreArtistsButton, true) + if (searchResult!!.artists.size > artists.size) { + list.add(MoreButton(0, ::expandArtists)) } } - val albums = searchResult.albums + val albums = result.albums if (albums.isNotEmpty()) { list.add(DividerBinder.Divider(R.string.search_albums)) list.addAll(albums) - // mergeAdapter!!.addAdapter(albumAdapter) -// if (albums.size > DEFAULT_ALBUMS) { -// moreAlbumsAdapter = mergeAdapter!!.addView(moreAlbumsButton, true) -// } + if (searchResult!!.albums.size > albums.size) { + list.add(MoreButton(1, ::expandAlbums)) + } } - val songs = searchResult.songs + val songs = result.songs if (songs.isNotEmpty()) { list.add(DividerBinder.Divider(R.string.search_songs)) list.addAll(songs) -// if (songs.size > DEFAULT_SONGS) { -// moreSongsAdapter = mergeAdapter!!.addView(moreSongsButton, true) -// } + if (searchResult!!.songs.size > songs.size) { + list.add(MoreButton(2, ::expandSongs)) + } } // Show/hide the empty text view @@ -276,35 +253,17 @@ class SearchFragment : MultiListFragment(), KoinComponent { viewAdapter.submitList(list) } -// private fun expandArtists() { -// artistAdapter!!.clear() -// for (artist in searchResult!!.artists) { -// artistAdapter!!.add(artist) -// } -// artistAdapter!!.notifyDataSetChanged() -// mergeAdapter!!.removeAdapter(moreArtistsAdapter) -// mergeAdapter!!.notifyDataSetChanged() -// } -// -// 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 expandArtists() { + populateList(listModel.trimResultLength(searchResult!!, maxArtists = Int.MAX_VALUE)) + } + + private fun expandAlbums() { + populateList(listModel.trimResultLength(searchResult!!, maxAlbums = Int.MAX_VALUE)) + } + + private fun expandSongs() { + populateList(listModel.trimResultLength(searchResult!!, maxSongs = Int.MAX_VALUE)) + } private fun onArtistSelected(artist: Artist) { val bundle = Bundle() @@ -343,12 +302,6 @@ class SearchFragment : MultiListFragment(), KoinComponent { } } - companion object { - var DEFAULT_ARTISTS = Settings.defaultArtists - var DEFAULT_ALBUMS = Settings.defaultAlbums - var DEFAULT_SONGS = Settings.defaultSongs - } - override fun onItemClick(item: Identifiable) { when (item) { is Artist -> { @@ -464,4 +417,10 @@ class SearchFragment : MultiListFragment(), KoinComponent { return true } + + companion object { + var DEFAULT_ARTISTS = Settings.defaultArtists + var DEFAULT_ALBUMS = Settings.defaultAlbums + var DEFAULT_SONGS = Settings.defaultSongs + } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index 9fa39b9c..5cd254a5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -28,6 +28,8 @@ import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.launch import org.koin.android.ext.android.inject 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.TrackViewBinder 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.ShareHandler import org.moire.ultrasonic.subsonic.VideoPlayer -import org.moire.ultrasonic.util.AlbumHeader import org.moire.ultrasonic.util.CancellationToken import org.moire.ultrasonic.util.CommunicationError import org.moire.ultrasonic.util.Constants @@ -48,11 +49,16 @@ import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util /** + * * 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") -open class TrackCollectionFragment : MultiListFragment() { +open class TrackCollectionFragment : MultiListFragment() { private var albumButtons: View? = null internal var selectButton: ImageView? = null @@ -128,6 +134,15 @@ open class TrackCollectionFragment : MultiListFragment() { ) ) + viewAdapter.register( + AlbumRowBinder( + { entry -> onItemClick(entry) }, + { menuItem, entry -> onContextMenuItemSelected(menuItem, entry) }, + imageLoaderProvider.getImageLoader(), + context = requireContext() + ) + ) + enableButtons() // Update the buttons when the selection has changed @@ -447,9 +462,9 @@ open class TrackCollectionFragment : MultiListFragment() { } } - override val defaultObserver: (List) -> Unit = { + override val defaultObserver: (List) -> Unit = { - val entryList: MutableList = it.toMutableList() + val entryList: MutableList = it.toMutableList() if (listModel.currentListIsSortable && Settings.shouldSortByDisc) { Collections.sort(entryList, EntryByDiscAndTrackComparator()) @@ -470,7 +485,7 @@ open class TrackCollectionFragment : MultiListFragment() { val listSize = arguments?.getInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0) ?: 0 // 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 (listSize == 0 || songCount < listSize) { @@ -550,12 +565,11 @@ open class TrackCollectionFragment : MultiListFragment() { } @Suppress("LongMethod") - override fun getLiveData(args: Bundle?): LiveData> { + override fun getLiveData(args: Bundle?, refresh: Boolean): LiveData> { if (args == null) return listModel.currentList val id = args.getString(Constants.INTENT_EXTRA_NAME_ID) val isAlbum = args.getBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, false) 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 podcastChannelId = args.getString( Constants.INTENT_EXTRA_NAME_PODCAST_CHANNEL_ID @@ -574,7 +588,7 @@ open class TrackCollectionFragment : MultiListFragment() { val albumListOffset = args.getInt( 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) { refreshListView?.isRefreshing = true @@ -621,7 +635,7 @@ open class TrackCollectionFragment : MultiListFragment() { @Suppress("LongMethod") override fun onContextMenuItemSelected( menuItem: MenuItem, - item: MusicDirectory.Entry + item: MusicDirectory.Child ): Boolean { val entryId = item.id @@ -673,13 +687,12 @@ open class TrackCollectionFragment : MultiListFragment() { playAll() } R.id.menu_item_share -> { - val entries: MutableList = ArrayList(1) - entries.add(item) - shareHandler.createShare( - this, entries, refreshListView, - cancellationToken!! - ) - return true + if (item is MusicDirectory.Entry) { + shareHandler.createShare( + this, listOf(item), refreshListView, + cancellationToken!! + ) + } } else -> { return super.onContextItemSelected(menuItem) @@ -688,7 +701,7 @@ open class TrackCollectionFragment : MultiListFragment() { return true } - override fun onItemClick(item: MusicDirectory.Entry) { + override fun onItemClick(item: MusicDirectory.Child) { when { item.isDirectory -> { val bundle = Bundle() @@ -701,7 +714,7 @@ open class TrackCollectionFragment : MultiListFragment() { bundle ) } - item.isVideo -> { + item is MusicDirectory.Entry && item.isVideo -> { VideoPlayer.playVideo(requireContext(), item) } else -> { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt index 7ac20f1a..0b935e36 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt @@ -33,7 +33,12 @@ class AlbumListModel(application: Application) : GenericListModel(application) { 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)) } @@ -51,7 +56,7 @@ class AlbumListModel(application: Application) : GenericListModel(application) { var offset = args.getInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0) val append = args.getBoolean(Constants.INTENT_EXTRA_NAME_APPEND, false) - val musicDirectory: MusicDirectory + val musicDirectory: List val musicFolderId = if (showSelectFolderHeader(args)) { activeServerProvider.getActiveServer().musicFolderId } else { @@ -72,10 +77,11 @@ class AlbumListModel(application: Application) : GenericListModel(application) { } if (useId3Tags) { - musicDirectory = musicService.getAlbumList2( - albumListType, size, - offset, musicFolderId - ) + musicDirectory = + musicService.getAlbumList2( + albumListType, size, + offset, musicFolderId + ) } else { musicDirectory = musicService.getAlbumList( albumListType, size, @@ -85,15 +91,13 @@ class AlbumListModel(application: Application) : GenericListModel(application) { currentListIsSortable = isCollectionSortable(albumListType) - // TODO: Change signature of musicService.getAlbumList to return a List - @Suppress("UNCHECKED_CAST") if (append && list.value != null) { - val list = ArrayList() - list.addAll(this.list.value!!) - list.addAll(musicDirectory.getChildren()) - this.list.postValue(list as List) + val newList = ArrayList() + newList.addAll(list.value!!) + newList.addAll(musicDirectory) + this.list.postValue(newList) } else { - list.postValue(musicDirectory.getChildren() as List) + list.postValue(musicDirectory) } loadedUntil = offset diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt index 1919c7be..c03880fe 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt @@ -16,7 +16,6 @@ import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ServerSetting -import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.MusicFolder import org.moire.ultrasonic.service.MusicService import org.moire.ultrasonic.service.MusicServiceFactory @@ -44,7 +43,7 @@ open class GenericListModel(application: Application) : @Suppress("UNUSED_PARAMETER") open fun showSelectFolderHeader(args: Bundle?): Boolean { - return true + return false } /** @@ -109,20 +108,11 @@ open class GenericListModel(application: Application) : args: Bundle ) { // Update the list of available folders if enabled - // FIXME && refresh ? - if (showSelectFolderHeader(args) && !isOffline && !useId3Tags) { + @Suppress("ComplexCondition") + if (showSelectFolderHeader(args) && !isOffline && !useId3Tags && refresh) { musicFolders.postValue( 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 } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt index a5907352..252c48cb 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt @@ -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( - artists = result.artists.take(SearchFragment.DEFAULT_ARTISTS), - albums = result.albums.take(SearchFragment.DEFAULT_ALBUMS), - songs = result.songs.take(SearchFragment.DEFAULT_SONGS) + artists = result.artists.take(maxArtists), + albums = result.albums.take(maxAlbums), + songs = result.songs.take(maxSongs) ) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt index a3eebe3d..3b658ae6 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt @@ -21,7 +21,7 @@ import org.moire.ultrasonic.util.Util */ class TrackCollectionModel(application: Application) : GenericListModel(application) { - val currentList: MutableLiveData> = MutableLiveData() + val currentList: MutableLiveData> = MutableLiveData() val songsForGenre: MutableLiveData = MutableLiveData() /* @@ -72,7 +72,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat } else { musicDirectory = Util.getSongsFromSearchResult(service.getStarred()) } - + updateList(musicDirectory) } } @@ -83,7 +83,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat withContext(Dispatchers.IO) { val service = MusicServiceFactory.getMusicService() val videos = service.getVideos(refresh) - + if (videos != null) { updateList(videos) } @@ -97,7 +97,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat val musicDirectory = service.getRandomSongs(size) currentListIsSortable = false - + updateList(musicDirectory) } } @@ -117,7 +117,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat withContext(Dispatchers.IO) { val service = MusicServiceFactory.getMusicService() val musicDirectory = service.getPodcastEpisodes(podcastChannelId) - + if (musicDirectory != null) { updateList(musicDirectory) } @@ -140,7 +140,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat break } } - + updateList(musicDirectory) } } @@ -149,12 +149,12 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat withContext(Dispatchers.IO) { val service = MusicServiceFactory.getMusicService() val musicDirectory = Util.getSongsFromBookmarks(service.getBookmarks()) - + updateList(musicDirectory) } } private fun updateList(root: MusicDirectory) { - currentList.postValue(root.getTracks()) + currentList.postValue(root.getChildren()) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt index a8c8b91c..02f897fc 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt @@ -575,7 +575,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { } } - albums?.getChildren()?.map { album -> + albums?.map { album -> mediaItems.add( album.title ?: "", listOf(MEDIA_ALBUM_ITEM, album.id, album.name) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt index bca7aa51..7ebbdd11 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt @@ -249,7 +249,7 @@ class CachedMusicService(private val musicService: MusicService) : MusicService, size: Int, offset: Int, musicFolderId: String? - ): MusicDirectory { + ): List { return musicService.getAlbumList(type, size, offset, musicFolderId) } @@ -259,7 +259,7 @@ class CachedMusicService(private val musicService: MusicService) : MusicService, size: Int, offset: Int, musicFolderId: String? - ): MusicDirectory { + ): List { return musicService.getAlbumList2(type, size, offset, musicFolderId) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt index 5d78644f..410558b8 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt @@ -90,7 +90,12 @@ interface MusicService { fun scrobble(id: String, submission: Boolean) @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 @Throws(Exception::class) fun getAlbumList2( @@ -98,7 +103,7 @@ interface MusicService { size: Int, offset: Int, musicFolderId: String? - ): MusicDirectory + ): List @Throws(Exception::class) fun getRandomSongs(size: Int): MusicDirectory diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt index 7714a0ea..fad431f7 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt @@ -296,10 +296,20 @@ class OfflineMusicService : MusicService, KoinComponent { size: Int, offset: Int, musicFolderId: String? - ): MusicDirectory { + ): List { throw OfflineException("Album lists not available in offline mode") } + @Throws(OfflineException::class) + override fun getAlbumList2( + type: String, + size: Int, + offset: Int, + musicFolderId: String? + ): List { + throw OfflineException("getAlbumList2 isn't available in offline mode") + } + @Throws(Exception::class) override fun updateJukeboxPlaylist(ids: List?): JukeboxStatus { 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") } - @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) override fun getVideoUrl(id: String): String? { throw OfflineException("getVideoUrl isn't available in offline mode") @@ -512,7 +512,6 @@ class OfflineMusicService : MusicService, KoinComponent { return album } - /* * 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 * a given track file. @@ -559,7 +557,7 @@ class OfflineMusicService : MusicService, KoinComponent { artist = meta.artist ?: file.parentFile!!.parentFile!!.name album = meta.album ?: file.parentFile!!.name - title = meta.title?: title + title = meta.title ?: title isVideo = meta.hasVideo != null track = parseSlashedNumber(meta.track) discNumber = parseSlashedNumber(meta.disc) @@ -660,7 +658,6 @@ class OfflineMusicService : MusicService, KoinComponent { return closeness } - private fun listFilesRecursively(parent: File, children: MutableList) { for (file in FileUtil.listMediaFiles(parent)) { if (file.isFile) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt index 3f89c136..545fab3d 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt @@ -350,7 +350,7 @@ open class RESTMusicService( size: Int, offset: Int, musicFolderId: String? - ): MusicDirectory { + ): List { val response = API.getAlbumList( fromName(type), size, @@ -361,11 +361,8 @@ open class RESTMusicService( musicFolderId ).execute().throwOnFailure() - val childList = response.body()!!.albumList.toDomainEntityList() - val result = MusicDirectory() - result.addAll(childList) - return result + return response.body()!!.albumList.toDomainEntityList() } @Throws(Exception::class) @@ -374,7 +371,7 @@ open class RESTMusicService( size: Int, offset: Int, musicFolderId: String? - ): MusicDirectory { + ): List { val response = API.getAlbumList2( fromName(type), size, @@ -385,10 +382,7 @@ open class RESTMusicService( musicFolderId ).execute().throwOnFailure() - val result = MusicDirectory() - result.addAll(response.body()!!.albumList.toDomainEntityList()) - - return result + return response.body()!!.albumList.toDomainEntityList() } @Throws(Exception::class) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/EntryByDiscAndTrackComparator.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/EntryByDiscAndTrackComparator.kt new file mode 100644 index 00000000..ec3ef5cd --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/EntryByDiscAndTrackComparator.kt @@ -0,0 +1,50 @@ +package org.moire.ultrasonic.util + +import java.util.Comparator +import org.moire.ultrasonic.domain.MusicDirectory + +class EntryByDiscAndTrackComparator : Comparator { + 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) + } + } +} diff --git a/ultrasonic/src/main/res/layout/list_item_more_button.xml b/ultrasonic/src/main/res/layout/list_item_more_button.xml new file mode 100644 index 00000000..8d9b886c --- /dev/null +++ b/ultrasonic/src/main/res/layout/list_item_more_button.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/ultrasonic/src/main/res/layout/search_buttons.xml b/ultrasonic/src/main/res/layout/search_buttons.xml deleted file mode 100644 index 1666bdd1..00000000 --- a/ultrasonic/src/main/res/layout/search_buttons.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - -