Introduce new Generic Fragments, ViewModels, and Adapters for the display of API data.
* Splits former SelectAlbumFragment into separate fragments for Albums and general collections of tracks * Renames and refactors SelectArtist view to extend the new Generic classes * Adds error handling (Fixes #484) * Adds EndlessScrolling capabilities to all Album Lists * Uses RecyclerViews and LiveData for performance and in-memory caching * Refreshes the UI to be aligned with the ArtistList UI * Add a new GenericEntry to the domain data classes, and make other types extend it
This commit is contained in:
parent
c6a744cc14
commit
72c03cc500
|
@ -3,13 +3,13 @@ package org.moire.ultrasonic.domain
|
|||
import java.io.Serializable
|
||||
|
||||
data class Artist(
|
||||
var id: String? = null,
|
||||
var name: String? = null,
|
||||
override var id: String? = null,
|
||||
override var name: String? = null,
|
||||
var index: String? = null,
|
||||
var coverArt: String? = null,
|
||||
var albumCount: Long? = null,
|
||||
var closeness: Int = 0
|
||||
) : Serializable {
|
||||
) : Serializable, GenericEntry() {
|
||||
companion object {
|
||||
private const val serialVersionUID = -5790532593784846982L
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
package org.moire.ultrasonic.domain
|
||||
|
||||
abstract class GenericEntry {
|
||||
// TODO Should be non-null!
|
||||
abstract val id: String?
|
||||
open val name: String? = null
|
||||
}
|
|
@ -36,7 +36,7 @@ class MusicDirectory {
|
|||
}
|
||||
|
||||
data class Entry(
|
||||
var id: String? = null,
|
||||
override var id: String? = null,
|
||||
var parent: String? = null,
|
||||
var isDirectory: Boolean = false,
|
||||
var title: String? = null,
|
||||
|
@ -66,7 +66,7 @@ class MusicDirectory {
|
|||
var bookmarkPosition: Int = 0,
|
||||
var userRating: Int? = null,
|
||||
var averageRating: Float? = null
|
||||
) : Serializable {
|
||||
) : Serializable, GenericEntry() {
|
||||
fun setDuration(duration: Long) {
|
||||
this.duration = duration.toInt()
|
||||
}
|
||||
|
|
|
@ -4,6 +4,6 @@ package org.moire.ultrasonic.domain
|
|||
* Represents a top level directory in which music or other media is stored.
|
||||
*/
|
||||
data class MusicFolder(
|
||||
val id: String,
|
||||
val name: String
|
||||
)
|
||||
override val id: String,
|
||||
override val name: String
|
||||
) : GenericEntry()
|
||||
|
|
|
@ -3,14 +3,14 @@ package org.moire.ultrasonic.domain
|
|||
import java.io.Serializable
|
||||
|
||||
data class Playlist @JvmOverloads constructor(
|
||||
val id: String,
|
||||
var name: String,
|
||||
override val id: String,
|
||||
override var name: String,
|
||||
val owner: String = "",
|
||||
val comment: String = "",
|
||||
val songCount: String = "",
|
||||
val created: String = "",
|
||||
val public: Boolean? = null
|
||||
) : Serializable {
|
||||
) : Serializable, GenericEntry() {
|
||||
companion object {
|
||||
private const val serialVersionUID = -4160515427075433798L
|
||||
}
|
||||
|
|
|
@ -3,12 +3,12 @@ package org.moire.ultrasonic.domain
|
|||
import java.io.Serializable
|
||||
|
||||
data class PodcastsChannel(
|
||||
val id: String,
|
||||
override val id: String,
|
||||
val title: String?,
|
||||
val url: String?,
|
||||
val description: String?,
|
||||
val status: String?
|
||||
) : Serializable {
|
||||
) : Serializable, GenericEntry() {
|
||||
companion object {
|
||||
private const val serialVersionUID = -4160515427075433798L
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import java.io.Serializable
|
|||
import org.moire.ultrasonic.domain.MusicDirectory.Entry
|
||||
|
||||
data class Share(
|
||||
var id: String? = null,
|
||||
override var id: String? = null,
|
||||
var url: String? = null,
|
||||
var description: String? = null,
|
||||
var username: String? = null,
|
||||
|
@ -13,8 +13,8 @@ data class Share(
|
|||
var expires: String? = null,
|
||||
var visitCount: Long? = null,
|
||||
private val entries: MutableList<Entry> = mutableListOf()
|
||||
) : Serializable {
|
||||
val name: String?
|
||||
) : Serializable, GenericEntry() {
|
||||
override val name: String?
|
||||
get() = url?.let { urlPattern.matcher(url).replaceFirst("$1") }
|
||||
|
||||
fun getEntries(): List<Entry> {
|
||||
|
|
|
@ -4,7 +4,6 @@ import android.os.Bundle;
|
|||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
|
||||
|
@ -142,71 +141,66 @@ public class MainFragment extends Fragment {
|
|||
}
|
||||
|
||||
list.setAdapter(adapter);
|
||||
list.setOnItemClickListener(new AdapterView.OnItemClickListener()
|
||||
{
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id)
|
||||
list.setOnItemClickListener((parent, view, position, id) -> {
|
||||
if (view == serverButton)
|
||||
{
|
||||
if (view == serverButton)
|
||||
{
|
||||
showServers();
|
||||
}
|
||||
else if (view == albumsNewestButton)
|
||||
{
|
||||
showAlbumList("newest", R.string.main_albums_newest);
|
||||
}
|
||||
else if (view == albumsRandomButton)
|
||||
{
|
||||
showAlbumList("random", R.string.main_albums_random);
|
||||
}
|
||||
else if (view == albumsHighestButton)
|
||||
{
|
||||
showAlbumList("highest", R.string.main_albums_highest);
|
||||
}
|
||||
else if (view == albumsRecentButton)
|
||||
{
|
||||
showAlbumList("recent", R.string.main_albums_recent);
|
||||
}
|
||||
else if (view == albumsFrequentButton)
|
||||
{
|
||||
showAlbumList("frequent", R.string.main_albums_frequent);
|
||||
}
|
||||
else if (view == albumsStarredButton)
|
||||
{
|
||||
showAlbumList(Constants.STARRED, R.string.main_albums_starred);
|
||||
}
|
||||
else if (view == albumsAlphaByNameButton)
|
||||
{
|
||||
showAlbumList(Constants.ALPHABETICAL_BY_NAME, R.string.main_albums_alphaByName);
|
||||
}
|
||||
else if (view == albumsAlphaByArtistButton)
|
||||
{
|
||||
showAlbumList("alphabeticalByArtist", R.string.main_albums_alphaByArtist);
|
||||
}
|
||||
else if (view == songsStarredButton)
|
||||
{
|
||||
showStarredSongs();
|
||||
}
|
||||
else if (view == artistsButton)
|
||||
{
|
||||
showArtists();
|
||||
}
|
||||
else if (view == albumsButton)
|
||||
{
|
||||
showAlbumList(Constants.ALPHABETICAL_BY_NAME, R.string.main_albums_title);
|
||||
}
|
||||
else if (view == randomSongsButton)
|
||||
{
|
||||
showRandomSongs();
|
||||
}
|
||||
else if (view == genresButton)
|
||||
{
|
||||
showGenres();
|
||||
}
|
||||
else if (view == videosButton)
|
||||
{
|
||||
showVideos();
|
||||
}
|
||||
showServers();
|
||||
}
|
||||
else if (view == albumsNewestButton)
|
||||
{
|
||||
showAlbumList("newest", R.string.main_albums_newest);
|
||||
}
|
||||
else if (view == albumsRandomButton)
|
||||
{
|
||||
showAlbumList("random", R.string.main_albums_random);
|
||||
}
|
||||
else if (view == albumsHighestButton)
|
||||
{
|
||||
showAlbumList("highest", R.string.main_albums_highest);
|
||||
}
|
||||
else if (view == albumsRecentButton)
|
||||
{
|
||||
showAlbumList("recent", R.string.main_albums_recent);
|
||||
}
|
||||
else if (view == albumsFrequentButton)
|
||||
{
|
||||
showAlbumList("frequent", R.string.main_albums_frequent);
|
||||
}
|
||||
else if (view == albumsStarredButton)
|
||||
{
|
||||
showAlbumList(Constants.STARRED, R.string.main_albums_starred);
|
||||
}
|
||||
else if (view == albumsAlphaByNameButton)
|
||||
{
|
||||
showAlbumList(Constants.ALPHABETICAL_BY_NAME, R.string.main_albums_alphaByName);
|
||||
}
|
||||
else if (view == albumsAlphaByArtistButton)
|
||||
{
|
||||
showAlbumList("alphabeticalByArtist", R.string.main_albums_alphaByArtist);
|
||||
}
|
||||
else if (view == songsStarredButton)
|
||||
{
|
||||
showStarredSongs();
|
||||
}
|
||||
else if (view == artistsButton)
|
||||
{
|
||||
showArtists();
|
||||
}
|
||||
else if (view == albumsButton)
|
||||
{
|
||||
showAlbumList(Constants.ALPHABETICAL_BY_NAME, R.string.main_albums_title);
|
||||
}
|
||||
else if (view == randomSongsButton)
|
||||
{
|
||||
showRandomSongs();
|
||||
}
|
||||
else if (view == genresButton)
|
||||
{
|
||||
showGenres();
|
||||
}
|
||||
else if (view == videosButton)
|
||||
{
|
||||
showVideos();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -219,21 +213,11 @@ public class MainFragment extends Fragment {
|
|||
currentSetting.getLdapSupport(), currentSetting.getMinimumApiVersion());
|
||||
}
|
||||
|
||||
private void showAlbumList(final String type, final int title)
|
||||
{
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, type);
|
||||
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, title);
|
||||
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, Util.getMaxAlbums());
|
||||
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0);
|
||||
Navigation.findNavController(getView()).navigate(R.id.mainToSelectAlbum, bundle);
|
||||
}
|
||||
|
||||
private void showStarredSongs()
|
||||
{
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putInt(Constants.INTENT_EXTRA_NAME_STARRED, 1);
|
||||
Navigation.findNavController(getView()).navigate(R.id.mainToSelectAlbum, bundle);
|
||||
Navigation.findNavController(getView()).navigate(R.id.mainToTrackCollection, bundle);
|
||||
}
|
||||
|
||||
private void showRandomSongs()
|
||||
|
@ -241,14 +225,23 @@ public class MainFragment extends Fragment {
|
|||
Bundle bundle = new Bundle();
|
||||
bundle.putInt(Constants.INTENT_EXTRA_NAME_RANDOM, 1);
|
||||
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, Util.getMaxSongs());
|
||||
Navigation.findNavController(getView()).navigate(R.id.mainToSelectAlbum, bundle);
|
||||
Navigation.findNavController(getView()).navigate(R.id.mainToTrackCollection, bundle);
|
||||
}
|
||||
|
||||
private void showArtists()
|
||||
{
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, getContext().getResources().getString(R.string.main_artists_title));
|
||||
Navigation.findNavController(getView()).navigate(R.id.selectArtistFragment, bundle);
|
||||
Navigation.findNavController(getView()).navigate(R.id.mainToArtistList, bundle);
|
||||
}
|
||||
|
||||
private void showAlbumList(final String type, final int title) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, type);
|
||||
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, title);
|
||||
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, Util.getMaxAlbums());
|
||||
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0);
|
||||
Navigation.findNavController(getView()).navigate(R.id.mainToAlbumList, bundle);
|
||||
}
|
||||
|
||||
private void showGenres()
|
||||
|
@ -260,7 +253,7 @@ public class MainFragment extends Fragment {
|
|||
{
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putInt(Constants.INTENT_EXTRA_NAME_VIDEOS, 1);
|
||||
Navigation.findNavController(getView()).navigate(R.id.mainToSelectAlbum, bundle);
|
||||
Navigation.findNavController(getView()).navigate(R.id.mainToTrackCollection, bundle);
|
||||
}
|
||||
|
||||
private void showServers()
|
||||
|
|
|
@ -125,7 +125,7 @@ public class NowPlayingFragment extends Fragment {
|
|||
|
||||
bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, song.getAlbum());
|
||||
bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, song.getAlbum());
|
||||
Navigation.findNavController(getActivity(), R.id.nav_host_fragment).navigate(R.id.selectAlbumFragment, bundle);
|
||||
Navigation.findNavController(getActivity(), R.id.nav_host_fragment).navigate(R.id.trackCollectionFragment, bundle);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -105,7 +105,7 @@ public class PlaylistsFragment extends Fragment {
|
|||
bundle.putString(Constants.INTENT_EXTRA_NAME_ID, playlist.getId());
|
||||
bundle.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, playlist.getId());
|
||||
bundle.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME, playlist.getName());
|
||||
Navigation.findNavController(getView()).navigate(R.id.selectAlbumFragment, bundle);
|
||||
Navigation.findNavController(getView()).navigate(R.id.trackCollectionFragment, bundle);
|
||||
}
|
||||
});
|
||||
registerForContextMenu(playlistsListView);
|
||||
|
@ -154,7 +154,7 @@ public class PlaylistsFragment extends Fragment {
|
|||
if (ActiveServerProvider.Companion.isOffline()) inflater.inflate(R.menu.select_playlist_context_offline, menu);
|
||||
else inflater.inflate(R.menu.select_playlist_context, menu);
|
||||
|
||||
MenuItem downloadMenuItem = menu.findItem(R.id.album_menu_download);
|
||||
MenuItem downloadMenuItem = menu.findItem(R.id.playlist_menu_download);
|
||||
|
||||
if (downloadMenuItem != null)
|
||||
{
|
||||
|
@ -190,14 +190,14 @@ public class PlaylistsFragment extends Fragment {
|
|||
bundle.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, playlist.getId());
|
||||
bundle.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME, playlist.getName());
|
||||
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true);
|
||||
Navigation.findNavController(getView()).navigate(R.id.selectAlbumFragment, bundle);
|
||||
Navigation.findNavController(getView()).navigate(R.id.trackCollectionFragment, bundle);
|
||||
} else if (itemId == R.id.playlist_menu_play_shuffled) {
|
||||
bundle = new Bundle();
|
||||
bundle.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, playlist.getId());
|
||||
bundle.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME, playlist.getName());
|
||||
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true);
|
||||
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE, true);
|
||||
Navigation.findNavController(getView()).navigate(R.id.selectAlbumFragment, bundle);
|
||||
Navigation.findNavController(getView()).navigate(R.id.trackCollectionFragment, bundle);
|
||||
} else if (itemId == R.id.playlist_menu_delete) {
|
||||
deletePlaylist(playlist);
|
||||
} else if (itemId == R.id.playlist_info) {
|
||||
|
|
|
@ -76,7 +76,7 @@ public class PodcastFragment extends Fragment {
|
|||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(Constants.INTENT_EXTRA_NAME_PODCAST_CHANNEL_ID, pc.getId());
|
||||
Navigation.findNavController(view).navigate(R.id.selectAlbumFragment, bundle);
|
||||
Navigation.findNavController(view).navigate(R.id.trackCollectionFragment, bundle);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -272,11 +272,11 @@ public class SearchFragment extends Fragment {
|
|||
}
|
||||
else
|
||||
{
|
||||
inflater.inflate(R.menu.select_album_context, menu);
|
||||
inflater.inflate(R.menu.generic_context_menu, menu);
|
||||
}
|
||||
|
||||
MenuItem shareButton = menu.findItem(R.id.menu_item_share);
|
||||
MenuItem downloadMenuItem = menu.findItem(R.id.album_menu_download);
|
||||
MenuItem downloadMenuItem = menu.findItem(R.id.menu_download);
|
||||
|
||||
if (downloadMenuItem != null)
|
||||
{
|
||||
|
@ -324,17 +324,17 @@ public class SearchFragment extends Fragment {
|
|||
List<MusicDirectory.Entry> songs = new ArrayList<>(1);
|
||||
|
||||
int itemId = menuItem.getItemId();
|
||||
if (itemId == R.id.album_menu_play_now) {
|
||||
if (itemId == R.id.menu_play_now) {
|
||||
downloadHandler.getValue().downloadRecursively(this, id, false, false, true, false, false, false, false, false);
|
||||
} else if (itemId == R.id.album_menu_play_next) {
|
||||
} else if (itemId == R.id.menu_play_next) {
|
||||
downloadHandler.getValue().downloadRecursively(this, id, false, true, false, true, false, true, false, false);
|
||||
} else if (itemId == R.id.album_menu_play_last) {
|
||||
} else if (itemId == R.id.menu_play_last) {
|
||||
downloadHandler.getValue().downloadRecursively(this, id, false, true, false, false, false, false, false, false);
|
||||
} else if (itemId == R.id.album_menu_pin) {
|
||||
} else if (itemId == R.id.menu_pin) {
|
||||
downloadHandler.getValue().downloadRecursively(this, id, true, true, false, false, false, false, false, false);
|
||||
} else if (itemId == R.id.album_menu_unpin) {
|
||||
} else if (itemId == R.id.menu_unpin) {
|
||||
downloadHandler.getValue().downloadRecursively(this, id, false, false, false, false, false, false, true, false);
|
||||
} else if (itemId == R.id.album_menu_download) {
|
||||
} else if (itemId == R.id.menu_download) {
|
||||
downloadHandler.getValue().downloadRecursively(this, id, false, false, false, false, true, false, false, false);
|
||||
} else if (itemId == R.id.song_menu_play_now) {
|
||||
if (entry != null) {
|
||||
|
|
|
@ -77,7 +77,7 @@ public class SelectGenreFragment extends Fragment {
|
|||
bundle.putString(Constants.INTENT_EXTRA_NAME_GENRE_NAME, genre.getName());
|
||||
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, Util.getMaxSongs());
|
||||
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0);
|
||||
Navigation.findNavController(view).navigate(R.id.selectAlbumFragment, bundle);
|
||||
Navigation.findNavController(view).navigate(R.id.trackCollectionFragment, bundle);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -106,7 +106,7 @@ public class SharesFragment extends Fragment {
|
|||
Bundle bundle = new Bundle();
|
||||
bundle.putString(Constants.INTENT_EXTRA_NAME_SHARE_ID, share.getId());
|
||||
bundle.putString(Constants.INTENT_EXTRA_NAME_SHARE_NAME, share.getName());
|
||||
Navigation.findNavController(view).navigate(R.id.selectAlbumFragment, bundle);
|
||||
Navigation.findNavController(view).navigate(R.id.trackCollectionFragment, bundle);
|
||||
}
|
||||
});
|
||||
registerForContextMenu(sharesListView);
|
||||
|
|
|
@ -58,6 +58,7 @@ public final class Constants
|
|||
public static final String INTENT_EXTRA_NAME_IS_ALBUM = "subsonic.isalbum";
|
||||
public static final String INTENT_EXTRA_NAME_VIDEOS = "subsonic.videos";
|
||||
public static final String INTENT_EXTRA_NAME_SHOW_PLAYER = "subsonic.showplayer";
|
||||
public static final String INTENT_EXTRA_NAME_APPEND = "subsonic.append";
|
||||
|
||||
// Names for Intent Actions
|
||||
public static final String CMD_PROCESS_KEYCODE = "org.moire.ultrasonic.CMD_PROCESS_KEYCODE";
|
||||
|
|
|
@ -71,7 +71,7 @@ public class AlbumView extends UpdateView
|
|||
|
||||
public void setLayout()
|
||||
{
|
||||
LayoutInflater.from(context).inflate(R.layout.album_list_item, this, true);
|
||||
LayoutInflater.from(context).inflate(R.layout.album_list_item_legacy, this, true);
|
||||
viewHolder = new EntryAdapter.AlbumViewHolder();
|
||||
viewHolder.title = findViewById(R.id.album_title);
|
||||
viewHolder.artist = findViewById(R.id.album_artist);
|
||||
|
|
|
@ -103,7 +103,7 @@ class NavigationActivity : AppCompatActivity() {
|
|||
appBarConfiguration = AppBarConfiguration(
|
||||
setOf(
|
||||
R.id.mainFragment,
|
||||
R.id.selectArtistFragment,
|
||||
R.id.mediaLibraryFragment,
|
||||
R.id.searchFragment,
|
||||
R.id.playlistsFragment,
|
||||
R.id.sharesFragment,
|
||||
|
|
|
@ -4,7 +4,6 @@ package org.moire.ultrasonic.di
|
|||
import kotlin.math.abs
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.android.viewmodel.dsl.viewModel
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
import org.moire.ultrasonic.BuildConfig
|
||||
|
@ -13,7 +12,6 @@ import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions
|
|||
import org.moire.ultrasonic.api.subsonic.SubsonicClientConfiguration
|
||||
import org.moire.ultrasonic.cache.PermanentFileStorage
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||
import org.moire.ultrasonic.fragment.ArtistListModel
|
||||
import org.moire.ultrasonic.log.TimberOkHttpLogger
|
||||
import org.moire.ultrasonic.service.ApiCallResponseChecker
|
||||
import org.moire.ultrasonic.service.CachedMusicService
|
||||
|
@ -81,8 +79,6 @@ val musicServiceModule = module {
|
|||
|
||||
single { SubsonicImageLoader(androidContext(), get()) }
|
||||
|
||||
viewModel { ArtistListModel(get()) }
|
||||
|
||||
single { DownloadHandler(get(), get()) }
|
||||
single { NetworkAndStorageChecker(androidContext()) }
|
||||
single { VideoPlayer() }
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
package org.moire.ultrasonic.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.koin.core.component.KoinApiExtension
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
import org.moire.ultrasonic.domain.MusicFolder
|
||||
import org.moire.ultrasonic.util.Constants
|
||||
|
||||
/**
|
||||
* Displays a list of Albums from the media library
|
||||
* TODO: Check refresh is working
|
||||
*/
|
||||
@KoinApiExtension
|
||||
class AlbumListFragment : GenericListFragment<MusicDirectory.Entry, AlbumRowAdapter>() {
|
||||
|
||||
/**
|
||||
* The ViewModel to use to get the data
|
||||
*/
|
||||
override val listModel: AlbumListModel by viewModels()
|
||||
|
||||
/**
|
||||
* The id of the main layout
|
||||
*/
|
||||
override val mainLayout: Int = R.layout.generic_list
|
||||
|
||||
/**
|
||||
* The id of the refresh view
|
||||
*/
|
||||
override val refreshListId: Int = R.id.generic_list_refresh
|
||||
|
||||
/**
|
||||
* The id of the RecyclerView
|
||||
*/
|
||||
override val recyclerViewId = R.id.generic_list_recycler
|
||||
|
||||
/**
|
||||
* The id of the target in the navigation graph where we should go,
|
||||
* after the user has clicked on an item
|
||||
*/
|
||||
override val itemClickTarget: Int = R.id.trackCollectionFragment
|
||||
|
||||
/**
|
||||
* Whether to show the folder selector
|
||||
*/
|
||||
override var folderHeaderEnabled = false
|
||||
|
||||
/**
|
||||
* The central function to pass a query to the model and return a LiveData object
|
||||
*/
|
||||
override fun getLiveData(args: Bundle?): LiveData<List<MusicDirectory.Entry>> {
|
||||
if (args == null) throw IllegalArgumentException("Required arguments are missing")
|
||||
|
||||
val refresh = args.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH)
|
||||
|
||||
return listModel.getAlbumList(refresh, refreshListView!!, args)
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide the Adapter for the RecyclerView with a lazy delegate
|
||||
*/
|
||||
override val viewAdapter: AlbumRowAdapter by lazy {
|
||||
AlbumRowAdapter(
|
||||
liveDataItems.value ?: listOf(),
|
||||
selectFolderHeader,
|
||||
{ entry -> onItemClick(entry) },
|
||||
{ menuItem, entry -> onContextMenuItemSelected(menuItem, entry) },
|
||||
imageLoaderProvider.getImageLoader()
|
||||
)
|
||||
}
|
||||
|
||||
val newBundleClone: Bundle
|
||||
get() = arguments?.clone() as Bundle
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// Attach our onScrollListener
|
||||
listView = view.findViewById<RecyclerView>(recyclerViewId).apply {
|
||||
val scrollListener = object : EndlessScrollListener(viewManager) {
|
||||
override fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?) {
|
||||
// Triggered only when new data needs to be appended to the list
|
||||
// Add whatever code is needed to append new items to the bottom of the list
|
||||
val appendArgs = newBundleClone
|
||||
appendArgs.putBoolean(Constants.INTENT_EXTRA_NAME_APPEND, true)
|
||||
getLiveData(appendArgs)
|
||||
}
|
||||
}
|
||||
addOnScrollListener(scrollListener)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemClick(item: MusicDirectory.Entry) {
|
||||
val bundle = Bundle()
|
||||
bundle.putString(Constants.INTENT_EXTRA_NAME_ID, item.id)
|
||||
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, item.isDirectory)
|
||||
bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, item.title)
|
||||
bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, item.parent)
|
||||
findNavController().navigate(itemClickTarget, bundle)
|
||||
}
|
||||
|
||||
override val musicFolderObserver = { _: List<MusicFolder> ->
|
||||
// Do nothing
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
package org.moire.ultrasonic.fragment
|
||||
|
||||
import android.app.Application
|
||||
import android.os.Bundle
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import org.koin.core.component.KoinApiExtension
|
||||
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
import org.moire.ultrasonic.service.MusicService
|
||||
import org.moire.ultrasonic.util.Constants
|
||||
import org.moire.ultrasonic.util.Util
|
||||
|
||||
@KoinApiExtension
|
||||
class AlbumListModel(application: Application) : GenericListModel(application) {
|
||||
|
||||
val albumList: MutableLiveData<List<MusicDirectory.Entry>> = MutableLiveData()
|
||||
private var loadedUntil: Int = 0
|
||||
|
||||
fun getAlbumList(
|
||||
refresh: Boolean,
|
||||
swipe: SwipeRefreshLayout,
|
||||
args: Bundle
|
||||
): LiveData<List<MusicDirectory.Entry>> {
|
||||
|
||||
backgroundLoadFromServer(refresh, swipe, args)
|
||||
return albumList
|
||||
}
|
||||
|
||||
override fun load(
|
||||
isOffline: Boolean,
|
||||
useId3Tags: Boolean,
|
||||
musicService: MusicService,
|
||||
refresh: Boolean,
|
||||
args: Bundle
|
||||
) {
|
||||
val musicDirectory: MusicDirectory
|
||||
val musicFolderId = if (showSelectFolderHeader) {
|
||||
activeServerProvider.getActiveServer().musicFolderId
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val albumListType = args.getString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE)!!
|
||||
val size = args.getInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0)
|
||||
var offset = args.getInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0)
|
||||
val append = args.getBoolean(Constants.INTENT_EXTRA_NAME_APPEND, false)
|
||||
|
||||
showHeader = showHeader(albumListType)
|
||||
|
||||
// Handle the logic for endless scrolling:
|
||||
// If appending the existing list, set the offset from where to load
|
||||
if (append) offset += (size + loadedUntil)
|
||||
|
||||
if (useId3Tags) {
|
||||
musicDirectory = musicService.getAlbumList2(
|
||||
albumListType, size,
|
||||
offset, musicFolderId
|
||||
)
|
||||
} else {
|
||||
musicDirectory = musicService.getAlbumList(
|
||||
albumListType, size,
|
||||
offset, musicFolderId
|
||||
)
|
||||
}
|
||||
|
||||
currentListIsSortable = sortableCollection(albumListType)
|
||||
|
||||
if (append && albumList.value != null) {
|
||||
val list = ArrayList<MusicDirectory.Entry>()
|
||||
list.addAll(albumList.value!!)
|
||||
list.addAll(musicDirectory.getAllChild())
|
||||
albumList.postValue(list)
|
||||
} else {
|
||||
albumList.postValue(musicDirectory.getAllChild())
|
||||
}
|
||||
|
||||
loadedUntil = offset
|
||||
}
|
||||
|
||||
private fun showHeader(albumListType: String): Boolean {
|
||||
val isAlphabetical = (albumListType == AlbumListType.SORTED_BY_NAME.toString()) ||
|
||||
(albumListType == AlbumListType.SORTED_BY_ARTIST.toString())
|
||||
|
||||
return !isOffline() && !Util.getShouldUseId3Tags() && isAlphabetical
|
||||
}
|
||||
|
||||
private fun sortableCollection(albumListType: String): Boolean {
|
||||
return albumListType != "newest" && albumListType != "random" &&
|
||||
albumListType != "highest" && albumListType != "recent" &&
|
||||
albumListType != "frequent"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* ArtistRowAdapter.kt
|
||||
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.fragment
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
import org.moire.ultrasonic.util.ImageLoader
|
||||
import org.moire.ultrasonic.view.SelectMusicFolderView
|
||||
|
||||
/**
|
||||
* Creates a Row in a RecyclerView which contains the details of an Artist
|
||||
*/
|
||||
class AlbumRowAdapter(
|
||||
albumList: List<MusicDirectory.Entry>,
|
||||
private var selectFolderHeader: SelectMusicFolderView?,
|
||||
onItemClick: (MusicDirectory.Entry) -> Unit,
|
||||
onContextMenuClick: (MenuItem, MusicDirectory.Entry) -> Boolean,
|
||||
private val imageLoader: ImageLoader
|
||||
) : GenericRowAdapter<MusicDirectory.Entry>(
|
||||
selectFolderHeader,
|
||||
onItemClick,
|
||||
onContextMenuClick,
|
||||
imageLoader
|
||||
) {
|
||||
|
||||
override var itemList = albumList
|
||||
|
||||
// Set our layout files
|
||||
override val layout = R.layout.album_list_item
|
||||
override val contextMenuLayout = R.menu.artist_context_menu
|
||||
|
||||
// Sets the data to be displayed in the RecyclerView
|
||||
override fun setData(data: List<MusicDirectory.Entry>) {
|
||||
itemList = data
|
||||
super.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): RecyclerView.ViewHolder {
|
||||
if (viewType == TYPE_ITEM) {
|
||||
val row = LayoutInflater.from(parent.context)
|
||||
.inflate(layout, parent, false)
|
||||
return AlbumViewHolder(row)
|
||||
}
|
||||
return selectFolderHeader!!
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
if (holder is AlbumViewHolder) {
|
||||
val listPosition = if (selectFolderHeader != null) position - 1 else position
|
||||
val entry = itemList[listPosition]
|
||||
holder.album.text = entry.title
|
||||
holder.artist.text = entry.artist
|
||||
holder.details.setOnClickListener { onItemClick(entry) }
|
||||
holder.details.setOnLongClickListener { view -> createPopupMenu(view, listPosition) }
|
||||
holder.coverArtId = entry.coverArt
|
||||
|
||||
imageLoader.loadImage(
|
||||
holder.coverArt,
|
||||
MusicDirectory.Entry().apply { coverArt = holder.coverArtId },
|
||||
false, 0, false, true, R.drawable.ic_contact_picture
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
if (selectFolderHeader != null)
|
||||
return itemList.size + 1
|
||||
else
|
||||
return itemList.size
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds the view properties of an Item row
|
||||
*/
|
||||
class AlbumViewHolder(
|
||||
itemView: View
|
||||
) : RecyclerView.ViewHolder(itemView) {
|
||||
var album: TextView = itemView.findViewById(R.id.album_title)
|
||||
var artist: TextView = itemView.findViewById(R.id.album_artist)
|
||||
var details: LinearLayout = itemView.findViewById(R.id.row_album_details)
|
||||
var coverArt: ImageView = itemView.findViewById(R.id.album_coverart)
|
||||
var coverArtId: String? = null
|
||||
}
|
||||
}
|
|
@ -1,222 +1,72 @@
|
|||
package org.moire.ultrasonic.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.android.viewmodel.ext.android.viewModel
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.LiveData
|
||||
import org.koin.core.component.KoinApiExtension
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||
import org.moire.ultrasonic.domain.Artist
|
||||
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
|
||||
import org.moire.ultrasonic.subsonic.DownloadHandler
|
||||
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
|
||||
import org.moire.ultrasonic.domain.MusicFolder
|
||||
import org.moire.ultrasonic.util.Constants
|
||||
import org.moire.ultrasonic.util.Util
|
||||
import org.moire.ultrasonic.view.SelectMusicFolderView
|
||||
|
||||
/**
|
||||
* Displays the list of Artists from the media library
|
||||
*/
|
||||
class ArtistListFragment : Fragment() {
|
||||
private val activeServerProvider: ActiveServerProvider by inject()
|
||||
private val serverSettingsModel: ServerSettingsModel by viewModel()
|
||||
private val artistListModel: ArtistListModel by viewModel()
|
||||
private val imageLoaderProvider: ImageLoaderProvider by inject()
|
||||
private val downloadHandler: DownloadHandler by inject()
|
||||
@KoinApiExtension
|
||||
class ArtistListFragment : GenericListFragment<Artist, ArtistRowAdapter>() {
|
||||
|
||||
private var refreshArtistListView: SwipeRefreshLayout? = null
|
||||
private var artistListView: RecyclerView? = null
|
||||
private lateinit var viewManager: RecyclerView.LayoutManager
|
||||
private lateinit var viewAdapter: ArtistRowAdapter
|
||||
private var selectFolderHeader: SelectMusicFolderView? = null
|
||||
/**
|
||||
* The ViewModel to use to get the data
|
||||
*/
|
||||
override val listModel: ArtistListModel by viewModels()
|
||||
|
||||
@Override
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
Util.applyTheme(this.context)
|
||||
super.onCreate(savedInstanceState)
|
||||
/**
|
||||
* The id of the main layout
|
||||
*/
|
||||
override val mainLayout = R.layout.generic_list
|
||||
|
||||
/**
|
||||
* The id of the refresh view
|
||||
*/
|
||||
override val refreshListId = R.id.generic_list_refresh
|
||||
|
||||
/**
|
||||
* The id of the RecyclerView
|
||||
*/
|
||||
override val recyclerViewId = R.id.generic_list_recycler
|
||||
|
||||
/**
|
||||
* The id of the target in the navigation graph where we should go,
|
||||
* after the user has clicked on an item
|
||||
*/
|
||||
override val itemClickTarget = R.id.selectArtistToSelectAlbum
|
||||
|
||||
/**
|
||||
* The central function to pass a query to the model and return a LiveData object
|
||||
*/
|
||||
override fun getLiveData(args: Bundle?): LiveData<List<Artist>> {
|
||||
val refresh = args?.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH) ?: false
|
||||
return listModel.getItems(refresh, refreshListView!!)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.select_artist, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
refreshArtistListView = view.findViewById(R.id.select_artist_refresh)
|
||||
refreshArtistListView!!.setOnRefreshListener {
|
||||
artistListModel.refresh(refreshArtistListView!!)
|
||||
}
|
||||
|
||||
if (!ActiveServerProvider.isOffline() &&
|
||||
!Util.getShouldUseId3Tags()
|
||||
) {
|
||||
selectFolderHeader = SelectMusicFolderView(
|
||||
requireContext(), view as ViewGroup,
|
||||
{ selectedFolderId ->
|
||||
if (!ActiveServerProvider.isOffline()) {
|
||||
val currentSetting = activeServerProvider.getActiveServer()
|
||||
currentSetting.musicFolderId = selectedFolderId
|
||||
serverSettingsModel.updateItem(currentSetting)
|
||||
}
|
||||
viewAdapter.notifyDataSetChanged()
|
||||
artistListModel.refresh(refreshArtistListView!!)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val title = arguments?.getString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE)
|
||||
|
||||
if (title == null) {
|
||||
setTitle(
|
||||
this,
|
||||
if (ActiveServerProvider.isOffline())
|
||||
R.string.music_library_label_offline
|
||||
else R.string.music_library_label
|
||||
)
|
||||
} else {
|
||||
setTitle(this, title)
|
||||
}
|
||||
|
||||
val refresh = arguments?.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH) ?: false
|
||||
|
||||
artistListModel.getMusicFolders()
|
||||
.observe(
|
||||
viewLifecycleOwner,
|
||||
Observer { changedFolders ->
|
||||
if (changedFolders != null) {
|
||||
viewAdapter.notifyDataSetChanged()
|
||||
selectFolderHeader!!.setData(
|
||||
activeServerProvider.getActiveServer().musicFolderId,
|
||||
changedFolders
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
val artists = artistListModel.getArtists(refresh, refreshArtistListView!!)
|
||||
artists.observe(
|
||||
viewLifecycleOwner, Observer { changedArtists -> viewAdapter.setData(changedArtists) }
|
||||
)
|
||||
|
||||
viewManager = LinearLayoutManager(this.context)
|
||||
viewAdapter = ArtistRowAdapter(
|
||||
artists.value ?: listOf(),
|
||||
/**
|
||||
* Provide the Adapter for the RecyclerView with a lazy delegate
|
||||
*/
|
||||
override val viewAdapter: ArtistRowAdapter by lazy {
|
||||
ArtistRowAdapter(
|
||||
liveDataItems.value ?: listOf(),
|
||||
selectFolderHeader,
|
||||
{ artist -> onItemClick(artist) },
|
||||
{ menuItem, artist -> onArtistMenuItemSelected(menuItem, artist) },
|
||||
{ entry -> onItemClick(entry) },
|
||||
{ menuItem, entry -> onContextMenuItemSelected(menuItem, entry) },
|
||||
imageLoaderProvider.getImageLoader()
|
||||
)
|
||||
|
||||
artistListView = view.findViewById<RecyclerView>(R.id.select_artist_list).apply {
|
||||
setHasFixedSize(true)
|
||||
layoutManager = viewManager
|
||||
adapter = viewAdapter
|
||||
}
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
}
|
||||
|
||||
private fun onItemClick(artist: Artist) {
|
||||
val bundle = Bundle()
|
||||
bundle.putString(Constants.INTENT_EXTRA_NAME_ID, artist.id)
|
||||
bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.name)
|
||||
bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, artist.id)
|
||||
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true)
|
||||
findNavController().navigate(R.id.selectArtistToSelectAlbum, bundle)
|
||||
}
|
||||
|
||||
private fun onArtistMenuItemSelected(menuItem: MenuItem, artist: Artist): Boolean {
|
||||
when (menuItem.itemId) {
|
||||
R.id.artist_menu_play_now ->
|
||||
downloadHandler.downloadRecursively(
|
||||
this,
|
||||
artist.id,
|
||||
save = false,
|
||||
append = false,
|
||||
autoPlay = true,
|
||||
shuffle = false,
|
||||
background = false,
|
||||
playNext = false,
|
||||
unpin = false,
|
||||
isArtist = true
|
||||
)
|
||||
R.id.artist_menu_play_next ->
|
||||
downloadHandler.downloadRecursively(
|
||||
this,
|
||||
artist.id,
|
||||
save = false,
|
||||
append = false,
|
||||
autoPlay = true,
|
||||
shuffle = true,
|
||||
background = false,
|
||||
playNext = true,
|
||||
unpin = false,
|
||||
isArtist = true
|
||||
)
|
||||
R.id.artist_menu_play_last ->
|
||||
downloadHandler.downloadRecursively(
|
||||
this,
|
||||
artist.id,
|
||||
save = false,
|
||||
append = true,
|
||||
autoPlay = false,
|
||||
shuffle = false,
|
||||
background = false,
|
||||
playNext = false,
|
||||
unpin = false,
|
||||
isArtist = true
|
||||
)
|
||||
R.id.artist_menu_pin ->
|
||||
downloadHandler.downloadRecursively(
|
||||
this,
|
||||
artist.id,
|
||||
save = true,
|
||||
append = true,
|
||||
autoPlay = false,
|
||||
shuffle = false,
|
||||
background = false,
|
||||
playNext = false,
|
||||
unpin = false,
|
||||
isArtist = true
|
||||
)
|
||||
R.id.artist_menu_unpin ->
|
||||
downloadHandler.downloadRecursively(
|
||||
this,
|
||||
artist.id,
|
||||
save = false,
|
||||
append = false,
|
||||
autoPlay = false,
|
||||
shuffle = false,
|
||||
background = false,
|
||||
playNext = false,
|
||||
unpin = true,
|
||||
isArtist = true
|
||||
)
|
||||
R.id.artist_menu_download ->
|
||||
downloadHandler.downloadRecursively(
|
||||
this,
|
||||
artist.id,
|
||||
save = false,
|
||||
append = false,
|
||||
autoPlay = false,
|
||||
shuffle = false,
|
||||
background = true,
|
||||
playNext = false,
|
||||
unpin = false,
|
||||
isArtist = true
|
||||
)
|
||||
}
|
||||
return true
|
||||
override val musicFolderObserver = { changedFolders: List<MusicFolder> ->
|
||||
viewAdapter.notifyDataSetChanged()
|
||||
selectFolderHeader!!.setData(
|
||||
activeServerProvider.getActiveServer().musicFolderId,
|
||||
changedFolders
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,90 +18,53 @@
|
|||
*/
|
||||
package org.moire.ultrasonic.fragment
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.app.Application
|
||||
import android.os.Bundle
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||
import org.koin.core.component.KoinApiExtension
|
||||
import org.moire.ultrasonic.domain.Artist
|
||||
import org.moire.ultrasonic.domain.MusicFolder
|
||||
import org.moire.ultrasonic.service.CommunicationErrorHandler
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory
|
||||
import org.moire.ultrasonic.util.Util
|
||||
import org.moire.ultrasonic.service.MusicService
|
||||
|
||||
/**
|
||||
* Provides ViewModel which contains the list of available Artists
|
||||
*/
|
||||
class ArtistListModel(
|
||||
private val activeServerProvider: ActiveServerProvider
|
||||
) : ViewModel() {
|
||||
private val musicFolders: MutableLiveData<List<MusicFolder>> = MutableLiveData()
|
||||
@KoinApiExtension
|
||||
class ArtistListModel(application: Application) : GenericListModel(application) {
|
||||
private val artists: MutableLiveData<List<Artist>> = MutableLiveData()
|
||||
|
||||
/**
|
||||
* Retrieves the available Artists in a LiveData
|
||||
* Retrieves all available Artists in a LiveData
|
||||
*/
|
||||
fun getArtists(refresh: Boolean, swipe: SwipeRefreshLayout): LiveData<List<Artist>> {
|
||||
fun getItems(refresh: Boolean, swipe: SwipeRefreshLayout): LiveData<List<Artist>> {
|
||||
backgroundLoadFromServer(refresh, swipe)
|
||||
return artists
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the available Music Folders in a LiveData
|
||||
*/
|
||||
fun getMusicFolders(): LiveData<List<MusicFolder>> {
|
||||
return musicFolders
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the cached Artists from the server
|
||||
*/
|
||||
fun refresh(swipe: SwipeRefreshLayout) {
|
||||
backgroundLoadFromServer(true, swipe)
|
||||
}
|
||||
|
||||
private fun backgroundLoadFromServer(refresh: Boolean, swipe: SwipeRefreshLayout) {
|
||||
viewModelScope.launch {
|
||||
swipe.isRefreshing = true
|
||||
loadFromServer(refresh, swipe)
|
||||
swipe.isRefreshing = false
|
||||
override fun load(
|
||||
isOffline: Boolean,
|
||||
useId3Tags: Boolean,
|
||||
musicService: MusicService,
|
||||
refresh: Boolean,
|
||||
args: Bundle
|
||||
) {
|
||||
if (!isOffline && !useId3Tags) {
|
||||
musicFolders.postValue(
|
||||
musicService.getMusicFolders(refresh)
|
||||
)
|
||||
}
|
||||
|
||||
val musicFolderId = activeServer.musicFolderId
|
||||
|
||||
val result = if (!isOffline && useId3Tags)
|
||||
musicService.getArtists(refresh)
|
||||
else musicService.getIndexes(musicFolderId, refresh)
|
||||
|
||||
val retrievedArtists: MutableList<Artist> =
|
||||
ArrayList(result.shortcuts.size + result.artists.size)
|
||||
retrievedArtists.addAll(result.shortcuts)
|
||||
retrievedArtists.addAll(result.artists)
|
||||
artists.postValue(retrievedArtists)
|
||||
}
|
||||
|
||||
private suspend fun loadFromServer(refresh: Boolean, swipe: SwipeRefreshLayout) =
|
||||
withContext(Dispatchers.IO) {
|
||||
val musicService = MusicServiceFactory.getMusicService()
|
||||
val isOffline = ActiveServerProvider.isOffline()
|
||||
val useId3Tags = Util.getShouldUseId3Tags()
|
||||
|
||||
try {
|
||||
if (!isOffline && !useId3Tags) {
|
||||
musicFolders.postValue(
|
||||
musicService.getMusicFolders(refresh)
|
||||
)
|
||||
}
|
||||
|
||||
val musicFolderId = activeServerProvider.getActiveServer().musicFolderId
|
||||
|
||||
val result = if (!isOffline && useId3Tags)
|
||||
musicService.getArtists(refresh)
|
||||
else musicService.getIndexes(musicFolderId, refresh)
|
||||
|
||||
val retrievedArtists: MutableList<Artist> =
|
||||
ArrayList(result.shortcuts.size + result.artists.size)
|
||||
retrievedArtists.addAll(result.shortcuts)
|
||||
retrievedArtists.addAll(result.artists)
|
||||
artists.postValue(retrievedArtists)
|
||||
} catch (exception: Exception) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
CommunicationErrorHandler.handleError(exception, swipe.context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,37 +1,18 @@
|
|||
/*
|
||||
This file is part of Subsonic.
|
||||
|
||||
Subsonic is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Subsonic is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Copyright 2020 (C) Jozsef Varga
|
||||
* ArtistRowAdapter.kt
|
||||
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.fragment
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.PopupMenu
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView.SectionedAdapter
|
||||
import java.text.Collator
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
||||
import org.moire.ultrasonic.domain.Artist
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
import org.moire.ultrasonic.util.ImageLoader
|
||||
|
@ -42,61 +23,41 @@ import org.moire.ultrasonic.view.SelectMusicFolderView
|
|||
* Creates a Row in a RecyclerView which contains the details of an Artist
|
||||
*/
|
||||
class ArtistRowAdapter(
|
||||
private var artistList: List<Artist>,
|
||||
artistList: List<Artist>,
|
||||
private var selectFolderHeader: SelectMusicFolderView?,
|
||||
val onArtistClick: (Artist) -> Unit,
|
||||
val onContextMenuClick: (MenuItem, Artist) -> Boolean,
|
||||
onItemClick: (Artist) -> Unit,
|
||||
onContextMenuClick: (MenuItem, Artist) -> Boolean,
|
||||
private val imageLoader: ImageLoader
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>(), SectionedAdapter {
|
||||
) : GenericRowAdapter<Artist>(
|
||||
selectFolderHeader,
|
||||
onItemClick,
|
||||
onContextMenuClick,
|
||||
imageLoader
|
||||
),
|
||||
SectionedAdapter {
|
||||
|
||||
override var itemList = artistList
|
||||
|
||||
// Set our layout files
|
||||
override val layout = R.layout.artist_list_item
|
||||
override val contextMenuLayout = R.menu.artist_context_menu
|
||||
|
||||
/**
|
||||
* Sets the data to be displayed in the RecyclerView
|
||||
*/
|
||||
fun setData(data: List<Artist>) {
|
||||
artistList = data.sortedWith(compareBy(Collator.getInstance()) { t -> t.name })
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds the view properties of an Artist row
|
||||
*/
|
||||
class ArtistViewHolder(
|
||||
itemView: View
|
||||
) : RecyclerView.ViewHolder(itemView) {
|
||||
var section: TextView = itemView.findViewById(R.id.row_section)
|
||||
var textView: TextView = itemView.findViewById(R.id.row_artist_name)
|
||||
var layout: RelativeLayout = itemView.findViewById(R.id.row_artist_layout)
|
||||
var coverArt: ImageView = itemView.findViewById(R.id.artist_coverart)
|
||||
var coverArtId: String? = null
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): RecyclerView.ViewHolder {
|
||||
if (viewType == TYPE_ITEM) {
|
||||
val row = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.artist_list_item, parent, false)
|
||||
return ArtistViewHolder(row)
|
||||
}
|
||||
return selectFolderHeader!!
|
||||
}
|
||||
|
||||
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
|
||||
if ((holder is ArtistViewHolder) && (holder.coverArtId != null)) {
|
||||
imageLoader.cancel(holder.coverArtId)
|
||||
}
|
||||
super.onViewRecycled(holder)
|
||||
override fun setData(data: List<Artist>) {
|
||||
itemList = data.sortedWith(compareBy(Collator.getInstance()) { t -> t.name })
|
||||
super.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
if (holder is ArtistViewHolder) {
|
||||
if (holder is ItemViewHolder) {
|
||||
val listPosition = if (selectFolderHeader != null) position - 1 else position
|
||||
holder.textView.text = artistList[listPosition].name
|
||||
holder.textView.text = itemList[listPosition].name
|
||||
holder.section.text = getSectionForArtist(listPosition)
|
||||
holder.layout.setOnClickListener { onArtistClick(artistList[listPosition]) }
|
||||
holder.layout.setOnClickListener { onItemClick(itemList[listPosition]) }
|
||||
holder.layout.setOnLongClickListener { view -> createPopupMenu(view, listPosition) }
|
||||
holder.coverArtId = artistList[listPosition].coverArt
|
||||
holder.coverArtId = itemList[listPosition].coverArt
|
||||
|
||||
if (Util.getShouldShowArtistPicture()) {
|
||||
holder.coverArt.visibility = View.VISIBLE
|
||||
|
@ -111,15 +72,6 @@ class ArtistRowAdapter(
|
|||
}
|
||||
}
|
||||
|
||||
override fun getItemCount() = if (selectFolderHeader != null)
|
||||
artistList.size + 1
|
||||
else
|
||||
artistList.size
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return if (position == 0 && selectFolderHeader != null) TYPE_HEADER else TYPE_ITEM
|
||||
}
|
||||
|
||||
override fun getSectionName(position: Int): String {
|
||||
var listPosition = if (selectFolderHeader != null) position - 1 else position
|
||||
|
||||
|
@ -127,18 +79,18 @@ class ArtistRowAdapter(
|
|||
// scrolled up to the "Select Folder" row
|
||||
if (listPosition < 0) listPosition = 0
|
||||
|
||||
return getSectionFromName(artistList[listPosition].name ?: " ")
|
||||
return getSectionFromName(itemList[listPosition].name ?: " ")
|
||||
}
|
||||
|
||||
private fun getSectionForArtist(artistPosition: Int): String {
|
||||
if (artistPosition == 0)
|
||||
return getSectionFromName(artistList[artistPosition].name ?: " ")
|
||||
return getSectionFromName(itemList[artistPosition].name ?: " ")
|
||||
|
||||
val previousArtistSection = getSectionFromName(
|
||||
artistList[artistPosition - 1].name ?: " "
|
||||
itemList[artistPosition - 1].name ?: " "
|
||||
)
|
||||
val currentArtistSection = getSectionFromName(
|
||||
artistList[artistPosition].name ?: " "
|
||||
itemList[artistPosition].name ?: " "
|
||||
)
|
||||
|
||||
return if (previousArtistSection == currentArtistSection) "" else currentArtistSection
|
||||
|
@ -149,24 +101,4 @@ class ArtistRowAdapter(
|
|||
if (!section.isLetter()) section = '#'
|
||||
return section.toString()
|
||||
}
|
||||
|
||||
private fun createPopupMenu(view: View, position: Int): Boolean {
|
||||
val popup = PopupMenu(view.context, view)
|
||||
val inflater: MenuInflater = popup.menuInflater
|
||||
inflater.inflate(R.menu.select_artist_context, popup.menu)
|
||||
|
||||
val downloadMenuItem = popup.menu.findItem(R.id.artist_menu_download)
|
||||
downloadMenuItem?.isVisible = !isOffline()
|
||||
|
||||
popup.setOnMenuItemClickListener { menuItem ->
|
||||
onContextMenuClick(menuItem, artistList[position])
|
||||
}
|
||||
popup.show()
|
||||
return true
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TYPE_HEADER = 0
|
||||
private const val TYPE_ITEM = 1
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
package org.moire.ultrasonic.fragment
|
||||
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.StaggeredGridLayoutManager
|
||||
|
||||
/*
|
||||
* An abstract ScrollListener, which can be extended to provide endless scrolling capabilities
|
||||
*/
|
||||
abstract class EndlessScrollListener : RecyclerView.OnScrollListener {
|
||||
// The minimum amount of items to have below your current scroll position
|
||||
// before loading more.
|
||||
private var treshold = VISIBLE_TRESHOLD
|
||||
|
||||
// The current offset index of data you have loaded
|
||||
private var currentPage = 0
|
||||
|
||||
// The total number of items in the dataset after the last load
|
||||
private var previousTotalItemCount = 0
|
||||
|
||||
// True if we are still waiting for the last set of data to load.
|
||||
private var loading = true
|
||||
|
||||
// Sets the starting page index
|
||||
private val startingPageIndex = 0
|
||||
var thisManager: RecyclerView.LayoutManager
|
||||
|
||||
constructor(layoutManager: LinearLayoutManager) {
|
||||
thisManager = layoutManager
|
||||
}
|
||||
|
||||
@Suppress("Unused")
|
||||
constructor(layoutManager: GridLayoutManager) {
|
||||
thisManager = layoutManager
|
||||
treshold *= layoutManager.spanCount
|
||||
}
|
||||
|
||||
@Suppress("Unused")
|
||||
constructor(layoutManager: StaggeredGridLayoutManager) {
|
||||
thisManager = layoutManager
|
||||
treshold *= layoutManager.spanCount
|
||||
}
|
||||
|
||||
private fun getLastVisibleItem(lastVisibleItemPositions: IntArray): Int {
|
||||
var maxSize = 0
|
||||
for (i in lastVisibleItemPositions.indices) {
|
||||
if (i == 0) {
|
||||
maxSize = lastVisibleItemPositions[i]
|
||||
} else if (lastVisibleItemPositions[i] > maxSize) {
|
||||
maxSize = lastVisibleItemPositions[i]
|
||||
}
|
||||
}
|
||||
return maxSize
|
||||
}
|
||||
|
||||
// This happens many times a second during a scroll, so be wary of the code you place here.
|
||||
// We are given a few useful parameters to help us work out if we need to load some more data,
|
||||
// but first we check if we are waiting for the previous load to finish.
|
||||
override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) {
|
||||
var lastVisibleItemPosition = 0
|
||||
|
||||
val thisManager: RecyclerView.LayoutManager = thisManager
|
||||
val totalItemCount = thisManager.itemCount
|
||||
|
||||
when (thisManager) {
|
||||
is StaggeredGridLayoutManager -> {
|
||||
val lastVisibleItemPositions =
|
||||
thisManager.findLastVisibleItemPositions(null)
|
||||
// get maximum element within the list
|
||||
lastVisibleItemPosition = getLastVisibleItem(lastVisibleItemPositions)
|
||||
}
|
||||
is GridLayoutManager -> {
|
||||
lastVisibleItemPosition =
|
||||
thisManager.findLastVisibleItemPosition()
|
||||
}
|
||||
is LinearLayoutManager -> {
|
||||
lastVisibleItemPosition =
|
||||
thisManager.findLastVisibleItemPosition()
|
||||
}
|
||||
}
|
||||
|
||||
// If the total item count is zero and the previous isn't, assume the
|
||||
// list is invalidated and should be reset back to initial state
|
||||
if (totalItemCount < previousTotalItemCount) {
|
||||
currentPage = startingPageIndex
|
||||
previousTotalItemCount = totalItemCount
|
||||
if (totalItemCount == 0) {
|
||||
loading = true
|
||||
}
|
||||
}
|
||||
// If it’s still loading, we check to see if the dataset count has
|
||||
// changed, if so we conclude it has finished loading and update the current page
|
||||
// number and total item count.
|
||||
if (loading && totalItemCount > previousTotalItemCount) {
|
||||
loading = false
|
||||
previousTotalItemCount = totalItemCount
|
||||
}
|
||||
|
||||
// If it isn’t currently loading, we check to see if we have breached
|
||||
// the visibleThreshold and need to reload more data.
|
||||
// If we do need to reload some more data, we execute onLoadMore to fetch the data.
|
||||
// threshold should reflect how many total columns there are too
|
||||
if (!loading && lastVisibleItemPosition + treshold > totalItemCount) {
|
||||
currentPage++
|
||||
onLoadMore(currentPage, totalItemCount, view)
|
||||
loading = true
|
||||
}
|
||||
}
|
||||
|
||||
// Call this method whenever performing new searches
|
||||
fun resetState() {
|
||||
currentPage = startingPageIndex
|
||||
previousTotalItemCount = 0
|
||||
loading = true
|
||||
}
|
||||
|
||||
// Defines the process for actually loading more data based on page
|
||||
abstract fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?)
|
||||
|
||||
companion object {
|
||||
// The minimum amount of items to have below your current scroll position
|
||||
// before loading more.
|
||||
const val VISIBLE_TRESHOLD = 7
|
||||
}
|
||||
}
|
|
@ -0,0 +1,271 @@
|
|||
package org.moire.ultrasonic.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.android.viewmodel.ext.android.viewModel
|
||||
import org.koin.core.component.KoinApiExtension
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||
import org.moire.ultrasonic.domain.Artist
|
||||
import org.moire.ultrasonic.domain.GenericEntry
|
||||
import org.moire.ultrasonic.domain.MusicFolder
|
||||
import org.moire.ultrasonic.subsonic.DownloadHandler
|
||||
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
|
||||
import org.moire.ultrasonic.util.Constants
|
||||
import org.moire.ultrasonic.util.Util
|
||||
import org.moire.ultrasonic.view.SelectMusicFolderView
|
||||
|
||||
/**
|
||||
* An abstract Model, which can be extended to display a list of items of type T from the API
|
||||
* @param T: The type of data which will be used (must extend GenericEntry)
|
||||
* @param TA: The Adapter to use (must extend GenericRowAdapter)
|
||||
*/
|
||||
@KoinApiExtension
|
||||
abstract class GenericListFragment<T : GenericEntry, TA : GenericRowAdapter<T>> : Fragment() {
|
||||
internal val activeServerProvider: ActiveServerProvider by inject()
|
||||
internal val serverSettingsModel: ServerSettingsModel by viewModel()
|
||||
internal val imageLoaderProvider: ImageLoaderProvider by inject()
|
||||
protected val downloadHandler: DownloadHandler by inject()
|
||||
protected var refreshListView: SwipeRefreshLayout? = null
|
||||
internal var listView: RecyclerView? = null
|
||||
internal lateinit var viewManager: LinearLayoutManager
|
||||
internal var selectFolderHeader: SelectMusicFolderView? = null
|
||||
|
||||
/**
|
||||
* The Adapter for the RecyclerView
|
||||
* Recommendation: Implement this as a lazy delegate
|
||||
*/
|
||||
internal abstract val viewAdapter: TA
|
||||
|
||||
/**
|
||||
* The ViewModel to use to get the data
|
||||
*/
|
||||
open val listModel: GenericListModel by viewModels()
|
||||
|
||||
/**
|
||||
* The LiveData containing the list provided by the model
|
||||
* Implement this as a getter
|
||||
*/
|
||||
internal lateinit var liveDataItems: LiveData<List<T>>
|
||||
|
||||
/**
|
||||
* The central function to pass a query to the model and return a LiveData object
|
||||
*/
|
||||
abstract fun getLiveData(args: Bundle? = null): LiveData<List<T>>
|
||||
|
||||
/**
|
||||
* The id of the target in the navigation graph where we should go,
|
||||
* after the user has clicked on an item
|
||||
*/
|
||||
protected abstract val itemClickTarget: Int
|
||||
|
||||
/**
|
||||
* The id of the RecyclerView
|
||||
*/
|
||||
protected abstract val recyclerViewId: Int
|
||||
|
||||
/**
|
||||
* The id of the main layout
|
||||
*/
|
||||
abstract val mainLayout: Int
|
||||
|
||||
/**
|
||||
* The id of the refresh view
|
||||
*/
|
||||
abstract val refreshListId: Int
|
||||
|
||||
/**
|
||||
* The observer to be called if the available music folders have changed
|
||||
*/
|
||||
abstract val musicFolderObserver: (List<MusicFolder>) -> Unit
|
||||
|
||||
/**
|
||||
* Whether to show the folder selector
|
||||
*/
|
||||
internal open var folderHeaderEnabled: Boolean = true
|
||||
|
||||
fun showFolderHeader(): Boolean {
|
||||
return folderHeaderEnabled && listModel.isOffline() && !Util.getShouldUseId3Tags()
|
||||
}
|
||||
|
||||
fun setTitle(title: String?) {
|
||||
if (title == null) {
|
||||
FragmentTitle.setTitle(
|
||||
this,
|
||||
if (listModel.isOffline())
|
||||
R.string.music_library_label_offline
|
||||
else R.string.music_library_label
|
||||
)
|
||||
} else {
|
||||
FragmentTitle.setTitle(this, title)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// Set the title if available
|
||||
setTitle(arguments?.getString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE))
|
||||
|
||||
// Setup refresh handler
|
||||
refreshListView = view.findViewById(refreshListId)
|
||||
refreshListView?.setOnRefreshListener {
|
||||
listModel.refresh(refreshListView!!, arguments)
|
||||
}
|
||||
|
||||
// Populate the LiveData. This starts an API request in most cases
|
||||
liveDataItems = getLiveData(arguments)
|
||||
|
||||
// Register an observer to update our UI when the data changes
|
||||
liveDataItems.observe(viewLifecycleOwner, { newItems -> viewAdapter.setData(newItems) })
|
||||
|
||||
// Setup the Music folder handling
|
||||
listModel.getMusicFolders().observe(viewLifecycleOwner, musicFolderObserver)
|
||||
|
||||
// Create a View Manager
|
||||
viewManager = LinearLayoutManager(this.context)
|
||||
|
||||
// Show folder selector UI if enabled
|
||||
if (showFolderHeader()) {
|
||||
selectFolderHeader = SelectMusicFolderView(
|
||||
requireContext(), view as ViewGroup
|
||||
) { selectedFolderId ->
|
||||
if (!listModel.isOffline()) {
|
||||
val currentSetting = listModel.activeServer
|
||||
currentSetting.musicFolderId = selectedFolderId
|
||||
serverSettingsModel.updateItem(currentSetting)
|
||||
}
|
||||
viewAdapter.notifyDataSetChanged()
|
||||
listModel.refresh(refreshListView!!, arguments)
|
||||
}
|
||||
}
|
||||
|
||||
// Hook up the view with the manager and the adapter
|
||||
listView = view.findViewById<RecyclerView>(recyclerViewId).apply {
|
||||
setHasFixedSize(true)
|
||||
layoutManager = viewManager
|
||||
adapter = viewAdapter
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
Util.applyTheme(this.context)
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(mainLayout, container, false)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
fun onContextMenuItemSelected(menuItem: MenuItem, item: T): Boolean {
|
||||
val isArtist = (item is Artist)
|
||||
|
||||
when (menuItem.itemId) {
|
||||
R.id.menu_play_now ->
|
||||
downloadHandler.downloadRecursively(
|
||||
this,
|
||||
item.id,
|
||||
save = false,
|
||||
append = false,
|
||||
autoPlay = true,
|
||||
shuffle = false,
|
||||
background = false,
|
||||
playNext = false,
|
||||
unpin = false,
|
||||
isArtist = isArtist
|
||||
)
|
||||
R.id.menu_play_next ->
|
||||
downloadHandler.downloadRecursively(
|
||||
this,
|
||||
item.id,
|
||||
save = false,
|
||||
append = false,
|
||||
autoPlay = true,
|
||||
shuffle = true,
|
||||
background = false,
|
||||
playNext = true,
|
||||
unpin = false,
|
||||
isArtist = isArtist
|
||||
)
|
||||
R.id.menu_play_last ->
|
||||
downloadHandler.downloadRecursively(
|
||||
this,
|
||||
item.id,
|
||||
save = false,
|
||||
append = true,
|
||||
autoPlay = false,
|
||||
shuffle = false,
|
||||
background = false,
|
||||
playNext = false,
|
||||
unpin = false,
|
||||
isArtist = isArtist
|
||||
)
|
||||
R.id.menu_pin ->
|
||||
downloadHandler.downloadRecursively(
|
||||
this,
|
||||
item.id,
|
||||
save = true,
|
||||
append = true,
|
||||
autoPlay = false,
|
||||
shuffle = false,
|
||||
background = false,
|
||||
playNext = false,
|
||||
unpin = false,
|
||||
isArtist = isArtist
|
||||
)
|
||||
R.id.menu_unpin ->
|
||||
downloadHandler.downloadRecursively(
|
||||
this,
|
||||
item.id,
|
||||
save = false,
|
||||
append = false,
|
||||
autoPlay = false,
|
||||
shuffle = false,
|
||||
background = false,
|
||||
playNext = false,
|
||||
unpin = true,
|
||||
isArtist = isArtist
|
||||
)
|
||||
R.id.menu_download ->
|
||||
downloadHandler.downloadRecursively(
|
||||
this,
|
||||
item.id,
|
||||
save = false,
|
||||
append = false,
|
||||
autoPlay = false,
|
||||
shuffle = false,
|
||||
background = true,
|
||||
playNext = false,
|
||||
unpin = false,
|
||||
isArtist = isArtist
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
open fun onItemClick(item: T) {
|
||||
val bundle = Bundle()
|
||||
bundle.putString(Constants.INTENT_EXTRA_NAME_ID, item.id)
|
||||
bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, item.name)
|
||||
bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, item.id)
|
||||
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, (item is Artist))
|
||||
findNavController().navigate(itemClickTarget, bundle)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
package org.moire.ultrasonic.fragment
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import java.net.ConnectException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinApiExtension
|
||||
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.MusicFolder
|
||||
import org.moire.ultrasonic.service.CommunicationErrorHandler
|
||||
import org.moire.ultrasonic.service.MusicService
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory
|
||||
import org.moire.ultrasonic.util.Util
|
||||
|
||||
/**
|
||||
* An abstract Model, which can be extended to retrieve a list of items from the API
|
||||
*/
|
||||
@KoinApiExtension
|
||||
abstract class GenericListModel(application: Application) :
|
||||
AndroidViewModel(application), KoinComponent {
|
||||
|
||||
val activeServerProvider: ActiveServerProvider by inject()
|
||||
|
||||
val activeServer: ServerSetting
|
||||
get() = activeServerProvider.getActiveServer()
|
||||
|
||||
val context: Context
|
||||
get() = getApplication<Application>().applicationContext
|
||||
|
||||
var currentListIsSortable = true
|
||||
var showHeader = true
|
||||
var showSelectFolderHeader = false
|
||||
|
||||
internal val musicFolders: MutableLiveData<List<MusicFolder>> = MutableLiveData()
|
||||
|
||||
/**
|
||||
* Helper function to check online status
|
||||
*/
|
||||
fun isOffline(): Boolean {
|
||||
return ActiveServerProvider.isOffline()
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the cached items from the server
|
||||
*/
|
||||
fun refresh(swipe: SwipeRefreshLayout, bundle: Bundle?) {
|
||||
backgroundLoadFromServer(true, swipe, bundle ?: Bundle())
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a load() and notify the UI that we are loading
|
||||
*/
|
||||
fun backgroundLoadFromServer(
|
||||
refresh: Boolean,
|
||||
swipe: SwipeRefreshLayout,
|
||||
bundle: Bundle = Bundle()
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
swipe.isRefreshing = true
|
||||
loadFromServer(refresh, swipe, bundle)
|
||||
swipe.isRefreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the load() function with error handling
|
||||
*/
|
||||
suspend fun loadFromServer(refresh: Boolean, swipe: SwipeRefreshLayout, bundle: Bundle) =
|
||||
withContext(Dispatchers.IO) {
|
||||
val musicService = MusicServiceFactory.getMusicService()
|
||||
val isOffline = ActiveServerProvider.isOffline()
|
||||
val useId3Tags = Util.getShouldUseId3Tags()
|
||||
|
||||
try {
|
||||
load(isOffline, useId3Tags, musicService, refresh, bundle)
|
||||
} catch (exception: ConnectException) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
CommunicationErrorHandler.handleError(exception, swipe.context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the central function you need to implement if you want to extend this class
|
||||
*/
|
||||
abstract fun load(
|
||||
isOffline: Boolean,
|
||||
useId3Tags: Boolean,
|
||||
musicService: MusicService,
|
||||
refresh: Boolean,
|
||||
args: Bundle
|
||||
)
|
||||
|
||||
/**
|
||||
* Retrieves the available Music Folders in a LiveData
|
||||
*/
|
||||
fun getMusicFolders(): LiveData<List<MusicFolder>> {
|
||||
return musicFolders
|
||||
}
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* GenericRowAdapter.kt
|
||||
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.fragment
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.PopupMenu
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||
import org.moire.ultrasonic.util.ImageLoader
|
||||
import org.moire.ultrasonic.view.SelectMusicFolderView
|
||||
|
||||
/*
|
||||
* An abstract Adapter, which can be extended to display a List of <T> in a RecyclerView
|
||||
*/
|
||||
abstract class GenericRowAdapter<T>(
|
||||
private var selectFolderHeader: SelectMusicFolderView?,
|
||||
val onItemClick: (T) -> Unit,
|
||||
val onContextMenuClick: (MenuItem, T) -> Boolean,
|
||||
private val imageLoader: ImageLoader
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
open var itemList: List<T> = listOf()
|
||||
protected abstract val layout: Int
|
||||
protected abstract val contextMenuLayout: Int
|
||||
|
||||
/**
|
||||
* Sets the data to be displayed in the RecyclerView
|
||||
*/
|
||||
open fun setData(data: List<T>) {
|
||||
itemList = data
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): RecyclerView.ViewHolder {
|
||||
if (viewType == TYPE_ITEM) {
|
||||
val row = LayoutInflater.from(parent.context)
|
||||
.inflate(layout, parent, false)
|
||||
return ItemViewHolder(row)
|
||||
}
|
||||
return selectFolderHeader!!
|
||||
}
|
||||
|
||||
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
|
||||
if ((holder is ItemViewHolder) && (holder.coverArtId != null)) {
|
||||
imageLoader.cancel(holder.coverArtId)
|
||||
}
|
||||
super.onViewRecycled(holder)
|
||||
}
|
||||
|
||||
abstract override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int)
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
if (selectFolderHeader != null)
|
||||
return itemList.size + 1
|
||||
else
|
||||
return itemList.size
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return if (position == 0 && selectFolderHeader != null) TYPE_HEADER else TYPE_ITEM
|
||||
}
|
||||
|
||||
internal fun createPopupMenu(view: View, position: Int): Boolean {
|
||||
val popup = PopupMenu(view.context, view)
|
||||
val inflater: MenuInflater = popup.menuInflater
|
||||
inflater.inflate(contextMenuLayout, popup.menu)
|
||||
|
||||
val downloadMenuItem = popup.menu.findItem(R.id.menu_download)
|
||||
downloadMenuItem?.isVisible = !ActiveServerProvider.isOffline()
|
||||
|
||||
popup.setOnMenuItemClickListener { menuItem ->
|
||||
onContextMenuClick(menuItem, itemList[position])
|
||||
}
|
||||
popup.show()
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds the view properties of an Item row
|
||||
*/
|
||||
class ItemViewHolder(
|
||||
itemView: View
|
||||
) : RecyclerView.ViewHolder(itemView) {
|
||||
var section: TextView = itemView.findViewById(R.id.row_section)
|
||||
var textView: TextView = itemView.findViewById(R.id.row_artist_name)
|
||||
var layout: RelativeLayout = itemView.findViewById(R.id.row_artist_layout)
|
||||
var coverArt: ImageView = itemView.findViewById(R.id.artist_coverart)
|
||||
var coverArtId: String? = null
|
||||
}
|
||||
|
||||
companion object {
|
||||
internal const val TYPE_HEADER = 0
|
||||
internal const val TYPE_ITEM = 1
|
||||
}
|
||||
}
|
|
@ -223,7 +223,7 @@ class ServerSettingsModel(
|
|||
/**
|
||||
* Checks if there are any missing indexes in the ServerSetting list
|
||||
* For displaying the Server Settings in a ListView, it is mandatory that their indexes
|
||||
* are'nt missing. Ideally the indexes are continuous, but some circumstances (e.g.
|
||||
* aren't missing. Ideally the indexes are continuous, but some circumstances (e.g.
|
||||
* concurrency or migration errors) may get them out of order.
|
||||
* This would make the List Adapter crash, so it is best to prepare and check the list.
|
||||
*/
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* SelectAlbumFragment.kt
|
||||
* TrackCollectionFragment.kt
|
||||
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
|
@ -8,6 +8,8 @@
|
|||
package org.moire.ultrasonic.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.ContextMenu
|
||||
import android.view.ContextMenu.ContextMenuInfo
|
||||
import android.view.LayoutInflater
|
||||
|
@ -29,6 +31,7 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
|||
import java.security.SecureRandom
|
||||
import java.util.Collections
|
||||
import java.util.Random
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.android.viewmodel.ext.android.viewModel
|
||||
|
@ -40,6 +43,7 @@ import org.moire.ultrasonic.domain.MusicDirectory
|
|||
import org.moire.ultrasonic.domain.MusicFolder
|
||||
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.getTitle
|
||||
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
|
||||
import org.moire.ultrasonic.service.CommunicationErrorHandler
|
||||
import org.moire.ultrasonic.service.MediaPlayerController
|
||||
import org.moire.ultrasonic.subsonic.DownloadHandler
|
||||
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
|
||||
|
@ -58,8 +62,8 @@ import org.moire.ultrasonic.view.SongView
|
|||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Displays a group of playable media from the library, which can be an Album, a Playlist, etc.
|
||||
* TODO: Break up this class into smaller more specific classes, extending a base class if necessary
|
||||
* Displays a group of tracks, eg. the songs of an album, of a playlist etc.
|
||||
* TODO: Refactor this fragment and model to extend the GenericListFragment
|
||||
*/
|
||||
@KoinApiExtension
|
||||
class TrackCollectionFragment : Fragment() {
|
||||
|
@ -94,7 +98,6 @@ class TrackCollectionFragment : Fragment() {
|
|||
private val activeServerProvider: ActiveServerProvider by inject()
|
||||
|
||||
private val model: TrackCollectionModel by viewModels()
|
||||
|
||||
private val random: Random = SecureRandom()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
@ -143,7 +146,6 @@ class TrackCollectionFragment : Fragment() {
|
|||
model.musicFolders.observe(viewLifecycleOwner, musicFolderObserver)
|
||||
model.currentDirectory.observe(viewLifecycleOwner, defaultObserver)
|
||||
model.songsForGenre.observe(viewLifecycleOwner, songsForGenreObserver)
|
||||
model.albumList.observe(viewLifecycleOwner, albumListObserver)
|
||||
|
||||
albumListView!!.choiceMode = ListView.CHOICE_MODE_MULTIPLE
|
||||
albumListView!!.setOnItemClickListener { parent, theView, position, _ ->
|
||||
|
@ -156,7 +158,7 @@ class TrackCollectionFragment : Fragment() {
|
|||
bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, entry.title)
|
||||
bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, entry.parent)
|
||||
Navigation.findNavController(theView).navigate(
|
||||
R.id.selectAlbumFragment,
|
||||
R.id.trackCollectionFragment,
|
||||
bundle
|
||||
)
|
||||
} else if (entry != null && entry.isVideo) {
|
||||
|
@ -229,6 +231,14 @@ class TrackCollectionFragment : Fragment() {
|
|||
updateDisplay(false)
|
||||
}
|
||||
|
||||
val handler = CoroutineExceptionHandler { _, exception ->
|
||||
println("CoroutineExceptionHandler got $exception")
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
context?.let { CommunicationErrorHandler.handleError(exception, it) }
|
||||
}
|
||||
refreshAlbumListView!!.isRefreshing = false
|
||||
}
|
||||
|
||||
private fun updateDisplay(refresh: Boolean) {
|
||||
val args = requireArguments()
|
||||
val id = args.getString(Constants.INTENT_EXTRA_NAME_ID)
|
||||
|
@ -242,13 +252,8 @@ class TrackCollectionFragment : Fragment() {
|
|||
val playlistName = args.getString(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME)
|
||||
val shareId = args.getString(Constants.INTENT_EXTRA_NAME_SHARE_ID)
|
||||
val shareName = args.getString(Constants.INTENT_EXTRA_NAME_SHARE_NAME)
|
||||
val albumListType = args.getString(
|
||||
Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE
|
||||
)
|
||||
val genreName = args.getString(Constants.INTENT_EXTRA_NAME_GENRE_NAME)
|
||||
val albumListTitle = args.getInt(
|
||||
Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, 0
|
||||
)
|
||||
|
||||
val getStarredTracks = args.getInt(Constants.INTENT_EXTRA_NAME_STARRED, 0)
|
||||
val getVideos = args.getInt(Constants.INTENT_EXTRA_NAME_VIDEOS, 0)
|
||||
val getRandomTracks = args.getInt(Constants.INTENT_EXTRA_NAME_RANDOM, 0)
|
||||
|
@ -267,7 +272,7 @@ class TrackCollectionFragment : Fragment() {
|
|||
setTitle(this@TrackCollectionFragment, name)
|
||||
}
|
||||
|
||||
model.viewModelScope.launch {
|
||||
model.viewModelScope.launch(handler) {
|
||||
refreshAlbumListView!!.isRefreshing = true
|
||||
|
||||
model.getMusicFolders(refresh)
|
||||
|
@ -281,9 +286,6 @@ class TrackCollectionFragment : Fragment() {
|
|||
} else if (shareId != null) {
|
||||
setTitle(shareName)
|
||||
model.getShare(shareId)
|
||||
} else if (albumListType != null) {
|
||||
setTitle(albumListTitle)
|
||||
model.getAlbumList(albumListType, albumListSize, albumListOffset)
|
||||
} else if (genreName != null) {
|
||||
setTitle(genreName)
|
||||
model.getSongsForGenre(genreName, albumListSize, albumListOffset)
|
||||
|
@ -321,7 +323,7 @@ class TrackCollectionFragment : Fragment() {
|
|||
|
||||
if (entry != null && entry.isDirectory) {
|
||||
val inflater = requireActivity().menuInflater
|
||||
inflater.inflate(R.menu.select_album_context, menu)
|
||||
inflater.inflate(R.menu.generic_context_menu, menu)
|
||||
}
|
||||
|
||||
shareButton = menu.findItem(R.id.menu_item_share)
|
||||
|
@ -330,7 +332,7 @@ class TrackCollectionFragment : Fragment() {
|
|||
shareButton!!.isVisible = !isOffline()
|
||||
}
|
||||
|
||||
val downloadMenuItem = menu.findItem(R.id.album_menu_download)
|
||||
val downloadMenuItem = menu.findItem(R.id.menu_download)
|
||||
if (downloadMenuItem != null) {
|
||||
downloadMenuItem.isVisible = !isOffline()
|
||||
}
|
||||
|
@ -346,42 +348,42 @@ class TrackCollectionFragment : Fragment() {
|
|||
val entryId = entry.id
|
||||
|
||||
when (menuItem.itemId) {
|
||||
R.id.album_menu_play_now -> {
|
||||
R.id.menu_play_now -> {
|
||||
downloadHandler.downloadRecursively(
|
||||
this, entryId, save = false, append = false,
|
||||
autoPlay = true, shuffle = false, background = false,
|
||||
playNext = false, unpin = false, isArtist = false
|
||||
)
|
||||
}
|
||||
R.id.album_menu_play_next -> {
|
||||
R.id.menu_play_next -> {
|
||||
downloadHandler.downloadRecursively(
|
||||
this, entryId, save = false, append = false,
|
||||
autoPlay = false, shuffle = false, background = false,
|
||||
playNext = true, unpin = false, isArtist = false
|
||||
)
|
||||
}
|
||||
R.id.album_menu_play_last -> {
|
||||
R.id.menu_play_last -> {
|
||||
downloadHandler.downloadRecursively(
|
||||
this, entryId, save = false, append = true,
|
||||
autoPlay = false, shuffle = false, background = false,
|
||||
playNext = false, unpin = false, isArtist = false
|
||||
)
|
||||
}
|
||||
R.id.album_menu_pin -> {
|
||||
R.id.menu_pin -> {
|
||||
downloadHandler.downloadRecursively(
|
||||
this, entryId, save = true, append = true,
|
||||
autoPlay = false, shuffle = false, background = false,
|
||||
playNext = false, unpin = false, isArtist = false
|
||||
)
|
||||
}
|
||||
R.id.album_menu_unpin -> {
|
||||
R.id.menu_unpin -> {
|
||||
downloadHandler.downloadRecursively(
|
||||
this, entryId, save = false, append = false,
|
||||
autoPlay = false, shuffle = false, background = false,
|
||||
playNext = false, unpin = true, isArtist = false
|
||||
)
|
||||
}
|
||||
R.id.album_menu_download -> {
|
||||
R.id.menu_download -> {
|
||||
downloadHandler.downloadRecursively(
|
||||
this, entryId, save = false, append = false,
|
||||
autoPlay = false, shuffle = false, background = true,
|
||||
|
@ -389,6 +391,7 @@ class TrackCollectionFragment : Fragment() {
|
|||
)
|
||||
}
|
||||
R.id.select_album_play_all -> {
|
||||
// TODO: Why is this being handled here?!
|
||||
playAll()
|
||||
}
|
||||
R.id.menu_item_share -> {
|
||||
|
@ -629,54 +632,6 @@ class TrackCollectionFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
private val albumListObserver = Observer<MusicDirectory> { musicDirectory ->
|
||||
if (musicDirectory.getChildren().isNotEmpty()) {
|
||||
pinButton!!.visibility = View.GONE
|
||||
unpinButton!!.visibility = View.GONE
|
||||
downloadButton!!.visibility = View.GONE
|
||||
deleteButton!!.visibility = View.GONE
|
||||
|
||||
// Hide more button when results are less than album list size
|
||||
if (musicDirectory.getChildren().size < requireArguments().getInt(
|
||||
Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0
|
||||
)
|
||||
) {
|
||||
moreButton!!.visibility = View.GONE
|
||||
} else {
|
||||
moreButton!!.visibility = View.VISIBLE
|
||||
moreButton!!.setOnClickListener {
|
||||
val theAlbumListTitle = requireArguments().getInt(
|
||||
Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, 0
|
||||
)
|
||||
val type = requireArguments().getString(
|
||||
Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE
|
||||
)
|
||||
val theSize = requireArguments().getInt(
|
||||
Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0
|
||||
)
|
||||
val theOffset = requireArguments().getInt(
|
||||
Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0
|
||||
) + theSize
|
||||
|
||||
val bundle = Bundle()
|
||||
bundle.putInt(
|
||||
Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, theAlbumListTitle
|
||||
)
|
||||
bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, type)
|
||||
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, theSize)
|
||||
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, theOffset)
|
||||
Navigation.findNavController(requireView()).navigate(
|
||||
R.id.selectAlbumFragment, bundle
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
moreButton!!.visibility = View.GONE
|
||||
}
|
||||
|
||||
updateInterfaceWithEntries(musicDirectory)
|
||||
}
|
||||
|
||||
private val songsForGenreObserver = Observer<MusicDirectory> { musicDirectory ->
|
||||
|
||||
// Hide more button when results are less than album list size
|
||||
|
@ -699,7 +654,9 @@ class TrackCollectionFragment : Fragment() {
|
|||
bundle.putString(Constants.INTENT_EXTRA_NAME_GENRE_NAME, theGenre)
|
||||
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, size)
|
||||
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, theOffset)
|
||||
Navigation.findNavController(requireView()).navigate(R.id.selectAlbumFragment, bundle)
|
||||
|
||||
Navigation.findNavController(requireView())
|
||||
.navigate(R.id.trackCollectionFragment, bundle)
|
||||
}
|
||||
|
||||
updateInterfaceWithEntries(musicDirectory)
|
||||
|
@ -710,7 +667,7 @@ class TrackCollectionFragment : Fragment() {
|
|||
private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory) {
|
||||
val entries = musicDirectory.getChildren()
|
||||
|
||||
if (model.currentDirectoryIsSortable && Util.getShouldSortByDisc()) {
|
||||
if (model.currentListIsSortable && Util.getShouldSortByDisc()) {
|
||||
Collections.sort(entries, EntryByDiscAndTrackComparator())
|
||||
}
|
||||
|
||||
|
@ -764,7 +721,7 @@ class TrackCollectionFragment : Fragment() {
|
|||
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, listSize)
|
||||
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, offset)
|
||||
Navigation.findNavController(requireView()).navigate(
|
||||
R.id.selectAlbumFragment, bundle
|
||||
R.id.trackCollectionFragment, bundle
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -829,7 +786,7 @@ class TrackCollectionFragment : Fragment() {
|
|||
)
|
||||
}
|
||||
|
||||
model.currentDirectoryIsSortable = true
|
||||
model.currentListIsSortable = true
|
||||
}
|
||||
|
||||
private fun createHeader(
|
||||
|
|
|
@ -1,46 +1,40 @@
|
|||
/*
|
||||
* TrackCollectionModel.kt
|
||||
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.fragment
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import android.os.Bundle
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import java.util.LinkedList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinApiExtension
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
import org.moire.ultrasonic.domain.MusicFolder
|
||||
import org.moire.ultrasonic.service.MusicService
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory
|
||||
import org.moire.ultrasonic.util.Util
|
||||
|
||||
// TODO: Break up this class into smaller more specific classes, extending a base class if necessary
|
||||
/*
|
||||
* Model for retrieving different collections of tracks from the API
|
||||
* TODO: Refactor this model to extend the GenericListModel
|
||||
*/
|
||||
@KoinApiExtension
|
||||
class TrackCollectionModel(application: Application) : AndroidViewModel(application), KoinComponent {
|
||||
|
||||
private val context: Context
|
||||
get() = getApplication<Application>().applicationContext
|
||||
|
||||
private val activeServerProvider: ActiveServerProvider by inject()
|
||||
class TrackCollectionModel(application: Application) : GenericListModel(application) {
|
||||
|
||||
private val allSongsId = "-1"
|
||||
|
||||
val musicFolders: MutableLiveData<List<MusicFolder>> = MutableLiveData()
|
||||
val albumList: MutableLiveData<MusicDirectory> = MutableLiveData()
|
||||
val currentDirectory: MutableLiveData<MusicDirectory> = MutableLiveData()
|
||||
val songsForGenre: MutableLiveData<MusicDirectory> = MutableLiveData()
|
||||
|
||||
var currentDirectoryIsSortable = true
|
||||
var showHeader = true
|
||||
var showSelectFolderHeader = false
|
||||
|
||||
suspend fun getMusicFolders(refresh: Boolean) {
|
||||
withContext(Dispatchers.IO) {
|
||||
if (!ActiveServerProvider.isOffline()) {
|
||||
if (!isOffline()) {
|
||||
val musicService = MusicServiceFactory.getMusicService()
|
||||
musicFolders.postValue(musicService.getMusicFolders(refresh))
|
||||
}
|
||||
|
@ -124,6 +118,10 @@ class TrackCollectionModel(application: Application) : AndroidViewModel(applicat
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* TODO: This method should be moved to AlbumListModel,
|
||||
* since it displays a list of albums by a specified artist.
|
||||
*/
|
||||
suspend fun getArtist(refresh: Boolean, id: String?, name: String?) {
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
|
@ -164,7 +162,7 @@ class TrackCollectionModel(application: Application) : AndroidViewModel(applicat
|
|||
|
||||
val musicDirectory: MusicDirectory
|
||||
|
||||
musicDirectory = if (allSongsId == id) {
|
||||
if (allSongsId == id) {
|
||||
val root = MusicDirectory()
|
||||
|
||||
val songs: MutableCollection<MusicDirectory.Entry> = LinkedList()
|
||||
|
@ -189,10 +187,11 @@ class TrackCollectionModel(application: Application) : AndroidViewModel(applicat
|
|||
root.addChild(song)
|
||||
}
|
||||
}
|
||||
root
|
||||
musicDirectory = root
|
||||
} else {
|
||||
service.getAlbum(id, name, refresh)
|
||||
musicDirectory = service.getAlbum(id, name, refresh)
|
||||
}
|
||||
|
||||
currentDirectory.postValue(musicDirectory)
|
||||
}
|
||||
}
|
||||
|
@ -237,7 +236,7 @@ class TrackCollectionModel(application: Application) : AndroidViewModel(applicat
|
|||
val service = MusicServiceFactory.getMusicService()
|
||||
val musicDirectory = service.getRandomSongs(size)
|
||||
|
||||
currentDirectoryIsSortable = false
|
||||
currentListIsSortable = false
|
||||
currentDirectory.postValue(musicDirectory)
|
||||
}
|
||||
}
|
||||
|
@ -281,49 +280,18 @@ class TrackCollectionModel(application: Application) : AndroidViewModel(applicat
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun getAlbumList(albumListType: String, size: Int, offset: Int) {
|
||||
|
||||
showHeader = false
|
||||
showSelectFolderHeader = !ActiveServerProvider.isOffline() &&
|
||||
!Util.getShouldUseId3Tags() && (
|
||||
(albumListType == AlbumListType.SORTED_BY_NAME.toString()) ||
|
||||
(albumListType == AlbumListType.SORTED_BY_ARTIST.toString())
|
||||
)
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
val service = MusicServiceFactory.getMusicService()
|
||||
val musicDirectory: MusicDirectory
|
||||
val musicFolderId = if (showSelectFolderHeader) {
|
||||
activeServerProvider.getActiveServer().musicFolderId
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
if (Util.getShouldUseId3Tags()) {
|
||||
musicDirectory = service.getAlbumList2(
|
||||
albumListType, size,
|
||||
offset, musicFolderId
|
||||
)
|
||||
} else {
|
||||
musicDirectory = service.getAlbumList(
|
||||
albumListType, size,
|
||||
offset, musicFolderId
|
||||
)
|
||||
}
|
||||
|
||||
currentDirectoryIsSortable = sortableCollection(albumListType)
|
||||
albumList.postValue(musicDirectory)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sortableCollection(albumListType: String): Boolean {
|
||||
return albumListType != "newest" && albumListType != "random" &&
|
||||
albumListType != "highest" && albumListType != "recent" &&
|
||||
albumListType != "frequent"
|
||||
}
|
||||
|
||||
// Returns true if the directory contains only folders
|
||||
private fun hasOnlyFolders(musicDirectory: MusicDirectory) =
|
||||
musicDirectory.getChildren(includeDirs = true, includeFiles = false).size ==
|
||||
musicDirectory.getChildren(includeDirs = true, includeFiles = true).size
|
||||
|
||||
override fun load(
|
||||
isOffline: Boolean,
|
||||
useId3Tags: Boolean,
|
||||
musicService: MusicService,
|
||||
refresh: Boolean,
|
||||
args: Bundle
|
||||
) {
|
||||
// See To_Do at the top
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,51 +1,95 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
|
||||
a:orientation="horizontal"
|
||||
a:layout_width="fill_parent"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:a="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
a:id="@+id/row_artist_layout"
|
||||
a:layout_width="match_parent"
|
||||
a:layout_height="wrap_content"
|
||||
a:minHeight="?android:attr/listPreferredItemHeight">
|
||||
a:background="?android:attr/selectableItemBackground"
|
||||
a:clickable="true"
|
||||
a:focusable="true">
|
||||
|
||||
<ImageView
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
a:id="@+id/album_coverart"
|
||||
a:layout_width="64dp"
|
||||
a:layout_height="64dp"
|
||||
a:layout_gravity="left|center_vertical"
|
||||
a:paddingLeft="3dip" />
|
||||
a:layout_gravity="center_horizontal|center_vertical"
|
||||
a:layout_marginStart="6dp"
|
||||
a:layout_marginLeft="6dp"
|
||||
a:layout_marginTop="6dp"
|
||||
a:scaleType="fitCenter"
|
||||
a:src="@drawable/unknown_album"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:shapeAppearanceOverlay="@style/largeRoundedImageView" />
|
||||
|
||||
<LinearLayout
|
||||
a:id="@+id/row_album_details"
|
||||
a:layout_width="0dp"
|
||||
a:layout_height="74dp"
|
||||
a:layout_marginStart="10dp"
|
||||
a:layout_marginLeft="10dp"
|
||||
a:drawablePadding="6dip"
|
||||
a:gravity="center_vertical"
|
||||
a:minHeight="56dip"
|
||||
a:orientation="vertical"
|
||||
a:layout_width="0dip"
|
||||
a:layout_height="wrap_content"
|
||||
a:layout_weight="1"
|
||||
a:layout_gravity="left|center_vertical"
|
||||
a:paddingLeft="6dip"
|
||||
a:paddingRight="3dip">
|
||||
a:paddingLeft="3dip"
|
||||
a:paddingRight="3dip"
|
||||
a:textAppearance="?android:attr/textAppearanceMedium"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline2"
|
||||
app:layout_constraintLeft_toRightOf="@+id/album_coverart"
|
||||
app:layout_constraintStart_toEndOf="@+id/album_coverart"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<TextView
|
||||
a:id="@+id/album_title"
|
||||
a:layout_width="wrap_content"
|
||||
a:layout_height="wrap_content"
|
||||
a:textAppearance="?android:attr/textAppearanceMedium"
|
||||
a:ellipsize="marquee"
|
||||
a:singleLine="true"
|
||||
a:ellipsize="marquee" />
|
||||
a:textAppearance="?android:attr/textAppearanceMedium"
|
||||
tools:text="TITLE" />
|
||||
|
||||
<TextView
|
||||
a:id="@+id/album_artist"
|
||||
a:layout_width="wrap_content"
|
||||
a:layout_height="wrap_content"
|
||||
a:singleLine="true"
|
||||
a:textAppearance="?android:attr/textAppearanceSmall"
|
||||
a:singleLine="true" />
|
||||
tools:text="ARTIST" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
a:id="@+id/album_star"
|
||||
a:layout_width="38dp"
|
||||
a:layout_height="fill_parent"
|
||||
a:gravity="center_vertical"
|
||||
a:layout_height="38dp"
|
||||
a:layout_marginStart="16dp"
|
||||
a:layout_marginLeft="16dp"
|
||||
a:layout_marginTop="16dp"
|
||||
a:background="@android:color/transparent"
|
||||
a:src="?attr/star_hollow"
|
||||
a:focusable="false"
|
||||
a:paddingRight="3dip" />
|
||||
a:gravity="center_horizontal"
|
||||
a:paddingRight="3dip"
|
||||
a:src="?attr/star_hollow"
|
||||
app:layout_constraintLeft_toRightOf="@+id/row_album_details"
|
||||
app:layout_constraintStart_toEndOf="@+id/row_album_details"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@drawable/ic_star_hollow_dark"
|
||||
a:paddingEnd="3dip" />
|
||||
|
||||
</LinearLayout>
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
a:id="@+id/guideline"
|
||||
a:layout_width="wrap_content"
|
||||
a:layout_height="wrap_content"
|
||||
a:orientation="vertical"
|
||||
app:layout_constraintGuide_begin="76dp" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
a:id="@+id/guideline2"
|
||||
a:layout_width="wrap_content"
|
||||
a:layout_height="wrap_content"
|
||||
a:orientation="vertical"
|
||||
app:layout_constraintGuide_begin="346dp" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
|
||||
a:orientation="horizontal"
|
||||
a:layout_width="fill_parent"
|
||||
a:layout_height="wrap_content"
|
||||
a:minHeight="?android:attr/listPreferredItemHeight">
|
||||
|
||||
<ImageView
|
||||
a:id="@+id/album_coverart"
|
||||
a:layout_width="64dp"
|
||||
a:layout_height="64dp"
|
||||
a:layout_gravity="left|center_vertical"
|
||||
a:paddingLeft="3dip" />
|
||||
|
||||
<LinearLayout
|
||||
a:orientation="vertical"
|
||||
a:layout_width="0dip"
|
||||
a:layout_height="wrap_content"
|
||||
a:layout_weight="1"
|
||||
a:layout_gravity="left|center_vertical"
|
||||
a:paddingLeft="6dip"
|
||||
a:paddingRight="3dip">
|
||||
|
||||
<TextView
|
||||
a:id="@+id/album_title"
|
||||
a:layout_width="wrap_content"
|
||||
a:layout_height="wrap_content"
|
||||
a:textAppearance="?android:attr/textAppearanceMedium"
|
||||
a:singleLine="true"
|
||||
a:ellipsize="marquee" />
|
||||
|
||||
<TextView
|
||||
a:id="@+id/album_artist"
|
||||
a:layout_width="wrap_content"
|
||||
a:layout_height="wrap_content"
|
||||
a:textAppearance="?android:attr/textAppearanceSmall"
|
||||
a:singleLine="true" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
a:id="@+id/album_star"
|
||||
a:layout_width="38dp"
|
||||
a:layout_height="fill_parent"
|
||||
a:gravity="center_vertical"
|
||||
a:background="@android:color/transparent"
|
||||
a:src="?attr/star_hollow"
|
||||
a:focusable="false"
|
||||
a:paddingRight="3dip" />
|
||||
|
||||
</LinearLayout>
|
|
@ -6,13 +6,13 @@
|
|||
a:orientation="vertical">
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
a:id="@+id/select_artist_refresh"
|
||||
a:id="@+id/generic_list_refresh"
|
||||
a:layout_width="fill_parent"
|
||||
a:layout_height="0dip"
|
||||
a:layout_weight="1.0">
|
||||
|
||||
<com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView
|
||||
a:id="@+id/select_artist_list"
|
||||
a:id="@+id/generic_list_recycler"
|
||||
a:layout_width="match_parent"
|
||||
a:layout_height="match_parent"
|
||||
a:paddingTop="8dp"
|
|
@ -19,8 +19,10 @@
|
|||
android:id="@+id/now_playing_image"
|
||||
android:layout_width="64.0dip"
|
||||
android:layout_height="64.0dip"
|
||||
android:layout_marginLeft="6dp"
|
||||
android:focusable="true"
|
||||
android:gravity="center" />
|
||||
android:gravity="center"
|
||||
android:layout_marginStart="6dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0.0dp"
|
||||
|
@ -28,7 +30,8 @@
|
|||
android:layout_gravity="center_vertical"
|
||||
android:layout_weight="1.0"
|
||||
android:orientation="vertical"
|
||||
android:paddingLeft="11.0dip">
|
||||
android:paddingLeft="11.0dip"
|
||||
android:paddingStart="11.0dip">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/now_playing_trackname"
|
||||
|
|
|
@ -2,22 +2,22 @@
|
|||
<menu xmlns:a="http://schemas.android.com/apk/res/android" >
|
||||
|
||||
<item
|
||||
a:id="@+id/artist_menu_play_now"
|
||||
a:id="@+id/menu_play_now"
|
||||
a:title="@string/common.play_now"/>
|
||||
<item
|
||||
a:id="@+id/artist_menu_play_next"
|
||||
a:id="@+id/menu_play_next"
|
||||
a:title="@string/common.play_next"/>
|
||||
<item
|
||||
a:id="@+id/artist_menu_play_last"
|
||||
a:id="@+id/menu_play_last"
|
||||
a:title="@string/common.play_last"/>
|
||||
<item
|
||||
a:id="@+id/artist_menu_pin"
|
||||
a:id="@+id/menu_pin"
|
||||
a:title="@string/common.pin"/>
|
||||
<item
|
||||
a:id="@+id/artist_menu_unpin"
|
||||
a:id="@+id/menu_unpin"
|
||||
a:title="@string/common.unpin"/>
|
||||
<item
|
||||
a:id="@+id/artist_menu_download"
|
||||
a:id="@+id/menu_download"
|
||||
a:title="@string/common.download"/>
|
||||
|
||||
</menu>
|
|
@ -2,22 +2,22 @@
|
|||
<menu xmlns:a="http://schemas.android.com/apk/res/android" >
|
||||
|
||||
<item
|
||||
a:id="@+id/album_menu_play_now"
|
||||
a:id="@+id/menu_play_now"
|
||||
a:title="@string/common.play_now"/>
|
||||
<item
|
||||
a:id="@+id/album_menu_play_next"
|
||||
a:title="@string/common.play_next"/>
|
||||
<item
|
||||
a:id="@+id/album_menu_play_last"
|
||||
a:id="@+id/menu_play_last"
|
||||
a:title="@string/common.play_last"/>
|
||||
<item
|
||||
a:id="@+id/album_menu_pin"
|
||||
a:id="@+id/menu_pin"
|
||||
a:title="@string/common.pin"/>
|
||||
<item
|
||||
a:id="@+id/album_menu_unpin"
|
||||
a:id="@+id/menu_unpin"
|
||||
a:title="@string/common.unpin"/>
|
||||
<item
|
||||
a:id="@+id/album_menu_download"
|
||||
a:id="@+id/menu_download"
|
||||
a:title="@string/common.download"/>
|
||||
<item
|
||||
a:id="@+id/menu_item_share"
|
|
@ -11,7 +11,7 @@
|
|||
a:icon="?attr/home"
|
||||
a:title="@string/button_bar.home" />
|
||||
<item
|
||||
a:id="@+id/selectArtistFragment"
|
||||
a:id="@+id/mediaLibraryFragment"
|
||||
a:checkable="true"
|
||||
a:icon="?attr/browse"
|
||||
a:title="@string/button_bar.browse" />
|
||||
|
|
|
@ -8,8 +8,14 @@
|
|||
android:name="org.moire.ultrasonic.fragment.MainFragment"
|
||||
android:label="@string/common.appname" >
|
||||
<action
|
||||
android:id="@+id/mainToSelectAlbum"
|
||||
app:destination="@id/selectAlbumFragment" />
|
||||
android:id="@+id/mainToTrackCollection"
|
||||
app:destination="@id/trackCollectionFragment" />
|
||||
<action
|
||||
android:id="@+id/mainToAlbumList"
|
||||
app:destination="@id/albumListFragment" />
|
||||
<action
|
||||
android:id="@+id/mainToArtistList"
|
||||
app:destination="@id/artistListFragment" />
|
||||
<action
|
||||
android:id="@+id/mainToSelectGenre"
|
||||
app:destination="@id/selectGenreFragment" />
|
||||
|
@ -18,37 +24,48 @@
|
|||
app:destination="@id/serverSelectorFragment" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/selectArtistFragment"
|
||||
android:id="@+id/mediaLibraryFragment"
|
||||
android:name="org.moire.ultrasonic.fragment.ArtistListFragment"
|
||||
android:label="@string/music_library.label" >
|
||||
<action
|
||||
android:id="@+id/selectArtistToSelectAlbum"
|
||||
app:destination="@id/selectAlbumFragment" />
|
||||
app:destination="@id/trackCollectionFragment" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/selectAlbumFragment"
|
||||
android:id="@+id/artistListFragment"
|
||||
android:name="org.moire.ultrasonic.fragment.ArtistListFragment" >
|
||||
<action
|
||||
android:id="@+id/selectArtistToSelectAlbum"
|
||||
app:destination="@id/trackCollectionFragment" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/trackCollectionFragment"
|
||||
android:name="org.moire.ultrasonic.fragment.TrackCollectionFragment" >
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/albumListFragment"
|
||||
android:name="org.moire.ultrasonic.fragment.AlbumListFragment" >
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/searchFragment"
|
||||
android:name="org.moire.ultrasonic.fragment.SearchFragment" >
|
||||
<action
|
||||
android:id="@+id/searchToSelectAlbum"
|
||||
app:destination="@id/selectAlbumFragment" />
|
||||
app:destination="@id/trackCollectionFragment" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/playlistsFragment"
|
||||
android:name="org.moire.ultrasonic.fragment.PlaylistsFragment" >
|
||||
<action
|
||||
android:id="@+id/playlistsToSelectAlbum"
|
||||
app:destination="@id/selectAlbumFragment" />
|
||||
app:destination="@id/trackCollectionFragment" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/sharesFragment"
|
||||
android:name="org.moire.ultrasonic.fragment.SharesFragment" >
|
||||
<action
|
||||
android:id="@+id/sharesToSelectAlbum"
|
||||
app:destination="@id/selectAlbumFragment" />
|
||||
app:destination="@id/trackCollectionFragment" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/bookmarksFragment"
|
||||
|
@ -61,7 +78,7 @@
|
|||
android:name="org.moire.ultrasonic.fragment.PodcastFragment" >
|
||||
<action
|
||||
android:id="@+id/podcastToSelectAlbum"
|
||||
app:destination="@id/selectAlbumFragment" />
|
||||
app:destination="@id/trackCollectionFragment" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/settingsFragment"
|
||||
|
@ -81,7 +98,7 @@
|
|||
android:name="org.moire.ultrasonic.fragment.PlayerFragment" >
|
||||
<action
|
||||
android:id="@+id/playerToSelectAlbum"
|
||||
app:destination="@id/selectAlbumFragment" />
|
||||
app:destination="@id/trackCollectionFragment" />
|
||||
<action
|
||||
android:id="@+id/playerToLyrics"
|
||||
app:destination="@id/lyricsFragment" />
|
||||
|
|
|
@ -25,6 +25,11 @@
|
|||
<item name="cornerSize">8dp</item>
|
||||
</style>
|
||||
|
||||
<style name="largeRoundedImageView" parent="">
|
||||
<item name="cornerFamily">rounded</item>
|
||||
<item name="cornerSize">2dp</item>
|
||||
</style>
|
||||
|
||||
<style name="ThemeOverlay.AppCompat.navTheme">
|
||||
<item name="colorPrimary">?attr/color_menu_selected</item>
|
||||
<item name="colorControlHighlight">?attr/color_selected</item>
|
||||
|
|
Loading…
Reference in New Issue