BookmarksFragment is now based on TrackCollectionFragment

Also start SearchFragment.kt
This commit is contained in:
tzugen 2021-11-23 20:38:26 +01:00
parent 7640f4c4aa
commit f8a87f7c85
No known key found for this signature in database
GPG Key ID: 61E9C34BC10EC930
51 changed files with 1539 additions and 2114 deletions

View File

@ -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()
) )

View File

@ -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));
}
}
}

View File

@ -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);
}
}
}

View File

@ -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) {
}
}

View File

@ -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

View File

@ -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();
} }
} }

View File

@ -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();
} }
} }

View File

@ -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();
} }
} }

View File

@ -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

View File

@ -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))
}
} }

View File

@ -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)
}
}

View File

@ -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))
}
}

View File

@ -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
) )

View File

@ -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)
}
}
}

View File

@ -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
}
}
}
}

View File

@ -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) {

View File

@ -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
}
}

View File

@ -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"
}
}

View File

@ -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

View File

@ -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()
} }
) )

View File

@ -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) {

View File

@ -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"

View File

@ -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)
} }
} }

View File

@ -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
} }

View File

@ -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)
}
}

View File

@ -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

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -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)
// }
// }

View File

@ -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
} }

View File

@ -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

View File

@ -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()
}
}
} }
} }

View File

@ -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"
} }
} }

View File

@ -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

View File

@ -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"
} }

View File

@ -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
// }
}

View File

@ -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

View File

@ -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())
}
} }

View File

@ -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) {

View File

@ -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)

View File

@ -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")
} }

View File

@ -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()
} }

View File

@ -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
} }

View File

@ -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
} }

View File

@ -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)

View File

@ -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>

View File

@ -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"

View File

@ -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>

View File

@ -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"

View File

@ -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>