From f8a87f7c85841a57156c7cdbabc98f69f51cd534 Mon Sep 17 00:00:00 2001 From: tzugen Date: Tue, 23 Nov 2021 20:38:26 +0100 Subject: [PATCH] BookmarksFragment is now based on TrackCollectionFragment Also start SearchFragment.kt --- .../moire/ultrasonic/domain/SearchResult.kt | 6 +- .../fragment/BookmarksFragment.java | 387 ------------ .../ultrasonic/fragment/SearchFragment.java | 593 ------------------ .../ultrasonic/fragment/SearchFragment.kt | 555 ++++++++++++++++ .../org/moire/ultrasonic/util/AlbumHeader.kt | 3 +- .../moire/ultrasonic/view/PlaylistView.java | 9 +- ...mView.java => PodcastChannelItemView.java} | 11 +- .../org/moire/ultrasonic/view/ShareView.java | 11 +- .../ultrasonic/activity/NavigationActivity.kt | 2 +- .../{AlbumRowAdapter.kt => AlbumRowBinder.kt} | 80 ++- .../ultrasonic/adapters/ArtistRowAdapter.kt | 106 ---- .../ultrasonic/adapters/ArtistRowBinder.kt | 114 ++++ ...MultiTypeDiffAdapter.kt => BaseAdapter.kt} | 4 +- .../adapters/FolderSelectorBinder.kt | 127 ++++ .../ultrasonic/adapters/GenericRowAdapter.kt | 149 ----- .../ultrasonic/adapters/HeaderViewBinder.kt | 8 +- .../org/moire/ultrasonic/adapters/Helper.kt | 22 + .../ultrasonic/adapters/SectionedAdapter.kt | 18 + .../ultrasonic/adapters/ServerRowAdapter.kt | 1 + .../ultrasonic/adapters/TrackViewBinder.kt | 25 +- .../ultrasonic/adapters/TrackViewHolder.kt | 76 +-- .../di/AppPermanentStorageModule.kt | 2 +- .../ultrasonic/fragment/AlbumListFragment.kt | 37 +- .../ultrasonic/fragment/ArtistListFragment.kt | 48 +- .../ultrasonic/fragment/BookmarksFragment.kt | 66 ++ .../ultrasonic/fragment/DownloadsFragment.kt | 1 + .../ultrasonic/fragment/EditServerFragment.kt | 1 + .../ultrasonic/fragment/EntryListFragment.kt | 140 +++++ .../fragment/GenericListFragment.kt | 281 --------- .../ultrasonic/fragment/MultiListFragment.kt | 157 +---- .../ultrasonic/fragment/PlayerFragment.kt | 8 +- .../fragment/ServerSelectorFragment.kt | 1 + .../fragment/TrackCollectionFragment.kt | 281 ++++----- .../{fragment => model}/AlbumListModel.kt | 55 +- .../{fragment => model}/ArtistListModel.kt | 2 +- .../{fragment => model}/GenericListModel.kt | 23 +- .../moire/ultrasonic/model/SearchListModel.kt | 73 +++ .../ServerSettingsModel.kt | 2 +- .../TrackCollectionModel.kt | 78 +-- .../ultrasonic/service/CachedMusicService.kt | 2 +- .../moire/ultrasonic/service/MusicService.kt | 2 +- .../ultrasonic/service/OfflineMusicService.kt | 2 +- .../org/moire/ultrasonic/service/RxBus.kt | 6 + .../org/moire/ultrasonic/util/Constants.kt | 1 + .../moire/ultrasonic/util/DragSortCallback.kt | 4 +- .../kotlin/org/moire/ultrasonic/util/Util.kt | 3 +- .../src/main/res/layout/album_buttons.xml | 27 +- ultrasonic/src/main/res/layout/search.xml | 2 +- .../src/main/res/layout/select_album.xml | 36 -- .../main/res/navigation/navigation_graph.xml | 4 +- ultrasonic/src/main/res/values/strings.xml | 1 + 51 files changed, 1539 insertions(+), 2114 deletions(-) delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/fragment/BookmarksFragment.java delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SearchFragment.java create mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SearchFragment.kt rename ultrasonic/src/main/java/org/moire/ultrasonic/view/{PodcatsChannelItemView.java => PodcastChannelItemView.java} (85%) rename ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/{AlbumRowAdapter.kt => AlbumRowBinder.kt} (60%) delete mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowAdapter.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowBinder.kt rename ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/{MultiTypeDiffAdapter.kt => BaseAdapter.kt} (97%) create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/FolderSelectorBinder.kt delete mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/GenericRowAdapter.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/Helper.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/SectionedAdapter.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt delete mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListFragment.kt rename ultrasonic/src/main/kotlin/org/moire/ultrasonic/{fragment => model}/AlbumListModel.kt (66%) rename ultrasonic/src/main/kotlin/org/moire/ultrasonic/{fragment => model}/ArtistListModel.kt (98%) rename ultrasonic/src/main/kotlin/org/moire/ultrasonic/{fragment => model}/GenericListModel.kt (88%) create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt rename ultrasonic/src/main/kotlin/org/moire/ultrasonic/{fragment => model}/ServerSettingsModel.kt (99%) rename ultrasonic/src/main/kotlin/org/moire/ultrasonic/{fragment => model}/TrackCollectionModel.kt (80%) delete mode 100644 ultrasonic/src/main/res/layout/select_album.xml diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/SearchResult.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/SearchResult.kt index 82479b70..11c4c97c 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/SearchResult.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/SearchResult.kt @@ -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, - val albums: List, - val songs: List + val artists: List = listOf(), + val albums: List = listOf(), + val songs: List = listOf() ) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/BookmarksFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/BookmarksFragment.java deleted file mode 100644 index 9375de80..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/BookmarksFragment.java +++ /dev/null @@ -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 = inject(MediaPlayerController.class); - private final Lazy imageLoader = inject(ImageLoaderProvider.class); - private final Lazy 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 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 getSelectedSongs(ListView albumListView) - { - List 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 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 songs = getSelectedSongs(albumListView); - - if (songs.isEmpty()) - { - selectAll(true, false); - songs = getSelectedSongs(albumListView); - } - - downloadBackground(save, songs); - } - - private void downloadBackground(final boolean save, final List 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 songs = getSelectedSongs(albumListView); - - if (songs.isEmpty()) - { - selectAll(true, false); - songs = getSelectedSongs(albumListView); - } - - mediaPlayerController.getValue().delete(songs); - } - - private void unpin() - { - List 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> - { - public LoadTask() - { - super(BookmarksFragment.this.getActivity(), true, refreshAlbumListView, cancellationToken); - } - - protected abstract MusicDirectory load(MusicService service) throws Exception; - - @Override - protected Pair 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 result) - { - MusicDirectory musicDirectory = result.first; - List 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)); - } - } -} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SearchFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SearchFragment.java deleted file mode 100644 index fd2797c1..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SearchFragment.java +++ /dev/null @@ -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 mediaPlayerControllerLazy = inject(MediaPlayerController.class); - private final Lazy imageLoaderProvider = inject(ImageLoaderProvider.class); - private final Lazy downloadHandler = inject(DownloadHandler.class); - private final Lazy shareHandler = inject(ShareHandler.class); - private final Lazy 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 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 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 task = new FragmentBackgroundTask(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 artists = searchResult.getArtists(); - if (!artists.isEmpty()) - { - mergeAdapter.addView(artistsHeading); - List 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 albums = searchResult.getAlbums(); - if (!albums.isEmpty()) - { - mergeAdapter.addView(albumsHeading); - List 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 songs = searchResult.getSongs(); - if (!songs.isEmpty()) - { - mergeAdapter.addView(songsHeading); - List 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); - } - } -} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SearchFragment.kt b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SearchFragment.kt new file mode 100644 index 00000000..0bd32d29 --- /dev/null +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SearchFragment.kt @@ -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(), 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 = 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) { + 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() + + 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> { + 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) { + + } +} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/AlbumHeader.kt b/ultrasonic/src/main/java/org/moire/ultrasonic/util/AlbumHeader.kt index f2a38aa9..c049d49c 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/AlbumHeader.kt +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/AlbumHeader.kt @@ -8,8 +8,7 @@ import org.moire.ultrasonic.util.Util.getGrandparent class AlbumHeader( var entries: List, - var name: String, - songCount: Int + var name: String? ) : Identifiable { var isAllVideo: Boolean private set diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/view/PlaylistView.java b/ultrasonic/src/main/java/org/moire/ultrasonic/view/PlaylistView.java index f8919709..16c396ad 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/PlaylistView.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/view/PlaylistView.java @@ -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(); } } \ No newline at end of file diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/view/PodcatsChannelItemView.java b/ultrasonic/src/main/java/org/moire/ultrasonic/view/PodcastChannelItemView.java similarity index 85% rename from ultrasonic/src/main/java/org/moire/ultrasonic/view/PodcatsChannelItemView.java rename to ultrasonic/src/main/java/org/moire/ultrasonic/view/PodcastChannelItemView.java index 89163d86..367d01f4 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/PodcatsChannelItemView.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/view/PodcastChannelItemView.java @@ -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(); } } \ No newline at end of file diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/view/ShareView.java b/ultrasonic/src/main/java/org/moire/ultrasonic/view/ShareView.java index ffe5fdce..0bed3b2c 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/ShareView.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/view/ShareView.java @@ -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(); } } \ No newline at end of file diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt index af38aaf4..7ec5c6ff 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt @@ -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 diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowBinder.kt similarity index 60% rename from ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowAdapter.kt rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowBinder.kt index 61c79fea..6f2b7b2a 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowBinder.kt @@ -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, - 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( - onItemClick, - onContextMenuClick, - onMusicFolderUpdate -) { - - init { - super.submitList(itemList) - } +) : ItemViewBinder(), 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)) + } } + diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowAdapter.kt deleted file mode 100644 index 88837b68..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowAdapter.kt +++ /dev/null @@ -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, - onItemClick: (ArtistOrIndex) -> Unit, - onContextMenuClick: (MenuItem, ArtistOrIndex) -> Boolean, - private val imageLoader: ImageLoader, - onMusicFolderUpdate: (String?) -> Unit -) : GenericRowAdapter( - 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) - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowBinder.kt new file mode 100644 index 00000000..73aad3bf --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowBinder.kt @@ -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(), 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)) + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/MultiTypeDiffAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt similarity index 97% rename from ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/MultiTypeDiffAdapter.kt rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt index 86929dd6..9cf73253 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/MultiTypeDiffAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt @@ -11,7 +11,7 @@ import com.drakeet.multitype.MultiTypeAdapter import java.util.TreeSet import org.moire.ultrasonic.domain.Identifiable -class MultiTypeDiffAdapter : MultiTypeAdapter() { +class BaseAdapter : MultiTypeAdapter() { internal var selectedSet: TreeSet = TreeSet() internal var selectionRevision: MutableLiveData = MutableLiveData(0) @@ -43,7 +43,7 @@ class MultiTypeDiffAdapter : MultiTypeAdapter() { private val mListener = ListListener { previousList, currentList -> - this@MultiTypeDiffAdapter.onCurrentListChanged( + this@BaseAdapter.onCurrentListChanged( previousList, currentList ) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/FolderSelectorBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/FolderSelectorBinder.kt new file mode 100644 index 00000000..40e8290b --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/FolderSelectorBinder.kt @@ -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(), KoinComponent { + + private val weakContext: WeakReference = 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 + ) : RecyclerView.ViewHolder(view) { + private var musicFolders: List = 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) { + 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, + 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) + } + } + +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/GenericRowAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/GenericRowAdapter.kt deleted file mode 100644 index e95fb2c5..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/GenericRowAdapter.kt +++ /dev/null @@ -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 in a RecyclerView -*/ -abstract class GenericRowAdapter( - val onItemClick: (T) -> Unit, - val onContextMenuClick: (MenuItem, T) -> Boolean, - private val onMusicFolderUpdate: (String?) -> Unit -) : ListAdapter(GenericDiffCallback()) { - - protected abstract val layout: Int - protected abstract val contextMenuLayout: Int - - var folderHeaderEnabled: Boolean = true - var selectFolderHeader: SelectMusicFolderView? = null - var musicFolders: List = listOf() - var selectedFolder: String? = null - - /** - * Sets the content and state of the music folder selector row - */ - fun setFolderList(changedFolders: List, 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 : DiffUtil.ItemCallback() { - @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 - } - } - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/HeaderViewBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/HeaderViewBinder.kt index 9a82885d..33826e48 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/HeaderViewBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/HeaderViewBinder.kt @@ -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) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/Helper.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/Helper.kt new file mode 100644 index 00000000..ca510db5 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/Helper.kt @@ -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 + } +} \ No newline at end of file diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/SectionedAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/SectionedAdapter.kt new file mode 100644 index 00000000..aabd48f1 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/SectionedAdapter.kt @@ -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 : 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" + } +} \ No newline at end of file diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ServerRowAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ServerRowAdapter.kt index 2b9e4be1..89c7a5ea 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ServerRowAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ServerRowAdapter.kt @@ -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 diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt index 5fdd494b..537847b5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt @@ -22,16 +22,6 @@ class TrackViewBinder( private val onClickCallback: ((View, DownloadFile?) -> Unit)? = null ) : ItemViewBinder(), 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() } ) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt index 68a5c24f..e861836a 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt @@ -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) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/AppPermanentStorageModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/AppPermanentStorageModule.kt index 992141e1..79209e5b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/AppPermanentStorageModule.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/AppPermanentStorageModule.kt @@ -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" diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt index 559c721e..d2d29d49 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt @@ -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() { @@ -54,24 +56,6 @@ class AlbumListFragment : EntryListFragment() { 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() { 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() { bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, item.parent) findNavController().navigate(itemClickTarget, bundle) } + } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt index d0c30d59..2b092a46 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt @@ -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() { */ 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() { 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 + } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt new file mode 100644 index 00000000..f0e2e4bd --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt @@ -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> { + listModel.viewModelScope.launch(handler) { + refreshListView?.isRefreshing = true + listModel.getBookmarks() + refreshListView?.isRefreshing = false + } + return listModel.currentList + } + + override fun enableButtons(selection: List) { + 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) + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt index 6acaafff..86dc63af 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt @@ -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 diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt index 1addcf40..48d75a62 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt @@ -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 diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt new file mode 100644 index 00000000..5cef4c8f --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt @@ -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 : MultiListFragment() { + + /** + * 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) + } + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListFragment.kt deleted file mode 100644 index befd90e8..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListFragment.kt +++ /dev/null @@ -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> : 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> - - /** - * The central function to pass a query to the model and return a LiveData object - */ - abstract fun getLiveData(args: Bundle? = null): LiveData> - - /** - * 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 -> - 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(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 : MultiListFragment() { - @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) - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt index 49cf0e56..22748d89 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt @@ -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 : Fragment() { * The Adapter for the RecyclerView * Recommendation: Implement this as a lazy delegate */ - internal val viewAdapter: MultiTypeDiffAdapter by lazy { - MultiTypeDiffAdapter() + internal val viewAdapter: BaseAdapter by lazy { + BaseAdapter() } /** @@ -61,7 +62,9 @@ abstract class MultiListFragment : Fragment() { /** * The central function to pass a query to the model and return a LiveData object */ - abstract fun getLiveData(args: Bundle? = null): LiveData> + open fun getLiveData(args: Bundle? = null): LiveData> { + return MutableLiveData(listOf()) + } /** * The id of the target in the navigation graph where we should go, @@ -84,35 +87,6 @@ abstract class MultiListFragment : 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 -> - // 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 : 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 : 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> : -// GenericListFragment() { -// @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) -// } -// } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt index e6a9eb21..3ca32373 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -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 by lazy { - MultiTypeDiffAdapter() + internal val viewAdapter: BaseAdapter 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 } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt index 05d7b568..9e1eeabe 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt @@ -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 diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index fb86404f..18f8f577 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -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() { +open class TrackCollectionFragment : MultiListFragment() { 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 } - 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 = getSelectedSongs()) { + internal open fun enableButtons(selection: List = 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 = 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() + } + } } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt similarity index 66% rename from ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListModel.kt rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt index a9216173..a969afbd 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt @@ -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> = MutableLiveData(listOf()) + + + val list: MutableLiveData> = 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() - 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" } + } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ArtistListModel.kt similarity index 98% rename from ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListModel.kt rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ArtistListModel.kt index e87477b2..ca2bed1f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ArtistListModel.kt @@ -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 diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt similarity index 88% rename from ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListModel.kt rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt index 5ec1db0e..dd29662d 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt @@ -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> = 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> { - 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" + } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt new file mode 100644 index 00000000..e3c63086 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt @@ -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 = 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 { +// val list = mutableListOf() +// list.add(result.artists) +// list.add(result.albums) +// list.add(result.songs) +// return list +// } + +} \ No newline at end of file diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSettingsModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ServerSettingsModel.kt similarity index 99% rename from ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSettingsModel.kt rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ServerSettingsModel.kt index 65c2c6e6..2f520617 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSettingsModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ServerSettingsModel.kt @@ -1,4 +1,4 @@ -package org.moire.ultrasonic.fragment +package org.moire.ultrasonic.model import android.app.Application import android.content.SharedPreferences diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt similarity index 80% rename from ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionModel.kt rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt index 69a5b15d..3b097401 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt @@ -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 = MutableLiveData() val currentList: MutableLiveData> = MutableLiveData() val songsForGenre: MutableLiveData = 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()) + } + } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt index d7753662..fed88a9b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt @@ -399,7 +399,7 @@ class CachedMusicService(private val musicService: MusicService) : MusicService, } @Throws(Exception::class) - override fun getBookmarks(): List? = musicService.getBookmarks() + override fun getBookmarks(): List = musicService.getBookmarks() @Throws(Exception::class) override fun deleteBookmark(id: String) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt index 1a086d73..902ab3f9 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt @@ -154,7 +154,7 @@ interface MusicService { fun addChatMessage(message: String) @Throws(Exception::class) - fun getBookmarks(): List? + fun getBookmarks(): List @Throws(Exception::class) fun deleteBookmark(id: String) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt index d705119a..9ec622b7 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt @@ -411,7 +411,7 @@ class OfflineMusicService : MusicService, KoinComponent { } @Throws(OfflineException::class) - override fun getBookmarks(): List? { + override fun getBookmarks(): List { throw OfflineException("getBookmarks isn't available in offline mode") } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt index eeca3ffc..9ce64a0e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt @@ -30,6 +30,11 @@ class RxBus { val themeChangedEventObservable: Observable = themeChangedEventPublisher.observeOn(AndroidSchedulers.mainThread()) + val musicFolderChangedEventPublisher: PublishSubject = + PublishSubject.create() + val musicFolderChangedEventObservable: Observable = + musicFolderChangedEventPublisher.observeOn(AndroidSchedulers.mainThread()) + val playerStatePublisher: PublishSubject = PublishSubject.create() val playerStateObservable: Observable = @@ -73,6 +78,7 @@ class RxBus { val skipToQueueItemCommandObservable: Observable = skipToQueueItemCommandPublisher.observeOn(AndroidSchedulers.mainThread()) + fun releaseMediaSessionToken() { mediaSessionTokenPublisher = PublishSubject.create() } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt index cde2df4c..ea604676 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt @@ -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 } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/DragSortCallback.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/DragSortCallback.kt index 72e27f54..9f1efe0b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/DragSortCallback.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/DragSortCallback.kt @@ -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 } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt index 6851e09c..a8d798bf 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt @@ -525,11 +525,10 @@ object Util { } @JvmStatic - fun getSongsFromBookmarks(bookmarks: Iterable): MusicDirectory { + fun getSongsFromBookmarks(bookmarks: Iterable): 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) diff --git a/ultrasonic/src/main/res/layout/album_buttons.xml b/ultrasonic/src/main/res/layout/album_buttons.xml index 323ddd81..906f80d5 100644 --- a/ultrasonic/src/main/res/layout/album_buttons.xml +++ b/ultrasonic/src/main/res/layout/album_buttons.xml @@ -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" /> + android:visibility="gone" + android:contentDescription="@string/common.play_now" /> + android:visibility="gone" + android:contentDescription="@string/common.play_next" /> + android:visibility="gone" + android:contentDescription="@string/common.play_last" /> + android:visibility="gone" + android:contentDescription="@string/common.pin" /> + android:visibility="gone" + android:contentDescription="@string/common.unpin" /> + android:visibility="gone" + android:contentDescription="@string/common.download" /> + android:visibility="gone" + android:contentDescription="@string/common.delete" /> + android:visibility="gone" + android:contentDescription="@string/search.more" /> \ No newline at end of file diff --git a/ultrasonic/src/main/res/layout/search.xml b/ultrasonic/src/main/res/layout/search.xml index 5ef70eaa..a5132493 100644 --- a/ultrasonic/src/main/res/layout/search.xml +++ b/ultrasonic/src/main/res/layout/search.xml @@ -10,7 +10,7 @@ a:layout_height="0dip" a:layout_weight="1.0"> - - - - - - - - - - - - - - - - diff --git a/ultrasonic/src/main/res/navigation/navigation_graph.xml b/ultrasonic/src/main/res/navigation/navigation_graph.xml index c76a0720..096b5d1a 100644 --- a/ultrasonic/src/main/res/navigation/navigation_graph.xml +++ b/ultrasonic/src/main/res/navigation/navigation_graph.xml @@ -26,14 +26,14 @@ android:label="@string/music_library.label" > + app:destination="@id/albumListFragment" /> + app:destination="@id/albumListFragment" /> Play Shuffled Public Save + Select all Title Unpin Various Artists