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.
*/
data class SearchResult(
val artists: List<Artist>,
val albums: List<Entry>,
val songs: List<Entry>
val artists: List<Artist> = listOf(),
val albums: List<Entry> = listOf(),
val songs: List<Entry> = listOf()
)

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(
var entries: List<MusicDirectory.Entry>,
var name: String,
songCount: Int
var name: String?
) : Identifiable {
var isAllVideo: Boolean
private set

View File

@ -20,7 +20,7 @@ package org.moire.ultrasonic.view;
import android.content.Context;
import android.view.LayoutInflater;
import android.widget.TextView;
import android.widget.LinearLayout;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.domain.Playlist;
@ -30,9 +30,9 @@ import org.moire.ultrasonic.domain.Playlist;
*
* @author Sindre Mehus
*/
public class PlaylistView extends UpdateView
public class PlaylistView extends LinearLayout
{
private Context context;
private final Context context;
private PlaylistAdapter.ViewHolder viewHolder;
public PlaylistView(Context context)
@ -45,7 +45,7 @@ public class PlaylistView extends UpdateView
{
LayoutInflater.from(context).inflate(R.layout.playlist_list_item, this, true);
viewHolder = new PlaylistAdapter.ViewHolder();
viewHolder.name = (TextView) findViewById(R.id.playlist_name);
viewHolder.name = findViewById(R.id.playlist_name);
setTag(viewHolder);
}
@ -58,6 +58,5 @@ public class PlaylistView extends UpdateView
public void setPlaylist(Playlist playlist)
{
viewHolder.name.setText(playlist.getName());
update();
}
}

View File

@ -20,7 +20,7 @@ package org.moire.ultrasonic.view;
import android.content.Context;
import android.view.LayoutInflater;
import android.widget.TextView;
import android.widget.LinearLayout;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.domain.Playlist;
@ -30,12 +30,12 @@ import org.moire.ultrasonic.domain.Playlist;
*
* @author Sindre Mehus
*/
public class PodcatsChannelItemView extends UpdateView
public class PodcastChannelItemView extends LinearLayout
{
private Context context;
private final Context context;
private PlaylistAdapter.ViewHolder viewHolder;
public PodcatsChannelItemView(Context context)
public PodcastChannelItemView(Context context)
{
super(context);
this.context = context;
@ -45,7 +45,7 @@ public class PodcatsChannelItemView extends UpdateView
{
LayoutInflater.from(context).inflate(R.layout.playlist_list_item, this, true);
viewHolder = new PlaylistAdapter.ViewHolder();
viewHolder.name = (TextView) findViewById(R.id.playlist_name);
viewHolder.name = findViewById(R.id.playlist_name);
setTag(viewHolder);
}
@ -58,6 +58,5 @@ public class PodcatsChannelItemView extends UpdateView
public void setPlaylist(Playlist playlist)
{
viewHolder.name.setText(playlist.getName());
update();
}
}

View File

@ -20,7 +20,7 @@ package org.moire.ultrasonic.view;
import android.content.Context;
import android.view.LayoutInflater;
import android.widget.TextView;
import android.widget.LinearLayout;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.domain.Share;
@ -30,9 +30,9 @@ import org.moire.ultrasonic.domain.Share;
*
* @author Joshua Bahnsen
*/
public class ShareView extends UpdateView
public class ShareView extends LinearLayout
{
private Context context;
private final Context context;
private ShareAdapter.ViewHolder viewHolder;
public ShareView(Context context)
@ -45,8 +45,8 @@ public class ShareView extends UpdateView
{
LayoutInflater.from(context).inflate(R.layout.share_list_item, this, true);
viewHolder = new ShareAdapter.ViewHolder();
viewHolder.url = (TextView) findViewById(R.id.share_url);
viewHolder.description = (TextView) findViewById(R.id.share_description);
viewHolder.url = findViewById(R.id.share_url);
viewHolder.description = findViewById(R.id.share_description);
setTag(viewHolder);
}
@ -60,6 +60,5 @@ public class ShareView extends UpdateView
{
viewHolder.url.setText(share.getName());
viewHolder.description.setText(share.getDescription());
update();
}
}

View File

@ -39,7 +39,7 @@ import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.data.ServerSettingDao
import org.moire.ultrasonic.domain.PlayerState
import org.moire.ultrasonic.fragment.OnBackPressedHandler
import org.moire.ultrasonic.fragment.ServerSettingsModel
import org.moire.ultrasonic.model.ServerSettingsModel
import org.moire.ultrasonic.provider.SearchSuggestionProvider
import org.moire.ultrasonic.service.DownloadFile
import org.moire.ultrasonic.service.MediaPlayerController

View File

@ -1,5 +1,5 @@
/*
* AlbumRowAdapter.kt
* AlbumRowBinder.kt
* Copyright (C) 2009-2021 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
@ -9,13 +9,16 @@ package org.moire.ultrasonic.adapters
import android.content.Context
import android.graphics.drawable.Drawable
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import java.lang.Exception
import com.drakeet.multitype.ItemViewBinder
import org.koin.core.component.KoinComponent
import org.moire.ultrasonic.R
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.imageloader.ImageLoader
@ -27,22 +30,12 @@ import timber.log.Timber
/**
* Creates a Row in a RecyclerView which contains the details of an Album
*/
class AlbumRowAdapter(
itemList: List<MusicDirectory.Entry>,
onItemClick: (MusicDirectory.Entry) -> Unit,
onContextMenuClick: (MenuItem, MusicDirectory.Entry) -> Boolean,
class AlbumRowBinder(
val onItemClick: (MusicDirectory.Entry) -> Unit,
val onContextMenuClick: (MenuItem, MusicDirectory.Entry) -> Boolean,
private val imageLoader: ImageLoader,
onMusicFolderUpdate: (String?) -> Unit,
context: Context,
) : GenericRowAdapter<MusicDirectory.Entry>(
onItemClick,
onContextMenuClick,
onMusicFolderUpdate
) {
init {
super.submitList(itemList)
}
) : ItemViewBinder<MusicDirectory.Entry, AlbumRowBinder.ViewHolder>(), KoinComponent {
private val starDrawable: Drawable =
Util.getDrawableFromAttribute(context, R.attr.star_full)
@ -50,34 +43,32 @@ class AlbumRowAdapter(
Util.getDrawableFromAttribute(context, R.attr.star_hollow)
// Set our layout files
override val layout = R.layout.album_list_item
override val contextMenuLayout = R.menu.artist_context_menu
val layout = R.layout.album_list_item
val contextMenuLayout = R.menu.artist_context_menu
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is ViewHolder) {
val listPosition = if (selectFolderHeader != null) position - 1 else position
val entry = currentList[listPosition]
holder.album.text = entry.title
holder.artist.text = entry.artist
holder.details.setOnClickListener { onItemClick(entry) }
holder.details.setOnLongClickListener { view -> createPopupMenu(view, listPosition) }
holder.coverArtId = entry.coverArt
holder.star.setImageDrawable(if (entry.starred) starDrawable else starHollowDrawable)
holder.star.setOnClickListener { onStarClick(entry, holder.star) }
override fun onBindViewHolder(holder: ViewHolder, item: MusicDirectory.Entry) {
holder.album.text = item.title
holder.artist.text = item.artist
holder.details.setOnClickListener { onItemClick(item) }
holder.details.setOnLongClickListener {
val popup = Helper.createPopupMenu(holder.itemView)
imageLoader.loadImage(
holder.coverArt, entry,
false, 0, R.drawable.unknown_album
)
popup.setOnMenuItemClickListener { menuItem ->
onContextMenuClick(menuItem, item)
}
true
}
holder.coverArtId = item.coverArt
holder.star.setImageDrawable(if (item.starred) starDrawable else starHollowDrawable)
holder.star.setOnClickListener { onStarClick(item, holder.star) }
imageLoader.loadImage(
holder.coverArt, item,
false, 0, R.drawable.unknown_album
)
}
override fun getItemCount(): Int {
if (selectFolderHeader != null)
return currentList.size + 1
else
return currentList.size
}
/**
* Holds the view properties of an Item row
@ -93,12 +84,6 @@ class AlbumRowAdapter(
var coverArtId: String? = null
}
/**
* Creates an instance of our ViewHolder class
*/
override fun newViewHolder(view: View): RecyclerView.ViewHolder {
return ViewHolder(view)
}
/**
* Handles the star / unstar action for an album
@ -128,4 +113,9 @@ class AlbumRowAdapter(
}
}.start()
}
override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): ViewHolder {
return ViewHolder(inflater.inflate(layout, parent, false))
}
}

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 org.moire.ultrasonic.domain.Identifiable
class MultiTypeDiffAdapter<T : Identifiable> : MultiTypeAdapter() {
class BaseAdapter<T : Identifiable> : MultiTypeAdapter() {
internal var selectedSet: TreeSet<Long> = TreeSet()
internal var selectionRevision: MutableLiveData<Int> = MutableLiveData(0)
@ -43,7 +43,7 @@ class MultiTypeDiffAdapter<T : Identifiable> : MultiTypeAdapter() {
private val mListener =
ListListener<T> { previousList, currentList ->
this@MultiTypeDiffAdapter.onCurrentListChanged(
this@BaseAdapter.onCurrentListChanged(
previousList,
currentList
)

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.widget.ImageView
import android.widget.TextView
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import com.drakeet.multitype.ItemViewBinder
import java.lang.ref.WeakReference
@ -57,7 +58,12 @@ class HeaderViewBinder(
Util.getAlbumImageSize(context)
)
holder.titleView.text = item.name
if (item.name != null) {
holder.titleView.isVisible = true
holder.titleView.text = item.name
} else {
holder.titleView.isVisible = false
}
// Don't show a header if all entries are videos
if (item.isAllVideo) {

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.data.ActiveServerProvider
import org.moire.ultrasonic.data.ServerSetting
import org.moire.ultrasonic.model.ServerSettingsModel
import org.moire.ultrasonic.util.ServerColor
import org.moire.ultrasonic.util.Util

View File

@ -22,16 +22,6 @@ class TrackViewBinder(
private val onClickCallback: ((View, DownloadFile?) -> Unit)? = null
) : ItemViewBinder<Identifiable, TrackViewHolder>(), KoinComponent {
// //
// onItemClick: (MusicDirectory.Entry) -> Unit,
// onContextMenuClick: (MenuItem, MusicDirectory.Entry) -> Boolean,
// onMusicFolderUpdate: (String?) -> Unit,
// context: Context,
// val lifecycleOwner: LifecycleOwner,
// init {
// super.submitList(itemList)
// }
// Set our layout files
val layout = R.layout.song_list_item
val contextMenuLayout = R.menu.artist_context_menu
@ -44,9 +34,8 @@ class TrackViewBinder(
}
override fun onBindViewHolder(holder: TrackViewHolder, item: Identifiable) {
val downloadFile: DownloadFile?
val _adapter = adapter as MultiTypeDiffAdapter<*>
val diffAdapter = adapter as BaseAdapter<*>
when (item) {
is MusicDirectory.Entry -> {
@ -66,7 +55,7 @@ class TrackViewBinder(
file = downloadFile,
checkable = checkable,
draggable = draggable,
_adapter.isSelected(item.longId)
diffAdapter.isSelected(item.longId)
)
// Notify the adapter of selection changes
@ -74,18 +63,18 @@ class TrackViewBinder(
lifecycleOwner,
{ newValue ->
if (newValue) {
_adapter.notifySelected(item.longId)
diffAdapter.notifySelected(item.longId)
} else {
_adapter.notifyUnselected(item.longId)
diffAdapter.notifyUnselected(item.longId)
}
}
)
// Listen to changes in selection status and update ourselves
_adapter.selectionRevision.observe(
diffAdapter.selectionRevision.observe(
lifecycleOwner,
{
val newStatus = _adapter.isSelected(item.longId)
val newStatus = diffAdapter.isSelected(item.longId)
if (newStatus != holder.check.isChecked) holder.check.isChecked = newStatus
}
@ -96,7 +85,7 @@ class TrackViewBinder(
lifecycleOwner,
{
holder.updateStatus(it)
_adapter.notifyChanged()
diffAdapter.notifyChanged()
}
)

View File

@ -23,13 +23,14 @@ import org.moire.ultrasonic.service.DownloadFile
import org.moire.ultrasonic.service.DownloadStatus
import org.moire.ultrasonic.service.MediaPlayerController
import org.moire.ultrasonic.service.MusicServiceFactory
import org.moire.ultrasonic.service.RxBus
import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Util
import timber.log.Timber
/**
* Used to display songs and videos in a `ListView`.
* TODO: Video List item
* FIXME: Add video List item
*/
class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable, KoinComponent {
@ -58,7 +59,7 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable
private var isMaximized = false
private var cachedStatus = DownloadStatus.UNKNOWN
private var statusImage: Drawable? = null
private var playing = false
private var isPlayingCached = false
var observableChecked = MutableLiveData(false)
@ -67,8 +68,6 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable
features.isFeatureEnabled(Feature.FIVE_STAR_RATING)
}
private val mediaPlayerController: MediaPlayerController by inject()
lateinit var imageHelper: ImageHelper
init {
@ -116,9 +115,44 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable
setupStarButtons(song)
}
update()
updateProgress(downloadFile!!.progress.value!!)
updateStatus(downloadFile!!.status.value!!)
if (useFiveStarRating) {
setFiveStars(entry?.userRating ?: 0)
} else {
setSingleStar(entry!!.starred)
}
RxBus.playerStateObservable.subscribe {
setPlayIcon(it.track == downloadFile)
}
// Minimize or maximize the Text view (if song title is very long)
itemView.setOnLongClickListener {
if (!song.isDirectory) {
maximizeOrMinimize()
true
}
false
}
}
private fun setPlayIcon(isPlaying: Boolean) {
if (isPlaying && !isPlayingCached) {
isPlayingCached = true
title.setCompoundDrawablesWithIntrinsicBounds(
imageHelper.playingImage, null, null, null
)
} else if (!isPlaying && isPlayingCached) {
isPlayingCached = false
title.setCompoundDrawablesWithIntrinsicBounds(
0, 0, 0, 0
)
}
}
private fun setupStarButtons(song: MusicDirectory.Entry) {
if (useFiveStarRating) {
// Hide single star
@ -157,38 +191,6 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable
}
}
@Synchronized
// TODO: Should be removed
fun update() {
updateProgress(downloadFile!!.progress.value!!)
updateStatus(downloadFile!!.status.value!!)
if (useFiveStarRating) {
val rating = entry?.userRating ?: 0
setFiveStars(rating)
} else {
setSingleStar(entry!!.starred)
}
val playing = mediaPlayerController.currentPlaying === downloadFile
if (playing) {
if (!this.playing) {
this.playing = true
title.setCompoundDrawablesWithIntrinsicBounds(
imageHelper.playingImage, null, null, null
)
}
} else {
if (this.playing) {
this.playing = false
title.setCompoundDrawablesWithIntrinsicBounds(
0, 0, 0, 0
)
}
}
}
@Suppress("MagicNumber")
private fun setFiveStars(rating: Int) {

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_2_3
import org.moire.ultrasonic.data.MIGRATION_3_4
import org.moire.ultrasonic.fragment.ServerSettingsModel
import org.moire.ultrasonic.model.ServerSettingsModel
import org.moire.ultrasonic.util.Settings
const val SP_NAME = "Default_SP"

View File

@ -7,12 +7,14 @@ import androidx.lifecycle.LiveData
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.RecyclerView
import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.AlbumRowBinder
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.model.AlbumListModel
import org.moire.ultrasonic.util.Constants
/**
* Displays a list of Albums from the media library
* TODO: Check refresh is working
* FIXME: Add music folder support
*/
class AlbumListFragment : EntryListFragment<MusicDirectory.Entry>() {
@ -54,24 +56,6 @@ class AlbumListFragment : EntryListFragment<MusicDirectory.Entry>() {
return listModel.getAlbumList(refresh or append, refreshListView!!, args)
}
// FIXME
// /**
// * Provide the Adapter for the RecyclerView with a lazy delegate
// */
// override val viewAdapter: AlbumRowAdapter by lazy {
// AlbumRowAdapter(
// liveDataItems.value ?: listOf(),
// { entry -> onItemClick(entry) },
// { menuItem, entry -> onContextMenuItemSelected(menuItem, entry) },
// imageLoaderProvider.getImageLoader(),
// onMusicFolderUpdate,
// requireContext()
// )
// }
val newBundleClone: Bundle
get() = arguments?.clone() as Bundle
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@ -81,13 +65,25 @@ class AlbumListFragment : EntryListFragment<MusicDirectory.Entry>() {
override fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?) {
// Triggered only when new data needs to be appended to the list
// Add whatever code is needed to append new items to the bottom of the list
val appendArgs = newBundleClone
val appendArgs = getArgumentsClone()
appendArgs.putBoolean(Constants.INTENT_EXTRA_NAME_APPEND, true)
getLiveData(appendArgs)
}
}
addOnScrollListener(scrollListener)
}
viewAdapter.register(
AlbumRowBinder(
{ entry -> onItemClick(entry) },
{ menuItem, entry -> onContextMenuItemSelected(menuItem, entry) },
imageLoaderProvider.getImageLoader(),
context = requireContext()
)
)
}
override fun onItemClick(item: MusicDirectory.Entry) {
@ -98,4 +94,5 @@ class AlbumListFragment : EntryListFragment<MusicDirectory.Entry>() {
bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, item.parent)
findNavController().navigate(itemClickTarget, bundle)
}
}

View File

@ -1,12 +1,17 @@
package org.moire.ultrasonic.fragment
import android.os.Bundle
import android.view.View
import androidx.fragment.app.viewModels
import androidx.lifecycle.LiveData
import androidx.navigation.fragment.findNavController
import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.ArtistRowAdapter
import org.moire.ultrasonic.adapters.ArtistRowBinder
import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.domain.ArtistOrIndex
import org.moire.ultrasonic.model.ArtistListModel
import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.Settings
/**
* Displays the list of Artists from the media library
@ -39,6 +44,7 @@ class ArtistListFragment : EntryListFragment<ArtistOrIndex>() {
*/
override val itemClickTarget = R.id.selectArtistToSelectAlbum
/**
* The central function to pass a query to the model and return a LiveData object
*/
@ -47,17 +53,31 @@ class ArtistListFragment : EntryListFragment<ArtistOrIndex>() {
return listModel.getItems(refresh, refreshListView!!)
}
/**
* Provide the Adapter for the RecyclerView with a lazy delegate
*/
// FIXME
// override val viewAdapter: ArtistRowAdapter by lazy {
// ArtistRowAdapter(
// liveDataItems.value ?: listOf(),
// { entry -> onItemClick(entry) },
// { menuItem, entry -> onContextMenuItemSelected(menuItem, entry) },
// imageLoaderProvider.getImageLoader(),
// onMusicFolderUpdate
// )
// }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewAdapter.register(
ArtistRowBinder(
{ entry -> onItemClick(entry) },
{ menuItem, entry -> onContextMenuItemSelected(menuItem, entry) },
imageLoaderProvider.getImageLoader()
)
)
}
override fun onItemClick(item: ArtistOrIndex) {
val bundle = Bundle()
bundle.putString(Constants.INTENT_EXTRA_NAME_ID, item.id)
bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, item.name)
bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, item.id)
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, (item is Artist))
bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, Constants.ALPHABETICAL_BY_NAME)
bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, item.name)
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 1000)
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0)
findNavController().navigate(itemClickTarget, bundle)
}
//Constants.ALPHABETICAL_BY_NAME
}

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.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.TrackViewBinder
import org.moire.ultrasonic.model.GenericListModel
import org.moire.ultrasonic.service.DownloadFile
import org.moire.ultrasonic.service.Downloader
import org.moire.ultrasonic.util.Util

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.data.ActiveServerProvider
import org.moire.ultrasonic.data.ServerSetting
import org.moire.ultrasonic.model.ServerSettingsModel
import org.moire.ultrasonic.service.MusicServiceFactory
import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.ErrorDialog

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.viewModels
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.MultiTypeDiffAdapter
import org.moire.ultrasonic.adapters.BaseAdapter
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.domain.MusicFolder
import org.moire.ultrasonic.model.GenericListModel
import org.moire.ultrasonic.model.ServerSettingsModel
import org.moire.ultrasonic.subsonic.DownloadHandler
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Util
import org.moire.ultrasonic.view.SelectMusicFolderView
@ -43,8 +44,8 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
* The Adapter for the RecyclerView
* Recommendation: Implement this as a lazy delegate
*/
internal val viewAdapter: MultiTypeDiffAdapter<Identifiable> by lazy {
MultiTypeDiffAdapter()
internal val viewAdapter: BaseAdapter<Identifiable> by lazy {
BaseAdapter()
}
/**
@ -61,7 +62,9 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
/**
* The central function to pass a query to the model and return a LiveData object
*/
abstract fun getLiveData(args: Bundle? = null): LiveData<List<T>>
open fun getLiveData(args: Bundle? = null): LiveData<List<T>> {
return MutableLiveData(listOf())
}
/**
* The id of the target in the navigation graph where we should go,
@ -84,35 +87,6 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
*/
open val recyclerViewId = R.id.generic_list_recycler
/**
* The observer to be called if the available music folders have changed
*/
@Suppress("CommentOverPrivateProperty")
private val musicFolderObserver = { folders: List<MusicFolder> ->
// viewAdapter.setFolderList(folders, listModel.activeServer.musicFolderId)
}
/**
* What to do when the user has modified the folder filter
*/
val onMusicFolderUpdate = { selectedFolderId: String? ->
if (!listModel.isOffline()) {
val currentSetting = listModel.activeServer
currentSetting.musicFolderId = selectedFolderId
serverSettingsModel.updateItem(currentSetting)
}
viewAdapter.notifyDataSetChanged()
listModel.refresh(refreshListView!!, arguments)
}
/**
* Whether to show the folder selector
*/
fun showFolderHeader(): Boolean {
return listModel.showSelectFolderHeader(arguments) &&
!listModel.isOffline() && !Settings.shouldUseId3Tags
}
open fun setTitle(title: String?) {
if (title == null) {
FragmentTitle.setTitle(
@ -150,9 +124,6 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
}
)
// Setup the Music folder handling
listModel.getMusicFolders().observe(viewLifecycleOwner, musicFolderObserver)
// Create a View Manager
viewManager = LinearLayoutManager(this.context)
@ -184,103 +155,17 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
abstract fun onContextMenuItemSelected(menuItem: MenuItem, item: T): Boolean
abstract fun onItemClick(item: T)
fun getArgumentsClone(): Bundle {
var bundle: Bundle
try {
bundle = arguments?.clone() as Bundle
} catch (ignored: Exception) {
bundle = Bundle()
}
return bundle
}
}
// abstract class EntryListFragment<T : GenericEntry, TA : GenericRowAdapter<T>> :
// GenericListFragment<T, TA>() {
// @Suppress("LongMethod")
// override fun onContextMenuItemSelected(menuItem: MenuItem, item: T): Boolean {
// val isArtist = (item is Artist)
//
// when (menuItem.itemId) {
// R.id.menu_play_now ->
// downloadHandler.downloadRecursively(
// this,
// item.id,
// save = false,
// append = false,
// autoPlay = true,
// shuffle = false,
// background = false,
// playNext = false,
// unpin = false,
// isArtist = isArtist
// )
// R.id.menu_play_next ->
// downloadHandler.downloadRecursively(
// this,
// item.id,
// save = false,
// append = false,
// autoPlay = true,
// shuffle = true,
// background = false,
// playNext = true,
// unpin = false,
// isArtist = isArtist
// )
// R.id.menu_play_last ->
// downloadHandler.downloadRecursively(
// this,
// item.id,
// save = false,
// append = true,
// autoPlay = false,
// shuffle = false,
// background = false,
// playNext = false,
// unpin = false,
// isArtist = isArtist
// )
// R.id.menu_pin ->
// downloadHandler.downloadRecursively(
// this,
// item.id,
// save = true,
// append = true,
// autoPlay = false,
// shuffle = false,
// background = false,
// playNext = false,
// unpin = false,
// isArtist = isArtist
// )
// R.id.menu_unpin ->
// downloadHandler.downloadRecursively(
// this,
// item.id,
// save = false,
// append = false,
// autoPlay = false,
// shuffle = false,
// background = false,
// playNext = false,
// unpin = true,
// isArtist = isArtist
// )
// R.id.menu_download ->
// downloadHandler.downloadRecursively(
// this,
// item.id,
// save = false,
// append = false,
// autoPlay = false,
// shuffle = false,
// background = true,
// playNext = false,
// unpin = false,
// isArtist = isArtist
// )
// }
// return true
// }
//
// override fun onItemClick(item: T) {
// val bundle = Bundle()
// bundle.putString(Constants.INTENT_EXTRA_NAME_ID, item.id)
// bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, item.name)
// bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, item.id)
// bundle.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, (item is Artist))
// findNavController().navigate(itemClickTarget, bundle)
// }
// }

View File

@ -60,7 +60,7 @@ import org.koin.android.ext.android.inject
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.MultiTypeDiffAdapter
import org.moire.ultrasonic.adapters.BaseAdapter
import org.moire.ultrasonic.adapters.TrackViewBinder
import org.moire.ultrasonic.audiofx.EqualizerController
import org.moire.ultrasonic.audiofx.VisualizerController
@ -154,8 +154,8 @@ class PlayerFragment :
private lateinit var fullStar: Drawable
private lateinit var progressBar: SeekBar
internal val viewAdapter: MultiTypeDiffAdapter<Identifiable> by lazy {
MultiTypeDiffAdapter()
internal val viewAdapter: BaseAdapter<Identifiable> by lazy {
BaseAdapter()
}
override fun onCreate(savedInstanceState: Bundle?) {
@ -890,7 +890,7 @@ class PlayerFragment :
// FIXME:
// Needs to be changed in the playlist as well...
// Move it in the data set
(recyclerView.adapter as MultiTypeDiffAdapter<*>).moveItem(from, to)
(recyclerView.adapter as BaseAdapter<*>).moveItem(from, to)
return true
}

View File

@ -18,6 +18,7 @@ import org.koin.androidx.viewmodel.ext.android.viewModel
import org.moire.ultrasonic.R
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.fragment.EditServerFragment.Companion.EDIT_SERVER_INTENT_INDEX
import org.moire.ultrasonic.model.ServerSettingsModel
import org.moire.ultrasonic.service.MediaPlayerController
import org.moire.ultrasonic.util.Util
import timber.log.Timber

View File

@ -25,7 +25,6 @@ import androidx.lifecycle.viewModelScope
import androidx.navigation.Navigation
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import java.util.Collections
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
@ -36,9 +35,11 @@ import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
import org.moire.ultrasonic.model.TrackCollectionModel
import org.moire.ultrasonic.service.MediaPlayerController
import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker
import org.moire.ultrasonic.subsonic.ShareHandler
import org.moire.ultrasonic.subsonic.VideoPlayer
import org.moire.ultrasonic.util.AlbumHeader
import org.moire.ultrasonic.util.CancellationToken
import org.moire.ultrasonic.util.CommunicationError
@ -47,35 +48,34 @@ import org.moire.ultrasonic.util.EntryByDiscAndTrackComparator
import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Util
import timber.log.Timber
import java.util.Collections
/**
* Displays a group of tracks, eg. the songs of an album, of a playlist etc.
* TODO: Move Clickhandler into ViewBinders
* TODO: Fix clikc handlers and context menus etc.
* FIXME: Offset when navigating to?
*/
class TrackCollectionFragment :
MultiListFragment<MusicDirectory.Entry>() {
open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Entry>() {
private var albumButtons: View? = null
private var emptyView: TextView? = null
private var selectButton: ImageView? = null
private var playNowButton: ImageView? = null
private var playNextButton: ImageView? = null
private var playLastButton: ImageView? = null
private var pinButton: ImageView? = null
private var unpinButton: ImageView? = null
private var downloadButton: ImageView? = null
private var deleteButton: ImageView? = null
private var moreButton: ImageView? = null
internal var selectButton: ImageView? = null
internal var playNowButton: ImageView? = null
internal var playNextButton: ImageView? = null
internal var playLastButton: ImageView? = null
internal var pinButton: ImageView? = null
internal var unpinButton: ImageView? = null
internal var downloadButton: ImageView? = null
internal var deleteButton: ImageView? = null
internal var moreButton: ImageView? = null
private var playAllButtonVisible = false
private var shareButtonVisible = false
private var playAllButton: MenuItem? = null
private var shareButton: MenuItem? = null
private val mediaPlayerController: MediaPlayerController by inject()
internal val mediaPlayerController: MediaPlayerController by inject()
private val networkAndStorageChecker: NetworkAndStorageChecker by inject()
private val shareHandler: ShareHandler by inject()
private var cancellationToken: CancellationToken? = null
internal var cancellationToken: CancellationToken? = null
override val listModel: TrackCollectionModel by viewModels()
@ -98,7 +98,6 @@ class TrackCollectionFragment :
* The id of the target in the navigation graph where we should go,
* after the user has clicked on an item
*/
// FIXME
override val itemClickTarget: Int = R.id.trackCollectionFragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -110,90 +109,15 @@ class TrackCollectionFragment :
// Setup refresh handler
refreshListView = view.findViewById(refreshListId)
refreshListView?.setOnRefreshListener {
updateDisplay(true)
refreshData(true)
}
listModel.currentList.observe(viewLifecycleOwner, updateInterfaceWithEntries)
listModel.songsForGenre.observe(viewLifecycleOwner, songsForGenreObserver)
// listView!!.setOnItemClickListener { parent, theView, position, _ ->
// if (position >= 0) {
// val entry = parent.getItemAtPosition(position) as MusicDirectory.Entry?
// if (entry != null && entry.isDirectory) {
// val bundle = Bundle()
// bundle.putString(Constants.INTENT_EXTRA_NAME_ID, entry.id)
// bundle.putBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, entry.isDirectory)
// bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, entry.title)
// bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, entry.parent)
// Navigation.findNavController(theView).navigate(
// R.id.trackCollectionFragment,
// bundle
// )
// } else if (entry != null && entry.isVideo) {
// VideoPlayer.playVideo(requireContext(), entry)
// } else {
// enableButtons()
// }
// }
// }
//
// listView!!.setOnItemLongClickListener { _, theView, _, _ ->
// if (theView is AlbumView) {
// return@setOnItemLongClickListener false
// }
// if (theView is SongView) {
// theView.maximizeOrMinimize()
// return@setOnItemLongClickListener true
// }
// return@setOnItemLongClickListener false
// }
setupButtons(view)
selectButton = view.findViewById(R.id.select_album_select)
playNowButton = view.findViewById(R.id.select_album_play_now)
playNextButton = view.findViewById(R.id.select_album_play_next)
playLastButton = view.findViewById(R.id.select_album_play_last)
pinButton = view.findViewById(R.id.select_album_pin)
unpinButton = view.findViewById(R.id.select_album_unpin)
downloadButton = view.findViewById(R.id.select_album_download)
deleteButton = view.findViewById(R.id.select_album_delete)
moreButton = view.findViewById(R.id.select_album_more)
emptyView = TextView(requireContext())
selectButton!!.setOnClickListener {
selectAllOrNone()
}
playNowButton!!.setOnClickListener {
playNow(false)
}
playNextButton!!.setOnClickListener {
downloadHandler.download(
this@TrackCollectionFragment, append = true,
save = false, autoPlay = false, playNext = true, shuffle = false,
songs = getSelectedSongs()
)
}
playLastButton!!.setOnClickListener {
playNow(true)
}
pinButton!!.setOnClickListener {
downloadBackground(true)
}
unpinButton!!.setOnClickListener {
unpin()
}
downloadButton!!.setOnClickListener {
downloadBackground(false)
}
deleteButton!!.setOnClickListener {
delete()
}
emptyView = view.findViewById(R.id.select_album_empty)
registerForContextMenu(listView!!)
setHasOptionsMenu(true)
@ -234,19 +158,68 @@ class TrackCollectionFragment :
)
// Loads the data
updateDisplay(false)
refreshData(false)
}
internal open fun setupButtons(view: View) {
selectButton = view.findViewById(R.id.select_album_select)
playNowButton = view.findViewById(R.id.select_album_play_now)
playNextButton = view.findViewById(R.id.select_album_play_next)
playLastButton = view.findViewById(R.id.select_album_play_last)
pinButton = view.findViewById(R.id.select_album_pin)
unpinButton = view.findViewById(R.id.select_album_unpin)
downloadButton = view.findViewById(R.id.select_album_download)
deleteButton = view.findViewById(R.id.select_album_delete)
moreButton = view.findViewById(R.id.select_album_more)
selectButton?.setOnClickListener {
selectAllOrNone()
}
playNowButton?.setOnClickListener {
playNow(false)
}
playNextButton?.setOnClickListener {
downloadHandler.download(
this@TrackCollectionFragment, append = true,
save = false, autoPlay = false, playNext = true, shuffle = false,
songs = getSelectedSongs()
)
}
playLastButton!!.setOnClickListener {
playNow(true)
}
pinButton?.setOnClickListener {
downloadBackground(true)
}
unpinButton?.setOnClickListener {
unpin()
}
downloadButton?.setOnClickListener {
downloadBackground(false)
}
deleteButton?.setOnClickListener {
delete()
}
}
val handler = CoroutineExceptionHandler { _, exception ->
Handler(Looper.getMainLooper()).post {
CommunicationError.handleError(exception, context)
}
refreshListView!!.isRefreshing = false
refreshListView?.isRefreshing = false
}
private fun updateDisplay(refresh: Boolean) {
// FIXME: Use refresh
getLiveData(requireArguments())
private fun refreshData(refresh: Boolean = false) {
val args = getArgumentsClone()
args.putBoolean(Constants.INTENT_EXTRA_NAME_REFRESH, refresh)
getLiveData(args)
}
override fun onContextItemSelected(menuItem: MenuItem): Boolean {
@ -370,7 +343,6 @@ class TrackCollectionFragment :
this, append, false, !append, playNext = false,
shuffle = false, songs = selectedSongs
)
selectAll(selected = false, toast = false)
} else {
playAll(false, append)
}
@ -399,8 +371,10 @@ class TrackCollectionFragment :
}
}
val isArtist = requireArguments().getBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, false)
val id = requireArguments().getString(Constants.INTENT_EXTRA_NAME_ID)
val isArtist = arguments?.getBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, false)?: false
// FIXME WHICH id if no arguments?
val id = arguments?.getString(Constants.INTENT_EXTRA_NAME_ID)
if (hasSubFolders && id != null) {
downloadHandler.downloadRecursively(
@ -435,13 +409,13 @@ class TrackCollectionFragment :
} as List<MusicDirectory.Entry>
}
private fun selectAllOrNone() {
internal fun selectAllOrNone() {
val someUnselected = viewAdapter.selectedSet.size < childCount
selectAll(someUnselected, true)
}
private fun selectAll(selected: Boolean, toast: Boolean) {
internal fun selectAll(selected: Boolean, toast: Boolean) {
var selectedCount = viewAdapter.selectedSet.size * -1
selectedCount += viewAdapter.setSelectionStatusOfAll(selected)
@ -453,7 +427,7 @@ class TrackCollectionFragment :
}
}
private fun enableButtons(selection: List<MusicDirectory.Entry> = getSelectedSongs()) {
internal open fun enableButtons(selection: List<MusicDirectory.Entry> = getSelectedSongs()) {
val enabled = selection.isNotEmpty()
var unpinEnabled = false
var deleteEnabled = false
@ -480,7 +454,7 @@ class TrackCollectionFragment :
deleteButton?.isVisible = (enabled && deleteEnabled)
}
private fun downloadBackground(save: Boolean) {
internal fun downloadBackground(save: Boolean) {
var songs = getSelectedSongs()
if (songs.isEmpty()) {
@ -514,7 +488,7 @@ class TrackCollectionFragment :
onValid.run()
}
private fun delete() {
internal fun delete() {
val songs = getSelectedSongs()
Util.toast(
@ -527,7 +501,7 @@ class TrackCollectionFragment :
mediaPlayerController.delete(songs)
}
private fun unpin() {
internal fun unpin() {
val songs = getSelectedSongs()
Util.toast(
context,
@ -586,23 +560,17 @@ class TrackCollectionFragment :
}
}
val listSize = requireArguments().getInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0)
val listSize = arguments?.getInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0) ?: 0
// Hide select button for video lists
selectButton!!.isVisible = !allVideos
if (songCount > 0) {
pinButton!!.visibility = View.VISIBLE
unpinButton!!.visibility = View.VISIBLE
downloadButton!!.visibility = View.VISIBLE
deleteButton!!.visibility = View.VISIBLE
selectButton!!.visibility = if (allVideos) View.GONE else View.VISIBLE
playNowButton!!.visibility = View.VISIBLE
playNextButton!!.visibility = View.VISIBLE
playLastButton!!.visibility = View.VISIBLE
if (listSize == 0 || songCount < listSize) {
moreButton!!.visibility = View.GONE
} else {
moreButton!!.visibility = View.VISIBLE
if (requireArguments().getInt(Constants.INTENT_EXTRA_NAME_RANDOM, 0) > 0) {
if (arguments?.getInt(Constants.INTENT_EXTRA_NAME_RANDOM, 0) ?:0 > 0) {
moreButton!!.setOnClickListener {
val offset = requireArguments().getInt(
Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0
@ -617,58 +585,41 @@ class TrackCollectionFragment :
}
}
}
} else {
// TODO: This code path can be removed when getArtist has been moved to
// AlbumListFragment (getArtist returns the albums of an artist)
pinButton!!.visibility = View.GONE
unpinButton!!.visibility = View.GONE
downloadButton!!.visibility = View.GONE
deleteButton!!.visibility = View.GONE
selectButton!!.visibility = View.GONE
playNowButton!!.visibility = View.GONE
playNextButton!!.visibility = View.GONE
playLastButton!!.visibility = View.GONE
if (listSize == 0 || entryList.size < listSize) {
albumButtons!!.visibility = View.GONE
} else {
moreButton!!.visibility = View.VISIBLE
}
}
// Show a text if we have no entries
emptyView?.isVisible = entryList.isEmpty()
enableButtons()
val isAlbumList = requireArguments().containsKey(
val isAlbumList = arguments?.containsKey(
Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE
)
)?:false
playAllButtonVisible = !(isAlbumList || entryList.isEmpty()) && !allVideos
shareButtonVisible = !isOffline() && songCount > 0
if (playAllButton != null) {
playAllButton!!.isVisible = playAllButtonVisible
}
if (shareButton != null) {
shareButton!!.isVisible = shareButtonVisible
}
playAllButton?.isVisible = playAllButtonVisible
shareButton?.isVisible = shareButtonVisible
if (songCount > 0 && listModel.showHeader) {
val name = listModel.currentDirectory.value?.name
val intentAlbumName = requireArguments().getString(Constants.INTENT_EXTRA_NAME_NAME, "Name")!!
val albumHeader = AlbumHeader(it, name ?: intentAlbumName, songCount)
val intentAlbumName = arguments?.getString(Constants.INTENT_EXTRA_NAME_NAME, "")
val albumHeader = AlbumHeader(it, name ?: intentAlbumName)
val mixedList: MutableList<Identifiable> = mutableListOf(albumHeader)
mixedList.addAll(entryList)
Timber.e("SUBMITTING MIXED LIST")
viewAdapter.submitList(mixedList)
} else {
Timber.e("SUBMITTING ENTRY LIST")
viewAdapter.submitList(entryList)
}
val playAll = requireArguments().getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false)
val playAll = arguments?.getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false)?:false
if (playAll && songCount > 0) {
playAll(
requireArguments().getBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE, false),
arguments?.getBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE, false)?:false,
false
)
}
@ -722,9 +673,7 @@ class TrackCollectionFragment :
val refresh = args.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH, true)
listModel.viewModelScope.launch(handler) {
refreshListView!!.isRefreshing = true
listModel.getMusicFolders(refresh)
refreshListView?.isRefreshing = true
if (playlistId != null) {
setTitle(playlistName!!)
@ -753,14 +702,14 @@ class TrackCollectionFragment :
if (isAlbum) {
listModel.getAlbum(refresh, id!!, name, parentId)
} else {
listModel.getArtist(refresh, id!!, name)
throw IllegalAccessException("Use AlbumFragment instead!")
}
} else {
listModel.getMusicDirectory(refresh, id!!, name, parentId)
}
}
refreshListView!!.isRefreshing = false
refreshListView?.isRefreshing = false
}
return listModel.currentList
}
@ -774,6 +723,24 @@ class TrackCollectionFragment :
}
override fun onItemClick(item: MusicDirectory.Entry) {
// nothing
when {
item.isDirectory -> {
val bundle = Bundle()
bundle.putString(Constants.INTENT_EXTRA_NAME_ID, item.id)
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, item.isDirectory)
bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, item.title)
bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, item.parent)
Navigation.findNavController(requireView()).navigate(
R.id.trackCollectionFragment,
bundle
)
}
item.isVideo -> {
VideoPlayer.playVideo(requireContext(), item)
}
else -> {
enableButtons()
}
}
}
}

View File

@ -1,10 +1,11 @@
package org.moire.ultrasonic.fragment
package org.moire.ultrasonic.model
import android.app.Application
import android.os.Bundle
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import org.moire.ultrasonic.R
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.service.MusicService
@ -13,7 +14,9 @@ import org.moire.ultrasonic.util.Settings
class AlbumListModel(application: Application) : GenericListModel(application) {
val albumList: MutableLiveData<List<MusicDirectory.Entry>> = MutableLiveData(listOf())
val list: MutableLiveData<List<MusicDirectory.Entry>> = MutableLiveData(listOf())
var lastType: String? = null
private var loadedUntil: Int = 0
@ -26,11 +29,37 @@ class AlbumListModel(application: Application) : GenericListModel(application) {
// This way, we keep the scroll position
val albumListType = args.getString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE)!!
if (refresh || albumList.value!!.isEmpty() || albumListType != lastType) {
if (refresh || list.value!!.isEmpty() || albumListType != lastType) {
lastType = albumListType
backgroundLoadFromServer(refresh, swipe, args)
}
return albumList
return list
}
fun getAlbumsOfArtist(musicService: MusicService, refresh: Boolean, id: String, name: String?) {
var root = MusicDirectory()
val musicDirectory = musicService.getArtist(id, name, refresh)
if (Settings.shouldShowAllSongsByArtist &&
musicDirectory.findChild(allSongsId) == null &&
hasOnlyFolders(musicDirectory)
) {
val allSongs = MusicDirectory.Entry(allSongsId)
allSongs.isDirectory = true
allSongs.artist = name
allSongs.parent = id
allSongs.title = String.format(
context.resources.getString(R.string.select_album_all_songs), name
)
root.addFirst(allSongs)
root.addAll(musicDirectory.getChildren())
} else {
root = musicDirectory
}
list.postValue(root.getChildren())
}
override fun load(
@ -58,6 +87,15 @@ class AlbumListModel(application: Application) : GenericListModel(application) {
// If appending the existing list, set the offset from where to load
if (append) offset += (size + loadedUntil)
if (albumListType == Constants.ALBUMS_OF_ARTIST) {
return getAlbumsOfArtist(
musicService,
refresh,
args.getString(Constants.INTENT_EXTRA_NAME_ID, ""),
args.getString(Constants.INTENT_EXTRA_NAME_NAME, "")
)
}
if (useId3Tags) {
musicDirectory = musicService.getAlbumList2(
albumListType, size,
@ -72,13 +110,13 @@ class AlbumListModel(application: Application) : GenericListModel(application) {
currentListIsSortable = isCollectionSortable(albumListType)
if (append && albumList.value != null) {
if (append && list.value != null) {
val list = ArrayList<MusicDirectory.Entry>()
list.addAll(albumList.value!!)
list.addAll(this.list.value!!)
list.addAll(musicDirectory.getAllChild())
albumList.postValue(list)
this.list.postValue(list)
} else {
albumList.postValue(musicDirectory.getAllChild())
list.postValue(musicDirectory.getAllChild())
}
loadedUntil = offset
@ -100,4 +138,5 @@ class AlbumListModel(application: Application) : GenericListModel(application) {
albumListType != "highest" && albumListType != "recent" &&
albumListType != "frequent"
}
}

View File

@ -16,7 +16,7 @@
Copyright 2020 (C) Jozsef Varga
*/
package org.moire.ultrasonic.fragment
package org.moire.ultrasonic.model
import android.app.Application
import android.os.Bundle

View File

@ -1,4 +1,4 @@
package org.moire.ultrasonic.fragment
package org.moire.ultrasonic.model
import android.app.Application
import android.content.Context
@ -17,6 +17,7 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.data.ServerSetting
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.domain.MusicFolder
import org.moire.ultrasonic.service.MusicService
import org.moire.ultrasonic.service.MusicServiceFactory
@ -45,8 +46,6 @@ open class GenericListModel(application: Application) :
return true
}
internal val musicFolders: MutableLiveData<List<MusicFolder>> = MutableLiveData(listOf())
/**
* Helper function to check online status
*/
@ -110,16 +109,20 @@ open class GenericListModel(application: Application) :
) {
// Update the list of available folders if enabled
if (showSelectFolderHeader(args) && !isOffline && !useId3Tags) {
musicFolders.postValue(
musicService.getMusicFolders(refresh)
)
//FIXME
}
}
/**
* Retrieves the available Music Folders in a LiveData
* Some shared helper functions
*/
fun getMusicFolders(): LiveData<List<MusicFolder>> {
return musicFolders
}
// Returns true if the directory contains only folders
internal fun hasOnlyFolders(musicDirectory: MusicDirectory) =
musicDirectory.getChildren(includeDirs = true, includeFiles = false).size ==
musicDirectory.getChildren(includeDirs = true, includeFiles = true).size
internal val allSongsId = "-1"
}

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.content.SharedPreferences

View File

@ -5,7 +5,7 @@
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.fragment
package org.moire.ultrasonic.model
import android.app.Application
import android.os.Bundle
@ -22,25 +22,13 @@ import org.moire.ultrasonic.util.Util
/*
* Model for retrieving different collections of tracks from the API
* TODO: Refactor this model to extend the GenericListModel
*/
class TrackCollectionModel(application: Application) : GenericListModel(application) {
private val allSongsId = "-1"
val currentDirectory: MutableLiveData<MusicDirectory> = MutableLiveData()
val currentList: MutableLiveData<List<MusicDirectory.Entry>> = MutableLiveData()
val songsForGenre: MutableLiveData<MusicDirectory> = MutableLiveData()
suspend fun getMusicFolders(refresh: Boolean) {
withContext(Dispatchers.IO) {
if (!isOffline()) {
val musicService = MusicServiceFactory.getMusicService()
musicFolders.postValue(musicService.getMusicFolders(refresh))
}
}
}
suspend fun getMusicDirectory(
refresh: Boolean,
id: String,
@ -94,9 +82,6 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
}
}
private fun updateList(root: MusicDirectory) {
currentList.postValue(root.getChildren())
}
// Given a Music directory "songs" it recursively adds all children to "songs"
private fun getSongsRecursively(
@ -122,42 +107,6 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
}
}
/*
* TODO: This method should be moved to AlbumListModel,
* since it displays a list of albums by a specified artist.
*/
suspend fun getArtist(refresh: Boolean, id: String, name: String?) {
withContext(Dispatchers.IO) {
val service = MusicServiceFactory.getMusicService()
var root = MusicDirectory()
val musicDirectory = service.getArtist(id, name, refresh)
if (Settings.shouldShowAllSongsByArtist &&
musicDirectory.findChild(allSongsId) == null &&
hasOnlyFolders(musicDirectory)
) {
val allSongs = MusicDirectory.Entry(allSongsId)
allSongs.isDirectory = true
allSongs.artist = name
allSongs.parent = id
allSongs.title = String.format(
context.resources.getString(R.string.select_album_all_songs), name
)
root.addFirst(allSongs)
root.addAll(musicDirectory.getChildren())
} else {
root = musicDirectory
}
currentDirectory.postValue(root)
updateList(root)
}
}
suspend fun getAlbum(refresh: Boolean, id: String, name: String?, parentId: String?) {
withContext(Dispatchers.IO) {
@ -296,18 +245,17 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
}
}
// Returns true if the directory contains only folders
private fun hasOnlyFolders(musicDirectory: MusicDirectory) =
musicDirectory.getChildren(includeDirs = true, includeFiles = false).size ==
musicDirectory.getChildren(includeDirs = true, includeFiles = true).size
override fun load(
isOffline: Boolean,
useId3Tags: Boolean,
musicService: MusicService,
refresh: Boolean,
args: Bundle
) {
// See To_Do at the top
suspend fun getBookmarks() {
withContext(Dispatchers.IO) {
val service = MusicServiceFactory.getMusicService()
val musicDirectory = Util.getSongsFromBookmarks(service.getBookmarks())
currentDirectory.postValue(musicDirectory)
updateList(musicDirectory)
}
}
private fun updateList(root: MusicDirectory) {
currentList.postValue(root.getChildren())
}
}

View File

@ -399,7 +399,7 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
}
@Throws(Exception::class)
override fun getBookmarks(): List<Bookmark?>? = musicService.getBookmarks()
override fun getBookmarks(): List<Bookmark> = musicService.getBookmarks()
@Throws(Exception::class)
override fun deleteBookmark(id: String) {

View File

@ -154,7 +154,7 @@ interface MusicService {
fun addChatMessage(message: String)
@Throws(Exception::class)
fun getBookmarks(): List<Bookmark?>?
fun getBookmarks(): List<Bookmark>
@Throws(Exception::class)
fun deleteBookmark(id: String)

View File

@ -411,7 +411,7 @@ class OfflineMusicService : MusicService, KoinComponent {
}
@Throws(OfflineException::class)
override fun getBookmarks(): List<Bookmark?>? {
override fun getBookmarks(): List<Bookmark> {
throw OfflineException("getBookmarks isn't available in offline mode")
}

View File

@ -30,6 +30,11 @@ class RxBus {
val themeChangedEventObservable: Observable<Unit> =
themeChangedEventPublisher.observeOn(AndroidSchedulers.mainThread())
val musicFolderChangedEventPublisher: PublishSubject<String> =
PublishSubject.create()
val musicFolderChangedEventObservable: Observable<String> =
musicFolderChangedEventPublisher.observeOn(AndroidSchedulers.mainThread())
val playerStatePublisher: PublishSubject<StateWithTrack> =
PublishSubject.create()
val playerStateObservable: Observable<StateWithTrack> =
@ -73,6 +78,7 @@ class RxBus {
val skipToQueueItemCommandObservable: Observable<Long> =
skipToQueueItemCommandPublisher.observeOn(AndroidSchedulers.mainThread())
fun releaseMediaSessionToken() {
mediaSessionTokenPublisher = PublishSubject.create()
}

View File

@ -121,5 +121,6 @@ object Constants {
const val ALBUM_ART_FILE = "folder.jpeg"
const val STARRED = "starred"
const val ALPHABETICAL_BY_NAME = "alphabeticalByName"
const val ALBUMS_OF_ARTIST = "albumsOfArtist"
const val RESULT_CLOSE_ALL = 1337
}

View File

@ -4,7 +4,7 @@ import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.ItemTouchHelper.DOWN
import androidx.recyclerview.widget.ItemTouchHelper.UP
import androidx.recyclerview.widget.RecyclerView
import org.moire.ultrasonic.adapters.MultiTypeDiffAdapter
import org.moire.ultrasonic.adapters.BaseAdapter
import timber.log.Timber
class DragSortCallback : ItemTouchHelper.SimpleCallback(UP or DOWN, 0) {
@ -21,7 +21,7 @@ class DragSortCallback : ItemTouchHelper.SimpleCallback(UP or DOWN, 0) {
Timber.w("MOVED %s %s", to, from)
// Move it in the data set
(recyclerView.adapter as MultiTypeDiffAdapter<*>).moveItem(from, to)
(recyclerView.adapter as BaseAdapter<*>).moveItem(from, to)
return true
}

View File

@ -525,11 +525,10 @@ object Util {
}
@JvmStatic
fun getSongsFromBookmarks(bookmarks: Iterable<Bookmark?>): MusicDirectory {
fun getSongsFromBookmarks(bookmarks: Iterable<Bookmark>): MusicDirectory {
val musicDirectory = MusicDirectory()
var song: MusicDirectory.Entry
for (bookmark in bookmarks) {
if (bookmark == null) continue
song = bookmark.entry
song.bookmarkPosition = bookmark.position
musicDirectory.addChild(song)

View File

@ -13,7 +13,8 @@
android:scaleType="fitCenter"
android:layout_weight="1"
android:src="?attr/select_all"
android:visibility="gone" />
android:visibility="gone"
android:contentDescription="@string/common.select_all" />
<ImageView
android:id="@+id/select_album_play_now"
@ -22,7 +23,8 @@
android:scaleType="fitCenter"
android:layout_weight="1"
android:src="?attr/media_play"
android:visibility="gone" />
android:visibility="gone"
android:contentDescription="@string/common.play_now" />
<ImageView
android:id="@+id/select_album_play_next"
@ -31,7 +33,8 @@
android:scaleType="fitCenter"
android:layout_weight="1"
android:src="?attr/media_play_next"
android:visibility="gone" />
android:visibility="gone"
android:contentDescription="@string/common.play_next" />
<ImageView
android:id="@+id/select_album_play_last"
@ -40,7 +43,8 @@
android:scaleType="fitCenter"
android:layout_weight="1"
android:src="?attr/add_to_queue"
android:visibility="gone" />
android:visibility="gone"
android:contentDescription="@string/common.play_last" />
<ImageView
android:id="@+id/select_album_pin"
@ -49,7 +53,8 @@
android:scaleType="fitCenter"
android:layout_weight="1"
android:src="?attr/pin"
android:visibility="gone" />
android:visibility="gone"
android:contentDescription="@string/common.pin" />
<ImageView
android:id="@+id/select_album_unpin"
@ -58,7 +63,8 @@
android:scaleType="fitCenter"
android:layout_weight="1"
android:src="?attr/unpin"
android:visibility="gone" />
android:visibility="gone"
android:contentDescription="@string/common.unpin" />
<ImageView
android:id="@+id/select_album_download"
@ -67,7 +73,8 @@
android:scaleType="fitCenter"
android:layout_weight="1"
android:src="?attr/download"
android:visibility="gone" />
android:visibility="gone"
android:contentDescription="@string/common.download" />
<ImageView
android:id="@+id/select_album_delete"
@ -76,7 +83,8 @@
android:scaleType="fitCenter"
android:layout_weight="1"
android:src="?attr/stop"
android:visibility="gone" />
android:visibility="gone"
android:contentDescription="@string/common.delete" />
<ImageView
android:id="@+id/select_album_more"
@ -85,6 +93,7 @@
android:scaleType="fitCenter"
android:layout_weight="1"
android:src="?attr/forward"
android:visibility="gone" />
android:visibility="gone"
android:contentDescription="@string/search.more" />
</LinearLayout>

View File

@ -10,7 +10,7 @@
a:layout_height="0dip"
a:layout_weight="1.0">
<ListView
<androidx.recyclerview.widget.RecyclerView
a:id="@+id/search_list"
a:layout_width="fill_parent"
a:layout_height="0dip"

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" >
<action
android:id="@+id/selectArtistToSelectAlbum"
app:destination="@id/trackCollectionFragment" />
app:destination="@id/albumListFragment" />
</fragment>
<fragment
android:id="@+id/artistListFragment"
android:name="org.moire.ultrasonic.fragment.ArtistListFragment" >
<action
android:id="@+id/selectArtistToSelectAlbum"
app:destination="@id/trackCollectionFragment" />
app:destination="@id/albumListFragment" />
</fragment>
<fragment
android:id="@+id/trackCollectionFragment"

View File

@ -50,6 +50,7 @@
<string name="common.play_shuffled">Play Shuffled</string>
<string name="common.public">Public</string>
<string name="common.save">Save</string>
<string name="common.select_all">Select all</string>
<string name="common.title">Title</string>
<string name="common.unpin">Unpin</string>
<string name="common.various_artists">Various Artists</string>