mirror of
https://gitlab.com/ultrasonic/ultrasonic.git
synced 2025-04-20 02:57:40 +03:00
BookmarksFragment is now based on TrackCollectionFragment
Also start SearchFragment.kt
This commit is contained in:
parent
7640f4c4aa
commit
f8a87f7c85
@ -6,7 +6,7 @@ import org.moire.ultrasonic.domain.MusicDirectory.Entry
|
||||
* The result of a search. Contains matching artists, albums and songs.
|
||||
*/
|
||||
data class SearchResult(
|
||||
val artists: List<Artist>,
|
||||
val albums: List<Entry>,
|
||||
val songs: List<Entry>
|
||||
val artists: List<Artist> = listOf(),
|
||||
val albums: List<Entry> = listOf(),
|
||||
val songs: List<Entry> = listOf()
|
||||
)
|
||||
|
@ -1,387 +0,0 @@
|
||||
package org.moire.ultrasonic.fragment;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.util.Pair;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ListView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
|
||||
import org.moire.ultrasonic.R;
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider;
|
||||
import org.moire.ultrasonic.domain.MusicDirectory;
|
||||
import org.moire.ultrasonic.service.DownloadFile;
|
||||
import org.moire.ultrasonic.service.MediaPlayerController;
|
||||
import org.moire.ultrasonic.service.MusicService;
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory;
|
||||
import org.moire.ultrasonic.subsonic.ImageLoaderProvider;
|
||||
import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker;
|
||||
import org.moire.ultrasonic.subsonic.VideoPlayer;
|
||||
import org.moire.ultrasonic.util.CancellationToken;
|
||||
import org.moire.ultrasonic.util.Constants;
|
||||
import org.moire.ultrasonic.util.FragmentBackgroundTask;
|
||||
import org.moire.ultrasonic.util.Util;
|
||||
import org.moire.ultrasonic.view.EntryAdapter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import kotlin.Lazy;
|
||||
|
||||
import static org.koin.java.KoinJavaComponent.inject;
|
||||
|
||||
/**
|
||||
* Lists the Bookmarks available on the server
|
||||
*/
|
||||
public class BookmarksFragment extends Fragment {
|
||||
|
||||
private SwipeRefreshLayout refreshAlbumListView;
|
||||
private ListView albumListView;
|
||||
private View albumButtons;
|
||||
private View emptyView;
|
||||
private ImageView playNowButton;
|
||||
private ImageView pinButton;
|
||||
private ImageView unpinButton;
|
||||
private ImageView downloadButton;
|
||||
private ImageView deleteButton;
|
||||
|
||||
private final Lazy<MediaPlayerController> mediaPlayerController = inject(MediaPlayerController.class);
|
||||
private final Lazy<ImageLoaderProvider> imageLoader = inject(ImageLoaderProvider.class);
|
||||
private final Lazy<NetworkAndStorageChecker> networkAndStorageChecker = inject(NetworkAndStorageChecker.class);
|
||||
private CancellationToken cancellationToken;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
Util.applyTheme(this.getContext());
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.select_album, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
cancellationToken = new CancellationToken();
|
||||
albumButtons = view.findViewById(R.id.menu_album);
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
refreshAlbumListView = view.findViewById(R.id.select_album_entries_refresh);
|
||||
albumListView = view.findViewById(R.id.select_album_entries_list);
|
||||
|
||||
refreshAlbumListView.setOnRefreshListener(() -> {
|
||||
enableButtons();
|
||||
getBookmarks();
|
||||
});
|
||||
|
||||
albumListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
|
||||
|
||||
albumListView.setOnItemClickListener((parent, view17, position, id) -> {
|
||||
if (position >= 0)
|
||||
{
|
||||
MusicDirectory.Entry entry = (MusicDirectory.Entry) parent.getItemAtPosition(position);
|
||||
|
||||
if (entry != null)
|
||||
{
|
||||
if (entry.isVideo())
|
||||
{
|
||||
VideoPlayer.Companion.playVideo(getContext(), entry);
|
||||
}
|
||||
else
|
||||
{
|
||||
enableButtons();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ImageView selectButton = view.findViewById(R.id.select_album_select);
|
||||
playNowButton = view.findViewById(R.id.select_album_play_now);
|
||||
ImageView playNextButton = view.findViewById(R.id.select_album_play_next);
|
||||
ImageView playLastButton = view.findViewById(R.id.select_album_play_last);
|
||||
pinButton = view.findViewById(R.id.select_album_pin);
|
||||
unpinButton = view.findViewById(R.id.select_album_unpin);
|
||||
downloadButton = view.findViewById(R.id.select_album_download);
|
||||
deleteButton = view.findViewById(R.id.select_album_delete);
|
||||
ImageView oreButton = view.findViewById(R.id.select_album_more);
|
||||
emptyView = view.findViewById(R.id.select_album_empty);
|
||||
|
||||
selectButton.setVisibility(View.GONE);
|
||||
playNextButton.setVisibility(View.GONE);
|
||||
playLastButton.setVisibility(View.GONE);
|
||||
oreButton.setVisibility(View.GONE);
|
||||
|
||||
playNowButton.setOnClickListener(view16 -> playNow(getSelectedSongs(albumListView)));
|
||||
|
||||
selectButton.setOnClickListener(view15 -> selectAllOrNone());
|
||||
pinButton.setOnClickListener(view14 -> {
|
||||
downloadBackground(true);
|
||||
selectAll(false, false);
|
||||
});
|
||||
unpinButton.setOnClickListener(view13 -> {
|
||||
unpin();
|
||||
selectAll(false, false);
|
||||
});
|
||||
downloadButton.setOnClickListener(view12 -> {
|
||||
downloadBackground(false);
|
||||
selectAll(false, false);
|
||||
});
|
||||
deleteButton.setOnClickListener(view1 -> {
|
||||
delete();
|
||||
selectAll(false, false);
|
||||
});
|
||||
|
||||
registerForContextMenu(albumListView);
|
||||
FragmentTitle.Companion.setTitle(this, R.string.button_bar_bookmarks);
|
||||
|
||||
enableButtons();
|
||||
getBookmarks();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
cancellationToken.cancel();
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
private void getBookmarks()
|
||||
{
|
||||
new LoadTask()
|
||||
{
|
||||
@Override
|
||||
protected MusicDirectory load(MusicService service) throws Exception
|
||||
{
|
||||
return Util.getSongsFromBookmarks(service.getBookmarks());
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
|
||||
private void playNow(List<MusicDirectory.Entry> songs)
|
||||
{
|
||||
if (!getSelectedSongs(albumListView).isEmpty())
|
||||
{
|
||||
int position = songs.get(0).getBookmarkPosition();
|
||||
mediaPlayerController.getValue().restore(songs, 0, position, true, true);
|
||||
selectAll(false, false);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<MusicDirectory.Entry> getSelectedSongs(ListView albumListView)
|
||||
{
|
||||
List<MusicDirectory.Entry> songs = new ArrayList<>(10);
|
||||
|
||||
if (albumListView != null)
|
||||
{
|
||||
int count = albumListView.getCount();
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
if (albumListView.isItemChecked(i))
|
||||
{
|
||||
MusicDirectory.Entry song = (MusicDirectory.Entry) albumListView.getItemAtPosition(i);
|
||||
if (song != null) songs.add(song);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return songs;
|
||||
}
|
||||
|
||||
private void selectAllOrNone()
|
||||
{
|
||||
boolean someUnselected = false;
|
||||
int count = albumListView.getCount();
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
if (!albumListView.isItemChecked(i) && albumListView.getItemAtPosition(i) instanceof MusicDirectory.Entry)
|
||||
{
|
||||
someUnselected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
selectAll(someUnselected, true);
|
||||
}
|
||||
|
||||
private void selectAll(boolean selected, boolean toast)
|
||||
{
|
||||
int count = albumListView.getCount();
|
||||
int selectedCount = 0;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
MusicDirectory.Entry entry = (MusicDirectory.Entry) albumListView.getItemAtPosition(i);
|
||||
if (entry != null && !entry.isDirectory() && !entry.isVideo())
|
||||
{
|
||||
albumListView.setItemChecked(i, selected);
|
||||
selectedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Display toast: N tracks selected
|
||||
if (toast)
|
||||
{
|
||||
int toastResId = R.string.select_album_n_selected;
|
||||
Util.toast(getContext(), getString(toastResId, selectedCount));
|
||||
}
|
||||
|
||||
enableButtons();
|
||||
}
|
||||
|
||||
private void enableButtons()
|
||||
{
|
||||
List<MusicDirectory.Entry> selection = getSelectedSongs(albumListView);
|
||||
boolean enabled = !selection.isEmpty();
|
||||
boolean unpinEnabled = false;
|
||||
boolean deleteEnabled = false;
|
||||
|
||||
int pinnedCount = 0;
|
||||
|
||||
for (MusicDirectory.Entry song : selection)
|
||||
{
|
||||
if (song == null) continue;
|
||||
DownloadFile downloadFile = mediaPlayerController.getValue().getDownloadFileForSong(song);
|
||||
if (downloadFile.isWorkDone())
|
||||
{
|
||||
deleteEnabled = true;
|
||||
}
|
||||
|
||||
if (downloadFile.isSaved())
|
||||
{
|
||||
pinnedCount++;
|
||||
unpinEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
playNowButton.setVisibility(enabled && deleteEnabled ? View.VISIBLE : View.GONE);
|
||||
pinButton.setVisibility((enabled && !ActiveServerProvider.Companion.isOffline() && selection.size() > pinnedCount) ? View.VISIBLE : View.GONE);
|
||||
unpinButton.setVisibility(enabled && unpinEnabled ? View.VISIBLE : View.GONE);
|
||||
downloadButton.setVisibility(enabled && !deleteEnabled && !ActiveServerProvider.Companion.isOffline() ? View.VISIBLE : View.GONE);
|
||||
deleteButton.setVisibility(enabled && deleteEnabled ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
private void downloadBackground(final boolean save)
|
||||
{
|
||||
List<MusicDirectory.Entry> songs = getSelectedSongs(albumListView);
|
||||
|
||||
if (songs.isEmpty())
|
||||
{
|
||||
selectAll(true, false);
|
||||
songs = getSelectedSongs(albumListView);
|
||||
}
|
||||
|
||||
downloadBackground(save, songs);
|
||||
}
|
||||
|
||||
private void downloadBackground(final boolean save, final List<MusicDirectory.Entry> songs)
|
||||
{
|
||||
Runnable onValid = () -> {
|
||||
networkAndStorageChecker.getValue().warnIfNetworkOrStorageUnavailable();
|
||||
mediaPlayerController.getValue().downloadBackground(songs, save);
|
||||
|
||||
if (save)
|
||||
{
|
||||
Util.toast(getContext(), getResources().getQuantityString(R.plurals.select_album_n_songs_pinned, songs.size(), songs.size()));
|
||||
}
|
||||
else
|
||||
{
|
||||
Util.toast(getContext(), getResources().getQuantityString(R.plurals.select_album_n_songs_downloaded, songs.size(), songs.size()));
|
||||
}
|
||||
};
|
||||
|
||||
onValid.run();
|
||||
}
|
||||
|
||||
private void delete()
|
||||
{
|
||||
List<MusicDirectory.Entry> songs = getSelectedSongs(albumListView);
|
||||
|
||||
if (songs.isEmpty())
|
||||
{
|
||||
selectAll(true, false);
|
||||
songs = getSelectedSongs(albumListView);
|
||||
}
|
||||
|
||||
mediaPlayerController.getValue().delete(songs);
|
||||
}
|
||||
|
||||
private void unpin()
|
||||
{
|
||||
List<MusicDirectory.Entry> songs = getSelectedSongs(albumListView);
|
||||
Util.toast(getContext(), getResources().getQuantityString(R.plurals.select_album_n_songs_unpinned, songs.size(), songs.size()));
|
||||
mediaPlayerController.getValue().unpin(songs);
|
||||
}
|
||||
|
||||
private abstract class LoadTask extends FragmentBackgroundTask<Pair<MusicDirectory, Boolean>>
|
||||
{
|
||||
public LoadTask()
|
||||
{
|
||||
super(BookmarksFragment.this.getActivity(), true, refreshAlbumListView, cancellationToken);
|
||||
}
|
||||
|
||||
protected abstract MusicDirectory load(MusicService service) throws Exception;
|
||||
|
||||
@Override
|
||||
protected Pair<MusicDirectory, Boolean> doInBackground() throws Throwable
|
||||
{
|
||||
MusicService musicService = MusicServiceFactory.getMusicService();
|
||||
MusicDirectory dir = load(musicService);
|
||||
boolean valid = musicService.isLicenseValid();
|
||||
return new Pair<>(dir, valid);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void done(Pair<MusicDirectory, Boolean> result)
|
||||
{
|
||||
MusicDirectory musicDirectory = result.first;
|
||||
List<MusicDirectory.Entry> entries = musicDirectory.getChildren();
|
||||
|
||||
int songCount = 0;
|
||||
for (MusicDirectory.Entry entry : entries)
|
||||
{
|
||||
if (!entry.isDirectory())
|
||||
{
|
||||
songCount++;
|
||||
}
|
||||
}
|
||||
|
||||
final int listSize = getArguments() == null? 0 : getArguments().getInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0);
|
||||
|
||||
if (songCount > 0)
|
||||
{
|
||||
pinButton.setVisibility(View.VISIBLE);
|
||||
unpinButton.setVisibility(View.VISIBLE);
|
||||
downloadButton.setVisibility(View.VISIBLE);
|
||||
deleteButton.setVisibility(View.VISIBLE);
|
||||
playNowButton.setVisibility(View.VISIBLE);
|
||||
}
|
||||
else
|
||||
{
|
||||
pinButton.setVisibility(View.GONE);
|
||||
unpinButton.setVisibility(View.GONE);
|
||||
downloadButton.setVisibility(View.GONE);
|
||||
deleteButton.setVisibility(View.GONE);
|
||||
playNowButton.setVisibility(View.GONE);
|
||||
|
||||
if (listSize == 0 || result.first.getChildren().size() < listSize)
|
||||
{
|
||||
albumButtons.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
enableButtons();
|
||||
|
||||
emptyView.setVisibility(entries.isEmpty() ? View.VISIBLE : View.GONE);
|
||||
|
||||
albumListView.setAdapter(new EntryAdapter(getContext(), imageLoader.getValue().getImageLoader(), entries, true));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,593 +0,0 @@
|
||||
package org.moire.ultrasonic.fragment;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.SearchManager;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ListAdapter;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.SearchView;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.navigation.Navigation;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.moire.ultrasonic.R;
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider;
|
||||
import org.moire.ultrasonic.domain.Artist;
|
||||
import org.moire.ultrasonic.domain.MusicDirectory;
|
||||
import org.moire.ultrasonic.domain.SearchCriteria;
|
||||
import org.moire.ultrasonic.domain.SearchResult;
|
||||
import org.moire.ultrasonic.service.MediaPlayerController;
|
||||
import org.moire.ultrasonic.service.MusicService;
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory;
|
||||
import org.moire.ultrasonic.subsonic.DownloadHandler;
|
||||
import org.moire.ultrasonic.subsonic.ImageLoaderProvider;
|
||||
import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker;
|
||||
import org.moire.ultrasonic.subsonic.ShareHandler;
|
||||
import org.moire.ultrasonic.subsonic.VideoPlayer;
|
||||
import org.moire.ultrasonic.util.BackgroundTask;
|
||||
import org.moire.ultrasonic.util.CancellationToken;
|
||||
import org.moire.ultrasonic.util.Constants;
|
||||
import org.moire.ultrasonic.util.MergeAdapter;
|
||||
import org.moire.ultrasonic.util.FragmentBackgroundTask;
|
||||
import org.moire.ultrasonic.util.Settings;
|
||||
import org.moire.ultrasonic.util.Util;
|
||||
import org.moire.ultrasonic.view.ArtistAdapter;
|
||||
import org.moire.ultrasonic.view.EntryAdapter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import kotlin.Lazy;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static org.koin.java.KoinJavaComponent.inject;
|
||||
|
||||
/**
|
||||
* Initiates a search on the media library and displays the results
|
||||
*/
|
||||
public class SearchFragment extends Fragment {
|
||||
|
||||
private static int DEFAULT_ARTISTS;
|
||||
private static int DEFAULT_ALBUMS;
|
||||
private static int DEFAULT_SONGS;
|
||||
|
||||
private ListView list;
|
||||
|
||||
private View artistsHeading;
|
||||
private View albumsHeading;
|
||||
private View songsHeading;
|
||||
private TextView notFound;
|
||||
private View moreArtistsButton;
|
||||
private View moreAlbumsButton;
|
||||
private View moreSongsButton;
|
||||
private SearchResult searchResult;
|
||||
private MergeAdapter mergeAdapter;
|
||||
private ArtistAdapter artistAdapter;
|
||||
private ListAdapter moreArtistsAdapter;
|
||||
private EntryAdapter albumAdapter;
|
||||
private ListAdapter moreAlbumsAdapter;
|
||||
private ListAdapter moreSongsAdapter;
|
||||
private EntryAdapter songAdapter;
|
||||
private SwipeRefreshLayout searchRefresh;
|
||||
|
||||
private final Lazy<MediaPlayerController> mediaPlayerControllerLazy = inject(MediaPlayerController.class);
|
||||
private final Lazy<ImageLoaderProvider> imageLoaderProvider = inject(ImageLoaderProvider.class);
|
||||
private final Lazy<DownloadHandler> downloadHandler = inject(DownloadHandler.class);
|
||||
private final Lazy<ShareHandler> shareHandler = inject(ShareHandler.class);
|
||||
private final Lazy<NetworkAndStorageChecker> networkAndStorageChecker = inject(NetworkAndStorageChecker.class);
|
||||
private CancellationToken cancellationToken;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
Util.applyTheme(this.getContext());
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.search, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
cancellationToken = new CancellationToken();
|
||||
|
||||
FragmentTitle.Companion.setTitle(this, R.string.search_title);
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
DEFAULT_ARTISTS = Settings.getDefaultArtists();
|
||||
DEFAULT_ALBUMS = Settings.getDefaultAlbums();
|
||||
DEFAULT_SONGS = Settings.getDefaultSongs();
|
||||
|
||||
View buttons = LayoutInflater.from(getContext()).inflate(R.layout.search_buttons, list, false);
|
||||
|
||||
if (buttons != null)
|
||||
{
|
||||
artistsHeading = buttons.findViewById(R.id.search_artists);
|
||||
albumsHeading = buttons.findViewById(R.id.search_albums);
|
||||
songsHeading = buttons.findViewById(R.id.search_songs);
|
||||
notFound = buttons.findViewById(R.id.search_not_found);
|
||||
moreArtistsButton = buttons.findViewById(R.id.search_more_artists);
|
||||
moreAlbumsButton = buttons.findViewById(R.id.search_more_albums);
|
||||
moreSongsButton = buttons.findViewById(R.id.search_more_songs);
|
||||
}
|
||||
|
||||
list = view.findViewById(R.id.search_list);
|
||||
searchRefresh = view.findViewById(R.id.search_entries_refresh);
|
||||
searchRefresh.setEnabled(false); // TODO: It should be enabled if it is a good feature to refresh search results
|
||||
|
||||
list.setOnItemClickListener(new AdapterView.OnItemClickListener()
|
||||
{
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id)
|
||||
{
|
||||
if (view == moreArtistsButton)
|
||||
{
|
||||
expandArtists();
|
||||
}
|
||||
else if (view == moreAlbumsButton)
|
||||
{
|
||||
expandAlbums();
|
||||
}
|
||||
else if (view == moreSongsButton)
|
||||
{
|
||||
expandSongs();
|
||||
}
|
||||
else
|
||||
{
|
||||
Object item = parent.getItemAtPosition(position);
|
||||
if (item instanceof Artist)
|
||||
{
|
||||
onArtistSelected((Artist) item);
|
||||
}
|
||||
else if (item instanceof MusicDirectory.Entry)
|
||||
{
|
||||
MusicDirectory.Entry entry = (MusicDirectory.Entry) item;
|
||||
if (entry.isDirectory())
|
||||
{
|
||||
onAlbumSelected(entry, false);
|
||||
}
|
||||
else if (entry.isVideo())
|
||||
{
|
||||
onVideoSelected(entry);
|
||||
}
|
||||
else
|
||||
{
|
||||
onSongSelected(entry, true);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registerForContextMenu(list);
|
||||
|
||||
// Fragment was started with a query (e.g. from voice search), try to execute search right away
|
||||
Bundle arguments = getArguments();
|
||||
if (arguments != null) {
|
||||
String query = arguments.getString(Constants.INTENT_EXTRA_NAME_QUERY);
|
||||
boolean autoPlay = arguments.getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false);
|
||||
|
||||
if (query != null) {
|
||||
mergeAdapter = new MergeAdapter();
|
||||
list.setAdapter(mergeAdapter);
|
||||
search(query, autoPlay);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fragment was started from the Menu, create empty list
|
||||
populateList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
|
||||
Activity activity = getActivity();
|
||||
if (activity == null) return;
|
||||
SearchManager searchManager = (SearchManager) activity.getSystemService(Context.SEARCH_SERVICE);
|
||||
|
||||
inflater.inflate(R.menu.search, menu);
|
||||
MenuItem searchItem = menu.findItem(R.id.search_item);
|
||||
final SearchView searchView = (SearchView) searchItem.getActionView();
|
||||
searchView.setSearchableInfo(searchManager.getSearchableInfo(getActivity().getComponentName()));
|
||||
|
||||
Bundle arguments = getArguments();
|
||||
final boolean autoPlay = arguments != null && arguments.getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false);
|
||||
String query = arguments == null? null : arguments.getString(Constants.INTENT_EXTRA_NAME_QUERY);
|
||||
// If started with a query, enter it to the searchView
|
||||
if (query != null) {
|
||||
searchView.setQuery(query, false);
|
||||
searchView.clearFocus();
|
||||
}
|
||||
|
||||
searchView.setOnSuggestionListener(new SearchView.OnSuggestionListener() {
|
||||
@Override
|
||||
public boolean onSuggestionSelect(int position) { return true; }
|
||||
|
||||
@Override
|
||||
public boolean onSuggestionClick(int position) {
|
||||
Timber.d("onSuggestionClick: %d", position);
|
||||
Cursor cursor= searchView.getSuggestionsAdapter().getCursor();
|
||||
cursor.moveToPosition(position);
|
||||
String suggestion = cursor.getString(2); // TODO: Try to do something with this magic const -- 2 is the index of col containing suggestion name.
|
||||
searchView.setQuery(suggestion,true);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
|
||||
@Override
|
||||
public boolean onQueryTextSubmit(String query) {
|
||||
Timber.d("onQueryTextSubmit: %s", query);
|
||||
mergeAdapter = new MergeAdapter();
|
||||
list.setAdapter(mergeAdapter);
|
||||
searchView.clearFocus();
|
||||
search(query, autoPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onQueryTextChange(String newText) { return true; }
|
||||
});
|
||||
|
||||
searchView.setIconifiedByDefault(false);
|
||||
searchItem.expandActionView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateContextMenu(@NotNull ContextMenu menu, @NotNull View view, ContextMenu.ContextMenuInfo menuInfo)
|
||||
{
|
||||
super.onCreateContextMenu(menu, view, menuInfo);
|
||||
if (getActivity() == null) return;
|
||||
|
||||
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
|
||||
Object selectedItem = list.getItemAtPosition(info.position);
|
||||
|
||||
boolean isArtist = selectedItem instanceof Artist;
|
||||
boolean isAlbum = selectedItem instanceof MusicDirectory.Entry && ((MusicDirectory.Entry) selectedItem).isDirectory();
|
||||
|
||||
MenuInflater inflater = getActivity().getMenuInflater();
|
||||
if (!isArtist && !isAlbum)
|
||||
{
|
||||
inflater.inflate(R.menu.select_song_context, menu);
|
||||
}
|
||||
else
|
||||
{
|
||||
inflater.inflate(R.menu.generic_context_menu, menu);
|
||||
}
|
||||
|
||||
MenuItem shareButton = menu.findItem(R.id.menu_item_share);
|
||||
MenuItem downloadMenuItem = menu.findItem(R.id.menu_download);
|
||||
|
||||
if (downloadMenuItem != null)
|
||||
{
|
||||
downloadMenuItem.setVisible(!ActiveServerProvider.Companion.isOffline());
|
||||
}
|
||||
|
||||
if (ActiveServerProvider.Companion.isOffline() || isArtist)
|
||||
{
|
||||
if (shareButton != null)
|
||||
{
|
||||
shareButton.setVisible(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContextItemSelected(MenuItem menuItem)
|
||||
{
|
||||
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
|
||||
|
||||
if (info == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
Object selectedItem = list.getItemAtPosition(info.position);
|
||||
|
||||
Artist artist = selectedItem instanceof Artist ? (Artist) selectedItem : null;
|
||||
MusicDirectory.Entry entry = selectedItem instanceof MusicDirectory.Entry ? (MusicDirectory.Entry) selectedItem : null;
|
||||
|
||||
String entryId = null;
|
||||
|
||||
if (entry != null)
|
||||
{
|
||||
entryId = entry.getId();
|
||||
}
|
||||
|
||||
String id = artist != null ? artist.getId() : entryId;
|
||||
|
||||
if (id == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
List<MusicDirectory.Entry> songs = new ArrayList<>(1);
|
||||
|
||||
int itemId = menuItem.getItemId();
|
||||
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.menu_play_next) {
|
||||
downloadHandler.getValue().downloadRecursively(this, id, false, true, false, true, false, true, false, false);
|
||||
} 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.menu_pin) {
|
||||
downloadHandler.getValue().downloadRecursively(this, id, true, true, false, false, false, false, false, false);
|
||||
} 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.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) {
|
||||
songs = new ArrayList<>(1);
|
||||
songs.add(entry);
|
||||
downloadHandler.getValue().download(this, false, false, true, false, false, songs);
|
||||
}
|
||||
} else if (itemId == R.id.song_menu_play_next) {
|
||||
if (entry != null) {
|
||||
songs = new ArrayList<>(1);
|
||||
songs.add(entry);
|
||||
downloadHandler.getValue().download(this, true, false, false, true, false, songs);
|
||||
}
|
||||
} else if (itemId == R.id.song_menu_play_last) {
|
||||
if (entry != null) {
|
||||
songs = new ArrayList<>(1);
|
||||
songs.add(entry);
|
||||
downloadHandler.getValue().download(this, true, false, false, false, false, songs);
|
||||
}
|
||||
} else if (itemId == R.id.song_menu_pin) {
|
||||
if (entry != null) {
|
||||
songs.add(entry);
|
||||
Util.toast(getContext(), getResources().getQuantityString(R.plurals.select_album_n_songs_pinned, songs.size(), songs.size()));
|
||||
downloadBackground(true, songs);
|
||||
}
|
||||
} else if (itemId == R.id.song_menu_download) {
|
||||
if (entry != null) {
|
||||
songs.add(entry);
|
||||
Util.toast(getContext(), getResources().getQuantityString(R.plurals.select_album_n_songs_downloaded, songs.size(), songs.size()));
|
||||
downloadBackground(false, songs);
|
||||
}
|
||||
} else if (itemId == R.id.song_menu_unpin) {
|
||||
if (entry != null) {
|
||||
songs.add(entry);
|
||||
Util.toast(getContext(), getResources().getQuantityString(R.plurals.select_album_n_songs_unpinned, songs.size(), songs.size()));
|
||||
mediaPlayerControllerLazy.getValue().unpin(songs);
|
||||
}
|
||||
} else if (itemId == R.id.menu_item_share) {
|
||||
if (entry != null) {
|
||||
songs = new ArrayList<>(1);
|
||||
songs.add(entry);
|
||||
shareHandler.getValue().createShare(this, songs, searchRefresh, cancellationToken);
|
||||
}
|
||||
|
||||
return super.onContextItemSelected(menuItem);
|
||||
} else {
|
||||
return super.onContextItemSelected(menuItem);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
cancellationToken.cancel();
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
private void downloadBackground(final boolean save, final List<MusicDirectory.Entry> songs)
|
||||
{
|
||||
Runnable onValid = new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
networkAndStorageChecker.getValue().warnIfNetworkOrStorageUnavailable();
|
||||
mediaPlayerControllerLazy.getValue().downloadBackground(songs, save);
|
||||
}
|
||||
};
|
||||
|
||||
onValid.run();
|
||||
}
|
||||
|
||||
private void search(final String query, final boolean autoplay)
|
||||
{
|
||||
final int maxArtists = Settings.getMaxArtists();
|
||||
final int maxAlbums = Settings.getMaxAlbums();
|
||||
final int maxSongs = Settings.getMaxSongs();
|
||||
|
||||
BackgroundTask<SearchResult> task = new FragmentBackgroundTask<SearchResult>(getActivity(), true, searchRefresh, cancellationToken)
|
||||
{
|
||||
@Override
|
||||
protected SearchResult doInBackground() throws Throwable
|
||||
{
|
||||
SearchCriteria criteria = new SearchCriteria(query, maxArtists, maxAlbums, maxSongs);
|
||||
MusicService service = MusicServiceFactory.getMusicService();
|
||||
return service.search(criteria);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void done(SearchResult result)
|
||||
{
|
||||
searchResult = result;
|
||||
|
||||
populateList();
|
||||
|
||||
if (autoplay)
|
||||
{
|
||||
autoplay();
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
task.execute();
|
||||
}
|
||||
|
||||
private void populateList()
|
||||
{
|
||||
mergeAdapter = new MergeAdapter();
|
||||
|
||||
if (searchResult != null)
|
||||
{
|
||||
List<Artist> artists = searchResult.getArtists();
|
||||
if (!artists.isEmpty())
|
||||
{
|
||||
mergeAdapter.addView(artistsHeading);
|
||||
List<Artist> displayedArtists = new ArrayList<>(artists.subList(0, Math.min(DEFAULT_ARTISTS, artists.size())));
|
||||
artistAdapter = new ArtistAdapter(getContext(), displayedArtists);
|
||||
mergeAdapter.addAdapter(artistAdapter);
|
||||
if (artists.size() > DEFAULT_ARTISTS)
|
||||
{
|
||||
moreArtistsAdapter = mergeAdapter.addView(moreArtistsButton, true);
|
||||
}
|
||||
}
|
||||
|
||||
List<MusicDirectory.Entry> albums = searchResult.getAlbums();
|
||||
if (!albums.isEmpty())
|
||||
{
|
||||
mergeAdapter.addView(albumsHeading);
|
||||
List<MusicDirectory.Entry> displayedAlbums = new ArrayList<>(albums.subList(0, Math.min(DEFAULT_ALBUMS, albums.size())));
|
||||
albumAdapter = new EntryAdapter(getContext(), imageLoaderProvider.getValue().getImageLoader(), displayedAlbums, false);
|
||||
mergeAdapter.addAdapter(albumAdapter);
|
||||
if (albums.size() > DEFAULT_ALBUMS)
|
||||
{
|
||||
moreAlbumsAdapter = mergeAdapter.addView(moreAlbumsButton, true);
|
||||
}
|
||||
}
|
||||
|
||||
List<MusicDirectory.Entry> songs = searchResult.getSongs();
|
||||
if (!songs.isEmpty())
|
||||
{
|
||||
mergeAdapter.addView(songsHeading);
|
||||
List<MusicDirectory.Entry> displayedSongs = new ArrayList<>(songs.subList(0, Math.min(DEFAULT_SONGS, songs.size())));
|
||||
songAdapter = new EntryAdapter(getContext(), imageLoaderProvider.getValue().getImageLoader(), displayedSongs, false);
|
||||
mergeAdapter.addAdapter(songAdapter);
|
||||
if (songs.size() > DEFAULT_SONGS)
|
||||
{
|
||||
moreSongsAdapter = mergeAdapter.addView(moreSongsButton, true);
|
||||
}
|
||||
}
|
||||
|
||||
boolean empty = searchResult.getArtists().isEmpty() && searchResult.getAlbums().isEmpty() && searchResult.getSongs().isEmpty();
|
||||
if (empty) mergeAdapter.addView(notFound, false);
|
||||
}
|
||||
|
||||
list.setAdapter(mergeAdapter);
|
||||
}
|
||||
|
||||
private void expandArtists()
|
||||
{
|
||||
artistAdapter.clear();
|
||||
|
||||
for (Artist artist : searchResult.getArtists())
|
||||
{
|
||||
artistAdapter.add(artist);
|
||||
}
|
||||
|
||||
artistAdapter.notifyDataSetChanged();
|
||||
mergeAdapter.removeAdapter(moreArtistsAdapter);
|
||||
mergeAdapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private void expandAlbums()
|
||||
{
|
||||
albumAdapter.clear();
|
||||
|
||||
for (MusicDirectory.Entry album : searchResult.getAlbums())
|
||||
{
|
||||
albumAdapter.add(album);
|
||||
}
|
||||
|
||||
albumAdapter.notifyDataSetChanged();
|
||||
mergeAdapter.removeAdapter(moreAlbumsAdapter);
|
||||
mergeAdapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private void expandSongs()
|
||||
{
|
||||
songAdapter.clear();
|
||||
|
||||
for (MusicDirectory.Entry song : searchResult.getSongs())
|
||||
{
|
||||
songAdapter.add(song);
|
||||
}
|
||||
|
||||
songAdapter.notifyDataSetChanged();
|
||||
mergeAdapter.removeAdapter(moreSongsAdapter);
|
||||
mergeAdapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private void onArtistSelected(Artist artist)
|
||||
{
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(Constants.INTENT_EXTRA_NAME_ID, artist.getId());
|
||||
bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.getId());
|
||||
Navigation.findNavController(getView()).navigate(R.id.searchToSelectAlbum, bundle);
|
||||
}
|
||||
|
||||
private void onAlbumSelected(MusicDirectory.Entry album, boolean autoplay)
|
||||
{
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(Constants.INTENT_EXTRA_NAME_ID, album.getId());
|
||||
bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, album.getTitle());
|
||||
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, album.isDirectory());
|
||||
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, autoplay);
|
||||
Navigation.findNavController(getView()).navigate(R.id.searchToSelectAlbum, bundle);
|
||||
}
|
||||
|
||||
private void onSongSelected(MusicDirectory.Entry song, boolean append)
|
||||
{
|
||||
MediaPlayerController mediaPlayerController = mediaPlayerControllerLazy.getValue();
|
||||
if (mediaPlayerController != null)
|
||||
{
|
||||
if (!append)
|
||||
{
|
||||
mediaPlayerController.clear();
|
||||
}
|
||||
|
||||
mediaPlayerController.addToPlaylist(Collections.singletonList(song), false, false, false, false, false);
|
||||
|
||||
if (true)
|
||||
{
|
||||
mediaPlayerController.play(mediaPlayerController.getPlaylistSize() - 1);
|
||||
}
|
||||
|
||||
Util.toast(getContext(), getResources().getQuantityString(R.plurals.select_album_n_songs_added, 1, 1));
|
||||
}
|
||||
}
|
||||
|
||||
private void onVideoSelected(MusicDirectory.Entry entry)
|
||||
{
|
||||
VideoPlayer.Companion.playVideo(getContext(), entry);
|
||||
}
|
||||
|
||||
private void autoplay()
|
||||
{
|
||||
if (!searchResult.getSongs().isEmpty())
|
||||
{
|
||||
onSongSelected(searchResult.getSongs().get(0), false);
|
||||
}
|
||||
else if (!searchResult.getAlbums().isEmpty())
|
||||
{
|
||||
onAlbumSelected(searchResult.getAlbums().get(0), true);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,555 @@
|
||||
package org.moire.ultrasonic.fragment
|
||||
|
||||
import android.app.SearchManager
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.ContextMenu
|
||||
import android.view.ContextMenu.ContextMenuInfo
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.AdapterView.AdapterContextMenuInfo
|
||||
import android.widget.ListAdapter
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.navigation.Navigation
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.adapters.ArtistRowBinder
|
||||
import org.moire.ultrasonic.adapters.TrackViewBinder
|
||||
import org.moire.ultrasonic.domain.Identifiable
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
import org.moire.ultrasonic.domain.SearchResult
|
||||
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
|
||||
import org.moire.ultrasonic.model.SearchListModel
|
||||
import org.moire.ultrasonic.service.MediaPlayerController
|
||||
import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker
|
||||
import org.moire.ultrasonic.subsonic.ShareHandler
|
||||
import org.moire.ultrasonic.subsonic.VideoPlayer.Companion.playVideo
|
||||
import org.moire.ultrasonic.util.CancellationToken
|
||||
import org.moire.ultrasonic.util.Constants
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
import org.moire.ultrasonic.util.Util.toast
|
||||
import org.moire.ultrasonic.view.ArtistAdapter
|
||||
import org.moire.ultrasonic.view.EntryAdapter
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Initiates a search on the media library and displays the results
|
||||
*/
|
||||
class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
||||
private var artistsHeading: View? = null
|
||||
private var albumsHeading: View? = null
|
||||
private var songsHeading: View? = null
|
||||
private var notFound: TextView? = null
|
||||
private var moreArtistsButton: View? = null
|
||||
private var moreAlbumsButton: View? = null
|
||||
private var moreSongsButton: View? = null
|
||||
private var searchResult: SearchResult? = null
|
||||
private var artistAdapter: ArtistAdapter? = null
|
||||
private var moreArtistsAdapter: ListAdapter? = null
|
||||
private var moreAlbumsAdapter: ListAdapter? = null
|
||||
private var moreSongsAdapter: ListAdapter? = null
|
||||
private var searchRefresh: SwipeRefreshLayout? = null
|
||||
|
||||
private val mediaPlayerController: MediaPlayerController by inject()
|
||||
|
||||
private val shareHandler: ShareHandler by inject()
|
||||
private val networkAndStorageChecker: NetworkAndStorageChecker by inject()
|
||||
|
||||
private var cancellationToken: CancellationToken? = null
|
||||
|
||||
override val listModel: SearchListModel by viewModels()
|
||||
|
||||
override val recyclerViewId = R.id.search_list
|
||||
|
||||
override val mainLayout: Int = R.layout.search
|
||||
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
cancellationToken = CancellationToken()
|
||||
setTitle(this, R.string.search_title)
|
||||
setHasOptionsMenu(true)
|
||||
|
||||
|
||||
val buttons = LayoutInflater.from(context).inflate(R.layout.search_buttons,
|
||||
listView, false)
|
||||
|
||||
if (buttons != null) {
|
||||
artistsHeading = buttons.findViewById(R.id.search_artists)
|
||||
albumsHeading = buttons.findViewById(R.id.search_albums)
|
||||
songsHeading = buttons.findViewById(R.id.search_songs)
|
||||
notFound = buttons.findViewById(R.id.search_not_found)
|
||||
moreArtistsButton = buttons.findViewById(R.id.search_more_artists)
|
||||
moreAlbumsButton = buttons.findViewById(R.id.search_more_albums)
|
||||
moreSongsButton = buttons.findViewById(R.id.search_more_songs)
|
||||
}
|
||||
|
||||
|
||||
listModel.searchResult.observe(viewLifecycleOwner, {
|
||||
if (it != null) populateList(it)
|
||||
})
|
||||
|
||||
|
||||
searchRefresh = view.findViewById(R.id.search_entries_refresh)
|
||||
searchRefresh!!.isEnabled = false
|
||||
|
||||
// list.setOnItemClickListener(OnItemClickListener { parent: AdapterView<*>, view1: View, position: Int, id: Long ->
|
||||
// if (view1 === moreArtistsButton) {
|
||||
// expandArtists()
|
||||
// } else if (view1 === moreAlbumsButton) {
|
||||
// expandAlbums()
|
||||
// } else if (view1 === moreSongsButton) {
|
||||
// expandSongs()
|
||||
// } else {
|
||||
// val item = parent.getItemAtPosition(position)
|
||||
// if (item is Artist) {
|
||||
// onArtistSelected(item)
|
||||
// } else if (item is MusicDirectory.Entry) {
|
||||
// val entry = item
|
||||
// if (entry.isDirectory) {
|
||||
// onAlbumSelected(entry, false)
|
||||
// } else if (entry.isVideo) {
|
||||
// onVideoSelected(entry)
|
||||
// } else {
|
||||
// onSongSelected(entry, true)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
|
||||
registerForContextMenu(listView!!)
|
||||
|
||||
|
||||
viewAdapter.register(
|
||||
TrackViewBinder(
|
||||
checkable = false,
|
||||
draggable = false,
|
||||
context = requireContext(),
|
||||
lifecycleOwner = viewLifecycleOwner
|
||||
)
|
||||
)
|
||||
|
||||
viewAdapter.register(
|
||||
ArtistRowBinder(
|
||||
{ entry -> onItemClick(entry) },
|
||||
{ menuItem, entry -> onContextMenuItemSelected(menuItem, entry) },
|
||||
imageLoaderProvider.getImageLoader()
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
// Fragment was started with a query (e.g. from voice search), try to execute search right away
|
||||
val arguments = arguments
|
||||
if (arguments != null) {
|
||||
val query = arguments.getString(Constants.INTENT_EXTRA_NAME_QUERY)
|
||||
val autoPlay = arguments.getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false)
|
||||
if (query != null) {
|
||||
return search(query, autoPlay)
|
||||
}
|
||||
}
|
||||
|
||||
// Fragment was started from the Menu, create empty list
|
||||
populateList(SearchResult())
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
val activity = activity ?: return
|
||||
val searchManager = activity.getSystemService(Context.SEARCH_SERVICE) as SearchManager
|
||||
inflater.inflate(R.menu.search, menu)
|
||||
val searchItem = menu.findItem(R.id.search_item)
|
||||
val searchView = searchItem.actionView as SearchView
|
||||
searchView.setSearchableInfo(searchManager.getSearchableInfo(requireActivity().componentName))
|
||||
val arguments = arguments
|
||||
val autoPlay =
|
||||
arguments != null && arguments.getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false)
|
||||
val query = arguments?.getString(Constants.INTENT_EXTRA_NAME_QUERY)
|
||||
// If started with a query, enter it to the searchView
|
||||
if (query != null) {
|
||||
searchView.setQuery(query, false)
|
||||
searchView.clearFocus()
|
||||
}
|
||||
searchView.setOnSuggestionListener(object : SearchView.OnSuggestionListener {
|
||||
override fun onSuggestionSelect(position: Int): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onSuggestionClick(position: Int): Boolean {
|
||||
Timber.d("onSuggestionClick: %d", position)
|
||||
val cursor = searchView.suggestionsAdapter.cursor
|
||||
cursor.moveToPosition(position)
|
||||
val suggestion =
|
||||
cursor.getString(2) // TODO: Try to do something with this magic const -- 2 is the index of col containing suggestion name.
|
||||
searchView.setQuery(suggestion, true)
|
||||
return true
|
||||
}
|
||||
})
|
||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String): Boolean {
|
||||
Timber.d("onQueryTextSubmit: %s", query)
|
||||
searchView.clearFocus()
|
||||
search(query, autoPlay)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(newText: String): Boolean {
|
||||
return true
|
||||
}
|
||||
})
|
||||
searchView.setIconifiedByDefault(false)
|
||||
searchItem.expandActionView()
|
||||
}
|
||||
|
||||
// FIXME
|
||||
override fun onCreateContextMenu(menu: ContextMenu, view: View, menuInfo: ContextMenuInfo?) {
|
||||
super.onCreateContextMenu(menu, view, menuInfo)
|
||||
if (activity == null) return
|
||||
val info = menuInfo as AdapterContextMenuInfo?
|
||||
// val selectedItem = list!!.getItemAtPosition(info!!.position)
|
||||
// val isArtist = selectedItem is Artist
|
||||
// val isAlbum = selectedItem is MusicDirectory.Entry && selectedItem.isDirectory
|
||||
// val inflater = requireActivity().menuInflater
|
||||
// if (!isArtist && !isAlbum) {
|
||||
// inflater.inflate(R.menu.select_song_context, menu)
|
||||
// } else {
|
||||
// inflater.inflate(R.menu.generic_context_menu, menu)
|
||||
// }
|
||||
// val shareButton = menu.findItem(R.id.menu_item_share)
|
||||
// val downloadMenuItem = menu.findItem(R.id.menu_download)
|
||||
// if (downloadMenuItem != null) {
|
||||
// downloadMenuItem.isVisible = !isOffline()
|
||||
// }
|
||||
// if (isOffline() || isArtist) {
|
||||
// if (shareButton != null) {
|
||||
// shareButton.isVisible = false
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
// FIXME
|
||||
override fun onContextItemSelected(menuItem: MenuItem): Boolean {
|
||||
val info = menuItem.menuInfo as AdapterContextMenuInfo
|
||||
// val selectedItem = list!!.getItemAtPosition(info.position)
|
||||
// val artist = if (selectedItem is Artist) selectedItem else null
|
||||
// val entry = if (selectedItem is MusicDirectory.Entry) selectedItem else null
|
||||
// var entryId: String? = null
|
||||
// if (entry != null) {
|
||||
// entryId = entry.id
|
||||
// }
|
||||
// val id = artist?.id ?: entryId ?: return true
|
||||
// var songs: MutableList<MusicDirectory.Entry?> = ArrayList(1)
|
||||
// val itemId = menuItem.itemId
|
||||
// if (itemId == R.id.menu_play_now) {
|
||||
// downloadHandler.downloadRecursively(
|
||||
// this,
|
||||
// id,
|
||||
// false,
|
||||
// false,
|
||||
// true,
|
||||
// false,
|
||||
// false,
|
||||
// false,
|
||||
// false,
|
||||
// false
|
||||
// )
|
||||
// } else if (itemId == R.id.menu_play_next) {
|
||||
// downloadHandler.downloadRecursively(
|
||||
// this,
|
||||
// id,
|
||||
// false,
|
||||
// true,
|
||||
// false,
|
||||
// true,
|
||||
// false,
|
||||
// true,
|
||||
// false,
|
||||
// false
|
||||
// )
|
||||
// } else if (itemId == R.id.menu_play_last) {
|
||||
// downloadHandler.downloadRecursively(
|
||||
// this,
|
||||
// id,
|
||||
// false,
|
||||
// true,
|
||||
// false,
|
||||
// false,
|
||||
// false,
|
||||
// false,
|
||||
// false,
|
||||
// false
|
||||
// )
|
||||
// } else if (itemId == R.id.menu_pin) {
|
||||
// downloadHandler.downloadRecursively(
|
||||
// this,
|
||||
// id,
|
||||
// true,
|
||||
// true,
|
||||
// false,
|
||||
// false,
|
||||
// false,
|
||||
// false,
|
||||
// false,
|
||||
// false
|
||||
// )
|
||||
// } else if (itemId == R.id.menu_unpin) {
|
||||
// downloadHandler.downloadRecursively(
|
||||
// this,
|
||||
// id,
|
||||
// false,
|
||||
// false,
|
||||
// false,
|
||||
// false,
|
||||
// false,
|
||||
// false,
|
||||
// true,
|
||||
// false
|
||||
// )
|
||||
// } else if (itemId == R.id.menu_download) {
|
||||
// downloadHandler.downloadRecursively(
|
||||
// this,
|
||||
// id,
|
||||
// false,
|
||||
// false,
|
||||
// false,
|
||||
// false,
|
||||
// true,
|
||||
// false,
|
||||
// false,
|
||||
// false
|
||||
// )
|
||||
// } else if (itemId == R.id.song_menu_play_now) {
|
||||
// if (entry != null) {
|
||||
// songs = ArrayList(1)
|
||||
// songs.add(entry)
|
||||
// downloadHandler.download(this, false, false, true, false, false, songs)
|
||||
// }
|
||||
// } else if (itemId == R.id.song_menu_play_next) {
|
||||
// if (entry != null) {
|
||||
// songs = ArrayList(1)
|
||||
// songs.add(entry)
|
||||
// downloadHandler.download(this, true, false, false, true, false, songs)
|
||||
// }
|
||||
// } else if (itemId == R.id.song_menu_play_last) {
|
||||
// if (entry != null) {
|
||||
// songs = ArrayList(1)
|
||||
// songs.add(entry)
|
||||
// downloadHandler.download(this, true, false, false, false, false, songs)
|
||||
// }
|
||||
// } else if (itemId == R.id.song_menu_pin) {
|
||||
// if (entry != null) {
|
||||
// songs.add(entry)
|
||||
// toast(
|
||||
// context,
|
||||
// resources.getQuantityString(
|
||||
// R.plurals.select_album_n_songs_pinned,
|
||||
// songs.size,
|
||||
// songs.size
|
||||
// )
|
||||
// )
|
||||
// downloadBackground(true, songs)
|
||||
// }
|
||||
// } else if (itemId == R.id.song_menu_download) {
|
||||
// if (entry != null) {
|
||||
// songs.add(entry)
|
||||
// toast(
|
||||
// context,
|
||||
// resources.getQuantityString(
|
||||
// R.plurals.select_album_n_songs_downloaded,
|
||||
// songs.size,
|
||||
// songs.size
|
||||
// )
|
||||
// )
|
||||
// downloadBackground(false, songs)
|
||||
// }
|
||||
// } else if (itemId == R.id.song_menu_unpin) {
|
||||
// if (entry != null) {
|
||||
// songs.add(entry)
|
||||
// toast(
|
||||
// context,
|
||||
// resources.getQuantityString(
|
||||
// R.plurals.select_album_n_songs_unpinned,
|
||||
// songs.size,
|
||||
// songs.size
|
||||
// )
|
||||
// )
|
||||
// mediaPlayerController.unpin(songs)
|
||||
// }
|
||||
// } else if (itemId == R.id.menu_item_share) {
|
||||
// if (entry != null) {
|
||||
// songs = ArrayList(1)
|
||||
// songs.add(entry)
|
||||
// shareHandler.createShare(this, songs, searchRefresh, cancellationToken!!)
|
||||
// }
|
||||
// return super.onContextItemSelected(menuItem)
|
||||
// } else {
|
||||
// return super.onContextItemSelected(menuItem)
|
||||
// }
|
||||
return true
|
||||
}
|
||||
|
||||
// OK!
|
||||
override fun onDestroyView() {
|
||||
cancellationToken?.cancel()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
// OK!
|
||||
private fun downloadBackground(save: Boolean, songs: List<MusicDirectory.Entry?>) {
|
||||
val onValid = Runnable {
|
||||
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
|
||||
mediaPlayerController.downloadBackground(songs, save)
|
||||
}
|
||||
onValid.run()
|
||||
}
|
||||
|
||||
private fun search(query: String, autoplay: Boolean) {
|
||||
// FIXME add error handler
|
||||
// FIXME support autoplay
|
||||
listModel.viewModelScope.launch {
|
||||
listModel.search(query)
|
||||
}
|
||||
}
|
||||
|
||||
private fun populateList(result: SearchResult) {
|
||||
val searchResult = listModel.trimResultLength(result)
|
||||
|
||||
val list = mutableListOf<Identifiable>()
|
||||
|
||||
val artists = searchResult.artists
|
||||
if (artists.isNotEmpty()) {
|
||||
// FIXME: addView(albumsHeading)
|
||||
list.addAll(artists)
|
||||
if (artists.size > DEFAULT_ARTISTS) {
|
||||
// FIXME
|
||||
//list.add((moreArtistsButton, true)
|
||||
}
|
||||
}
|
||||
val albums = searchResult.albums
|
||||
if (albums.isNotEmpty()) {
|
||||
//mergeAdapter!!.addView(albumsHeading)
|
||||
list.addAll(albums)
|
||||
//mergeAdapter!!.addAdapter(albumAdapter)
|
||||
// if (albums.size > DEFAULT_ALBUMS) {
|
||||
// moreAlbumsAdapter = mergeAdapter!!.addView(moreAlbumsButton, true)
|
||||
// }
|
||||
}
|
||||
val songs = searchResult.songs
|
||||
if (songs.isNotEmpty()) {
|
||||
// mergeAdapter!!.addView(songsHeading)
|
||||
|
||||
list.addAll(songs)
|
||||
// if (songs.size > DEFAULT_SONGS) {
|
||||
// moreSongsAdapter = mergeAdapter!!.addView(moreSongsButton, true)
|
||||
// }
|
||||
}
|
||||
|
||||
// FIXME
|
||||
if (list.isEmpty()) {
|
||||
// mergeAdapter!!.addView(notFound, false)
|
||||
}
|
||||
|
||||
viewAdapter.submitList(list)
|
||||
}
|
||||
|
||||
// private fun expandArtists() {
|
||||
// artistAdapter!!.clear()
|
||||
// for (artist in searchResult!!.artists) {
|
||||
// artistAdapter!!.add(artist)
|
||||
// }
|
||||
// artistAdapter!!.notifyDataSetChanged()
|
||||
// mergeAdapter!!.removeAdapter(moreArtistsAdapter)
|
||||
// mergeAdapter!!.notifyDataSetChanged()
|
||||
// }
|
||||
//
|
||||
// private fun expandAlbums() {
|
||||
// albumAdapter!!.clear()
|
||||
// for (album in searchResult!!.albums) {
|
||||
// albumAdapter!!.add(album)
|
||||
// }
|
||||
// albumAdapter!!.notifyDataSetChanged()
|
||||
// mergeAdapter!!.removeAdapter(moreAlbumsAdapter)
|
||||
// mergeAdapter!!.notifyDataSetChanged()
|
||||
// }
|
||||
//
|
||||
// private fun expandSongs() {
|
||||
// songAdapter!!.clear()
|
||||
// for (song in searchResult!!.songs) {
|
||||
// songAdapter!!.add(song)
|
||||
// }
|
||||
// songAdapter!!.notifyDataSetChanged()
|
||||
// mergeAdapter!!.removeAdapter(moreSongsAdapter)
|
||||
// mergeAdapter!!.notifyDataSetChanged()
|
||||
// }
|
||||
//
|
||||
// private fun onArtistSelected(artist: Artist) {
|
||||
// val bundle = Bundle()
|
||||
// bundle.putString(Constants.INTENT_EXTRA_NAME_ID, artist.id)
|
||||
// bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.id)
|
||||
// Navigation.findNavController(requireView()).navigate(R.id.searchToSelectAlbum, bundle)
|
||||
// }
|
||||
|
||||
private fun onAlbumSelected(album: MusicDirectory.Entry, autoplay: Boolean) {
|
||||
val bundle = Bundle()
|
||||
bundle.putString(Constants.INTENT_EXTRA_NAME_ID, album.id)
|
||||
bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, album.title)
|
||||
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, album.isDirectory)
|
||||
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, autoplay)
|
||||
Navigation.findNavController(requireView()).navigate(R.id.searchToSelectAlbum, bundle)
|
||||
}
|
||||
|
||||
private fun onSongSelected(song: MusicDirectory.Entry, append: Boolean) {
|
||||
if (!append) {
|
||||
mediaPlayerController.clear()
|
||||
}
|
||||
mediaPlayerController.addToPlaylist(listOf(song), false, false, false, false, false)
|
||||
mediaPlayerController.play(mediaPlayerController.playlistSize - 1)
|
||||
toast(context, resources.getQuantityString(R.plurals.select_album_n_songs_added, 1, 1))
|
||||
}
|
||||
|
||||
private fun onVideoSelected(entry: MusicDirectory.Entry) {
|
||||
playVideo(requireContext(), entry)
|
||||
}
|
||||
|
||||
private fun autoplay() {
|
||||
if (searchResult!!.songs.isNotEmpty()) {
|
||||
onSongSelected(searchResult!!.songs[0], false)
|
||||
} else if (searchResult!!.albums.isNotEmpty()) {
|
||||
onAlbumSelected(searchResult!!.albums[0], true)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
var DEFAULT_ARTISTS = Settings.defaultArtists
|
||||
var DEFAULT_ALBUMS = Settings.defaultAlbums
|
||||
var DEFAULT_SONGS = Settings.defaultSongs
|
||||
}
|
||||
|
||||
// FIXME!!
|
||||
override fun getLiveData(args: Bundle?): LiveData<List<Identifiable>> {
|
||||
return MutableLiveData(listOf())
|
||||
}
|
||||
|
||||
// FIXME
|
||||
override val itemClickTarget: Int = 0
|
||||
|
||||
// FIXME
|
||||
override fun onContextMenuItemSelected(menuItem: MenuItem, item: Identifiable): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
// FIXME
|
||||
override fun onItemClick(item: Identifiable) {
|
||||
|
||||
}
|
||||
}
|
@ -8,8 +8,7 @@ import org.moire.ultrasonic.util.Util.getGrandparent
|
||||
|
||||
class AlbumHeader(
|
||||
var entries: List<MusicDirectory.Entry>,
|
||||
var name: String,
|
||||
songCount: Int
|
||||
var name: String?
|
||||
) : Identifiable {
|
||||
var isAllVideo: Boolean
|
||||
private set
|
||||
|
@ -20,7 +20,7 @@ package org.moire.ultrasonic.view;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.widget.TextView;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import org.moire.ultrasonic.R;
|
||||
import org.moire.ultrasonic.domain.Playlist;
|
||||
@ -30,9 +30,9 @@ import org.moire.ultrasonic.domain.Playlist;
|
||||
*
|
||||
* @author Sindre Mehus
|
||||
*/
|
||||
public class PlaylistView extends UpdateView
|
||||
public class PlaylistView extends LinearLayout
|
||||
{
|
||||
private Context context;
|
||||
private final Context context;
|
||||
private PlaylistAdapter.ViewHolder viewHolder;
|
||||
|
||||
public PlaylistView(Context context)
|
||||
@ -45,7 +45,7 @@ public class PlaylistView extends UpdateView
|
||||
{
|
||||
LayoutInflater.from(context).inflate(R.layout.playlist_list_item, this, true);
|
||||
viewHolder = new PlaylistAdapter.ViewHolder();
|
||||
viewHolder.name = (TextView) findViewById(R.id.playlist_name);
|
||||
viewHolder.name = findViewById(R.id.playlist_name);
|
||||
setTag(viewHolder);
|
||||
}
|
||||
|
||||
@ -58,6 +58,5 @@ public class PlaylistView extends UpdateView
|
||||
public void setPlaylist(Playlist playlist)
|
||||
{
|
||||
viewHolder.name.setText(playlist.getName());
|
||||
update();
|
||||
}
|
||||
}
|
@ -20,7 +20,7 @@ package org.moire.ultrasonic.view;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.widget.TextView;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import org.moire.ultrasonic.R;
|
||||
import org.moire.ultrasonic.domain.Playlist;
|
||||
@ -30,12 +30,12 @@ import org.moire.ultrasonic.domain.Playlist;
|
||||
*
|
||||
* @author Sindre Mehus
|
||||
*/
|
||||
public class PodcatsChannelItemView extends UpdateView
|
||||
public class PodcastChannelItemView extends LinearLayout
|
||||
{
|
||||
private Context context;
|
||||
private final Context context;
|
||||
private PlaylistAdapter.ViewHolder viewHolder;
|
||||
|
||||
public PodcatsChannelItemView(Context context)
|
||||
public PodcastChannelItemView(Context context)
|
||||
{
|
||||
super(context);
|
||||
this.context = context;
|
||||
@ -45,7 +45,7 @@ public class PodcatsChannelItemView extends UpdateView
|
||||
{
|
||||
LayoutInflater.from(context).inflate(R.layout.playlist_list_item, this, true);
|
||||
viewHolder = new PlaylistAdapter.ViewHolder();
|
||||
viewHolder.name = (TextView) findViewById(R.id.playlist_name);
|
||||
viewHolder.name = findViewById(R.id.playlist_name);
|
||||
setTag(viewHolder);
|
||||
}
|
||||
|
||||
@ -58,6 +58,5 @@ public class PodcatsChannelItemView extends UpdateView
|
||||
public void setPlaylist(Playlist playlist)
|
||||
{
|
||||
viewHolder.name.setText(playlist.getName());
|
||||
update();
|
||||
}
|
||||
}
|
@ -20,7 +20,7 @@ package org.moire.ultrasonic.view;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.widget.TextView;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import org.moire.ultrasonic.R;
|
||||
import org.moire.ultrasonic.domain.Share;
|
||||
@ -30,9 +30,9 @@ import org.moire.ultrasonic.domain.Share;
|
||||
*
|
||||
* @author Joshua Bahnsen
|
||||
*/
|
||||
public class ShareView extends UpdateView
|
||||
public class ShareView extends LinearLayout
|
||||
{
|
||||
private Context context;
|
||||
private final Context context;
|
||||
private ShareAdapter.ViewHolder viewHolder;
|
||||
|
||||
public ShareView(Context context)
|
||||
@ -45,8 +45,8 @@ public class ShareView extends UpdateView
|
||||
{
|
||||
LayoutInflater.from(context).inflate(R.layout.share_list_item, this, true);
|
||||
viewHolder = new ShareAdapter.ViewHolder();
|
||||
viewHolder.url = (TextView) findViewById(R.id.share_url);
|
||||
viewHolder.description = (TextView) findViewById(R.id.share_description);
|
||||
viewHolder.url = findViewById(R.id.share_url);
|
||||
viewHolder.description = findViewById(R.id.share_description);
|
||||
setTag(viewHolder);
|
||||
}
|
||||
|
||||
@ -60,6 +60,5 @@ public class ShareView extends UpdateView
|
||||
{
|
||||
viewHolder.url.setText(share.getName());
|
||||
viewHolder.description.setText(share.getDescription());
|
||||
update();
|
||||
}
|
||||
}
|
@ -39,7 +39,7 @@ import org.moire.ultrasonic.data.ActiveServerProvider
|
||||
import org.moire.ultrasonic.data.ServerSettingDao
|
||||
import org.moire.ultrasonic.domain.PlayerState
|
||||
import org.moire.ultrasonic.fragment.OnBackPressedHandler
|
||||
import org.moire.ultrasonic.fragment.ServerSettingsModel
|
||||
import org.moire.ultrasonic.model.ServerSettingsModel
|
||||
import org.moire.ultrasonic.provider.SearchSuggestionProvider
|
||||
import org.moire.ultrasonic.service.DownloadFile
|
||||
import org.moire.ultrasonic.service.MediaPlayerController
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* AlbumRowAdapter.kt
|
||||
* AlbumRowBinder.kt
|
||||
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
@ -9,13 +9,16 @@ package org.moire.ultrasonic.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
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 java.lang.Exception
|
||||
import com.drakeet.multitype.ItemViewBinder
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
import org.moire.ultrasonic.imageloader.ImageLoader
|
||||
@ -27,22 +30,12 @@ import timber.log.Timber
|
||||
/**
|
||||
* Creates a Row in a RecyclerView which contains the details of an Album
|
||||
*/
|
||||
class AlbumRowAdapter(
|
||||
itemList: List<MusicDirectory.Entry>,
|
||||
onItemClick: (MusicDirectory.Entry) -> Unit,
|
||||
onContextMenuClick: (MenuItem, MusicDirectory.Entry) -> Boolean,
|
||||
class AlbumRowBinder(
|
||||
val onItemClick: (MusicDirectory.Entry) -> Unit,
|
||||
val onContextMenuClick: (MenuItem, MusicDirectory.Entry) -> Boolean,
|
||||
private val imageLoader: ImageLoader,
|
||||
onMusicFolderUpdate: (String?) -> Unit,
|
||||
context: Context,
|
||||
) : GenericRowAdapter<MusicDirectory.Entry>(
|
||||
onItemClick,
|
||||
onContextMenuClick,
|
||||
onMusicFolderUpdate
|
||||
) {
|
||||
|
||||
init {
|
||||
super.submitList(itemList)
|
||||
}
|
||||
) : ItemViewBinder<MusicDirectory.Entry, AlbumRowBinder.ViewHolder>(), KoinComponent {
|
||||
|
||||
private val starDrawable: Drawable =
|
||||
Util.getDrawableFromAttribute(context, R.attr.star_full)
|
||||
@ -50,34 +43,32 @@ class AlbumRowAdapter(
|
||||
Util.getDrawableFromAttribute(context, R.attr.star_hollow)
|
||||
|
||||
// Set our layout files
|
||||
override val layout = R.layout.album_list_item
|
||||
override val contextMenuLayout = R.menu.artist_context_menu
|
||||
val layout = R.layout.album_list_item
|
||||
val contextMenuLayout = R.menu.artist_context_menu
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
if (holder is ViewHolder) {
|
||||
val listPosition = if (selectFolderHeader != null) position - 1 else position
|
||||
val entry = currentList[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
|
||||
holder.star.setImageDrawable(if (entry.starred) starDrawable else starHollowDrawable)
|
||||
holder.star.setOnClickListener { onStarClick(entry, holder.star) }
|
||||
override fun onBindViewHolder(holder: ViewHolder, item: MusicDirectory.Entry) {
|
||||
holder.album.text = item.title
|
||||
holder.artist.text = item.artist
|
||||
holder.details.setOnClickListener { onItemClick(item) }
|
||||
holder.details.setOnLongClickListener {
|
||||
val popup = Helper.createPopupMenu(holder.itemView)
|
||||
|
||||
imageLoader.loadImage(
|
||||
holder.coverArt, entry,
|
||||
false, 0, R.drawable.unknown_album
|
||||
)
|
||||
popup.setOnMenuItemClickListener { menuItem ->
|
||||
onContextMenuClick(menuItem, item)
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
holder.coverArtId = item.coverArt
|
||||
holder.star.setImageDrawable(if (item.starred) starDrawable else starHollowDrawable)
|
||||
holder.star.setOnClickListener { onStarClick(item, holder.star) }
|
||||
|
||||
imageLoader.loadImage(
|
||||
holder.coverArt, item,
|
||||
false, 0, R.drawable.unknown_album
|
||||
)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
if (selectFolderHeader != null)
|
||||
return currentList.size + 1
|
||||
else
|
||||
return currentList.size
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds the view properties of an Item row
|
||||
@ -93,12 +84,6 @@ class AlbumRowAdapter(
|
||||
var coverArtId: String? = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance of our ViewHolder class
|
||||
*/
|
||||
override fun newViewHolder(view: View): RecyclerView.ViewHolder {
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the star / unstar action for an album
|
||||
@ -128,4 +113,9 @@ class AlbumRowAdapter(
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): ViewHolder {
|
||||
return ViewHolder(inflater.inflate(layout, parent, false))
|
||||
}
|
||||
}
|
||||
|
@ -1,106 +0,0 @@
|
||||
/*
|
||||
* ArtistRowAdapter.kt
|
||||
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.adapters
|
||||
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView.SectionedAdapter
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.domain.ArtistOrIndex
|
||||
import org.moire.ultrasonic.imageloader.ImageLoader
|
||||
import org.moire.ultrasonic.util.FileUtil
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
|
||||
/**
|
||||
* Creates a Row in a RecyclerView which contains the details of an Artist
|
||||
*/
|
||||
class ArtistRowAdapter(
|
||||
itemList: List<ArtistOrIndex>,
|
||||
onItemClick: (ArtistOrIndex) -> Unit,
|
||||
onContextMenuClick: (MenuItem, ArtistOrIndex) -> Boolean,
|
||||
private val imageLoader: ImageLoader,
|
||||
onMusicFolderUpdate: (String?) -> Unit
|
||||
) : GenericRowAdapter<ArtistOrIndex>(
|
||||
onItemClick,
|
||||
onContextMenuClick,
|
||||
onMusicFolderUpdate
|
||||
),
|
||||
SectionedAdapter {
|
||||
|
||||
init {
|
||||
super.submitList(itemList)
|
||||
}
|
||||
|
||||
// Set our layout files
|
||||
override val layout = R.layout.artist_list_item
|
||||
override val contextMenuLayout = R.menu.artist_context_menu
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
if (holder is ViewHolder) {
|
||||
val listPosition = if (selectFolderHeader != null) position - 1 else position
|
||||
holder.textView.text = currentList[listPosition].name
|
||||
holder.section.text = getSectionForArtist(listPosition)
|
||||
holder.layout.setOnClickListener { onItemClick(currentList[listPosition]) }
|
||||
holder.layout.setOnLongClickListener { view -> createPopupMenu(view, listPosition) }
|
||||
holder.coverArtId = currentList[listPosition].coverArt
|
||||
|
||||
if (Settings.shouldShowArtistPicture) {
|
||||
holder.coverArt.visibility = View.VISIBLE
|
||||
val key = FileUtil.getArtistArtKey(currentList[listPosition].name, false)
|
||||
imageLoader.loadImage(
|
||||
view = holder.coverArt,
|
||||
id = holder.coverArtId,
|
||||
key = key,
|
||||
large = false,
|
||||
size = 0,
|
||||
defaultResourceId = R.drawable.ic_contact_picture
|
||||
)
|
||||
} else {
|
||||
holder.coverArt.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getSectionName(position: Int): String {
|
||||
var listPosition = if (selectFolderHeader != null) position - 1 else position
|
||||
|
||||
// Show the first artist's initial in the popup when the list is
|
||||
// scrolled up to the "Select Folder" row
|
||||
if (listPosition < 0) listPosition = 0
|
||||
|
||||
return getSectionFromName(currentList[listPosition].name ?: " ")
|
||||
}
|
||||
|
||||
private fun getSectionForArtist(artistPosition: Int): String {
|
||||
if (artistPosition == 0)
|
||||
return getSectionFromName(currentList[artistPosition].name ?: " ")
|
||||
|
||||
val previousArtistSection = getSectionFromName(
|
||||
currentList[artistPosition - 1].name ?: " "
|
||||
)
|
||||
val currentArtistSection = getSectionFromName(
|
||||
currentList[artistPosition].name ?: " "
|
||||
)
|
||||
|
||||
return if (previousArtistSection == currentArtistSection) "" else currentArtistSection
|
||||
}
|
||||
|
||||
private fun getSectionFromName(name: String): String {
|
||||
var section = name.first().uppercaseChar()
|
||||
if (!section.isLetter()) section = '#'
|
||||
return section.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance of our ViewHolder class
|
||||
*/
|
||||
override fun newViewHolder(view: View): RecyclerView.ViewHolder {
|
||||
return ViewHolder(view)
|
||||
}
|
||||
}
|
@ -0,0 +1,114 @@
|
||||
/*
|
||||
* ArtistRowAdapter.kt
|
||||
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.drakeet.multitype.ItemViewBinder
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.domain.ArtistOrIndex
|
||||
import org.moire.ultrasonic.imageloader.ImageLoader
|
||||
import org.moire.ultrasonic.util.FileUtil
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
|
||||
/**
|
||||
* Creates a Row in a RecyclerView which contains the details of an Artist
|
||||
* FIXME: On click wrong display...
|
||||
*/
|
||||
class ArtistRowBinder(
|
||||
val onItemClick: (ArtistOrIndex) -> Unit,
|
||||
val onContextMenuClick: (MenuItem, ArtistOrIndex) -> Boolean,
|
||||
private val imageLoader: ImageLoader,
|
||||
): ItemViewBinder<ArtistOrIndex, ArtistRowBinder.ViewHolder>(), KoinComponent {
|
||||
|
||||
val layout = R.layout.artist_list_item
|
||||
val contextMenuLayout = R.menu.artist_context_menu
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, item: ArtistOrIndex) {
|
||||
holder.textView.text = item.name
|
||||
holder.section.text = getSectionForArtist(item)
|
||||
holder.layout.setOnClickListener { onItemClick(item) }
|
||||
holder.layout.setOnLongClickListener {
|
||||
val popup = Helper.createPopupMenu(holder.itemView)
|
||||
|
||||
popup.setOnMenuItemClickListener { menuItem ->
|
||||
onContextMenuClick(menuItem, item)
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
holder.coverArtId = item.coverArt
|
||||
|
||||
if (Settings.shouldShowArtistPicture) {
|
||||
holder.coverArt.visibility = View.VISIBLE
|
||||
val key = FileUtil.getArtistArtKey(item.name, false)
|
||||
imageLoader.loadImage(
|
||||
view = holder.coverArt,
|
||||
id = holder.coverArtId,
|
||||
key = key,
|
||||
large = false,
|
||||
size = 0,
|
||||
defaultResourceId = R.drawable.ic_contact_picture
|
||||
)
|
||||
} else {
|
||||
holder.coverArt.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSectionForArtist(item: ArtistOrIndex): String {
|
||||
val index = adapter.items.indexOf(item)
|
||||
|
||||
if (index == -1) return " "
|
||||
|
||||
if (index == 0) return getSectionFromName(item.name ?: " ")
|
||||
|
||||
val previousItem = adapter.items[index - 1]
|
||||
val previousSectionKey: String
|
||||
|
||||
if (previousItem is ArtistOrIndex) {
|
||||
previousSectionKey = getSectionFromName(previousItem.name ?: " ")
|
||||
} else {
|
||||
previousSectionKey = " "
|
||||
}
|
||||
|
||||
val currentSectionKey = getSectionFromName(item.name ?: "")
|
||||
|
||||
return if (previousSectionKey == currentSectionKey) "" else currentSectionKey
|
||||
}
|
||||
|
||||
private fun getSectionFromName(name: String): String {
|
||||
var section = name.first().uppercaseChar()
|
||||
if (!section.isLetter()) section = '#'
|
||||
return section.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance of our ViewHolder class
|
||||
*/
|
||||
class ViewHolder(
|
||||
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(inflater: LayoutInflater, parent: ViewGroup): ViewHolder {
|
||||
return ViewHolder(inflater.inflate(layout, parent, false))
|
||||
}
|
||||
}
|
@ -11,7 +11,7 @@ import com.drakeet.multitype.MultiTypeAdapter
|
||||
import java.util.TreeSet
|
||||
import org.moire.ultrasonic.domain.Identifiable
|
||||
|
||||
class MultiTypeDiffAdapter<T : Identifiable> : MultiTypeAdapter() {
|
||||
class BaseAdapter<T : Identifiable> : MultiTypeAdapter() {
|
||||
|
||||
internal var selectedSet: TreeSet<Long> = TreeSet()
|
||||
internal var selectionRevision: MutableLiveData<Int> = MutableLiveData(0)
|
||||
@ -43,7 +43,7 @@ class MultiTypeDiffAdapter<T : Identifiable> : MultiTypeAdapter() {
|
||||
|
||||
private val mListener =
|
||||
ListListener<T> { previousList, currentList ->
|
||||
this@MultiTypeDiffAdapter.onCurrentListChanged(
|
||||
this@BaseAdapter.onCurrentListChanged(
|
||||
previousList,
|
||||
currentList
|
||||
)
|
@ -0,0 +1,127 @@
|
||||
package org.moire.ultrasonic.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.PopupMenu
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.drakeet.multitype.ItemViewBinder
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.domain.Identifiable
|
||||
import org.moire.ultrasonic.domain.MusicFolder
|
||||
import org.moire.ultrasonic.service.RxBus
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
/**
|
||||
* This little view shows the currently selected Folder (or catalog) on the music server.
|
||||
* When clicked it will drop down a list of all available Folders and allow you to
|
||||
* select one. The intended usage is to supply a filter to lists of artists, albums, etc
|
||||
*/
|
||||
class FolderSelectorBinder(context: Context
|
||||
) : ItemViewBinder<FolderSelectorBinder.FolderHeader, FolderSelectorBinder.ViewHolder>(), KoinComponent {
|
||||
|
||||
private val weakContext: WeakReference<Context> = WeakReference(context)
|
||||
|
||||
// Set our layout files
|
||||
val layout = R.layout.select_album_header
|
||||
|
||||
override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): ViewHolder {
|
||||
return ViewHolder(inflater.inflate(layout, parent, false), weakContext)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, item: FolderHeader) {
|
||||
holder.setData(item.selected, item.folders)
|
||||
}
|
||||
|
||||
class ViewHolder(
|
||||
view: View,
|
||||
private val weakContext: WeakReference<Context>
|
||||
) : RecyclerView.ViewHolder(view) {
|
||||
private var musicFolders: List<MusicFolder> = mutableListOf()
|
||||
private var selectedFolderId: String? = null
|
||||
private val folderName: TextView = itemView.findViewById(R.id.select_folder_name)
|
||||
private val layout: LinearLayout = itemView.findViewById(R.id.select_folder_header)
|
||||
|
||||
init {
|
||||
folderName.text = weakContext.get()!!.getString(R.string.select_artist_all_folders)
|
||||
layout.setOnClickListener { onFolderClick() }
|
||||
}
|
||||
|
||||
fun setData(selectedId: String?, folders: List<MusicFolder>) {
|
||||
selectedFolderId = selectedId
|
||||
musicFolders = folders
|
||||
if (selectedFolderId != null) {
|
||||
for ((id, name) in musicFolders) {
|
||||
if (id == selectedFolderId) {
|
||||
folderName.text = name
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
folderName.text = weakContext.get()!!.getString(R.string.select_artist_all_folders)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onFolderClick() {
|
||||
val popup = PopupMenu(weakContext.get()!!, layout)
|
||||
|
||||
var menuItem = popup.menu.add(
|
||||
MENU_GROUP_MUSIC_FOLDER, -1, 0, R.string.select_artist_all_folders
|
||||
)
|
||||
if (selectedFolderId == null || selectedFolderId!!.isEmpty()) {
|
||||
menuItem.isChecked = true
|
||||
}
|
||||
musicFolders.forEachIndexed { i, musicFolder ->
|
||||
val (id, name) = musicFolder
|
||||
menuItem = popup.menu.add(MENU_GROUP_MUSIC_FOLDER, i, i + 1, name)
|
||||
if (id == selectedFolderId) {
|
||||
menuItem.isChecked = true
|
||||
}
|
||||
}
|
||||
|
||||
popup.menu.setGroupCheckable(MENU_GROUP_MUSIC_FOLDER, true, true)
|
||||
|
||||
popup.setOnMenuItemClickListener { item -> onFolderMenuItemSelected(item) }
|
||||
popup.show()
|
||||
}
|
||||
|
||||
private fun onFolderMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||
val selectedFolder = if (menuItem.itemId == -1) null else musicFolders[menuItem.itemId]
|
||||
val musicFolderName = selectedFolder?.name
|
||||
?: weakContext.get()!!.getString(R.string.select_artist_all_folders)
|
||||
selectedFolderId = selectedFolder?.id
|
||||
|
||||
menuItem.isChecked = true
|
||||
folderName.text = musicFolderName
|
||||
|
||||
RxBus.musicFolderChangedEventPublisher.onNext(selectedFolderId)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MENU_GROUP_MUSIC_FOLDER = 10
|
||||
}
|
||||
}
|
||||
|
||||
data class FolderHeader(
|
||||
val folders: List<MusicFolder>,
|
||||
val selected: String?
|
||||
): Identifiable {
|
||||
override val id: String
|
||||
get() = "FOLDERSELECTOR"
|
||||
|
||||
override val longId: Long
|
||||
get() = -1L
|
||||
|
||||
override fun compareTo(other: Identifiable): Int {
|
||||
return longId.compareTo(other.longId)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,149 +0,0 @@
|
||||
/*
|
||||
* GenericRowAdapter.kt
|
||||
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
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.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||
import org.moire.ultrasonic.domain.Identifiable
|
||||
import org.moire.ultrasonic.domain.MusicFolder
|
||||
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 : Identifiable>(
|
||||
val onItemClick: (T) -> Unit,
|
||||
val onContextMenuClick: (MenuItem, T) -> Boolean,
|
||||
private val onMusicFolderUpdate: (String?) -> Unit
|
||||
) : ListAdapter<T, RecyclerView.ViewHolder>(GenericDiffCallback()) {
|
||||
|
||||
protected abstract val layout: Int
|
||||
protected abstract val contextMenuLayout: Int
|
||||
|
||||
var folderHeaderEnabled: Boolean = true
|
||||
var selectFolderHeader: SelectMusicFolderView? = null
|
||||
var musicFolders: List<MusicFolder> = listOf()
|
||||
var selectedFolder: String? = null
|
||||
|
||||
/**
|
||||
* Sets the content and state of the music folder selector row
|
||||
*/
|
||||
fun setFolderList(changedFolders: List<MusicFolder>, selectedId: String?) {
|
||||
musicFolders = changedFolders
|
||||
selectedFolder = selectedId
|
||||
|
||||
selectFolderHeader?.setData(
|
||||
selectedFolder,
|
||||
musicFolders
|
||||
)
|
||||
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
open fun newViewHolder(view: View): RecyclerView.ViewHolder {
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): RecyclerView.ViewHolder {
|
||||
if (viewType == TYPE_ITEM) {
|
||||
val row = LayoutInflater.from(parent.context)
|
||||
.inflate(layout, parent, false)
|
||||
return newViewHolder(row)
|
||||
} else {
|
||||
val row = LayoutInflater.from(parent.context)
|
||||
.inflate(
|
||||
R.layout.select_folder_header, parent, false
|
||||
)
|
||||
selectFolderHeader = SelectMusicFolderView(parent.context, row, onMusicFolderUpdate)
|
||||
|
||||
if (musicFolders.isNotEmpty()) {
|
||||
selectFolderHeader?.setData(
|
||||
selectedFolder,
|
||||
musicFolders
|
||||
)
|
||||
}
|
||||
|
||||
return selectFolderHeader!!
|
||||
}
|
||||
}
|
||||
|
||||
abstract override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int)
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
if (selectFolderHeader != null)
|
||||
return currentList.size + 1
|
||||
else
|
||||
return currentList.size
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return if (position == 0 && folderHeaderEnabled) 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, currentList[position])
|
||||
}
|
||||
popup.show()
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds the view properties of an Item row
|
||||
*/
|
||||
class ViewHolder(
|
||||
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
|
||||
|
||||
/**
|
||||
* Calculates the differences between data sets
|
||||
*/
|
||||
class GenericDiffCallback<T : Identifiable> : DiffUtil.ItemCallback<T>() {
|
||||
@SuppressLint("DiffUtilEquals")
|
||||
override fun areContentsTheSame(oldItem: T, newItem: T): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
override fun areItemsTheSame(oldItem: T, newItem: T): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.drakeet.multitype.ItemViewBinder
|
||||
import java.lang.ref.WeakReference
|
||||
@ -57,7 +58,12 @@ class HeaderViewBinder(
|
||||
Util.getAlbumImageSize(context)
|
||||
)
|
||||
|
||||
holder.titleView.text = item.name
|
||||
if (item.name != null) {
|
||||
holder.titleView.isVisible = true
|
||||
holder.titleView.text = item.name
|
||||
} else {
|
||||
holder.titleView.isVisible = false
|
||||
}
|
||||
|
||||
// Don't show a header if all entries are videos
|
||||
if (item.isAllVideo) {
|
||||
|
@ -0,0 +1,22 @@
|
||||
package org.moire.ultrasonic.adapters
|
||||
|
||||
import android.view.MenuInflater
|
||||
import android.view.View
|
||||
import android.widget.PopupMenu
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||
|
||||
object Helper {
|
||||
@JvmStatic
|
||||
fun createPopupMenu(view: View, contextMenuLayout: Int = R.menu.artist_context_menu): PopupMenu {
|
||||
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.show()
|
||||
return popup
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package org.moire.ultrasonic.adapters
|
||||
|
||||
import com.drakeet.multitype.MultiTypeAdapter
|
||||
import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView
|
||||
import org.moire.ultrasonic.domain.Identifiable
|
||||
|
||||
class SectionedAdapter<T : Identifiable> : MultiTypeAdapter(), FastScrollRecyclerView.SectionedAdapter {
|
||||
override fun getSectionName(position: Int): String {
|
||||
// var listPosition = if (selectFolderHeader != null) position - 1 else position
|
||||
//
|
||||
// // Show the first artist's initial in the popup when the list is
|
||||
// // scrolled up to the "Select Folder" row
|
||||
// if (listPosition < 0) listPosition = 0
|
||||
//
|
||||
// return getSectionFromName(currentList[listPosition].name ?: " ")
|
||||
return "X"
|
||||
}
|
||||
}
|
@ -17,6 +17,7 @@ import androidx.core.content.ContextCompat
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||
import org.moire.ultrasonic.data.ServerSetting
|
||||
import org.moire.ultrasonic.model.ServerSettingsModel
|
||||
import org.moire.ultrasonic.util.ServerColor
|
||||
import org.moire.ultrasonic.util.Util
|
||||
|
||||
|
@ -22,16 +22,6 @@ class TrackViewBinder(
|
||||
private val onClickCallback: ((View, DownloadFile?) -> Unit)? = null
|
||||
) : ItemViewBinder<Identifiable, TrackViewHolder>(), KoinComponent {
|
||||
|
||||
// //
|
||||
// onItemClick: (MusicDirectory.Entry) -> Unit,
|
||||
// onContextMenuClick: (MenuItem, MusicDirectory.Entry) -> Boolean,
|
||||
// onMusicFolderUpdate: (String?) -> Unit,
|
||||
// context: Context,
|
||||
// val lifecycleOwner: LifecycleOwner,
|
||||
// init {
|
||||
// super.submitList(itemList)
|
||||
// }
|
||||
|
||||
// Set our layout files
|
||||
val layout = R.layout.song_list_item
|
||||
val contextMenuLayout = R.menu.artist_context_menu
|
||||
@ -44,9 +34,8 @@ class TrackViewBinder(
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: TrackViewHolder, item: Identifiable) {
|
||||
|
||||
val downloadFile: DownloadFile?
|
||||
val _adapter = adapter as MultiTypeDiffAdapter<*>
|
||||
val diffAdapter = adapter as BaseAdapter<*>
|
||||
|
||||
when (item) {
|
||||
is MusicDirectory.Entry -> {
|
||||
@ -66,7 +55,7 @@ class TrackViewBinder(
|
||||
file = downloadFile,
|
||||
checkable = checkable,
|
||||
draggable = draggable,
|
||||
_adapter.isSelected(item.longId)
|
||||
diffAdapter.isSelected(item.longId)
|
||||
)
|
||||
|
||||
// Notify the adapter of selection changes
|
||||
@ -74,18 +63,18 @@ class TrackViewBinder(
|
||||
lifecycleOwner,
|
||||
{ newValue ->
|
||||
if (newValue) {
|
||||
_adapter.notifySelected(item.longId)
|
||||
diffAdapter.notifySelected(item.longId)
|
||||
} else {
|
||||
_adapter.notifyUnselected(item.longId)
|
||||
diffAdapter.notifyUnselected(item.longId)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Listen to changes in selection status and update ourselves
|
||||
_adapter.selectionRevision.observe(
|
||||
diffAdapter.selectionRevision.observe(
|
||||
lifecycleOwner,
|
||||
{
|
||||
val newStatus = _adapter.isSelected(item.longId)
|
||||
val newStatus = diffAdapter.isSelected(item.longId)
|
||||
|
||||
if (newStatus != holder.check.isChecked) holder.check.isChecked = newStatus
|
||||
}
|
||||
@ -96,7 +85,7 @@ class TrackViewBinder(
|
||||
lifecycleOwner,
|
||||
{
|
||||
holder.updateStatus(it)
|
||||
_adapter.notifyChanged()
|
||||
diffAdapter.notifyChanged()
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -23,13 +23,14 @@ import org.moire.ultrasonic.service.DownloadFile
|
||||
import org.moire.ultrasonic.service.DownloadStatus
|
||||
import org.moire.ultrasonic.service.MediaPlayerController
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory
|
||||
import org.moire.ultrasonic.service.RxBus
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
import org.moire.ultrasonic.util.Util
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Used to display songs and videos in a `ListView`.
|
||||
* TODO: Video List item
|
||||
* FIXME: Add video List item
|
||||
*/
|
||||
class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable, KoinComponent {
|
||||
|
||||
@ -58,7 +59,7 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable
|
||||
private var isMaximized = false
|
||||
private var cachedStatus = DownloadStatus.UNKNOWN
|
||||
private var statusImage: Drawable? = null
|
||||
private var playing = false
|
||||
private var isPlayingCached = false
|
||||
|
||||
var observableChecked = MutableLiveData(false)
|
||||
|
||||
@ -67,8 +68,6 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable
|
||||
features.isFeatureEnabled(Feature.FIVE_STAR_RATING)
|
||||
}
|
||||
|
||||
private val mediaPlayerController: MediaPlayerController by inject()
|
||||
|
||||
lateinit var imageHelper: ImageHelper
|
||||
|
||||
init {
|
||||
@ -116,9 +115,44 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable
|
||||
setupStarButtons(song)
|
||||
}
|
||||
|
||||
update()
|
||||
updateProgress(downloadFile!!.progress.value!!)
|
||||
updateStatus(downloadFile!!.status.value!!)
|
||||
|
||||
if (useFiveStarRating) {
|
||||
setFiveStars(entry?.userRating ?: 0)
|
||||
} else {
|
||||
setSingleStar(entry!!.starred)
|
||||
}
|
||||
|
||||
RxBus.playerStateObservable.subscribe {
|
||||
setPlayIcon(it.track == downloadFile)
|
||||
}
|
||||
|
||||
// Minimize or maximize the Text view (if song title is very long)
|
||||
itemView.setOnLongClickListener {
|
||||
if (!song.isDirectory) {
|
||||
maximizeOrMinimize()
|
||||
true
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun setPlayIcon(isPlaying: Boolean) {
|
||||
if (isPlaying && !isPlayingCached) {
|
||||
isPlayingCached = true
|
||||
title.setCompoundDrawablesWithIntrinsicBounds(
|
||||
imageHelper.playingImage, null, null, null
|
||||
)
|
||||
} else if (!isPlaying && isPlayingCached) {
|
||||
isPlayingCached = false
|
||||
title.setCompoundDrawablesWithIntrinsicBounds(
|
||||
0, 0, 0, 0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun setupStarButtons(song: MusicDirectory.Entry) {
|
||||
if (useFiveStarRating) {
|
||||
// Hide single star
|
||||
@ -157,38 +191,6 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
// TODO: Should be removed
|
||||
fun update() {
|
||||
|
||||
updateProgress(downloadFile!!.progress.value!!)
|
||||
updateStatus(downloadFile!!.status.value!!)
|
||||
|
||||
if (useFiveStarRating) {
|
||||
val rating = entry?.userRating ?: 0
|
||||
setFiveStars(rating)
|
||||
} else {
|
||||
setSingleStar(entry!!.starred)
|
||||
}
|
||||
|
||||
val playing = mediaPlayerController.currentPlaying === downloadFile
|
||||
|
||||
if (playing) {
|
||||
if (!this.playing) {
|
||||
this.playing = true
|
||||
title.setCompoundDrawablesWithIntrinsicBounds(
|
||||
imageHelper.playingImage, null, null, null
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (this.playing) {
|
||||
this.playing = false
|
||||
title.setCompoundDrawablesWithIntrinsicBounds(
|
||||
0, 0, 0, 0
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun setFiveStars(rating: Int) {
|
||||
|
@ -9,7 +9,7 @@ import org.moire.ultrasonic.data.AppDatabase
|
||||
import org.moire.ultrasonic.data.MIGRATION_1_2
|
||||
import org.moire.ultrasonic.data.MIGRATION_2_3
|
||||
import org.moire.ultrasonic.data.MIGRATION_3_4
|
||||
import org.moire.ultrasonic.fragment.ServerSettingsModel
|
||||
import org.moire.ultrasonic.model.ServerSettingsModel
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
|
||||
const val SP_NAME = "Default_SP"
|
||||
|
@ -7,12 +7,14 @@ import androidx.lifecycle.LiveData
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.adapters.AlbumRowBinder
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
import org.moire.ultrasonic.model.AlbumListModel
|
||||
import org.moire.ultrasonic.util.Constants
|
||||
|
||||
/**
|
||||
* Displays a list of Albums from the media library
|
||||
* TODO: Check refresh is working
|
||||
* FIXME: Add music folder support
|
||||
*/
|
||||
class AlbumListFragment : EntryListFragment<MusicDirectory.Entry>() {
|
||||
|
||||
@ -54,24 +56,6 @@ class AlbumListFragment : EntryListFragment<MusicDirectory.Entry>() {
|
||||
return listModel.getAlbumList(refresh or append, refreshListView!!, args)
|
||||
}
|
||||
|
||||
// FIXME
|
||||
// /**
|
||||
// * Provide the Adapter for the RecyclerView with a lazy delegate
|
||||
// */
|
||||
// override val viewAdapter: AlbumRowAdapter by lazy {
|
||||
// AlbumRowAdapter(
|
||||
// liveDataItems.value ?: listOf(),
|
||||
// { entry -> onItemClick(entry) },
|
||||
// { menuItem, entry -> onContextMenuItemSelected(menuItem, entry) },
|
||||
// imageLoaderProvider.getImageLoader(),
|
||||
// onMusicFolderUpdate,
|
||||
// requireContext()
|
||||
// )
|
||||
// }
|
||||
|
||||
val newBundleClone: Bundle
|
||||
get() = arguments?.clone() as Bundle
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
@ -81,13 +65,25 @@ class AlbumListFragment : EntryListFragment<MusicDirectory.Entry>() {
|
||||
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
|
||||
val appendArgs = getArgumentsClone()
|
||||
appendArgs.putBoolean(Constants.INTENT_EXTRA_NAME_APPEND, true)
|
||||
getLiveData(appendArgs)
|
||||
}
|
||||
}
|
||||
addOnScrollListener(scrollListener)
|
||||
}
|
||||
|
||||
|
||||
viewAdapter.register(
|
||||
AlbumRowBinder(
|
||||
{ entry -> onItemClick(entry) },
|
||||
{ menuItem, entry -> onContextMenuItemSelected(menuItem, entry) },
|
||||
imageLoaderProvider.getImageLoader(),
|
||||
context = requireContext()
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
|
||||
override fun onItemClick(item: MusicDirectory.Entry) {
|
||||
@ -98,4 +94,5 @@ class AlbumListFragment : EntryListFragment<MusicDirectory.Entry>() {
|
||||
bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, item.parent)
|
||||
findNavController().navigate(itemClickTarget, bundle)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,12 +1,17 @@
|
||||
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 org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.adapters.ArtistRowAdapter
|
||||
import org.moire.ultrasonic.adapters.ArtistRowBinder
|
||||
import org.moire.ultrasonic.domain.Artist
|
||||
import org.moire.ultrasonic.domain.ArtistOrIndex
|
||||
import org.moire.ultrasonic.model.ArtistListModel
|
||||
import org.moire.ultrasonic.util.Constants
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
|
||||
/**
|
||||
* Displays the list of Artists from the media library
|
||||
@ -39,6 +44,7 @@ class ArtistListFragment : EntryListFragment<ArtistOrIndex>() {
|
||||
*/
|
||||
override val itemClickTarget = R.id.selectArtistToSelectAlbum
|
||||
|
||||
|
||||
/**
|
||||
* The central function to pass a query to the model and return a LiveData object
|
||||
*/
|
||||
@ -47,17 +53,31 @@ class ArtistListFragment : EntryListFragment<ArtistOrIndex>() {
|
||||
return listModel.getItems(refresh, refreshListView!!)
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide the Adapter for the RecyclerView with a lazy delegate
|
||||
*/
|
||||
// FIXME
|
||||
// override val viewAdapter: ArtistRowAdapter by lazy {
|
||||
// ArtistRowAdapter(
|
||||
// liveDataItems.value ?: listOf(),
|
||||
// { entry -> onItemClick(entry) },
|
||||
// { menuItem, entry -> onContextMenuItemSelected(menuItem, entry) },
|
||||
// imageLoaderProvider.getImageLoader(),
|
||||
// onMusicFolderUpdate
|
||||
// )
|
||||
// }
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
viewAdapter.register(
|
||||
ArtistRowBinder(
|
||||
{ entry -> onItemClick(entry) },
|
||||
{ menuItem, entry -> onContextMenuItemSelected(menuItem, entry) },
|
||||
imageLoaderProvider.getImageLoader()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onItemClick(item: ArtistOrIndex) {
|
||||
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))
|
||||
bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, Constants.ALPHABETICAL_BY_NAME)
|
||||
bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, item.name)
|
||||
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 1000)
|
||||
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0)
|
||||
findNavController().navigate(itemClickTarget, bundle)
|
||||
}
|
||||
|
||||
//Constants.ALPHABETICAL_BY_NAME
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,66 @@
|
||||
package org.moire.ultrasonic.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
|
||||
|
||||
/**
|
||||
* Lists the Bookmarks available on the server
|
||||
*/
|
||||
class BookmarksFragment : TrackCollectionFragment() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setTitle(this, R.string.button_bar_bookmarks)
|
||||
}
|
||||
|
||||
override fun setupButtons(view: View) {
|
||||
super.setupButtons(view)
|
||||
|
||||
// Why?
|
||||
selectButton?.visibility = View.GONE
|
||||
playNextButton?.visibility = View.GONE
|
||||
playLastButton?.visibility = View.GONE
|
||||
moreButton?.visibility = View.GONE
|
||||
}
|
||||
|
||||
override fun getLiveData(args: Bundle?): LiveData<List<MusicDirectory.Entry>> {
|
||||
listModel.viewModelScope.launch(handler) {
|
||||
refreshListView?.isRefreshing = true
|
||||
listModel.getBookmarks()
|
||||
refreshListView?.isRefreshing = false
|
||||
}
|
||||
return listModel.currentList
|
||||
}
|
||||
|
||||
override fun enableButtons(selection: List<MusicDirectory.Entry>) {
|
||||
val enabled = selection.isNotEmpty()
|
||||
var unpinEnabled = false
|
||||
var deleteEnabled = false
|
||||
var pinnedCount = 0
|
||||
|
||||
for (song in selection) {
|
||||
val downloadFile = mediaPlayerController.getDownloadFileForSong(song)
|
||||
if (downloadFile.isWorkDone) {
|
||||
deleteEnabled = true
|
||||
}
|
||||
if (downloadFile.isSaved) {
|
||||
pinnedCount++
|
||||
unpinEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
playNowButton?.isVisible = (enabled && deleteEnabled)
|
||||
pinButton?.isVisible = (enabled && !isOffline() && selection.size > pinnedCount)
|
||||
unpinButton!!.isVisible = (enabled && unpinEnabled)
|
||||
downloadButton!!.isVisible = (enabled && !deleteEnabled && !isOffline())
|
||||
deleteButton!!.isVisible = (enabled && deleteEnabled)
|
||||
}
|
||||
}
|
@ -9,6 +9,7 @@ import androidx.lifecycle.LiveData
|
||||
import org.koin.core.component.inject
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.adapters.TrackViewBinder
|
||||
import org.moire.ultrasonic.model.GenericListModel
|
||||
import org.moire.ultrasonic.service.DownloadFile
|
||||
import org.moire.ultrasonic.service.Downloader
|
||||
import org.moire.ultrasonic.util.Util
|
||||
|
@ -33,6 +33,7 @@ import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse
|
||||
import org.moire.ultrasonic.api.subsonic.throwOnFailure
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||
import org.moire.ultrasonic.data.ServerSetting
|
||||
import org.moire.ultrasonic.model.ServerSettingsModel
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory
|
||||
import org.moire.ultrasonic.util.Constants
|
||||
import org.moire.ultrasonic.util.ErrorDialog
|
||||
|
@ -0,0 +1,140 @@
|
||||
package org.moire.ultrasonic.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.domain.Artist
|
||||
import org.moire.ultrasonic.domain.GenericEntry
|
||||
import org.moire.ultrasonic.service.RxBus
|
||||
import org.moire.ultrasonic.util.Constants
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
|
||||
/**
|
||||
* An extension of the MultiListFragment, with a few helper functions geared
|
||||
* towards the display of MusicDirectory.Entries.
|
||||
* @param T: The type of data which will be used (must extend GenericEntry)
|
||||
*/
|
||||
abstract class EntryListFragment<T : GenericEntry> : MultiListFragment<T>() {
|
||||
|
||||
/**
|
||||
* Whether to show the folder selector
|
||||
*/
|
||||
// FIXME
|
||||
fun showFolderHeader(): Boolean {
|
||||
return listModel.showSelectFolderHeader(arguments) &&
|
||||
!listModel.isOffline() && !Settings.shouldUseId3Tags
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
override 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
|
||||
}
|
||||
|
||||
override 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)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// FIXME: What to do when the user has modified the folder filter
|
||||
RxBus.musicFolderChangedEventObservable.subscribe {
|
||||
if (!listModel.isOffline()) {
|
||||
val currentSetting = listModel.activeServer
|
||||
currentSetting.musicFolderId = it
|
||||
serverSettingsModel.updateItem(currentSetting)
|
||||
}
|
||||
viewAdapter.notifyDataSetChanged()
|
||||
listModel.refresh(refreshListView!!, arguments)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,281 +0,0 @@
|
||||
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.androidx.viewmodel.ext.android.viewModel
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.adapters.GenericRowAdapter
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||
import org.moire.ultrasonic.domain.Artist
|
||||
import org.moire.ultrasonic.domain.GenericEntry
|
||||
import org.moire.ultrasonic.domain.Identifiable
|
||||
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.Settings
|
||||
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)
|
||||
*/
|
||||
abstract class GenericListFragment<T : Identifiable, 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
|
||||
*/
|
||||
@Suppress("CommentOverPrivateProperty")
|
||||
private val musicFolderObserver = { folders: List<MusicFolder> ->
|
||||
viewAdapter.setFolderList(folders, listModel.activeServer.musicFolderId)
|
||||
}
|
||||
|
||||
/**
|
||||
* What to do when the user has modified the folder filter
|
||||
*/
|
||||
val onMusicFolderUpdate = { selectedFolderId: String? ->
|
||||
if (!listModel.isOffline()) {
|
||||
val currentSetting = listModel.activeServer
|
||||
currentSetting.musicFolderId = selectedFolderId
|
||||
serverSettingsModel.updateItem(currentSetting)
|
||||
}
|
||||
viewAdapter.notifyDataSetChanged()
|
||||
listModel.refresh(refreshListView!!, arguments)
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to show the folder selector
|
||||
*/
|
||||
fun showFolderHeader(): Boolean {
|
||||
return listModel.showSelectFolderHeader(arguments) &&
|
||||
!listModel.isOffline() && !Settings.shouldUseId3Tags
|
||||
}
|
||||
|
||||
open 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.submitList(newItems) })
|
||||
|
||||
// Setup the Music folder handling
|
||||
listModel.getMusicFolders().observe(viewLifecycleOwner, musicFolderObserver)
|
||||
|
||||
// Create a View Manager
|
||||
viewManager = LinearLayoutManager(this.context)
|
||||
|
||||
// Hook up the view with the manager and the adapter
|
||||
listView = view.findViewById<RecyclerView>(recyclerViewId).apply {
|
||||
setHasFixedSize(true)
|
||||
layoutManager = viewManager
|
||||
adapter = viewAdapter
|
||||
}
|
||||
|
||||
// Configure whether to show the folder header
|
||||
viewAdapter.folderHeaderEnabled = showFolderHeader()
|
||||
}
|
||||
|
||||
@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)
|
||||
}
|
||||
|
||||
abstract fun onContextMenuItemSelected(menuItem: MenuItem, item: T): Boolean
|
||||
|
||||
abstract fun onItemClick(item: T)
|
||||
}
|
||||
|
||||
abstract class EntryListFragment<T : GenericEntry> : MultiListFragment<T>() {
|
||||
@Suppress("LongMethod")
|
||||
override 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
|
||||
}
|
||||
|
||||
override 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)
|
||||
}
|
||||
}
|
@ -8,20 +8,21 @@ import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
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.androidx.viewmodel.ext.android.viewModel
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.adapters.MultiTypeDiffAdapter
|
||||
import org.moire.ultrasonic.adapters.BaseAdapter
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||
import org.moire.ultrasonic.domain.Identifiable
|
||||
import org.moire.ultrasonic.domain.MusicFolder
|
||||
import org.moire.ultrasonic.model.GenericListModel
|
||||
import org.moire.ultrasonic.model.ServerSettingsModel
|
||||
import org.moire.ultrasonic.subsonic.DownloadHandler
|
||||
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
|
||||
import org.moire.ultrasonic.util.Constants
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
import org.moire.ultrasonic.util.Util
|
||||
import org.moire.ultrasonic.view.SelectMusicFolderView
|
||||
|
||||
@ -43,8 +44,8 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
|
||||
* The Adapter for the RecyclerView
|
||||
* Recommendation: Implement this as a lazy delegate
|
||||
*/
|
||||
internal val viewAdapter: MultiTypeDiffAdapter<Identifiable> by lazy {
|
||||
MultiTypeDiffAdapter()
|
||||
internal val viewAdapter: BaseAdapter<Identifiable> by lazy {
|
||||
BaseAdapter()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -61,7 +62,9 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
|
||||
/**
|
||||
* The central function to pass a query to the model and return a LiveData object
|
||||
*/
|
||||
abstract fun getLiveData(args: Bundle? = null): LiveData<List<T>>
|
||||
open fun getLiveData(args: Bundle? = null): LiveData<List<T>> {
|
||||
return MutableLiveData(listOf())
|
||||
}
|
||||
|
||||
/**
|
||||
* The id of the target in the navigation graph where we should go,
|
||||
@ -84,35 +87,6 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
|
||||
*/
|
||||
open val recyclerViewId = R.id.generic_list_recycler
|
||||
|
||||
/**
|
||||
* The observer to be called if the available music folders have changed
|
||||
*/
|
||||
@Suppress("CommentOverPrivateProperty")
|
||||
private val musicFolderObserver = { folders: List<MusicFolder> ->
|
||||
// viewAdapter.setFolderList(folders, listModel.activeServer.musicFolderId)
|
||||
}
|
||||
|
||||
/**
|
||||
* What to do when the user has modified the folder filter
|
||||
*/
|
||||
val onMusicFolderUpdate = { selectedFolderId: String? ->
|
||||
if (!listModel.isOffline()) {
|
||||
val currentSetting = listModel.activeServer
|
||||
currentSetting.musicFolderId = selectedFolderId
|
||||
serverSettingsModel.updateItem(currentSetting)
|
||||
}
|
||||
viewAdapter.notifyDataSetChanged()
|
||||
listModel.refresh(refreshListView!!, arguments)
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to show the folder selector
|
||||
*/
|
||||
fun showFolderHeader(): Boolean {
|
||||
return listModel.showSelectFolderHeader(arguments) &&
|
||||
!listModel.isOffline() && !Settings.shouldUseId3Tags
|
||||
}
|
||||
|
||||
open fun setTitle(title: String?) {
|
||||
if (title == null) {
|
||||
FragmentTitle.setTitle(
|
||||
@ -150,9 +124,6 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
|
||||
}
|
||||
)
|
||||
|
||||
// Setup the Music folder handling
|
||||
listModel.getMusicFolders().observe(viewLifecycleOwner, musicFolderObserver)
|
||||
|
||||
// Create a View Manager
|
||||
viewManager = LinearLayoutManager(this.context)
|
||||
|
||||
@ -184,103 +155,17 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
|
||||
abstract fun onContextMenuItemSelected(menuItem: MenuItem, item: T): Boolean
|
||||
|
||||
abstract fun onItemClick(item: T)
|
||||
|
||||
fun getArgumentsClone(): Bundle {
|
||||
var bundle: Bundle
|
||||
|
||||
try {
|
||||
bundle = arguments?.clone() as Bundle
|
||||
} catch (ignored: Exception) {
|
||||
bundle = Bundle()
|
||||
}
|
||||
|
||||
return bundle
|
||||
}
|
||||
}
|
||||
|
||||
// abstract class EntryListFragment<T : GenericEntry, TA : GenericRowAdapter<T>> :
|
||||
// GenericListFragment<T, TA>() {
|
||||
// @Suppress("LongMethod")
|
||||
// override 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
|
||||
// }
|
||||
//
|
||||
// override 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)
|
||||
// }
|
||||
// }
|
||||
|
@ -60,7 +60,7 @@ import org.koin.android.ext.android.inject
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.adapters.MultiTypeDiffAdapter
|
||||
import org.moire.ultrasonic.adapters.BaseAdapter
|
||||
import org.moire.ultrasonic.adapters.TrackViewBinder
|
||||
import org.moire.ultrasonic.audiofx.EqualizerController
|
||||
import org.moire.ultrasonic.audiofx.VisualizerController
|
||||
@ -154,8 +154,8 @@ class PlayerFragment :
|
||||
private lateinit var fullStar: Drawable
|
||||
private lateinit var progressBar: SeekBar
|
||||
|
||||
internal val viewAdapter: MultiTypeDiffAdapter<Identifiable> by lazy {
|
||||
MultiTypeDiffAdapter()
|
||||
internal val viewAdapter: BaseAdapter<Identifiable> by lazy {
|
||||
BaseAdapter()
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@ -890,7 +890,7 @@ class PlayerFragment :
|
||||
// FIXME:
|
||||
// Needs to be changed in the playlist as well...
|
||||
// Move it in the data set
|
||||
(recyclerView.adapter as MultiTypeDiffAdapter<*>).moveItem(from, to)
|
||||
(recyclerView.adapter as BaseAdapter<*>).moveItem(from, to)
|
||||
|
||||
return true
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||
import org.moire.ultrasonic.fragment.EditServerFragment.Companion.EDIT_SERVER_INTENT_INDEX
|
||||
import org.moire.ultrasonic.model.ServerSettingsModel
|
||||
import org.moire.ultrasonic.service.MediaPlayerController
|
||||
import org.moire.ultrasonic.util.Util
|
||||
import timber.log.Timber
|
||||
|
@ -25,7 +25,6 @@ import androidx.lifecycle.viewModelScope
|
||||
import androidx.navigation.Navigation
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import java.util.Collections
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.ext.android.inject
|
||||
@ -36,9 +35,11 @@ import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
||||
import org.moire.ultrasonic.domain.Identifiable
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
|
||||
import org.moire.ultrasonic.model.TrackCollectionModel
|
||||
import org.moire.ultrasonic.service.MediaPlayerController
|
||||
import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker
|
||||
import org.moire.ultrasonic.subsonic.ShareHandler
|
||||
import org.moire.ultrasonic.subsonic.VideoPlayer
|
||||
import org.moire.ultrasonic.util.AlbumHeader
|
||||
import org.moire.ultrasonic.util.CancellationToken
|
||||
import org.moire.ultrasonic.util.CommunicationError
|
||||
@ -47,35 +48,34 @@ import org.moire.ultrasonic.util.EntryByDiscAndTrackComparator
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
import org.moire.ultrasonic.util.Util
|
||||
import timber.log.Timber
|
||||
import java.util.Collections
|
||||
|
||||
/**
|
||||
* Displays a group of tracks, eg. the songs of an album, of a playlist etc.
|
||||
* TODO: Move Clickhandler into ViewBinders
|
||||
* TODO: Fix clikc handlers and context menus etc.
|
||||
* FIXME: Offset when navigating to?
|
||||
*/
|
||||
class TrackCollectionFragment :
|
||||
MultiListFragment<MusicDirectory.Entry>() {
|
||||
open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Entry>() {
|
||||
|
||||
private var albumButtons: View? = null
|
||||
private var emptyView: TextView? = null
|
||||
private var selectButton: ImageView? = null
|
||||
private var playNowButton: ImageView? = null
|
||||
private var playNextButton: ImageView? = null
|
||||
private var playLastButton: ImageView? = null
|
||||
private var pinButton: ImageView? = null
|
||||
private var unpinButton: ImageView? = null
|
||||
private var downloadButton: ImageView? = null
|
||||
private var deleteButton: ImageView? = null
|
||||
private var moreButton: ImageView? = null
|
||||
internal var selectButton: ImageView? = null
|
||||
internal var playNowButton: ImageView? = null
|
||||
internal var playNextButton: ImageView? = null
|
||||
internal var playLastButton: ImageView? = null
|
||||
internal var pinButton: ImageView? = null
|
||||
internal var unpinButton: ImageView? = null
|
||||
internal var downloadButton: ImageView? = null
|
||||
internal var deleteButton: ImageView? = null
|
||||
internal var moreButton: ImageView? = null
|
||||
private var playAllButtonVisible = false
|
||||
private var shareButtonVisible = false
|
||||
private var playAllButton: MenuItem? = null
|
||||
private var shareButton: MenuItem? = null
|
||||
|
||||
private val mediaPlayerController: MediaPlayerController by inject()
|
||||
internal val mediaPlayerController: MediaPlayerController by inject()
|
||||
private val networkAndStorageChecker: NetworkAndStorageChecker by inject()
|
||||
private val shareHandler: ShareHandler by inject()
|
||||
private var cancellationToken: CancellationToken? = null
|
||||
internal var cancellationToken: CancellationToken? = null
|
||||
|
||||
override val listModel: TrackCollectionModel by viewModels()
|
||||
|
||||
@ -98,7 +98,6 @@ class TrackCollectionFragment :
|
||||
* The id of the target in the navigation graph where we should go,
|
||||
* after the user has clicked on an item
|
||||
*/
|
||||
// FIXME
|
||||
override val itemClickTarget: Int = R.id.trackCollectionFragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
@ -110,90 +109,15 @@ class TrackCollectionFragment :
|
||||
// Setup refresh handler
|
||||
refreshListView = view.findViewById(refreshListId)
|
||||
refreshListView?.setOnRefreshListener {
|
||||
updateDisplay(true)
|
||||
refreshData(true)
|
||||
}
|
||||
|
||||
listModel.currentList.observe(viewLifecycleOwner, updateInterfaceWithEntries)
|
||||
listModel.songsForGenre.observe(viewLifecycleOwner, songsForGenreObserver)
|
||||
|
||||
// listView!!.setOnItemClickListener { parent, theView, position, _ ->
|
||||
// if (position >= 0) {
|
||||
// val entry = parent.getItemAtPosition(position) as MusicDirectory.Entry?
|
||||
// if (entry != null && entry.isDirectory) {
|
||||
// val bundle = Bundle()
|
||||
// bundle.putString(Constants.INTENT_EXTRA_NAME_ID, entry.id)
|
||||
// bundle.putBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, entry.isDirectory)
|
||||
// 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.trackCollectionFragment,
|
||||
// bundle
|
||||
// )
|
||||
// } else if (entry != null && entry.isVideo) {
|
||||
// VideoPlayer.playVideo(requireContext(), entry)
|
||||
// } else {
|
||||
// enableButtons()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// listView!!.setOnItemLongClickListener { _, theView, _, _ ->
|
||||
// if (theView is AlbumView) {
|
||||
// return@setOnItemLongClickListener false
|
||||
// }
|
||||
// if (theView is SongView) {
|
||||
// theView.maximizeOrMinimize()
|
||||
// return@setOnItemLongClickListener true
|
||||
// }
|
||||
// return@setOnItemLongClickListener false
|
||||
// }
|
||||
setupButtons(view)
|
||||
|
||||
selectButton = view.findViewById(R.id.select_album_select)
|
||||
playNowButton = view.findViewById(R.id.select_album_play_now)
|
||||
playNextButton = view.findViewById(R.id.select_album_play_next)
|
||||
playLastButton = view.findViewById(R.id.select_album_play_last)
|
||||
pinButton = view.findViewById(R.id.select_album_pin)
|
||||
unpinButton = view.findViewById(R.id.select_album_unpin)
|
||||
downloadButton = view.findViewById(R.id.select_album_download)
|
||||
deleteButton = view.findViewById(R.id.select_album_delete)
|
||||
moreButton = view.findViewById(R.id.select_album_more)
|
||||
emptyView = TextView(requireContext())
|
||||
|
||||
selectButton!!.setOnClickListener {
|
||||
selectAllOrNone()
|
||||
}
|
||||
|
||||
playNowButton!!.setOnClickListener {
|
||||
playNow(false)
|
||||
}
|
||||
|
||||
playNextButton!!.setOnClickListener {
|
||||
downloadHandler.download(
|
||||
this@TrackCollectionFragment, append = true,
|
||||
save = false, autoPlay = false, playNext = true, shuffle = false,
|
||||
songs = getSelectedSongs()
|
||||
)
|
||||
}
|
||||
|
||||
playLastButton!!.setOnClickListener {
|
||||
playNow(true)
|
||||
}
|
||||
|
||||
pinButton!!.setOnClickListener {
|
||||
downloadBackground(true)
|
||||
}
|
||||
|
||||
unpinButton!!.setOnClickListener {
|
||||
unpin()
|
||||
}
|
||||
|
||||
downloadButton!!.setOnClickListener {
|
||||
downloadBackground(false)
|
||||
}
|
||||
|
||||
deleteButton!!.setOnClickListener {
|
||||
delete()
|
||||
}
|
||||
emptyView = view.findViewById(R.id.select_album_empty)
|
||||
|
||||
registerForContextMenu(listView!!)
|
||||
setHasOptionsMenu(true)
|
||||
@ -234,19 +158,68 @@ class TrackCollectionFragment :
|
||||
)
|
||||
|
||||
// Loads the data
|
||||
updateDisplay(false)
|
||||
refreshData(false)
|
||||
}
|
||||
|
||||
internal open fun setupButtons(view: View) {
|
||||
selectButton = view.findViewById(R.id.select_album_select)
|
||||
playNowButton = view.findViewById(R.id.select_album_play_now)
|
||||
playNextButton = view.findViewById(R.id.select_album_play_next)
|
||||
playLastButton = view.findViewById(R.id.select_album_play_last)
|
||||
pinButton = view.findViewById(R.id.select_album_pin)
|
||||
unpinButton = view.findViewById(R.id.select_album_unpin)
|
||||
downloadButton = view.findViewById(R.id.select_album_download)
|
||||
deleteButton = view.findViewById(R.id.select_album_delete)
|
||||
moreButton = view.findViewById(R.id.select_album_more)
|
||||
|
||||
selectButton?.setOnClickListener {
|
||||
selectAllOrNone()
|
||||
}
|
||||
|
||||
playNowButton?.setOnClickListener {
|
||||
playNow(false)
|
||||
}
|
||||
|
||||
playNextButton?.setOnClickListener {
|
||||
downloadHandler.download(
|
||||
this@TrackCollectionFragment, append = true,
|
||||
save = false, autoPlay = false, playNext = true, shuffle = false,
|
||||
songs = getSelectedSongs()
|
||||
)
|
||||
}
|
||||
|
||||
playLastButton!!.setOnClickListener {
|
||||
playNow(true)
|
||||
}
|
||||
|
||||
pinButton?.setOnClickListener {
|
||||
downloadBackground(true)
|
||||
}
|
||||
|
||||
unpinButton?.setOnClickListener {
|
||||
unpin()
|
||||
}
|
||||
|
||||
downloadButton?.setOnClickListener {
|
||||
downloadBackground(false)
|
||||
}
|
||||
|
||||
deleteButton?.setOnClickListener {
|
||||
delete()
|
||||
}
|
||||
}
|
||||
|
||||
val handler = CoroutineExceptionHandler { _, exception ->
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
CommunicationError.handleError(exception, context)
|
||||
}
|
||||
refreshListView!!.isRefreshing = false
|
||||
refreshListView?.isRefreshing = false
|
||||
}
|
||||
|
||||
private fun updateDisplay(refresh: Boolean) {
|
||||
// FIXME: Use refresh
|
||||
getLiveData(requireArguments())
|
||||
private fun refreshData(refresh: Boolean = false) {
|
||||
val args = getArgumentsClone()
|
||||
args.putBoolean(Constants.INTENT_EXTRA_NAME_REFRESH, refresh)
|
||||
getLiveData(args)
|
||||
}
|
||||
|
||||
override fun onContextItemSelected(menuItem: MenuItem): Boolean {
|
||||
@ -370,7 +343,6 @@ class TrackCollectionFragment :
|
||||
this, append, false, !append, playNext = false,
|
||||
shuffle = false, songs = selectedSongs
|
||||
)
|
||||
selectAll(selected = false, toast = false)
|
||||
} else {
|
||||
playAll(false, append)
|
||||
}
|
||||
@ -399,8 +371,10 @@ class TrackCollectionFragment :
|
||||
}
|
||||
}
|
||||
|
||||
val isArtist = requireArguments().getBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, false)
|
||||
val id = requireArguments().getString(Constants.INTENT_EXTRA_NAME_ID)
|
||||
val isArtist = arguments?.getBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, false)?: false
|
||||
|
||||
// FIXME WHICH id if no arguments?
|
||||
val id = arguments?.getString(Constants.INTENT_EXTRA_NAME_ID)
|
||||
|
||||
if (hasSubFolders && id != null) {
|
||||
downloadHandler.downloadRecursively(
|
||||
@ -435,13 +409,13 @@ class TrackCollectionFragment :
|
||||
} as List<MusicDirectory.Entry>
|
||||
}
|
||||
|
||||
private fun selectAllOrNone() {
|
||||
internal fun selectAllOrNone() {
|
||||
val someUnselected = viewAdapter.selectedSet.size < childCount
|
||||
|
||||
selectAll(someUnselected, true)
|
||||
}
|
||||
|
||||
private fun selectAll(selected: Boolean, toast: Boolean) {
|
||||
internal fun selectAll(selected: Boolean, toast: Boolean) {
|
||||
var selectedCount = viewAdapter.selectedSet.size * -1
|
||||
|
||||
selectedCount += viewAdapter.setSelectionStatusOfAll(selected)
|
||||
@ -453,7 +427,7 @@ class TrackCollectionFragment :
|
||||
}
|
||||
}
|
||||
|
||||
private fun enableButtons(selection: List<MusicDirectory.Entry> = getSelectedSongs()) {
|
||||
internal open fun enableButtons(selection: List<MusicDirectory.Entry> = getSelectedSongs()) {
|
||||
val enabled = selection.isNotEmpty()
|
||||
var unpinEnabled = false
|
||||
var deleteEnabled = false
|
||||
@ -480,7 +454,7 @@ class TrackCollectionFragment :
|
||||
deleteButton?.isVisible = (enabled && deleteEnabled)
|
||||
}
|
||||
|
||||
private fun downloadBackground(save: Boolean) {
|
||||
internal fun downloadBackground(save: Boolean) {
|
||||
var songs = getSelectedSongs()
|
||||
|
||||
if (songs.isEmpty()) {
|
||||
@ -514,7 +488,7 @@ class TrackCollectionFragment :
|
||||
onValid.run()
|
||||
}
|
||||
|
||||
private fun delete() {
|
||||
internal fun delete() {
|
||||
val songs = getSelectedSongs()
|
||||
|
||||
Util.toast(
|
||||
@ -527,7 +501,7 @@ class TrackCollectionFragment :
|
||||
mediaPlayerController.delete(songs)
|
||||
}
|
||||
|
||||
private fun unpin() {
|
||||
internal fun unpin() {
|
||||
val songs = getSelectedSongs()
|
||||
Util.toast(
|
||||
context,
|
||||
@ -586,23 +560,17 @@ class TrackCollectionFragment :
|
||||
}
|
||||
}
|
||||
|
||||
val listSize = requireArguments().getInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0)
|
||||
val listSize = arguments?.getInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0) ?: 0
|
||||
|
||||
// Hide select button for video lists
|
||||
selectButton!!.isVisible = !allVideos
|
||||
|
||||
if (songCount > 0) {
|
||||
pinButton!!.visibility = View.VISIBLE
|
||||
unpinButton!!.visibility = View.VISIBLE
|
||||
downloadButton!!.visibility = View.VISIBLE
|
||||
deleteButton!!.visibility = View.VISIBLE
|
||||
selectButton!!.visibility = if (allVideos) View.GONE else View.VISIBLE
|
||||
playNowButton!!.visibility = View.VISIBLE
|
||||
playNextButton!!.visibility = View.VISIBLE
|
||||
playLastButton!!.visibility = View.VISIBLE
|
||||
|
||||
if (listSize == 0 || songCount < listSize) {
|
||||
moreButton!!.visibility = View.GONE
|
||||
} else {
|
||||
moreButton!!.visibility = View.VISIBLE
|
||||
if (requireArguments().getInt(Constants.INTENT_EXTRA_NAME_RANDOM, 0) > 0) {
|
||||
if (arguments?.getInt(Constants.INTENT_EXTRA_NAME_RANDOM, 0) ?:0 > 0) {
|
||||
moreButton!!.setOnClickListener {
|
||||
val offset = requireArguments().getInt(
|
||||
Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0
|
||||
@ -617,58 +585,41 @@ class TrackCollectionFragment :
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
// TODO: This code path can be removed when getArtist has been moved to
|
||||
// AlbumListFragment (getArtist returns the albums of an artist)
|
||||
pinButton!!.visibility = View.GONE
|
||||
unpinButton!!.visibility = View.GONE
|
||||
downloadButton!!.visibility = View.GONE
|
||||
deleteButton!!.visibility = View.GONE
|
||||
selectButton!!.visibility = View.GONE
|
||||
playNowButton!!.visibility = View.GONE
|
||||
playNextButton!!.visibility = View.GONE
|
||||
playLastButton!!.visibility = View.GONE
|
||||
|
||||
if (listSize == 0 || entryList.size < listSize) {
|
||||
albumButtons!!.visibility = View.GONE
|
||||
} else {
|
||||
moreButton!!.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
// Show a text if we have no entries
|
||||
emptyView?.isVisible = entryList.isEmpty()
|
||||
|
||||
enableButtons()
|
||||
|
||||
val isAlbumList = requireArguments().containsKey(
|
||||
val isAlbumList = arguments?.containsKey(
|
||||
Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE
|
||||
)
|
||||
)?:false
|
||||
|
||||
playAllButtonVisible = !(isAlbumList || entryList.isEmpty()) && !allVideos
|
||||
shareButtonVisible = !isOffline() && songCount > 0
|
||||
|
||||
if (playAllButton != null) {
|
||||
playAllButton!!.isVisible = playAllButtonVisible
|
||||
}
|
||||
|
||||
if (shareButton != null) {
|
||||
shareButton!!.isVisible = shareButtonVisible
|
||||
}
|
||||
playAllButton?.isVisible = playAllButtonVisible
|
||||
shareButton?.isVisible = shareButtonVisible
|
||||
|
||||
if (songCount > 0 && listModel.showHeader) {
|
||||
val name = listModel.currentDirectory.value?.name
|
||||
val intentAlbumName = requireArguments().getString(Constants.INTENT_EXTRA_NAME_NAME, "Name")!!
|
||||
val albumHeader = AlbumHeader(it, name ?: intentAlbumName, songCount)
|
||||
val intentAlbumName = arguments?.getString(Constants.INTENT_EXTRA_NAME_NAME, "")
|
||||
val albumHeader = AlbumHeader(it, name ?: intentAlbumName)
|
||||
val mixedList: MutableList<Identifiable> = mutableListOf(albumHeader)
|
||||
mixedList.addAll(entryList)
|
||||
Timber.e("SUBMITTING MIXED LIST")
|
||||
viewAdapter.submitList(mixedList)
|
||||
} else {
|
||||
Timber.e("SUBMITTING ENTRY LIST")
|
||||
viewAdapter.submitList(entryList)
|
||||
}
|
||||
|
||||
val playAll = requireArguments().getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false)
|
||||
val playAll = arguments?.getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false)?:false
|
||||
|
||||
if (playAll && songCount > 0) {
|
||||
playAll(
|
||||
requireArguments().getBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE, false),
|
||||
arguments?.getBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE, false)?:false,
|
||||
false
|
||||
)
|
||||
}
|
||||
@ -722,9 +673,7 @@ class TrackCollectionFragment :
|
||||
val refresh = args.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH, true)
|
||||
|
||||
listModel.viewModelScope.launch(handler) {
|
||||
refreshListView!!.isRefreshing = true
|
||||
|
||||
listModel.getMusicFolders(refresh)
|
||||
refreshListView?.isRefreshing = true
|
||||
|
||||
if (playlistId != null) {
|
||||
setTitle(playlistName!!)
|
||||
@ -753,14 +702,14 @@ class TrackCollectionFragment :
|
||||
if (isAlbum) {
|
||||
listModel.getAlbum(refresh, id!!, name, parentId)
|
||||
} else {
|
||||
listModel.getArtist(refresh, id!!, name)
|
||||
throw IllegalAccessException("Use AlbumFragment instead!")
|
||||
}
|
||||
} else {
|
||||
listModel.getMusicDirectory(refresh, id!!, name, parentId)
|
||||
}
|
||||
}
|
||||
|
||||
refreshListView!!.isRefreshing = false
|
||||
refreshListView?.isRefreshing = false
|
||||
}
|
||||
return listModel.currentList
|
||||
}
|
||||
@ -774,6 +723,24 @@ class TrackCollectionFragment :
|
||||
}
|
||||
|
||||
override fun onItemClick(item: MusicDirectory.Entry) {
|
||||
// nothing
|
||||
when {
|
||||
item.isDirectory -> {
|
||||
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)
|
||||
Navigation.findNavController(requireView()).navigate(
|
||||
R.id.trackCollectionFragment,
|
||||
bundle
|
||||
)
|
||||
}
|
||||
item.isVideo -> {
|
||||
VideoPlayer.playVideo(requireContext(), item)
|
||||
}
|
||||
else -> {
|
||||
enableButtons()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,11 @@
|
||||
package org.moire.ultrasonic.fragment
|
||||
package org.moire.ultrasonic.model
|
||||
|
||||
import android.app.Application
|
||||
import android.os.Bundle
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
import org.moire.ultrasonic.service.MusicService
|
||||
@ -13,7 +14,9 @@ import org.moire.ultrasonic.util.Settings
|
||||
|
||||
class AlbumListModel(application: Application) : GenericListModel(application) {
|
||||
|
||||
val albumList: MutableLiveData<List<MusicDirectory.Entry>> = MutableLiveData(listOf())
|
||||
|
||||
|
||||
val list: MutableLiveData<List<MusicDirectory.Entry>> = MutableLiveData(listOf())
|
||||
var lastType: String? = null
|
||||
private var loadedUntil: Int = 0
|
||||
|
||||
@ -26,11 +29,37 @@ class AlbumListModel(application: Application) : GenericListModel(application) {
|
||||
// This way, we keep the scroll position
|
||||
val albumListType = args.getString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE)!!
|
||||
|
||||
if (refresh || albumList.value!!.isEmpty() || albumListType != lastType) {
|
||||
if (refresh || list.value!!.isEmpty() || albumListType != lastType) {
|
||||
lastType = albumListType
|
||||
backgroundLoadFromServer(refresh, swipe, args)
|
||||
}
|
||||
return albumList
|
||||
return list
|
||||
}
|
||||
|
||||
fun getAlbumsOfArtist(musicService: MusicService, refresh: Boolean, id: String, name: String?) {
|
||||
|
||||
var root = MusicDirectory()
|
||||
val musicDirectory = musicService.getArtist(id, name, refresh)
|
||||
|
||||
if (Settings.shouldShowAllSongsByArtist &&
|
||||
musicDirectory.findChild(allSongsId) == null &&
|
||||
hasOnlyFolders(musicDirectory)
|
||||
) {
|
||||
val allSongs = MusicDirectory.Entry(allSongsId)
|
||||
|
||||
allSongs.isDirectory = true
|
||||
allSongs.artist = name
|
||||
allSongs.parent = id
|
||||
allSongs.title = String.format(
|
||||
context.resources.getString(R.string.select_album_all_songs), name
|
||||
)
|
||||
|
||||
root.addFirst(allSongs)
|
||||
root.addAll(musicDirectory.getChildren())
|
||||
} else {
|
||||
root = musicDirectory
|
||||
}
|
||||
list.postValue(root.getChildren())
|
||||
}
|
||||
|
||||
override fun load(
|
||||
@ -58,6 +87,15 @@ class AlbumListModel(application: Application) : GenericListModel(application) {
|
||||
// If appending the existing list, set the offset from where to load
|
||||
if (append) offset += (size + loadedUntil)
|
||||
|
||||
if (albumListType == Constants.ALBUMS_OF_ARTIST) {
|
||||
return getAlbumsOfArtist(
|
||||
musicService,
|
||||
refresh,
|
||||
args.getString(Constants.INTENT_EXTRA_NAME_ID, ""),
|
||||
args.getString(Constants.INTENT_EXTRA_NAME_NAME, "")
|
||||
)
|
||||
}
|
||||
|
||||
if (useId3Tags) {
|
||||
musicDirectory = musicService.getAlbumList2(
|
||||
albumListType, size,
|
||||
@ -72,13 +110,13 @@ class AlbumListModel(application: Application) : GenericListModel(application) {
|
||||
|
||||
currentListIsSortable = isCollectionSortable(albumListType)
|
||||
|
||||
if (append && albumList.value != null) {
|
||||
if (append && list.value != null) {
|
||||
val list = ArrayList<MusicDirectory.Entry>()
|
||||
list.addAll(albumList.value!!)
|
||||
list.addAll(this.list.value!!)
|
||||
list.addAll(musicDirectory.getAllChild())
|
||||
albumList.postValue(list)
|
||||
this.list.postValue(list)
|
||||
} else {
|
||||
albumList.postValue(musicDirectory.getAllChild())
|
||||
list.postValue(musicDirectory.getAllChild())
|
||||
}
|
||||
|
||||
loadedUntil = offset
|
||||
@ -100,4 +138,5 @@ class AlbumListModel(application: Application) : GenericListModel(application) {
|
||||
albumListType != "highest" && albumListType != "recent" &&
|
||||
albumListType != "frequent"
|
||||
}
|
||||
|
||||
}
|
@ -16,7 +16,7 @@
|
||||
|
||||
Copyright 2020 (C) Jozsef Varga
|
||||
*/
|
||||
package org.moire.ultrasonic.fragment
|
||||
package org.moire.ultrasonic.model
|
||||
|
||||
import android.app.Application
|
||||
import android.os.Bundle
|
@ -1,4 +1,4 @@
|
||||
package org.moire.ultrasonic.fragment
|
||||
package org.moire.ultrasonic.model
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
@ -17,6 +17,7 @@ import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||
import org.moire.ultrasonic.data.ServerSetting
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
import org.moire.ultrasonic.domain.MusicFolder
|
||||
import org.moire.ultrasonic.service.MusicService
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory
|
||||
@ -45,8 +46,6 @@ open class GenericListModel(application: Application) :
|
||||
return true
|
||||
}
|
||||
|
||||
internal val musicFolders: MutableLiveData<List<MusicFolder>> = MutableLiveData(listOf())
|
||||
|
||||
/**
|
||||
* Helper function to check online status
|
||||
*/
|
||||
@ -110,16 +109,20 @@ open class GenericListModel(application: Application) :
|
||||
) {
|
||||
// Update the list of available folders if enabled
|
||||
if (showSelectFolderHeader(args) && !isOffline && !useId3Tags) {
|
||||
musicFolders.postValue(
|
||||
musicService.getMusicFolders(refresh)
|
||||
)
|
||||
//FIXME
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves the available Music Folders in a LiveData
|
||||
* Some shared helper functions
|
||||
*/
|
||||
fun getMusicFolders(): LiveData<List<MusicFolder>> {
|
||||
return musicFolders
|
||||
}
|
||||
|
||||
// Returns true if the directory contains only folders
|
||||
internal fun hasOnlyFolders(musicDirectory: MusicDirectory) =
|
||||
musicDirectory.getChildren(includeDirs = true, includeFiles = false).size ==
|
||||
musicDirectory.getChildren(includeDirs = true, includeFiles = true).size
|
||||
|
||||
internal val allSongsId = "-1"
|
||||
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
package org.moire.ultrasonic.model
|
||||
|
||||
import android.app.Application
|
||||
import android.os.Bundle
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.moire.ultrasonic.domain.Artist
|
||||
import org.moire.ultrasonic.domain.Identifiable
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
import org.moire.ultrasonic.domain.SearchCriteria
|
||||
import org.moire.ultrasonic.domain.SearchResult
|
||||
import org.moire.ultrasonic.fragment.SearchFragment
|
||||
import org.moire.ultrasonic.service.MusicService
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory
|
||||
import org.moire.ultrasonic.util.BackgroundTask
|
||||
import org.moire.ultrasonic.util.Constants
|
||||
import org.moire.ultrasonic.util.FragmentBackgroundTask
|
||||
import org.moire.ultrasonic.util.MergeAdapter
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
import org.moire.ultrasonic.view.ArtistAdapter
|
||||
import org.moire.ultrasonic.view.EntryAdapter
|
||||
import java.util.ArrayList
|
||||
|
||||
class SearchListModel(application: Application) : GenericListModel(application) {
|
||||
|
||||
var searchResult: MutableLiveData<SearchResult?> = MutableLiveData(null)
|
||||
|
||||
override fun load(
|
||||
isOffline: Boolean,
|
||||
useId3Tags: Boolean,
|
||||
musicService: MusicService,
|
||||
refresh: Boolean,
|
||||
args: Bundle
|
||||
) {
|
||||
super.load(isOffline, useId3Tags, musicService, refresh, args)
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
suspend fun search(query: String) {
|
||||
val maxArtists = Settings.maxArtists
|
||||
val maxAlbums = Settings.maxAlbums
|
||||
val maxSongs = Settings.maxSongs
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
val criteria = SearchCriteria(query, maxArtists, maxAlbums, maxSongs)
|
||||
val service = MusicServiceFactory.getMusicService()
|
||||
val result = service.search(criteria)
|
||||
|
||||
if (result != null) searchResult.postValue(result)
|
||||
}
|
||||
}
|
||||
|
||||
fun trimResultLength(result: SearchResult): SearchResult {
|
||||
return SearchResult(
|
||||
artists = result.artists.take(SearchFragment.DEFAULT_ARTISTS),
|
||||
albums = result.albums.take(SearchFragment.DEFAULT_ALBUMS),
|
||||
songs = result.songs.take(SearchFragment.DEFAULT_SONGS)
|
||||
)
|
||||
}
|
||||
|
||||
// fun mergeList(result: SearchResult): List<Identifiable> {
|
||||
// val list = mutableListOf<Identifiable>()
|
||||
// list.add(result.artists)
|
||||
// list.add(result.albums)
|
||||
// list.add(result.songs)
|
||||
// return list
|
||||
// }
|
||||
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package org.moire.ultrasonic.fragment
|
||||
package org.moire.ultrasonic.model
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
@ -5,7 +5,7 @@
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.fragment
|
||||
package org.moire.ultrasonic.model
|
||||
|
||||
import android.app.Application
|
||||
import android.os.Bundle
|
||||
@ -22,25 +22,13 @@ import org.moire.ultrasonic.util.Util
|
||||
|
||||
/*
|
||||
* Model for retrieving different collections of tracks from the API
|
||||
* TODO: Refactor this model to extend the GenericListModel
|
||||
*/
|
||||
class TrackCollectionModel(application: Application) : GenericListModel(application) {
|
||||
|
||||
private val allSongsId = "-1"
|
||||
|
||||
val currentDirectory: MutableLiveData<MusicDirectory> = MutableLiveData()
|
||||
val currentList: MutableLiveData<List<MusicDirectory.Entry>> = MutableLiveData()
|
||||
val songsForGenre: MutableLiveData<MusicDirectory> = MutableLiveData()
|
||||
|
||||
suspend fun getMusicFolders(refresh: Boolean) {
|
||||
withContext(Dispatchers.IO) {
|
||||
if (!isOffline()) {
|
||||
val musicService = MusicServiceFactory.getMusicService()
|
||||
musicFolders.postValue(musicService.getMusicFolders(refresh))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getMusicDirectory(
|
||||
refresh: Boolean,
|
||||
id: String,
|
||||
@ -94,9 +82,6 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateList(root: MusicDirectory) {
|
||||
currentList.postValue(root.getChildren())
|
||||
}
|
||||
|
||||
// Given a Music directory "songs" it recursively adds all children to "songs"
|
||||
private fun getSongsRecursively(
|
||||
@ -122,42 +107,6 @@ class TrackCollectionModel(application: Application) : GenericListModel(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) {
|
||||
val service = MusicServiceFactory.getMusicService()
|
||||
|
||||
var root = MusicDirectory()
|
||||
|
||||
val musicDirectory = service.getArtist(id, name, refresh)
|
||||
|
||||
if (Settings.shouldShowAllSongsByArtist &&
|
||||
musicDirectory.findChild(allSongsId) == null &&
|
||||
hasOnlyFolders(musicDirectory)
|
||||
) {
|
||||
val allSongs = MusicDirectory.Entry(allSongsId)
|
||||
|
||||
allSongs.isDirectory = true
|
||||
allSongs.artist = name
|
||||
allSongs.parent = id
|
||||
allSongs.title = String.format(
|
||||
context.resources.getString(R.string.select_album_all_songs), name
|
||||
)
|
||||
|
||||
root.addFirst(allSongs)
|
||||
root.addAll(musicDirectory.getChildren())
|
||||
} else {
|
||||
root = musicDirectory
|
||||
}
|
||||
currentDirectory.postValue(root)
|
||||
updateList(root)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getAlbum(refresh: Boolean, id: String, name: String?, parentId: String?) {
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
@ -296,18 +245,17 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
suspend fun getBookmarks() {
|
||||
withContext(Dispatchers.IO) {
|
||||
val service = MusicServiceFactory.getMusicService()
|
||||
val musicDirectory = Util.getSongsFromBookmarks(service.getBookmarks())
|
||||
currentDirectory.postValue(musicDirectory)
|
||||
updateList(musicDirectory)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateList(root: MusicDirectory) {
|
||||
currentList.postValue(root.getChildren())
|
||||
}
|
||||
|
||||
}
|
@ -399,7 +399,7 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
override fun getBookmarks(): List<Bookmark?>? = musicService.getBookmarks()
|
||||
override fun getBookmarks(): List<Bookmark> = musicService.getBookmarks()
|
||||
|
||||
@Throws(Exception::class)
|
||||
override fun deleteBookmark(id: String) {
|
||||
|
@ -154,7 +154,7 @@ interface MusicService {
|
||||
fun addChatMessage(message: String)
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun getBookmarks(): List<Bookmark?>?
|
||||
fun getBookmarks(): List<Bookmark>
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun deleteBookmark(id: String)
|
||||
|
@ -411,7 +411,7 @@ class OfflineMusicService : MusicService, KoinComponent {
|
||||
}
|
||||
|
||||
@Throws(OfflineException::class)
|
||||
override fun getBookmarks(): List<Bookmark?>? {
|
||||
override fun getBookmarks(): List<Bookmark> {
|
||||
throw OfflineException("getBookmarks isn't available in offline mode")
|
||||
}
|
||||
|
||||
|
@ -30,6 +30,11 @@ class RxBus {
|
||||
val themeChangedEventObservable: Observable<Unit> =
|
||||
themeChangedEventPublisher.observeOn(AndroidSchedulers.mainThread())
|
||||
|
||||
val musicFolderChangedEventPublisher: PublishSubject<String> =
|
||||
PublishSubject.create()
|
||||
val musicFolderChangedEventObservable: Observable<String> =
|
||||
musicFolderChangedEventPublisher.observeOn(AndroidSchedulers.mainThread())
|
||||
|
||||
val playerStatePublisher: PublishSubject<StateWithTrack> =
|
||||
PublishSubject.create()
|
||||
val playerStateObservable: Observable<StateWithTrack> =
|
||||
@ -73,6 +78,7 @@ class RxBus {
|
||||
val skipToQueueItemCommandObservable: Observable<Long> =
|
||||
skipToQueueItemCommandPublisher.observeOn(AndroidSchedulers.mainThread())
|
||||
|
||||
|
||||
fun releaseMediaSessionToken() {
|
||||
mediaSessionTokenPublisher = PublishSubject.create()
|
||||
}
|
||||
|
@ -121,5 +121,6 @@ object Constants {
|
||||
const val ALBUM_ART_FILE = "folder.jpeg"
|
||||
const val STARRED = "starred"
|
||||
const val ALPHABETICAL_BY_NAME = "alphabeticalByName"
|
||||
const val ALBUMS_OF_ARTIST = "albumsOfArtist"
|
||||
const val RESULT_CLOSE_ALL = 1337
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.ItemTouchHelper.DOWN
|
||||
import androidx.recyclerview.widget.ItemTouchHelper.UP
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.moire.ultrasonic.adapters.MultiTypeDiffAdapter
|
||||
import org.moire.ultrasonic.adapters.BaseAdapter
|
||||
import timber.log.Timber
|
||||
|
||||
class DragSortCallback : ItemTouchHelper.SimpleCallback(UP or DOWN, 0) {
|
||||
@ -21,7 +21,7 @@ class DragSortCallback : ItemTouchHelper.SimpleCallback(UP or DOWN, 0) {
|
||||
Timber.w("MOVED %s %s", to, from)
|
||||
|
||||
// Move it in the data set
|
||||
(recyclerView.adapter as MultiTypeDiffAdapter<*>).moveItem(from, to)
|
||||
(recyclerView.adapter as BaseAdapter<*>).moveItem(from, to)
|
||||
|
||||
return true
|
||||
}
|
||||
|
@ -525,11 +525,10 @@ object Util {
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getSongsFromBookmarks(bookmarks: Iterable<Bookmark?>): MusicDirectory {
|
||||
fun getSongsFromBookmarks(bookmarks: Iterable<Bookmark>): MusicDirectory {
|
||||
val musicDirectory = MusicDirectory()
|
||||
var song: MusicDirectory.Entry
|
||||
for (bookmark in bookmarks) {
|
||||
if (bookmark == null) continue
|
||||
song = bookmark.entry
|
||||
song.bookmarkPosition = bookmark.position
|
||||
musicDirectory.addChild(song)
|
||||
|
@ -13,7 +13,8 @@
|
||||
android:scaleType="fitCenter"
|
||||
android:layout_weight="1"
|
||||
android:src="?attr/select_all"
|
||||
android:visibility="gone" />
|
||||
android:visibility="gone"
|
||||
android:contentDescription="@string/common.select_all" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/select_album_play_now"
|
||||
@ -22,7 +23,8 @@
|
||||
android:scaleType="fitCenter"
|
||||
android:layout_weight="1"
|
||||
android:src="?attr/media_play"
|
||||
android:visibility="gone" />
|
||||
android:visibility="gone"
|
||||
android:contentDescription="@string/common.play_now" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/select_album_play_next"
|
||||
@ -31,7 +33,8 @@
|
||||
android:scaleType="fitCenter"
|
||||
android:layout_weight="1"
|
||||
android:src="?attr/media_play_next"
|
||||
android:visibility="gone" />
|
||||
android:visibility="gone"
|
||||
android:contentDescription="@string/common.play_next" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/select_album_play_last"
|
||||
@ -40,7 +43,8 @@
|
||||
android:scaleType="fitCenter"
|
||||
android:layout_weight="1"
|
||||
android:src="?attr/add_to_queue"
|
||||
android:visibility="gone" />
|
||||
android:visibility="gone"
|
||||
android:contentDescription="@string/common.play_last" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/select_album_pin"
|
||||
@ -49,7 +53,8 @@
|
||||
android:scaleType="fitCenter"
|
||||
android:layout_weight="1"
|
||||
android:src="?attr/pin"
|
||||
android:visibility="gone" />
|
||||
android:visibility="gone"
|
||||
android:contentDescription="@string/common.pin" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/select_album_unpin"
|
||||
@ -58,7 +63,8 @@
|
||||
android:scaleType="fitCenter"
|
||||
android:layout_weight="1"
|
||||
android:src="?attr/unpin"
|
||||
android:visibility="gone" />
|
||||
android:visibility="gone"
|
||||
android:contentDescription="@string/common.unpin" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/select_album_download"
|
||||
@ -67,7 +73,8 @@
|
||||
android:scaleType="fitCenter"
|
||||
android:layout_weight="1"
|
||||
android:src="?attr/download"
|
||||
android:visibility="gone" />
|
||||
android:visibility="gone"
|
||||
android:contentDescription="@string/common.download" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/select_album_delete"
|
||||
@ -76,7 +83,8 @@
|
||||
android:scaleType="fitCenter"
|
||||
android:layout_weight="1"
|
||||
android:src="?attr/stop"
|
||||
android:visibility="gone" />
|
||||
android:visibility="gone"
|
||||
android:contentDescription="@string/common.delete" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/select_album_more"
|
||||
@ -85,6 +93,7 @@
|
||||
android:scaleType="fitCenter"
|
||||
android:layout_weight="1"
|
||||
android:src="?attr/forward"
|
||||
android:visibility="gone" />
|
||||
android:visibility="gone"
|
||||
android:contentDescription="@string/search.more" />
|
||||
|
||||
</LinearLayout>
|
@ -10,7 +10,7 @@
|
||||
a:layout_height="0dip"
|
||||
a:layout_weight="1.0">
|
||||
|
||||
<ListView
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
a:id="@+id/search_list"
|
||||
a:layout_width="fill_parent"
|
||||
a:layout_height="0dip"
|
||||
|
@ -1,36 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
|
||||
a:layout_width="fill_parent"
|
||||
a:layout_height="fill_parent"
|
||||
a:orientation="vertical" >
|
||||
|
||||
<View
|
||||
a:layout_width="fill_parent"
|
||||
a:layout_height="1dp"
|
||||
a:background="@color/dividerColor" />
|
||||
|
||||
<TextView
|
||||
a:id="@+id/select_album_empty"
|
||||
a:layout_width="fill_parent"
|
||||
a:layout_height="wrap_content"
|
||||
a:padding="10dip"
|
||||
a:text="@string/select_album.empty"
|
||||
a:visibility="gone" />
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/select_album_entries_refresh"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="0dip"
|
||||
android:layout_weight="1.0">
|
||||
|
||||
<ListView
|
||||
android:id="@+id/select_album_entries_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
<include layout="@layout/album_buttons" />
|
||||
|
||||
</LinearLayout>
|
@ -26,14 +26,14 @@
|
||||
android:label="@string/music_library.label" >
|
||||
<action
|
||||
android:id="@+id/selectArtistToSelectAlbum"
|
||||
app:destination="@id/trackCollectionFragment" />
|
||||
app:destination="@id/albumListFragment" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/artistListFragment"
|
||||
android:name="org.moire.ultrasonic.fragment.ArtistListFragment" >
|
||||
<action
|
||||
android:id="@+id/selectArtistToSelectAlbum"
|
||||
app:destination="@id/trackCollectionFragment" />
|
||||
app:destination="@id/albumListFragment" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/trackCollectionFragment"
|
||||
|
@ -50,6 +50,7 @@
|
||||
<string name="common.play_shuffled">Play Shuffled</string>
|
||||
<string name="common.public">Public</string>
|
||||
<string name="common.save">Save</string>
|
||||
<string name="common.select_all">Select all</string>
|
||||
<string name="common.title">Title</string>
|
||||
<string name="common.unpin">Unpin</string>
|
||||
<string name="common.various_artists">Various Artists</string>
|
||||
|
Loading…
x
Reference in New Issue
Block a user