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.
|
* The result of a search. Contains matching artists, albums and songs.
|
||||||
*/
|
*/
|
||||||
data class SearchResult(
|
data class SearchResult(
|
||||||
val artists: List<Artist>,
|
val artists: List<Artist> = listOf(),
|
||||||
val albums: List<Entry>,
|
val albums: List<Entry> = listOf(),
|
||||||
val songs: List<Entry>
|
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(
|
class AlbumHeader(
|
||||||
var entries: List<MusicDirectory.Entry>,
|
var entries: List<MusicDirectory.Entry>,
|
||||||
var name: String,
|
var name: String?
|
||||||
songCount: Int
|
|
||||||
) : Identifiable {
|
) : Identifiable {
|
||||||
var isAllVideo: Boolean
|
var isAllVideo: Boolean
|
||||||
private set
|
private set
|
||||||
|
|
|
@ -20,7 +20,7 @@ package org.moire.ultrasonic.view;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.widget.TextView;
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
import org.moire.ultrasonic.R;
|
import org.moire.ultrasonic.R;
|
||||||
import org.moire.ultrasonic.domain.Playlist;
|
import org.moire.ultrasonic.domain.Playlist;
|
||||||
|
@ -30,9 +30,9 @@ import org.moire.ultrasonic.domain.Playlist;
|
||||||
*
|
*
|
||||||
* @author Sindre Mehus
|
* @author Sindre Mehus
|
||||||
*/
|
*/
|
||||||
public class PlaylistView extends UpdateView
|
public class PlaylistView extends LinearLayout
|
||||||
{
|
{
|
||||||
private Context context;
|
private final Context context;
|
||||||
private PlaylistAdapter.ViewHolder viewHolder;
|
private PlaylistAdapter.ViewHolder viewHolder;
|
||||||
|
|
||||||
public PlaylistView(Context context)
|
public PlaylistView(Context context)
|
||||||
|
@ -45,7 +45,7 @@ public class PlaylistView extends UpdateView
|
||||||
{
|
{
|
||||||
LayoutInflater.from(context).inflate(R.layout.playlist_list_item, this, true);
|
LayoutInflater.from(context).inflate(R.layout.playlist_list_item, this, true);
|
||||||
viewHolder = new PlaylistAdapter.ViewHolder();
|
viewHolder = new PlaylistAdapter.ViewHolder();
|
||||||
viewHolder.name = (TextView) findViewById(R.id.playlist_name);
|
viewHolder.name = findViewById(R.id.playlist_name);
|
||||||
setTag(viewHolder);
|
setTag(viewHolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,6 +58,5 @@ public class PlaylistView extends UpdateView
|
||||||
public void setPlaylist(Playlist playlist)
|
public void setPlaylist(Playlist playlist)
|
||||||
{
|
{
|
||||||
viewHolder.name.setText(playlist.getName());
|
viewHolder.name.setText(playlist.getName());
|
||||||
update();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -20,7 +20,7 @@ package org.moire.ultrasonic.view;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.widget.TextView;
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
import org.moire.ultrasonic.R;
|
import org.moire.ultrasonic.R;
|
||||||
import org.moire.ultrasonic.domain.Playlist;
|
import org.moire.ultrasonic.domain.Playlist;
|
||||||
|
@ -30,12 +30,12 @@ import org.moire.ultrasonic.domain.Playlist;
|
||||||
*
|
*
|
||||||
* @author Sindre Mehus
|
* @author Sindre Mehus
|
||||||
*/
|
*/
|
||||||
public class PodcatsChannelItemView extends UpdateView
|
public class PodcastChannelItemView extends LinearLayout
|
||||||
{
|
{
|
||||||
private Context context;
|
private final Context context;
|
||||||
private PlaylistAdapter.ViewHolder viewHolder;
|
private PlaylistAdapter.ViewHolder viewHolder;
|
||||||
|
|
||||||
public PodcatsChannelItemView(Context context)
|
public PodcastChannelItemView(Context context)
|
||||||
{
|
{
|
||||||
super(context);
|
super(context);
|
||||||
this.context = context;
|
this.context = context;
|
||||||
|
@ -45,7 +45,7 @@ public class PodcatsChannelItemView extends UpdateView
|
||||||
{
|
{
|
||||||
LayoutInflater.from(context).inflate(R.layout.playlist_list_item, this, true);
|
LayoutInflater.from(context).inflate(R.layout.playlist_list_item, this, true);
|
||||||
viewHolder = new PlaylistAdapter.ViewHolder();
|
viewHolder = new PlaylistAdapter.ViewHolder();
|
||||||
viewHolder.name = (TextView) findViewById(R.id.playlist_name);
|
viewHolder.name = findViewById(R.id.playlist_name);
|
||||||
setTag(viewHolder);
|
setTag(viewHolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,6 +58,5 @@ public class PodcatsChannelItemView extends UpdateView
|
||||||
public void setPlaylist(Playlist playlist)
|
public void setPlaylist(Playlist playlist)
|
||||||
{
|
{
|
||||||
viewHolder.name.setText(playlist.getName());
|
viewHolder.name.setText(playlist.getName());
|
||||||
update();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -20,7 +20,7 @@ package org.moire.ultrasonic.view;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.widget.TextView;
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
import org.moire.ultrasonic.R;
|
import org.moire.ultrasonic.R;
|
||||||
import org.moire.ultrasonic.domain.Share;
|
import org.moire.ultrasonic.domain.Share;
|
||||||
|
@ -30,9 +30,9 @@ import org.moire.ultrasonic.domain.Share;
|
||||||
*
|
*
|
||||||
* @author Joshua Bahnsen
|
* @author Joshua Bahnsen
|
||||||
*/
|
*/
|
||||||
public class ShareView extends UpdateView
|
public class ShareView extends LinearLayout
|
||||||
{
|
{
|
||||||
private Context context;
|
private final Context context;
|
||||||
private ShareAdapter.ViewHolder viewHolder;
|
private ShareAdapter.ViewHolder viewHolder;
|
||||||
|
|
||||||
public ShareView(Context context)
|
public ShareView(Context context)
|
||||||
|
@ -45,8 +45,8 @@ public class ShareView extends UpdateView
|
||||||
{
|
{
|
||||||
LayoutInflater.from(context).inflate(R.layout.share_list_item, this, true);
|
LayoutInflater.from(context).inflate(R.layout.share_list_item, this, true);
|
||||||
viewHolder = new ShareAdapter.ViewHolder();
|
viewHolder = new ShareAdapter.ViewHolder();
|
||||||
viewHolder.url = (TextView) findViewById(R.id.share_url);
|
viewHolder.url = findViewById(R.id.share_url);
|
||||||
viewHolder.description = (TextView) findViewById(R.id.share_description);
|
viewHolder.description = findViewById(R.id.share_description);
|
||||||
setTag(viewHolder);
|
setTag(viewHolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,6 +60,5 @@ public class ShareView extends UpdateView
|
||||||
{
|
{
|
||||||
viewHolder.url.setText(share.getName());
|
viewHolder.url.setText(share.getName());
|
||||||
viewHolder.description.setText(share.getDescription());
|
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.data.ServerSettingDao
|
||||||
import org.moire.ultrasonic.domain.PlayerState
|
import org.moire.ultrasonic.domain.PlayerState
|
||||||
import org.moire.ultrasonic.fragment.OnBackPressedHandler
|
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.provider.SearchSuggestionProvider
|
||||||
import org.moire.ultrasonic.service.DownloadFile
|
import org.moire.ultrasonic.service.DownloadFile
|
||||||
import org.moire.ultrasonic.service.MediaPlayerController
|
import org.moire.ultrasonic.service.MediaPlayerController
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* AlbumRowAdapter.kt
|
* AlbumRowBinder.kt
|
||||||
* Copyright (C) 2009-2021 Ultrasonic developers
|
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||||
*
|
*
|
||||||
* Distributed under terms of the GNU GPLv3 license.
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
@ -9,13 +9,16 @@ package org.moire.ultrasonic.adapters
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.view.LayoutInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
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.R
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory
|
import org.moire.ultrasonic.domain.MusicDirectory
|
||||||
import org.moire.ultrasonic.imageloader.ImageLoader
|
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
|
* Creates a Row in a RecyclerView which contains the details of an Album
|
||||||
*/
|
*/
|
||||||
class AlbumRowAdapter(
|
class AlbumRowBinder(
|
||||||
itemList: List<MusicDirectory.Entry>,
|
val onItemClick: (MusicDirectory.Entry) -> Unit,
|
||||||
onItemClick: (MusicDirectory.Entry) -> Unit,
|
val onContextMenuClick: (MenuItem, MusicDirectory.Entry) -> Boolean,
|
||||||
onContextMenuClick: (MenuItem, MusicDirectory.Entry) -> Boolean,
|
|
||||||
private val imageLoader: ImageLoader,
|
private val imageLoader: ImageLoader,
|
||||||
onMusicFolderUpdate: (String?) -> Unit,
|
|
||||||
context: Context,
|
context: Context,
|
||||||
) : GenericRowAdapter<MusicDirectory.Entry>(
|
) : ItemViewBinder<MusicDirectory.Entry, AlbumRowBinder.ViewHolder>(), KoinComponent {
|
||||||
onItemClick,
|
|
||||||
onContextMenuClick,
|
|
||||||
onMusicFolderUpdate
|
|
||||||
) {
|
|
||||||
|
|
||||||
init {
|
|
||||||
super.submitList(itemList)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val starDrawable: Drawable =
|
private val starDrawable: Drawable =
|
||||||
Util.getDrawableFromAttribute(context, R.attr.star_full)
|
Util.getDrawableFromAttribute(context, R.attr.star_full)
|
||||||
|
@ -50,34 +43,32 @@ class AlbumRowAdapter(
|
||||||
Util.getDrawableFromAttribute(context, R.attr.star_hollow)
|
Util.getDrawableFromAttribute(context, R.attr.star_hollow)
|
||||||
|
|
||||||
// Set our layout files
|
// Set our layout files
|
||||||
override val layout = R.layout.album_list_item
|
val layout = R.layout.album_list_item
|
||||||
override val contextMenuLayout = R.menu.artist_context_menu
|
val contextMenuLayout = R.menu.artist_context_menu
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: ViewHolder, item: MusicDirectory.Entry) {
|
||||||
if (holder is ViewHolder) {
|
holder.album.text = item.title
|
||||||
val listPosition = if (selectFolderHeader != null) position - 1 else position
|
holder.artist.text = item.artist
|
||||||
val entry = currentList[listPosition]
|
holder.details.setOnClickListener { onItemClick(item) }
|
||||||
holder.album.text = entry.title
|
holder.details.setOnLongClickListener {
|
||||||
holder.artist.text = entry.artist
|
val popup = Helper.createPopupMenu(holder.itemView)
|
||||||
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) }
|
|
||||||
|
|
||||||
imageLoader.loadImage(
|
popup.setOnMenuItemClickListener { menuItem ->
|
||||||
holder.coverArt, entry,
|
onContextMenuClick(menuItem, item)
|
||||||
false, 0, R.drawable.unknown_album
|
}
|
||||||
)
|
|
||||||
|
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
|
* Holds the view properties of an Item row
|
||||||
|
@ -93,12 +84,6 @@ class AlbumRowAdapter(
|
||||||
var coverArtId: String? = null
|
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
|
* Handles the star / unstar action for an album
|
||||||
|
@ -128,4 +113,9 @@ class AlbumRowAdapter(
|
||||||
}
|
}
|
||||||
}.start()
|
}.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 java.util.TreeSet
|
||||||
import org.moire.ultrasonic.domain.Identifiable
|
import org.moire.ultrasonic.domain.Identifiable
|
||||||
|
|
||||||
class MultiTypeDiffAdapter<T : Identifiable> : MultiTypeAdapter() {
|
class BaseAdapter<T : Identifiable> : MultiTypeAdapter() {
|
||||||
|
|
||||||
internal var selectedSet: TreeSet<Long> = TreeSet()
|
internal var selectedSet: TreeSet<Long> = TreeSet()
|
||||||
internal var selectionRevision: MutableLiveData<Int> = MutableLiveData(0)
|
internal var selectionRevision: MutableLiveData<Int> = MutableLiveData(0)
|
||||||
|
@ -43,7 +43,7 @@ class MultiTypeDiffAdapter<T : Identifiable> : MultiTypeAdapter() {
|
||||||
|
|
||||||
private val mListener =
|
private val mListener =
|
||||||
ListListener<T> { previousList, currentList ->
|
ListListener<T> { previousList, currentList ->
|
||||||
this@MultiTypeDiffAdapter.onCurrentListChanged(
|
this@BaseAdapter.onCurrentListChanged(
|
||||||
previousList,
|
previousList,
|
||||||
currentList
|
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.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.drakeet.multitype.ItemViewBinder
|
import com.drakeet.multitype.ItemViewBinder
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
|
@ -57,7 +58,12 @@ class HeaderViewBinder(
|
||||||
Util.getAlbumImageSize(context)
|
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
|
// Don't show a header if all entries are videos
|
||||||
if (item.isAllVideo) {
|
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.R
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||||
import org.moire.ultrasonic.data.ServerSetting
|
import org.moire.ultrasonic.data.ServerSetting
|
||||||
|
import org.moire.ultrasonic.model.ServerSettingsModel
|
||||||
import org.moire.ultrasonic.util.ServerColor
|
import org.moire.ultrasonic.util.ServerColor
|
||||||
import org.moire.ultrasonic.util.Util
|
import org.moire.ultrasonic.util.Util
|
||||||
|
|
||||||
|
|
|
@ -22,16 +22,6 @@ class TrackViewBinder(
|
||||||
private val onClickCallback: ((View, DownloadFile?) -> Unit)? = null
|
private val onClickCallback: ((View, DownloadFile?) -> Unit)? = null
|
||||||
) : ItemViewBinder<Identifiable, TrackViewHolder>(), KoinComponent {
|
) : 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
|
// Set our layout files
|
||||||
val layout = R.layout.song_list_item
|
val layout = R.layout.song_list_item
|
||||||
val contextMenuLayout = R.menu.artist_context_menu
|
val contextMenuLayout = R.menu.artist_context_menu
|
||||||
|
@ -44,9 +34,8 @@ class TrackViewBinder(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: TrackViewHolder, item: Identifiable) {
|
override fun onBindViewHolder(holder: TrackViewHolder, item: Identifiable) {
|
||||||
|
|
||||||
val downloadFile: DownloadFile?
|
val downloadFile: DownloadFile?
|
||||||
val _adapter = adapter as MultiTypeDiffAdapter<*>
|
val diffAdapter = adapter as BaseAdapter<*>
|
||||||
|
|
||||||
when (item) {
|
when (item) {
|
||||||
is MusicDirectory.Entry -> {
|
is MusicDirectory.Entry -> {
|
||||||
|
@ -66,7 +55,7 @@ class TrackViewBinder(
|
||||||
file = downloadFile,
|
file = downloadFile,
|
||||||
checkable = checkable,
|
checkable = checkable,
|
||||||
draggable = draggable,
|
draggable = draggable,
|
||||||
_adapter.isSelected(item.longId)
|
diffAdapter.isSelected(item.longId)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Notify the adapter of selection changes
|
// Notify the adapter of selection changes
|
||||||
|
@ -74,18 +63,18 @@ class TrackViewBinder(
|
||||||
lifecycleOwner,
|
lifecycleOwner,
|
||||||
{ newValue ->
|
{ newValue ->
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
_adapter.notifySelected(item.longId)
|
diffAdapter.notifySelected(item.longId)
|
||||||
} else {
|
} else {
|
||||||
_adapter.notifyUnselected(item.longId)
|
diffAdapter.notifyUnselected(item.longId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Listen to changes in selection status and update ourselves
|
// Listen to changes in selection status and update ourselves
|
||||||
_adapter.selectionRevision.observe(
|
diffAdapter.selectionRevision.observe(
|
||||||
lifecycleOwner,
|
lifecycleOwner,
|
||||||
{
|
{
|
||||||
val newStatus = _adapter.isSelected(item.longId)
|
val newStatus = diffAdapter.isSelected(item.longId)
|
||||||
|
|
||||||
if (newStatus != holder.check.isChecked) holder.check.isChecked = newStatus
|
if (newStatus != holder.check.isChecked) holder.check.isChecked = newStatus
|
||||||
}
|
}
|
||||||
|
@ -96,7 +85,7 @@ class TrackViewBinder(
|
||||||
lifecycleOwner,
|
lifecycleOwner,
|
||||||
{
|
{
|
||||||
holder.updateStatus(it)
|
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.DownloadStatus
|
||||||
import org.moire.ultrasonic.service.MediaPlayerController
|
import org.moire.ultrasonic.service.MediaPlayerController
|
||||||
import org.moire.ultrasonic.service.MusicServiceFactory
|
import org.moire.ultrasonic.service.MusicServiceFactory
|
||||||
|
import org.moire.ultrasonic.service.RxBus
|
||||||
import org.moire.ultrasonic.util.Settings
|
import org.moire.ultrasonic.util.Settings
|
||||||
import org.moire.ultrasonic.util.Util
|
import org.moire.ultrasonic.util.Util
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to display songs and videos in a `ListView`.
|
* 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 {
|
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 isMaximized = false
|
||||||
private var cachedStatus = DownloadStatus.UNKNOWN
|
private var cachedStatus = DownloadStatus.UNKNOWN
|
||||||
private var statusImage: Drawable? = null
|
private var statusImage: Drawable? = null
|
||||||
private var playing = false
|
private var isPlayingCached = false
|
||||||
|
|
||||||
var observableChecked = MutableLiveData(false)
|
var observableChecked = MutableLiveData(false)
|
||||||
|
|
||||||
|
@ -67,8 +68,6 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable
|
||||||
features.isFeatureEnabled(Feature.FIVE_STAR_RATING)
|
features.isFeatureEnabled(Feature.FIVE_STAR_RATING)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val mediaPlayerController: MediaPlayerController by inject()
|
|
||||||
|
|
||||||
lateinit var imageHelper: ImageHelper
|
lateinit var imageHelper: ImageHelper
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
@ -116,9 +115,44 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable
|
||||||
setupStarButtons(song)
|
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) {
|
private fun setupStarButtons(song: MusicDirectory.Entry) {
|
||||||
if (useFiveStarRating) {
|
if (useFiveStarRating) {
|
||||||
// Hide single star
|
// 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")
|
@Suppress("MagicNumber")
|
||||||
private fun setFiveStars(rating: Int) {
|
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_1_2
|
||||||
import org.moire.ultrasonic.data.MIGRATION_2_3
|
import org.moire.ultrasonic.data.MIGRATION_2_3
|
||||||
import org.moire.ultrasonic.data.MIGRATION_3_4
|
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
|
import org.moire.ultrasonic.util.Settings
|
||||||
|
|
||||||
const val SP_NAME = "Default_SP"
|
const val SP_NAME = "Default_SP"
|
||||||
|
|
|
@ -7,12 +7,14 @@ import androidx.lifecycle.LiveData
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
|
import org.moire.ultrasonic.adapters.AlbumRowBinder
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory
|
import org.moire.ultrasonic.domain.MusicDirectory
|
||||||
|
import org.moire.ultrasonic.model.AlbumListModel
|
||||||
import org.moire.ultrasonic.util.Constants
|
import org.moire.ultrasonic.util.Constants
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays a list of Albums from the media library
|
* Displays a list of Albums from the media library
|
||||||
* TODO: Check refresh is working
|
* FIXME: Add music folder support
|
||||||
*/
|
*/
|
||||||
class AlbumListFragment : EntryListFragment<MusicDirectory.Entry>() {
|
class AlbumListFragment : EntryListFragment<MusicDirectory.Entry>() {
|
||||||
|
|
||||||
|
@ -54,24 +56,6 @@ class AlbumListFragment : EntryListFragment<MusicDirectory.Entry>() {
|
||||||
return listModel.getAlbumList(refresh or append, refreshListView!!, args)
|
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?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
@ -81,13 +65,25 @@ class AlbumListFragment : EntryListFragment<MusicDirectory.Entry>() {
|
||||||
override fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?) {
|
override fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?) {
|
||||||
// Triggered only when new data needs to be appended to the list
|
// 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
|
// 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)
|
appendArgs.putBoolean(Constants.INTENT_EXTRA_NAME_APPEND, true)
|
||||||
getLiveData(appendArgs)
|
getLiveData(appendArgs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
addOnScrollListener(scrollListener)
|
addOnScrollListener(scrollListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
viewAdapter.register(
|
||||||
|
AlbumRowBinder(
|
||||||
|
{ entry -> onItemClick(entry) },
|
||||||
|
{ menuItem, entry -> onContextMenuItemSelected(menuItem, entry) },
|
||||||
|
imageLoaderProvider.getImageLoader(),
|
||||||
|
context = requireContext()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemClick(item: MusicDirectory.Entry) {
|
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)
|
bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, item.parent)
|
||||||
findNavController().navigate(itemClickTarget, bundle)
|
findNavController().navigate(itemClickTarget, bundle)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
package org.moire.ultrasonic.fragment
|
package org.moire.ultrasonic.fragment
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
import org.moire.ultrasonic.R
|
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.domain.ArtistOrIndex
|
||||||
|
import org.moire.ultrasonic.model.ArtistListModel
|
||||||
import org.moire.ultrasonic.util.Constants
|
import org.moire.ultrasonic.util.Constants
|
||||||
|
import org.moire.ultrasonic.util.Settings
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays the list of Artists from the media library
|
* Displays the list of Artists from the media library
|
||||||
|
@ -39,6 +44,7 @@ class ArtistListFragment : EntryListFragment<ArtistOrIndex>() {
|
||||||
*/
|
*/
|
||||||
override val itemClickTarget = R.id.selectArtistToSelectAlbum
|
override val itemClickTarget = R.id.selectArtistToSelectAlbum
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The central function to pass a query to the model and return a LiveData object
|
* The central function to pass a query to the model and return a LiveData object
|
||||||
*/
|
*/
|
||||||
|
@ -47,17 +53,31 @@ class ArtistListFragment : EntryListFragment<ArtistOrIndex>() {
|
||||||
return listModel.getItems(refresh, refreshListView!!)
|
return listModel.getItems(refresh, refreshListView!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
* Provide the Adapter for the RecyclerView with a lazy delegate
|
super.onViewCreated(view, savedInstanceState)
|
||||||
*/
|
|
||||||
// FIXME
|
viewAdapter.register(
|
||||||
// override val viewAdapter: ArtistRowAdapter by lazy {
|
ArtistRowBinder(
|
||||||
// ArtistRowAdapter(
|
{ entry -> onItemClick(entry) },
|
||||||
// liveDataItems.value ?: listOf(),
|
{ menuItem, entry -> onContextMenuItemSelected(menuItem, entry) },
|
||||||
// { entry -> onItemClick(entry) },
|
imageLoaderProvider.getImageLoader()
|
||||||
// { menuItem, entry -> onContextMenuItemSelected(menuItem, entry) },
|
)
|
||||||
// imageLoaderProvider.getImageLoader(),
|
)
|
||||||
// onMusicFolderUpdate
|
}
|
||||||
// )
|
|
||||||
// }
|
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.koin.core.component.inject
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.adapters.TrackViewBinder
|
import org.moire.ultrasonic.adapters.TrackViewBinder
|
||||||
|
import org.moire.ultrasonic.model.GenericListModel
|
||||||
import org.moire.ultrasonic.service.DownloadFile
|
import org.moire.ultrasonic.service.DownloadFile
|
||||||
import org.moire.ultrasonic.service.Downloader
|
import org.moire.ultrasonic.service.Downloader
|
||||||
import org.moire.ultrasonic.util.Util
|
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.api.subsonic.throwOnFailure
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||||
import org.moire.ultrasonic.data.ServerSetting
|
import org.moire.ultrasonic.data.ServerSetting
|
||||||
|
import org.moire.ultrasonic.model.ServerSettingsModel
|
||||||
import org.moire.ultrasonic.service.MusicServiceFactory
|
import org.moire.ultrasonic.service.MusicServiceFactory
|
||||||
import org.moire.ultrasonic.util.Constants
|
import org.moire.ultrasonic.util.Constants
|
||||||
import org.moire.ultrasonic.util.ErrorDialog
|
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.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
import org.moire.ultrasonic.R
|
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.data.ActiveServerProvider
|
||||||
import org.moire.ultrasonic.domain.Identifiable
|
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.DownloadHandler
|
||||||
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
|
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
|
||||||
import org.moire.ultrasonic.util.Constants
|
import org.moire.ultrasonic.util.Constants
|
||||||
import org.moire.ultrasonic.util.Settings
|
|
||||||
import org.moire.ultrasonic.util.Util
|
import org.moire.ultrasonic.util.Util
|
||||||
import org.moire.ultrasonic.view.SelectMusicFolderView
|
import org.moire.ultrasonic.view.SelectMusicFolderView
|
||||||
|
|
||||||
|
@ -43,8 +44,8 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
|
||||||
* The Adapter for the RecyclerView
|
* The Adapter for the RecyclerView
|
||||||
* Recommendation: Implement this as a lazy delegate
|
* Recommendation: Implement this as a lazy delegate
|
||||||
*/
|
*/
|
||||||
internal val viewAdapter: MultiTypeDiffAdapter<Identifiable> by lazy {
|
internal val viewAdapter: BaseAdapter<Identifiable> by lazy {
|
||||||
MultiTypeDiffAdapter()
|
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
|
* 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,
|
* 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
|
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?) {
|
open fun setTitle(title: String?) {
|
||||||
if (title == null) {
|
if (title == null) {
|
||||||
FragmentTitle.setTitle(
|
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
|
// Create a View Manager
|
||||||
viewManager = LinearLayoutManager(this.context)
|
viewManager = LinearLayoutManager(this.context)
|
||||||
|
|
||||||
|
@ -184,103 +155,17 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
|
||||||
abstract fun onContextMenuItemSelected(menuItem: MenuItem, item: T): Boolean
|
abstract fun onContextMenuItemSelected(menuItem: MenuItem, item: T): Boolean
|
||||||
|
|
||||||
abstract fun onItemClick(item: T)
|
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.KoinComponent
|
||||||
import org.koin.core.component.get
|
import org.koin.core.component.get
|
||||||
import org.moire.ultrasonic.R
|
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.adapters.TrackViewBinder
|
||||||
import org.moire.ultrasonic.audiofx.EqualizerController
|
import org.moire.ultrasonic.audiofx.EqualizerController
|
||||||
import org.moire.ultrasonic.audiofx.VisualizerController
|
import org.moire.ultrasonic.audiofx.VisualizerController
|
||||||
|
@ -154,8 +154,8 @@ class PlayerFragment :
|
||||||
private lateinit var fullStar: Drawable
|
private lateinit var fullStar: Drawable
|
||||||
private lateinit var progressBar: SeekBar
|
private lateinit var progressBar: SeekBar
|
||||||
|
|
||||||
internal val viewAdapter: MultiTypeDiffAdapter<Identifiable> by lazy {
|
internal val viewAdapter: BaseAdapter<Identifiable> by lazy {
|
||||||
MultiTypeDiffAdapter()
|
BaseAdapter()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
@ -890,7 +890,7 @@ class PlayerFragment :
|
||||||
// FIXME:
|
// FIXME:
|
||||||
// Needs to be changed in the playlist as well...
|
// Needs to be changed in the playlist as well...
|
||||||
// Move it in the data set
|
// Move it in the data set
|
||||||
(recyclerView.adapter as MultiTypeDiffAdapter<*>).moveItem(from, to)
|
(recyclerView.adapter as BaseAdapter<*>).moveItem(from, to)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||||
import org.moire.ultrasonic.fragment.EditServerFragment.Companion.EDIT_SERVER_INTENT_INDEX
|
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.service.MediaPlayerController
|
||||||
import org.moire.ultrasonic.util.Util
|
import org.moire.ultrasonic.util.Util
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
|
@ -25,7 +25,6 @@ import androidx.lifecycle.viewModelScope
|
||||||
import androidx.navigation.Navigation
|
import androidx.navigation.Navigation
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import java.util.Collections
|
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
|
@ -36,9 +35,11 @@ import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
||||||
import org.moire.ultrasonic.domain.Identifiable
|
import org.moire.ultrasonic.domain.Identifiable
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory
|
import org.moire.ultrasonic.domain.MusicDirectory
|
||||||
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
|
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
|
||||||
|
import org.moire.ultrasonic.model.TrackCollectionModel
|
||||||
import org.moire.ultrasonic.service.MediaPlayerController
|
import org.moire.ultrasonic.service.MediaPlayerController
|
||||||
import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker
|
import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker
|
||||||
import org.moire.ultrasonic.subsonic.ShareHandler
|
import org.moire.ultrasonic.subsonic.ShareHandler
|
||||||
|
import org.moire.ultrasonic.subsonic.VideoPlayer
|
||||||
import org.moire.ultrasonic.util.AlbumHeader
|
import org.moire.ultrasonic.util.AlbumHeader
|
||||||
import org.moire.ultrasonic.util.CancellationToken
|
import org.moire.ultrasonic.util.CancellationToken
|
||||||
import org.moire.ultrasonic.util.CommunicationError
|
import org.moire.ultrasonic.util.CommunicationError
|
||||||
|
@ -47,35 +48,34 @@ import org.moire.ultrasonic.util.EntryByDiscAndTrackComparator
|
||||||
import org.moire.ultrasonic.util.Settings
|
import org.moire.ultrasonic.util.Settings
|
||||||
import org.moire.ultrasonic.util.Util
|
import org.moire.ultrasonic.util.Util
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.util.Collections
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays a group of tracks, eg. the songs of an album, of a playlist etc.
|
* Displays a group of tracks, eg. the songs of an album, of a playlist etc.
|
||||||
* TODO: Move Clickhandler into ViewBinders
|
* FIXME: Offset when navigating to?
|
||||||
* TODO: Fix clikc handlers and context menus etc.
|
|
||||||
*/
|
*/
|
||||||
class TrackCollectionFragment :
|
open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Entry>() {
|
||||||
MultiListFragment<MusicDirectory.Entry>() {
|
|
||||||
|
|
||||||
private var albumButtons: View? = null
|
private var albumButtons: View? = null
|
||||||
private var emptyView: TextView? = null
|
private var emptyView: TextView? = null
|
||||||
private var selectButton: ImageView? = null
|
internal var selectButton: ImageView? = null
|
||||||
private var playNowButton: ImageView? = null
|
internal var playNowButton: ImageView? = null
|
||||||
private var playNextButton: ImageView? = null
|
internal var playNextButton: ImageView? = null
|
||||||
private var playLastButton: ImageView? = null
|
internal var playLastButton: ImageView? = null
|
||||||
private var pinButton: ImageView? = null
|
internal var pinButton: ImageView? = null
|
||||||
private var unpinButton: ImageView? = null
|
internal var unpinButton: ImageView? = null
|
||||||
private var downloadButton: ImageView? = null
|
internal var downloadButton: ImageView? = null
|
||||||
private var deleteButton: ImageView? = null
|
internal var deleteButton: ImageView? = null
|
||||||
private var moreButton: ImageView? = null
|
internal var moreButton: ImageView? = null
|
||||||
private var playAllButtonVisible = false
|
private var playAllButtonVisible = false
|
||||||
private var shareButtonVisible = false
|
private var shareButtonVisible = false
|
||||||
private var playAllButton: MenuItem? = null
|
private var playAllButton: MenuItem? = null
|
||||||
private var shareButton: 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 networkAndStorageChecker: NetworkAndStorageChecker by inject()
|
||||||
private val shareHandler: ShareHandler by inject()
|
private val shareHandler: ShareHandler by inject()
|
||||||
private var cancellationToken: CancellationToken? = null
|
internal var cancellationToken: CancellationToken? = null
|
||||||
|
|
||||||
override val listModel: TrackCollectionModel by viewModels()
|
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,
|
* The id of the target in the navigation graph where we should go,
|
||||||
* after the user has clicked on an item
|
* after the user has clicked on an item
|
||||||
*/
|
*/
|
||||||
// FIXME
|
|
||||||
override val itemClickTarget: Int = R.id.trackCollectionFragment
|
override val itemClickTarget: Int = R.id.trackCollectionFragment
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
@ -110,90 +109,15 @@ class TrackCollectionFragment :
|
||||||
// Setup refresh handler
|
// Setup refresh handler
|
||||||
refreshListView = view.findViewById(refreshListId)
|
refreshListView = view.findViewById(refreshListId)
|
||||||
refreshListView?.setOnRefreshListener {
|
refreshListView?.setOnRefreshListener {
|
||||||
updateDisplay(true)
|
refreshData(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
listModel.currentList.observe(viewLifecycleOwner, updateInterfaceWithEntries)
|
listModel.currentList.observe(viewLifecycleOwner, updateInterfaceWithEntries)
|
||||||
listModel.songsForGenre.observe(viewLifecycleOwner, songsForGenreObserver)
|
listModel.songsForGenre.observe(viewLifecycleOwner, songsForGenreObserver)
|
||||||
|
|
||||||
// listView!!.setOnItemClickListener { parent, theView, position, _ ->
|
setupButtons(view)
|
||||||
// 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
|
|
||||||
// }
|
|
||||||
|
|
||||||
selectButton = view.findViewById(R.id.select_album_select)
|
emptyView = view.findViewById(R.id.select_album_empty)
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
registerForContextMenu(listView!!)
|
registerForContextMenu(listView!!)
|
||||||
setHasOptionsMenu(true)
|
setHasOptionsMenu(true)
|
||||||
|
@ -234,19 +158,68 @@ class TrackCollectionFragment :
|
||||||
)
|
)
|
||||||
|
|
||||||
// Loads the data
|
// 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 ->
|
val handler = CoroutineExceptionHandler { _, exception ->
|
||||||
Handler(Looper.getMainLooper()).post {
|
Handler(Looper.getMainLooper()).post {
|
||||||
CommunicationError.handleError(exception, context)
|
CommunicationError.handleError(exception, context)
|
||||||
}
|
}
|
||||||
refreshListView!!.isRefreshing = false
|
refreshListView?.isRefreshing = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateDisplay(refresh: Boolean) {
|
private fun refreshData(refresh: Boolean = false) {
|
||||||
// FIXME: Use refresh
|
val args = getArgumentsClone()
|
||||||
getLiveData(requireArguments())
|
args.putBoolean(Constants.INTENT_EXTRA_NAME_REFRESH, refresh)
|
||||||
|
getLiveData(args)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onContextItemSelected(menuItem: MenuItem): Boolean {
|
override fun onContextItemSelected(menuItem: MenuItem): Boolean {
|
||||||
|
@ -370,7 +343,6 @@ class TrackCollectionFragment :
|
||||||
this, append, false, !append, playNext = false,
|
this, append, false, !append, playNext = false,
|
||||||
shuffle = false, songs = selectedSongs
|
shuffle = false, songs = selectedSongs
|
||||||
)
|
)
|
||||||
selectAll(selected = false, toast = false)
|
|
||||||
} else {
|
} else {
|
||||||
playAll(false, append)
|
playAll(false, append)
|
||||||
}
|
}
|
||||||
|
@ -399,8 +371,10 @@ class TrackCollectionFragment :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val isArtist = requireArguments().getBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, false)
|
val isArtist = arguments?.getBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, false)?: false
|
||||||
val id = requireArguments().getString(Constants.INTENT_EXTRA_NAME_ID)
|
|
||||||
|
// FIXME WHICH id if no arguments?
|
||||||
|
val id = arguments?.getString(Constants.INTENT_EXTRA_NAME_ID)
|
||||||
|
|
||||||
if (hasSubFolders && id != null) {
|
if (hasSubFolders && id != null) {
|
||||||
downloadHandler.downloadRecursively(
|
downloadHandler.downloadRecursively(
|
||||||
|
@ -435,13 +409,13 @@ class TrackCollectionFragment :
|
||||||
} as List<MusicDirectory.Entry>
|
} as List<MusicDirectory.Entry>
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun selectAllOrNone() {
|
internal fun selectAllOrNone() {
|
||||||
val someUnselected = viewAdapter.selectedSet.size < childCount
|
val someUnselected = viewAdapter.selectedSet.size < childCount
|
||||||
|
|
||||||
selectAll(someUnselected, true)
|
selectAll(someUnselected, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun selectAll(selected: Boolean, toast: Boolean) {
|
internal fun selectAll(selected: Boolean, toast: Boolean) {
|
||||||
var selectedCount = viewAdapter.selectedSet.size * -1
|
var selectedCount = viewAdapter.selectedSet.size * -1
|
||||||
|
|
||||||
selectedCount += viewAdapter.setSelectionStatusOfAll(selected)
|
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()
|
val enabled = selection.isNotEmpty()
|
||||||
var unpinEnabled = false
|
var unpinEnabled = false
|
||||||
var deleteEnabled = false
|
var deleteEnabled = false
|
||||||
|
@ -480,7 +454,7 @@ class TrackCollectionFragment :
|
||||||
deleteButton?.isVisible = (enabled && deleteEnabled)
|
deleteButton?.isVisible = (enabled && deleteEnabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun downloadBackground(save: Boolean) {
|
internal fun downloadBackground(save: Boolean) {
|
||||||
var songs = getSelectedSongs()
|
var songs = getSelectedSongs()
|
||||||
|
|
||||||
if (songs.isEmpty()) {
|
if (songs.isEmpty()) {
|
||||||
|
@ -514,7 +488,7 @@ class TrackCollectionFragment :
|
||||||
onValid.run()
|
onValid.run()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun delete() {
|
internal fun delete() {
|
||||||
val songs = getSelectedSongs()
|
val songs = getSelectedSongs()
|
||||||
|
|
||||||
Util.toast(
|
Util.toast(
|
||||||
|
@ -527,7 +501,7 @@ class TrackCollectionFragment :
|
||||||
mediaPlayerController.delete(songs)
|
mediaPlayerController.delete(songs)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun unpin() {
|
internal fun unpin() {
|
||||||
val songs = getSelectedSongs()
|
val songs = getSelectedSongs()
|
||||||
Util.toast(
|
Util.toast(
|
||||||
context,
|
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) {
|
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) {
|
if (listSize == 0 || songCount < listSize) {
|
||||||
moreButton!!.visibility = View.GONE
|
moreButton!!.visibility = View.GONE
|
||||||
} else {
|
} else {
|
||||||
moreButton!!.visibility = View.VISIBLE
|
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 {
|
moreButton!!.setOnClickListener {
|
||||||
val offset = requireArguments().getInt(
|
val offset = requireArguments().getInt(
|
||||||
Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0
|
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()
|
enableButtons()
|
||||||
|
|
||||||
val isAlbumList = requireArguments().containsKey(
|
val isAlbumList = arguments?.containsKey(
|
||||||
Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE
|
Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE
|
||||||
)
|
)?:false
|
||||||
|
|
||||||
playAllButtonVisible = !(isAlbumList || entryList.isEmpty()) && !allVideos
|
playAllButtonVisible = !(isAlbumList || entryList.isEmpty()) && !allVideos
|
||||||
shareButtonVisible = !isOffline() && songCount > 0
|
shareButtonVisible = !isOffline() && songCount > 0
|
||||||
|
|
||||||
if (playAllButton != null) {
|
playAllButton?.isVisible = playAllButtonVisible
|
||||||
playAllButton!!.isVisible = playAllButtonVisible
|
shareButton?.isVisible = shareButtonVisible
|
||||||
}
|
|
||||||
|
|
||||||
if (shareButton != null) {
|
|
||||||
shareButton!!.isVisible = shareButtonVisible
|
|
||||||
}
|
|
||||||
|
|
||||||
if (songCount > 0 && listModel.showHeader) {
|
if (songCount > 0 && listModel.showHeader) {
|
||||||
val name = listModel.currentDirectory.value?.name
|
val name = listModel.currentDirectory.value?.name
|
||||||
val intentAlbumName = requireArguments().getString(Constants.INTENT_EXTRA_NAME_NAME, "Name")!!
|
val intentAlbumName = arguments?.getString(Constants.INTENT_EXTRA_NAME_NAME, "")
|
||||||
val albumHeader = AlbumHeader(it, name ?: intentAlbumName, songCount)
|
val albumHeader = AlbumHeader(it, name ?: intentAlbumName)
|
||||||
val mixedList: MutableList<Identifiable> = mutableListOf(albumHeader)
|
val mixedList: MutableList<Identifiable> = mutableListOf(albumHeader)
|
||||||
mixedList.addAll(entryList)
|
mixedList.addAll(entryList)
|
||||||
|
Timber.e("SUBMITTING MIXED LIST")
|
||||||
viewAdapter.submitList(mixedList)
|
viewAdapter.submitList(mixedList)
|
||||||
} else {
|
} else {
|
||||||
|
Timber.e("SUBMITTING ENTRY LIST")
|
||||||
viewAdapter.submitList(entryList)
|
viewAdapter.submitList(entryList)
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
if (playAll && songCount > 0) {
|
||||||
playAll(
|
playAll(
|
||||||
requireArguments().getBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE, false),
|
arguments?.getBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE, false)?:false,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -722,9 +673,7 @@ class TrackCollectionFragment :
|
||||||
val refresh = args.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH, true)
|
val refresh = args.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH, true)
|
||||||
|
|
||||||
listModel.viewModelScope.launch(handler) {
|
listModel.viewModelScope.launch(handler) {
|
||||||
refreshListView!!.isRefreshing = true
|
refreshListView?.isRefreshing = true
|
||||||
|
|
||||||
listModel.getMusicFolders(refresh)
|
|
||||||
|
|
||||||
if (playlistId != null) {
|
if (playlistId != null) {
|
||||||
setTitle(playlistName!!)
|
setTitle(playlistName!!)
|
||||||
|
@ -753,14 +702,14 @@ class TrackCollectionFragment :
|
||||||
if (isAlbum) {
|
if (isAlbum) {
|
||||||
listModel.getAlbum(refresh, id!!, name, parentId)
|
listModel.getAlbum(refresh, id!!, name, parentId)
|
||||||
} else {
|
} else {
|
||||||
listModel.getArtist(refresh, id!!, name)
|
throw IllegalAccessException("Use AlbumFragment instead!")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
listModel.getMusicDirectory(refresh, id!!, name, parentId)
|
listModel.getMusicDirectory(refresh, id!!, name, parentId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshListView!!.isRefreshing = false
|
refreshListView?.isRefreshing = false
|
||||||
}
|
}
|
||||||
return listModel.currentList
|
return listModel.currentList
|
||||||
}
|
}
|
||||||
|
@ -774,6 +723,24 @@ class TrackCollectionFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemClick(item: MusicDirectory.Entry) {
|
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.app.Application
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
|
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory
|
import org.moire.ultrasonic.domain.MusicDirectory
|
||||||
import org.moire.ultrasonic.service.MusicService
|
import org.moire.ultrasonic.service.MusicService
|
||||||
|
@ -13,7 +14,9 @@ import org.moire.ultrasonic.util.Settings
|
||||||
|
|
||||||
class AlbumListModel(application: Application) : GenericListModel(application) {
|
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
|
var lastType: String? = null
|
||||||
private var loadedUntil: Int = 0
|
private var loadedUntil: Int = 0
|
||||||
|
|
||||||
|
@ -26,11 +29,37 @@ class AlbumListModel(application: Application) : GenericListModel(application) {
|
||||||
// This way, we keep the scroll position
|
// This way, we keep the scroll position
|
||||||
val albumListType = args.getString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE)!!
|
val albumListType = args.getString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE)!!
|
||||||
|
|
||||||
if (refresh || albumList.value!!.isEmpty() || albumListType != lastType) {
|
if (refresh || list.value!!.isEmpty() || albumListType != lastType) {
|
||||||
lastType = albumListType
|
lastType = albumListType
|
||||||
backgroundLoadFromServer(refresh, swipe, args)
|
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(
|
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 appending the existing list, set the offset from where to load
|
||||||
if (append) offset += (size + loadedUntil)
|
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) {
|
if (useId3Tags) {
|
||||||
musicDirectory = musicService.getAlbumList2(
|
musicDirectory = musicService.getAlbumList2(
|
||||||
albumListType, size,
|
albumListType, size,
|
||||||
|
@ -72,13 +110,13 @@ class AlbumListModel(application: Application) : GenericListModel(application) {
|
||||||
|
|
||||||
currentListIsSortable = isCollectionSortable(albumListType)
|
currentListIsSortable = isCollectionSortable(albumListType)
|
||||||
|
|
||||||
if (append && albumList.value != null) {
|
if (append && list.value != null) {
|
||||||
val list = ArrayList<MusicDirectory.Entry>()
|
val list = ArrayList<MusicDirectory.Entry>()
|
||||||
list.addAll(albumList.value!!)
|
list.addAll(this.list.value!!)
|
||||||
list.addAll(musicDirectory.getAllChild())
|
list.addAll(musicDirectory.getAllChild())
|
||||||
albumList.postValue(list)
|
this.list.postValue(list)
|
||||||
} else {
|
} else {
|
||||||
albumList.postValue(musicDirectory.getAllChild())
|
list.postValue(musicDirectory.getAllChild())
|
||||||
}
|
}
|
||||||
|
|
||||||
loadedUntil = offset
|
loadedUntil = offset
|
||||||
|
@ -100,4 +138,5 @@ class AlbumListModel(application: Application) : GenericListModel(application) {
|
||||||
albumListType != "highest" && albumListType != "recent" &&
|
albumListType != "highest" && albumListType != "recent" &&
|
||||||
albumListType != "frequent"
|
albumListType != "frequent"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
Copyright 2020 (C) Jozsef Varga
|
Copyright 2020 (C) Jozsef Varga
|
||||||
*/
|
*/
|
||||||
package org.moire.ultrasonic.fragment
|
package org.moire.ultrasonic.model
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
|
@ -1,4 +1,4 @@
|
||||||
package org.moire.ultrasonic.fragment
|
package org.moire.ultrasonic.model
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
@ -17,6 +17,7 @@ import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||||
import org.moire.ultrasonic.data.ServerSetting
|
import org.moire.ultrasonic.data.ServerSetting
|
||||||
|
import org.moire.ultrasonic.domain.MusicDirectory
|
||||||
import org.moire.ultrasonic.domain.MusicFolder
|
import org.moire.ultrasonic.domain.MusicFolder
|
||||||
import org.moire.ultrasonic.service.MusicService
|
import org.moire.ultrasonic.service.MusicService
|
||||||
import org.moire.ultrasonic.service.MusicServiceFactory
|
import org.moire.ultrasonic.service.MusicServiceFactory
|
||||||
|
@ -45,8 +46,6 @@ open class GenericListModel(application: Application) :
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
internal val musicFolders: MutableLiveData<List<MusicFolder>> = MutableLiveData(listOf())
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper function to check online status
|
* Helper function to check online status
|
||||||
*/
|
*/
|
||||||
|
@ -110,16 +109,20 @@ open class GenericListModel(application: Application) :
|
||||||
) {
|
) {
|
||||||
// Update the list of available folders if enabled
|
// Update the list of available folders if enabled
|
||||||
if (showSelectFolderHeader(args) && !isOffline && !useId3Tags) {
|
if (showSelectFolderHeader(args) && !isOffline && !useId3Tags) {
|
||||||
musicFolders.postValue(
|
//FIXME
|
||||||
musicService.getMusicFolders(refresh)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.app.Application
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
|
@ -5,7 +5,7 @@
|
||||||
* Distributed under terms of the GNU GPLv3 license.
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.moire.ultrasonic.fragment
|
package org.moire.ultrasonic.model
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
@ -22,25 +22,13 @@ import org.moire.ultrasonic.util.Util
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Model for retrieving different collections of tracks from the API
|
* Model for retrieving different collections of tracks from the API
|
||||||
* TODO: Refactor this model to extend the GenericListModel
|
|
||||||
*/
|
*/
|
||||||
class TrackCollectionModel(application: Application) : GenericListModel(application) {
|
class TrackCollectionModel(application: Application) : GenericListModel(application) {
|
||||||
|
|
||||||
private val allSongsId = "-1"
|
|
||||||
|
|
||||||
val currentDirectory: MutableLiveData<MusicDirectory> = MutableLiveData()
|
val currentDirectory: MutableLiveData<MusicDirectory> = MutableLiveData()
|
||||||
val currentList: MutableLiveData<List<MusicDirectory.Entry>> = MutableLiveData()
|
val currentList: MutableLiveData<List<MusicDirectory.Entry>> = MutableLiveData()
|
||||||
val songsForGenre: MutableLiveData<MusicDirectory> = 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(
|
suspend fun getMusicDirectory(
|
||||||
refresh: Boolean,
|
refresh: Boolean,
|
||||||
id: String,
|
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"
|
// Given a Music directory "songs" it recursively adds all children to "songs"
|
||||||
private fun getSongsRecursively(
|
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?) {
|
suspend fun getAlbum(refresh: Boolean, id: String, name: String?, parentId: String?) {
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
@ -296,18 +245,17 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns true if the directory contains only folders
|
suspend fun getBookmarks() {
|
||||||
private fun hasOnlyFolders(musicDirectory: MusicDirectory) =
|
withContext(Dispatchers.IO) {
|
||||||
musicDirectory.getChildren(includeDirs = true, includeFiles = false).size ==
|
val service = MusicServiceFactory.getMusicService()
|
||||||
musicDirectory.getChildren(includeDirs = true, includeFiles = true).size
|
val musicDirectory = Util.getSongsFromBookmarks(service.getBookmarks())
|
||||||
|
currentDirectory.postValue(musicDirectory)
|
||||||
override fun load(
|
updateList(musicDirectory)
|
||||||
isOffline: Boolean,
|
}
|
||||||
useId3Tags: Boolean,
|
|
||||||
musicService: MusicService,
|
|
||||||
refresh: Boolean,
|
|
||||||
args: Bundle
|
|
||||||
) {
|
|
||||||
// See To_Do at the top
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateList(root: MusicDirectory) {
|
||||||
|
currentList.postValue(root.getChildren())
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -399,7 +399,7 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getBookmarks(): List<Bookmark?>? = musicService.getBookmarks()
|
override fun getBookmarks(): List<Bookmark> = musicService.getBookmarks()
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun deleteBookmark(id: String) {
|
override fun deleteBookmark(id: String) {
|
||||||
|
|
|
@ -154,7 +154,7 @@ interface MusicService {
|
||||||
fun addChatMessage(message: String)
|
fun addChatMessage(message: String)
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun getBookmarks(): List<Bookmark?>?
|
fun getBookmarks(): List<Bookmark>
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun deleteBookmark(id: String)
|
fun deleteBookmark(id: String)
|
||||||
|
|
|
@ -411,7 +411,7 @@ class OfflineMusicService : MusicService, KoinComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(OfflineException::class)
|
@Throws(OfflineException::class)
|
||||||
override fun getBookmarks(): List<Bookmark?>? {
|
override fun getBookmarks(): List<Bookmark> {
|
||||||
throw OfflineException("getBookmarks isn't available in offline mode")
|
throw OfflineException("getBookmarks isn't available in offline mode")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,11 @@ class RxBus {
|
||||||
val themeChangedEventObservable: Observable<Unit> =
|
val themeChangedEventObservable: Observable<Unit> =
|
||||||
themeChangedEventPublisher.observeOn(AndroidSchedulers.mainThread())
|
themeChangedEventPublisher.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
|
||||||
|
val musicFolderChangedEventPublisher: PublishSubject<String> =
|
||||||
|
PublishSubject.create()
|
||||||
|
val musicFolderChangedEventObservable: Observable<String> =
|
||||||
|
musicFolderChangedEventPublisher.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
|
||||||
val playerStatePublisher: PublishSubject<StateWithTrack> =
|
val playerStatePublisher: PublishSubject<StateWithTrack> =
|
||||||
PublishSubject.create()
|
PublishSubject.create()
|
||||||
val playerStateObservable: Observable<StateWithTrack> =
|
val playerStateObservable: Observable<StateWithTrack> =
|
||||||
|
@ -73,6 +78,7 @@ class RxBus {
|
||||||
val skipToQueueItemCommandObservable: Observable<Long> =
|
val skipToQueueItemCommandObservable: Observable<Long> =
|
||||||
skipToQueueItemCommandPublisher.observeOn(AndroidSchedulers.mainThread())
|
skipToQueueItemCommandPublisher.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
|
||||||
|
|
||||||
fun releaseMediaSessionToken() {
|
fun releaseMediaSessionToken() {
|
||||||
mediaSessionTokenPublisher = PublishSubject.create()
|
mediaSessionTokenPublisher = PublishSubject.create()
|
||||||
}
|
}
|
||||||
|
|
|
@ -121,5 +121,6 @@ object Constants {
|
||||||
const val ALBUM_ART_FILE = "folder.jpeg"
|
const val ALBUM_ART_FILE = "folder.jpeg"
|
||||||
const val STARRED = "starred"
|
const val STARRED = "starred"
|
||||||
const val ALPHABETICAL_BY_NAME = "alphabeticalByName"
|
const val ALPHABETICAL_BY_NAME = "alphabeticalByName"
|
||||||
|
const val ALBUMS_OF_ARTIST = "albumsOfArtist"
|
||||||
const val RESULT_CLOSE_ALL = 1337
|
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.DOWN
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper.UP
|
import androidx.recyclerview.widget.ItemTouchHelper.UP
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.moire.ultrasonic.adapters.MultiTypeDiffAdapter
|
import org.moire.ultrasonic.adapters.BaseAdapter
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
class DragSortCallback : ItemTouchHelper.SimpleCallback(UP or DOWN, 0) {
|
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)
|
Timber.w("MOVED %s %s", to, from)
|
||||||
|
|
||||||
// Move it in the data set
|
// Move it in the data set
|
||||||
(recyclerView.adapter as MultiTypeDiffAdapter<*>).moveItem(from, to)
|
(recyclerView.adapter as BaseAdapter<*>).moveItem(from, to)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -525,11 +525,10 @@ object Util {
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getSongsFromBookmarks(bookmarks: Iterable<Bookmark?>): MusicDirectory {
|
fun getSongsFromBookmarks(bookmarks: Iterable<Bookmark>): MusicDirectory {
|
||||||
val musicDirectory = MusicDirectory()
|
val musicDirectory = MusicDirectory()
|
||||||
var song: MusicDirectory.Entry
|
var song: MusicDirectory.Entry
|
||||||
for (bookmark in bookmarks) {
|
for (bookmark in bookmarks) {
|
||||||
if (bookmark == null) continue
|
|
||||||
song = bookmark.entry
|
song = bookmark.entry
|
||||||
song.bookmarkPosition = bookmark.position
|
song.bookmarkPosition = bookmark.position
|
||||||
musicDirectory.addChild(song)
|
musicDirectory.addChild(song)
|
||||||
|
|
|
@ -13,7 +13,8 @@
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:src="?attr/select_all"
|
android:src="?attr/select_all"
|
||||||
android:visibility="gone" />
|
android:visibility="gone"
|
||||||
|
android:contentDescription="@string/common.select_all" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/select_album_play_now"
|
android:id="@+id/select_album_play_now"
|
||||||
|
@ -22,7 +23,8 @@
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:src="?attr/media_play"
|
android:src="?attr/media_play"
|
||||||
android:visibility="gone" />
|
android:visibility="gone"
|
||||||
|
android:contentDescription="@string/common.play_now" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/select_album_play_next"
|
android:id="@+id/select_album_play_next"
|
||||||
|
@ -31,7 +33,8 @@
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:src="?attr/media_play_next"
|
android:src="?attr/media_play_next"
|
||||||
android:visibility="gone" />
|
android:visibility="gone"
|
||||||
|
android:contentDescription="@string/common.play_next" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/select_album_play_last"
|
android:id="@+id/select_album_play_last"
|
||||||
|
@ -40,7 +43,8 @@
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:src="?attr/add_to_queue"
|
android:src="?attr/add_to_queue"
|
||||||
android:visibility="gone" />
|
android:visibility="gone"
|
||||||
|
android:contentDescription="@string/common.play_last" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/select_album_pin"
|
android:id="@+id/select_album_pin"
|
||||||
|
@ -49,7 +53,8 @@
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:src="?attr/pin"
|
android:src="?attr/pin"
|
||||||
android:visibility="gone" />
|
android:visibility="gone"
|
||||||
|
android:contentDescription="@string/common.pin" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/select_album_unpin"
|
android:id="@+id/select_album_unpin"
|
||||||
|
@ -58,7 +63,8 @@
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:src="?attr/unpin"
|
android:src="?attr/unpin"
|
||||||
android:visibility="gone" />
|
android:visibility="gone"
|
||||||
|
android:contentDescription="@string/common.unpin" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/select_album_download"
|
android:id="@+id/select_album_download"
|
||||||
|
@ -67,7 +73,8 @@
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:src="?attr/download"
|
android:src="?attr/download"
|
||||||
android:visibility="gone" />
|
android:visibility="gone"
|
||||||
|
android:contentDescription="@string/common.download" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/select_album_delete"
|
android:id="@+id/select_album_delete"
|
||||||
|
@ -76,7 +83,8 @@
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:src="?attr/stop"
|
android:src="?attr/stop"
|
||||||
android:visibility="gone" />
|
android:visibility="gone"
|
||||||
|
android:contentDescription="@string/common.delete" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/select_album_more"
|
android:id="@+id/select_album_more"
|
||||||
|
@ -85,6 +93,7 @@
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:src="?attr/forward"
|
android:src="?attr/forward"
|
||||||
android:visibility="gone" />
|
android:visibility="gone"
|
||||||
|
android:contentDescription="@string/search.more" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
|
@ -10,7 +10,7 @@
|
||||||
a:layout_height="0dip"
|
a:layout_height="0dip"
|
||||||
a:layout_weight="1.0">
|
a:layout_weight="1.0">
|
||||||
|
|
||||||
<ListView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
a:id="@+id/search_list"
|
a:id="@+id/search_list"
|
||||||
a:layout_width="fill_parent"
|
a:layout_width="fill_parent"
|
||||||
a:layout_height="0dip"
|
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" >
|
android:label="@string/music_library.label" >
|
||||||
<action
|
<action
|
||||||
android:id="@+id/selectArtistToSelectAlbum"
|
android:id="@+id/selectArtistToSelectAlbum"
|
||||||
app:destination="@id/trackCollectionFragment" />
|
app:destination="@id/albumListFragment" />
|
||||||
</fragment>
|
</fragment>
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/artistListFragment"
|
android:id="@+id/artistListFragment"
|
||||||
android:name="org.moire.ultrasonic.fragment.ArtistListFragment" >
|
android:name="org.moire.ultrasonic.fragment.ArtistListFragment" >
|
||||||
<action
|
<action
|
||||||
android:id="@+id/selectArtistToSelectAlbum"
|
android:id="@+id/selectArtistToSelectAlbum"
|
||||||
app:destination="@id/trackCollectionFragment" />
|
app:destination="@id/albumListFragment" />
|
||||||
</fragment>
|
</fragment>
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/trackCollectionFragment"
|
android:id="@+id/trackCollectionFragment"
|
||||||
|
|
|
@ -50,6 +50,7 @@
|
||||||
<string name="common.play_shuffled">Play Shuffled</string>
|
<string name="common.play_shuffled">Play Shuffled</string>
|
||||||
<string name="common.public">Public</string>
|
<string name="common.public">Public</string>
|
||||||
<string name="common.save">Save</string>
|
<string name="common.save">Save</string>
|
||||||
|
<string name="common.select_all">Select all</string>
|
||||||
<string name="common.title">Title</string>
|
<string name="common.title">Title</string>
|
||||||
<string name="common.unpin">Unpin</string>
|
<string name="common.unpin">Unpin</string>
|
||||||
<string name="common.various_artists">Various Artists</string>
|
<string name="common.various_artists">Various Artists</string>
|
||||||
|
|
Loading…
Reference in New Issue