From a4919ef6e908c13c42553fba7434e249e0d0d97f Mon Sep 17 00:00:00 2001 From: birdbird <6892457-tzugen@users.noreply.gitlab.com> Date: Thu, 4 Aug 2022 12:35:53 +0000 Subject: [PATCH] Migrate to use SafeArgs --- build.gradle | 1 + .../api/subsonic/models/AlbumListType.kt | 4 +- detekt-baseline.xml | 5 +- gradle/libs.versions.toml | 1 + ultrasonic/build.gradle | 1 + .../fragment/PlaylistsFragment.java | 342 ---------------- .../ultrasonic/fragment/PodcastFragment.java | 112 ------ .../fragment/SelectGenreFragment.java | 135 ------- .../ultrasonic/fragment/SharesFragment.java | 331 ---------------- .../ultrasonic/activity/NavigationActivity.kt | 47 ++- .../ultrasonic/data/ActiveServerProvider.kt | 6 +- .../ultrasonic/fragment/AlbumListFragment.kt | 65 +++- .../ultrasonic/fragment/ArtistListFragment.kt | 60 +-- .../ultrasonic/fragment/BookmarksFragment.kt | 1 - .../ultrasonic/fragment/DownloadsFragment.kt | 2 +- .../ultrasonic/fragment/EditServerFragment.kt | 7 + .../fragment/EndlessScrollListener.kt | 2 +- .../ultrasonic/fragment/EntryListFragment.kt | 43 ++- .../moire/ultrasonic/fragment/MainFragment.kt | 79 ++-- .../ultrasonic/fragment/MultiListFragment.kt | 38 +- .../ultrasonic/fragment/NowPlayingFragment.kt | 26 +- .../fragment/OnBackPressedHandler.kt | 7 + .../ultrasonic/fragment/PlayerFragment.kt | 56 +-- .../ultrasonic/fragment/SearchFragment.kt | 76 ++-- .../fragment/TrackCollectionFragment.kt | 157 ++++---- .../fragment/legacy/PlaylistsFragment.kt | 345 +++++++++++++++++ .../fragment/legacy/PodcastFragment.kt | 94 +++++ .../fragment/legacy/SelectGenreFragment.kt | 107 +++++ .../fragment/legacy/SharesFragment.kt | 364 ++++++++++++++++++ .../moire/ultrasonic/model/AlbumListModel.kt | 160 ++++---- .../moire/ultrasonic/model/ArtistListModel.kt | 8 +- .../ultrasonic/model/GenericListModel.kt | 32 +- .../moire/ultrasonic/model/SearchListModel.kt | 12 - .../org/moire/ultrasonic/playback/Plan.md | 17 - .../ultrasonic/service/CachedMusicService.kt | 10 +- .../service/MediaPlayerController.kt | 2 +- .../moire/ultrasonic/service/MusicService.kt | 4 +- .../ultrasonic/service/OfflineMusicService.kt | 17 +- .../ultrasonic/service/RESTMusicService.kt | 2 +- .../ultrasonic/subsonic/DownloadHandler.kt | 13 +- .../moire/ultrasonic/subsonic/ShareHandler.kt | 63 +-- .../org/moire/ultrasonic/util/Constants.kt | 26 +- .../main/res/navigation/navigation_graph.xml | 167 +++++++- 43 files changed, 1617 insertions(+), 1430 deletions(-) delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/fragment/PlaylistsFragment.java delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/fragment/PodcastFragment.java delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SelectGenreFragment.java delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SharesFragment.java create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PlaylistsFragment.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PodcastFragment.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SelectGenreFragment.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SharesFragment.kt delete mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/Plan.md diff --git a/build.gradle b/build.gradle index c44d7f84..efa20eed 100644 --- a/build.gradle +++ b/build.gradle @@ -17,6 +17,7 @@ buildscript { classpath libs.kotlin classpath libs.ktlintGradle classpath libs.detekt + classpath libs.navigationSafeArgs } } diff --git a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/AlbumListType.kt b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/AlbumListType.kt index 6d58eb0d..8d399c2a 100644 --- a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/AlbumListType.kt +++ b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/AlbumListType.kt @@ -16,7 +16,8 @@ enum class AlbumListType(val typeName: String) { SORTED_BY_ARTIST("alphabeticalByArtist"), STARRED("starred"), BY_YEAR("byYear"), - BY_GENRE("byGenre"); + BY_GENRE("byGenre"), + BY_ARTIST("albumsByArtist"); override fun toString(): String { return typeName @@ -35,6 +36,7 @@ enum class AlbumListType(val typeName: String) { in STARRED.typeName -> STARRED in BY_YEAR.typeName -> BY_YEAR in BY_GENRE.typeName -> BY_GENRE + in BY_ARTIST.typeName -> BY_ARTIST else -> throw IllegalArgumentException("Unknown type: $typeName") } diff --git a/detekt-baseline.xml b/detekt-baseline.xml index 205ff394..b625cb3f 100644 --- a/detekt-baseline.xml +++ b/detekt-baseline.xml @@ -9,7 +9,9 @@ ImplicitDefaultLocale:ShareHandler.kt$ShareHandler$String.format("%d:%s", timeSpanAmount, timeSpanType) LongMethod:EditServerFragment.kt$EditServerFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?) LongMethod:NavigationActivity.kt$NavigationActivity$override fun onCreate(savedInstanceState: Bundle?) - LongMethod:ShareHandler.kt$ShareHandler$private fun showDialog( fragment: Fragment, shareDetails: ShareDetails, swipe: SwipeRefreshLayout?, cancellationToken: CancellationToken ) + LongMethod:PlaylistsFragment.kt$PlaylistsFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean + LongMethod:ShareHandler.kt$ShareHandler$private fun showDialog( fragment: Fragment, shareDetails: ShareDetails, swipe: SwipeRefreshLayout?, cancellationToken: CancellationToken, additionalId: String? ) + LongMethod:SharesFragment.kt$SharesFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean LongParameterList:ServerRowAdapter.kt$ServerRowAdapter$( private var context: Context, passedData: Array<ServerSetting>, private val model: ServerSettingsModel, private val activeServerProvider: ActiveServerProvider, private val manageMode: Boolean, private val serverDeletedCallback: ((Int) -> Unit), private val serverEditRequestedCallback: ((Int) -> Unit) ) MagicNumber:ActiveServerProvider.kt$ActiveServerProvider$8192 MagicNumber:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$0.05f @@ -19,7 +21,6 @@ TooGenericExceptionCaught:FileLoggerTree.kt$FileLoggerTree$x: Throwable TooGenericExceptionCaught:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$x: Throwable TooGenericExceptionCaught:JukeboxMediaPlayer.kt$JukeboxMediaPlayer.TaskQueue$x: Throwable - TooGenericExceptionThrown:Downloader.kt$Downloader.DownloadTask$throw RuntimeException( String.format( Locale.ROOT, "Download of '%s' was cancelled", downloadFile.track ) ) TooManyFunctions:RESTMusicService.kt$RESTMusicService : MusicService UtilityClassWithPublicConstructor:FragmentTitle.kt$FragmentTitle diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d6912a5c..b3fb3732 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -64,6 +64,7 @@ navigationUi = { module = "androidx.navigation:navigation-ui", versio navigationFragmentKtx = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "navigation" } navigationUiKtx = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "navigation" } navigationFeature = { module = "androidx.navigation:navigation-dynamic-features-fragment", version.ref = "navigation" } +navigationSafeArgs = { module = "androidx.navigation:navigation-safe-args-gradle-plugin", version.ref = "navigation"} preferences = { module = "androidx.preference:preference", version.ref = "preferences" } media = { module = "androidx.media:media", version.ref = "media" } media3exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" } diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index 16892319..bb6ae42f 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -1,6 +1,7 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' +apply plugin: "androidx.navigation.safeargs.kotlin" apply from: "../gradle_scripts/code_quality.gradle" android { diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/PlaylistsFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/PlaylistsFragment.java deleted file mode 100644 index ba7e136e..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/PlaylistsFragment.java +++ /dev/null @@ -1,342 +0,0 @@ -package org.moire.ultrasonic.fragment; - -import android.app.AlertDialog; -import android.content.DialogInterface; -import android.os.Bundle; -import android.text.Editable; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.method.LinkMovementMethod; -import android.text.util.Linkify; -import android.view.ContextMenu; -import android.view.LayoutInflater; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.CheckBox; -import android.widget.EditText; -import android.widget.ListView; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -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.api.subsonic.ApiNotSupportedException; -import org.moire.ultrasonic.data.ActiveServerProvider; -import org.moire.ultrasonic.domain.Playlist; -import org.moire.ultrasonic.service.MusicService; -import org.moire.ultrasonic.service.MusicServiceFactory; -import org.moire.ultrasonic.service.OfflineException; -import org.moire.ultrasonic.subsonic.DownloadHandler; -import org.moire.ultrasonic.util.BackgroundTask; -import org.moire.ultrasonic.util.CacheCleaner; -import org.moire.ultrasonic.util.CancellationToken; -import org.moire.ultrasonic.util.Constants; -import org.moire.ultrasonic.util.LoadingTask; -import org.moire.ultrasonic.util.FragmentBackgroundTask; -import org.moire.ultrasonic.util.Util; -import org.moire.ultrasonic.view.PlaylistAdapter; - -import java.util.List; - -import kotlin.Lazy; - -import static org.koin.java.KoinJavaComponent.inject; - -/** - * Displays the playlists stored on the server - */ -public class PlaylistsFragment extends Fragment { - - private SwipeRefreshLayout refreshPlaylistsListView; - private ListView playlistsListView; - private View emptyTextView; - private PlaylistAdapter playlistAdapter; - - private final Lazy downloadHandler = inject(DownloadHandler.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_playlist, container, false); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - cancellationToken = new CancellationToken(); - - refreshPlaylistsListView = view.findViewById(R.id.select_playlist_refresh); - playlistsListView = view.findViewById(R.id.select_playlist_list); - - refreshPlaylistsListView.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() - { - @Override - public void onRefresh() { - load(true); - } - }); - - emptyTextView = view.findViewById(R.id.select_playlist_empty); - playlistsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - Playlist playlist = (Playlist) parent.getItemAtPosition(position); - - if (playlist == null) - { - return; - } - - Bundle bundle = new Bundle(); - bundle.putString(Constants.INTENT_ID, playlist.getId()); - bundle.putString(Constants.INTENT_PLAYLIST_ID, playlist.getId()); - bundle.putString(Constants.INTENT_PLAYLIST_NAME, playlist.getName()); - Navigation.findNavController(getView()).navigate(R.id.trackCollectionFragment, bundle); - } - }); - registerForContextMenu(playlistsListView); - FragmentTitle.Companion.setTitle(this, R.string.playlist_label); - - load(false); - } - - @Override - public void onDestroyView() { - cancellationToken.cancel(); - super.onDestroyView(); - } - - private void load(final boolean refresh) - { - BackgroundTask> task = new FragmentBackgroundTask>(getActivity(), true, refreshPlaylistsListView, cancellationToken) - { - @Override - protected List doInBackground() throws Throwable - { - MusicService musicService = MusicServiceFactory.getMusicService(); - List playlists = musicService.getPlaylists(refresh); - - if (!ActiveServerProvider.Companion.isOffline()) - new CacheCleaner().cleanPlaylists(playlists); - return playlists; - } - - @Override - protected void done(List result) - { - playlistsListView.setAdapter(playlistAdapter = new PlaylistAdapter(getContext(), result)); - emptyTextView.setVisibility(result.isEmpty() ? View.VISIBLE : View.GONE); - } - }; - task.execute(); - } - - @Override - public void onCreateContextMenu(@NotNull ContextMenu menu, @NotNull View view, ContextMenu.ContextMenuInfo menuInfo) - { - super.onCreateContextMenu(menu, view, menuInfo); - - MenuInflater inflater = getActivity().getMenuInflater(); - if (ActiveServerProvider.Companion.isOffline()) inflater.inflate(R.menu.select_playlist_context_offline, menu); - else inflater.inflate(R.menu.select_playlist_context, menu); - - MenuItem downloadMenuItem = menu.findItem(R.id.playlist_menu_download); - - if (downloadMenuItem != null) - { - downloadMenuItem.setVisible(!ActiveServerProvider.Companion.isOffline()); - } - } - - @Override - public boolean onContextItemSelected(MenuItem menuItem) - { - AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo(); - if (info == null) - { - return false; - } - - Playlist playlist = (Playlist) playlistsListView.getItemAtPosition(info.position); - if (playlist == null) - { - return false; - } - - Bundle bundle; - int itemId = menuItem.getItemId(); - if (itemId == R.id.playlist_menu_pin) { - downloadHandler.getValue().downloadPlaylist(this, playlist.getId(), playlist.getName(), true, true, false, false, true, false, false); - } else if (itemId == R.id.playlist_menu_unpin) { - downloadHandler.getValue().downloadPlaylist(this, playlist.getId(), playlist.getName(), false, false, false, false, true, false, true); - } else if (itemId == R.id.playlist_menu_download) { - downloadHandler.getValue().downloadPlaylist(this, playlist.getId(), playlist.getName(), false, false, false, false, true, false, false); - } else if (itemId == R.id.playlist_menu_play_now) { - bundle = new Bundle(); - bundle.putString(Constants.INTENT_PLAYLIST_ID, playlist.getId()); - bundle.putString(Constants.INTENT_PLAYLIST_NAME, playlist.getName()); - bundle.putBoolean(Constants.INTENT_AUTOPLAY, true); - Navigation.findNavController(getView()).navigate(R.id.trackCollectionFragment, bundle); - } else if (itemId == R.id.playlist_menu_play_shuffled) { - bundle = new Bundle(); - bundle.putString(Constants.INTENT_PLAYLIST_ID, playlist.getId()); - bundle.putString(Constants.INTENT_PLAYLIST_NAME, playlist.getName()); - bundle.putBoolean(Constants.INTENT_AUTOPLAY, true); - bundle.putBoolean(Constants.INTENT_SHUFFLE, true); - Navigation.findNavController(getView()).navigate(R.id.trackCollectionFragment, bundle); - } else if (itemId == R.id.playlist_menu_delete) { - deletePlaylist(playlist); - } else if (itemId == R.id.playlist_info) { - displayPlaylistInfo(playlist); - } else if (itemId == R.id.playlist_update_info) { - updatePlaylistInfo(playlist); - } else { - return super.onContextItemSelected(menuItem); - } - return true; - } - - private void deletePlaylist(final Playlist playlist) - { - new AlertDialog.Builder(getContext()).setIcon(R.drawable.ic_baseline_warning).setTitle(R.string.common_confirm).setMessage(getResources().getString(R.string.delete_playlist, playlist.getName())).setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() - { - @Override - public void onClick(DialogInterface dialog, int which) - { - new LoadingTask(getActivity(), refreshPlaylistsListView, cancellationToken) - { - @Override - protected Void doInBackground() throws Throwable - { - MusicService musicService = MusicServiceFactory.getMusicService(); - musicService.deletePlaylist(playlist.getId()); - return null; - } - - @Override - protected void done(Void result) - { - playlistAdapter.remove(playlist); - playlistAdapter.notifyDataSetChanged(); - Util.toast(getContext(), getResources().getString(R.string.menu_deleted_playlist, playlist.getName())); - } - - @Override - protected void error(Throwable error) - { - String msg; - msg = error instanceof OfflineException || error instanceof ApiNotSupportedException ? getErrorMessage(error) : String.format("%s %s", getResources().getString(R.string.menu_deleted_playlist_error, playlist.getName()), getErrorMessage(error)); - - Util.toast(getContext(), msg, false); - } - }.execute(); - } - - }).setNegativeButton(R.string.common_cancel, null).show(); - } - - private void displayPlaylistInfo(final Playlist playlist) - { - final TextView textView = new TextView(getContext()); - textView.setPadding(5, 5, 5, 5); - - final Spannable message = new SpannableString("Owner: " + playlist.getOwner() + "\nComments: " + - ((playlist.getComment() == null) ? "" : playlist.getComment()) + - "\nSong Count: " + playlist.getSongCount() + - ((playlist.getPublic() == null) ? "" : ("\nPublic: " + playlist.getPublic()) + ((playlist.getCreated() == null) ? "" : ("\nCreation Date: " + playlist.getCreated().replace('T', ' '))))); - - Linkify.addLinks(message, Linkify.WEB_URLS); - textView.setText(message); - textView.setMovementMethod(LinkMovementMethod.getInstance()); - - new AlertDialog.Builder(getContext()).setTitle(playlist.getName()).setCancelable(true).setIcon(R.drawable.ic_baseline_info).setView(textView).show(); - } - - private void updatePlaylistInfo(final Playlist playlist) - { - View dialogView = getLayoutInflater().inflate(R.layout.update_playlist, null); - - if (dialogView == null) - { - return; - } - - final EditText nameBox = dialogView.findViewById(R.id.get_playlist_name); - final EditText commentBox = dialogView.findViewById(R.id.get_playlist_comment); - final CheckBox publicBox = dialogView.findViewById(R.id.get_playlist_public); - - nameBox.setText(playlist.getName()); - commentBox.setText(playlist.getComment()); - Boolean pub = playlist.getPublic(); - - if (pub == null) - { - publicBox.setEnabled(false); - } - else - { - publicBox.setChecked(pub); - } - - AlertDialog.Builder alertDialog = new AlertDialog.Builder(getContext()); - - alertDialog.setIcon(R.drawable.ic_baseline_warning); - alertDialog.setTitle(R.string.playlist_update_info); - alertDialog.setView(dialogView); - alertDialog.setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() - { - @Override - public void onClick(DialogInterface dialog, int which) - { - new LoadingTask(getActivity(), refreshPlaylistsListView, cancellationToken) - { - @Override - protected Void doInBackground() throws Throwable - { - Editable nameBoxText = nameBox.getText(); - Editable commentBoxText = commentBox.getText(); - String name = nameBoxText != null ? nameBoxText.toString() : null; - String comment = commentBoxText != null ? commentBoxText.toString() : null; - - MusicService musicService = MusicServiceFactory.getMusicService(); - musicService.updatePlaylist(playlist.getId(), name, comment, publicBox.isChecked()); - return null; - } - - @Override - protected void done(Void result) - { - load(true); - Util.toast(getContext(), getResources().getString(R.string.playlist_updated_info, playlist.getName())); - } - - @Override - protected void error(Throwable error) - { - String msg; - msg = error instanceof OfflineException || error instanceof ApiNotSupportedException ? getErrorMessage(error) : String.format("%s %s", getResources().getString(R.string.playlist_updated_info_error, playlist.getName()), getErrorMessage(error)); - - Util.toast(getContext(), msg, false); - } - }.execute(); - } - - }); - alertDialog.setNegativeButton(R.string.common_cancel, null); - alertDialog.show(); - } -} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/PodcastFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/PodcastFragment.java deleted file mode 100644 index 00210dd0..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/PodcastFragment.java +++ /dev/null @@ -1,112 +0,0 @@ -package org.moire.ultrasonic.fragment; - -import android.content.Context; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.ListView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.navigation.Navigation; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; - -import org.moire.ultrasonic.R; -import org.moire.ultrasonic.domain.PodcastsChannel; -import org.moire.ultrasonic.service.MusicService; -import org.moire.ultrasonic.service.MusicServiceFactory; -import org.moire.ultrasonic.util.BackgroundTask; -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.PodcastsChannelsAdapter; - -import java.util.List; - -/** - * Displays the podcasts available on the server - */ -public class PodcastFragment extends Fragment { - - private View emptyTextView; - ListView channelItemsListView = null; - private CancellationToken cancellationToken; - private SwipeRefreshLayout swipeRefresh; - - @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.podcasts, container, false); - } - - @Override - public void onViewCreated(@NonNull final View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - cancellationToken = new CancellationToken(); - swipeRefresh = view.findViewById(R.id.podcasts_refresh); - swipeRefresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() - { - @Override - public void onRefresh() { - load(view.getContext(), true); - } - }); - - FragmentTitle.Companion.setTitle(this, R.string.podcasts_label); - - emptyTextView = view.findViewById(R.id.select_podcasts_empty); - channelItemsListView = view.findViewById(R.id.podcasts_channels_items_list); - channelItemsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - PodcastsChannel pc = (PodcastsChannel) parent.getItemAtPosition(position); - if (pc == null) { - return; - } - - Bundle bundle = new Bundle(); - bundle.putString(Constants.INTENT_PODCAST_CHANNEL_ID, pc.getId()); - Navigation.findNavController(view).navigate(R.id.trackCollectionFragment, bundle); - } - }); - - load(view.getContext(), false); - } - - @Override - public void onDestroyView() { - cancellationToken.cancel(); - super.onDestroyView(); - } - - private void load(final Context context, final boolean refresh) - { - BackgroundTask> task = new FragmentBackgroundTask>(getActivity(), true, swipeRefresh, cancellationToken) - { - @Override - protected List doInBackground() throws Throwable - { - MusicService musicService = MusicServiceFactory.getMusicService(); - return musicService.getPodcastsChannels(refresh); - } - - @Override - protected void done(List result) - { - channelItemsListView.setAdapter(new PodcastsChannelsAdapter(context, result)); - emptyTextView.setVisibility(result.isEmpty() ? View.VISIBLE : View.GONE); - } - }; - task.execute(); - } -} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SelectGenreFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SelectGenreFragment.java deleted file mode 100644 index dc6f3382..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SelectGenreFragment.java +++ /dev/null @@ -1,135 +0,0 @@ -package org.moire.ultrasonic.fragment; - -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.ListView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.navigation.Navigation; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; - -import org.moire.ultrasonic.R; -import org.moire.ultrasonic.domain.Genre; -import org.moire.ultrasonic.service.MusicService; -import org.moire.ultrasonic.service.MusicServiceFactory; -import org.moire.ultrasonic.util.BackgroundTask; -import org.moire.ultrasonic.util.CancellationToken; -import org.moire.ultrasonic.util.Constants; -import org.moire.ultrasonic.util.FragmentBackgroundTask; -import org.moire.ultrasonic.util.Settings; -import org.moire.ultrasonic.util.Util; -import org.moire.ultrasonic.view.GenreAdapter; - -import java.util.ArrayList; -import java.util.List; - -import timber.log.Timber; - -/** - * Displays the available genres in the media library - */ -public class SelectGenreFragment extends Fragment { - - private SwipeRefreshLayout refreshGenreListView; - private ListView genreListView; - private View emptyView; - 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_genre, container, false); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - cancellationToken = new CancellationToken(); - refreshGenreListView = view.findViewById(R.id.select_genre_refresh); - genreListView = view.findViewById(R.id.select_genre_list); - - refreshGenreListView.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() - { - @Override - public void onRefresh() - { - load(true); - } - }); - - genreListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - Genre genre = (Genre) parent.getItemAtPosition(position); - - if (genre != null) - { - Bundle bundle = new Bundle(); - bundle.putString(Constants.INTENT_GENRE_NAME, genre.getName()); - bundle.putInt(Constants.INTENT_ALBUM_LIST_SIZE, Settings.getMaxSongs()); - bundle.putInt(Constants.INTENT_ALBUM_LIST_OFFSET, 0); - Navigation.findNavController(view).navigate(R.id.trackCollectionFragment, bundle); - } - } - }); - - emptyView = view.findViewById(R.id.select_genre_empty); - registerForContextMenu(genreListView); - - FragmentTitle.Companion.setTitle(this, R.string.main_genres_title); - load(false); - } - - @Override - public void onDestroyView() { - cancellationToken.cancel(); - super.onDestroyView(); - } - - private void load(final boolean refresh) - { - BackgroundTask> task = new FragmentBackgroundTask>(getActivity(), true, refreshGenreListView, cancellationToken) - { - @Override - protected List doInBackground() - { - MusicService musicService = MusicServiceFactory.getMusicService(); - - List genres = new ArrayList<>(); - - try - { - genres = musicService.getGenres(refresh); - } - catch (Exception x) - { - Timber.e(x, "Failed to load genres"); - } - - return genres; - } - - @Override - protected void done(List result) - { - emptyView.setVisibility(result == null || result.isEmpty() ? View.VISIBLE : View.GONE); - - if (result != null) - { - genreListView.setAdapter(new GenreAdapter(getContext(), result)); - } - } - }; - task.execute(); - } -} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SharesFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SharesFragment.java deleted file mode 100644 index 4abc6df6..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SharesFragment.java +++ /dev/null @@ -1,331 +0,0 @@ -package org.moire.ultrasonic.fragment; - -import android.app.AlertDialog; -import android.content.DialogInterface; -import android.os.Bundle; -import android.text.Editable; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.method.LinkMovementMethod; -import android.text.util.Linkify; -import android.view.ContextMenu; -import android.view.LayoutInflater; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.CheckBox; -import android.widget.CompoundButton; -import android.widget.EditText; -import android.widget.ListView; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -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.api.subsonic.ApiNotSupportedException; -import org.moire.ultrasonic.domain.Share; -import org.moire.ultrasonic.service.MusicService; -import org.moire.ultrasonic.service.MusicServiceFactory; -import org.moire.ultrasonic.service.OfflineException; -import org.moire.ultrasonic.subsonic.DownloadHandler; -import org.moire.ultrasonic.util.BackgroundTask; -import org.moire.ultrasonic.util.CancellationToken; -import org.moire.ultrasonic.util.Constants; -import org.moire.ultrasonic.util.LoadingTask; -import org.moire.ultrasonic.util.FragmentBackgroundTask; -import org.moire.ultrasonic.util.TimeSpan; -import org.moire.ultrasonic.util.TimeSpanPicker; -import org.moire.ultrasonic.util.Util; -import org.moire.ultrasonic.view.ShareAdapter; - -import java.util.List; - -import kotlin.Lazy; - -import static org.koin.java.KoinJavaComponent.inject; - -/** - * Displays the shares in the media library - */ -public class SharesFragment extends Fragment { - - private SwipeRefreshLayout refreshSharesListView; - private ListView sharesListView; - private View emptyTextView; - private ShareAdapter shareAdapter; - - private final Lazy downloadHandler = inject(DownloadHandler.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_share, container, false); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - cancellationToken = new CancellationToken(); - - refreshSharesListView = view.findViewById(R.id.select_share_refresh); - sharesListView = view.findViewById(R.id.select_share_list); - - refreshSharesListView.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() - { - @Override - public void onRefresh() - { - load(true); - } - }); - - emptyTextView = view.findViewById(R.id.select_share_empty); - sharesListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - Share share = (Share) parent.getItemAtPosition(position); - - if (share == null) - { - return; - } - - Bundle bundle = new Bundle(); - bundle.putString(Constants.INTENT_SHARE_ID, share.getId()); - bundle.putString(Constants.INTENT_SHARE_NAME, share.getName()); - Navigation.findNavController(view).navigate(R.id.trackCollectionFragment, bundle); - } - }); - registerForContextMenu(sharesListView); - FragmentTitle.Companion.setTitle(this, R.string.button_bar_shares); - - load(false); - } - - @Override - public void onDestroyView() { - cancellationToken.cancel(); - super.onDestroyView(); - } - - private void load(final boolean refresh) - { - BackgroundTask> task = new FragmentBackgroundTask>(getActivity(), true, refreshSharesListView, cancellationToken) - { - @Override - protected List doInBackground() throws Throwable - { - MusicService musicService = MusicServiceFactory.getMusicService(); - return musicService.getShares(refresh); - } - - @Override - protected void done(List result) - { - sharesListView.setAdapter(shareAdapter = new ShareAdapter(getContext(), result)); - emptyTextView.setVisibility(result.isEmpty() ? View.VISIBLE : View.GONE); - } - }; - task.execute(); - } - - @Override - public void onCreateContextMenu(@NotNull ContextMenu menu, @NotNull View view, ContextMenu.ContextMenuInfo menuInfo) - { - super.onCreateContextMenu(menu, view, menuInfo); - - MenuInflater inflater = getActivity().getMenuInflater(); - inflater.inflate(R.menu.select_share_context, menu); - } - - @Override - public boolean onContextItemSelected(MenuItem menuItem) - { - AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo(); - if (info == null) return false; - - Share share = (Share) sharesListView.getItemAtPosition(info.position); - if (share == null || share.getId() == null) return false; - - int itemId = menuItem.getItemId(); - if (itemId == R.id.share_menu_pin) { - downloadHandler.getValue().downloadShare(this, share.getId(), share.getName(), true, true, false, false, true, false, false); - } else if (itemId == R.id.share_menu_unpin) { - downloadHandler.getValue().downloadShare(this, share.getId(), share.getName(), false, false, false, false, true, false, true); - } else if (itemId == R.id.share_menu_download) { - downloadHandler.getValue().downloadShare(this, share.getId(), share.getName(), false, false, false, false, true, false, false); - } else if (itemId == R.id.share_menu_play_now) { - downloadHandler.getValue().downloadShare(this, share.getId(), share.getName(), false, false, true, false, false, false, false); - } else if (itemId == R.id.share_menu_play_shuffled) { - downloadHandler.getValue().downloadShare(this, share.getId(), share.getName(), false, false, true, true, false, false, false); - } else if (itemId == R.id.share_menu_delete) { - deleteShare(share); - } else if (itemId == R.id.share_info) { - displayShareInfo(share); - } else if (itemId == R.id.share_update_info) { - updateShareInfo(share); - } else { - return super.onContextItemSelected(menuItem); - } - return true; - } - - private void deleteShare(final Share share) - { - new AlertDialog.Builder(getContext()).setIcon(R.drawable.ic_baseline_warning).setTitle(R.string.common_confirm).setMessage(getResources().getString(R.string.delete_playlist, share.getName())).setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() - { - @Override - public void onClick(DialogInterface dialog, int which) - { - new LoadingTask(getActivity(), refreshSharesListView, cancellationToken) - { - @Override - protected Void doInBackground() throws Throwable - { - MusicService musicService = MusicServiceFactory.getMusicService(); - musicService.deleteShare(share.getId()); - return null; - } - - @Override - protected void done(Void result) - { - shareAdapter.remove(share); - shareAdapter.notifyDataSetChanged(); - Util.toast(getContext(), getResources().getString(R.string.menu_deleted_share, share.getName())); - } - - @Override - protected void error(Throwable error) - { - String msg; - msg = error instanceof OfflineException || error instanceof ApiNotSupportedException ? getErrorMessage(error) : String.format("%s %s", getResources().getString(R.string.menu_deleted_share_error, share.getName()), getErrorMessage(error)); - - Util.toast(getContext(), msg, false); - } - }.execute(); - } - - }).setNegativeButton(R.string.common_cancel, null).show(); - } - - private void displayShareInfo(final Share share) - { - final TextView textView = new TextView(getContext()); - textView.setPadding(5, 5, 5, 5); - - final Spannable message = new SpannableString("Owner: " + share.getUsername() + - "\nComments: " + ((share.getDescription() == null) ? "" : share.getDescription()) + - "\nURL: " + share.getUrl() + - "\nEntry Count: " + share.getEntries().size() + - "\nVisit Count: " + share.getVisitCount() + - ((share.getCreated() == null) ? "" : ("\nCreation Date: " + share.getCreated().replace('T', ' '))) + - ((share.getLastVisited() == null) ? "" : ("\nLast Visited Date: " + share.getLastVisited().replace('T', ' '))) + - ((share.getExpires() == null) ? "" : ("\nExpiration Date: " + share.getExpires().replace('T', ' ')))); - - Linkify.addLinks(message, Linkify.WEB_URLS); - textView.setText(message); - textView.setMovementMethod(LinkMovementMethod.getInstance()); - - new AlertDialog.Builder(getContext()).setTitle("Share Details").setCancelable(true).setIcon(R.drawable.ic_baseline_info).setView(textView).show(); - } - - private void updateShareInfo(final Share share) - { - View dialogView = getLayoutInflater().inflate(R.layout.share_details, null); - if (dialogView == null) - { - return; - } - - final EditText shareDescription = dialogView.findViewById(R.id.share_description); - final TimeSpanPicker timeSpanPicker = dialogView.findViewById(R.id.date_picker); - - shareDescription.setText(share.getDescription()); - - CheckBox hideDialogCheckBox = dialogView.findViewById(R.id.hide_dialog); - CheckBox saveAsDefaultsCheckBox = dialogView.findViewById(R.id.save_as_defaults); - CheckBox noExpirationCheckBox = dialogView.findViewById(R.id.timeSpanDisableCheckBox); - - noExpirationCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() - { - @Override - public void onCheckedChanged(CompoundButton compoundButton, boolean b) - { - timeSpanPicker.setEnabled(!b); - } - }); - - noExpirationCheckBox.setChecked(true); - - timeSpanPicker.setTimeSpanDisableText(getResources().getText(R.string.no_expiration)); - - hideDialogCheckBox.setVisibility(View.GONE); - saveAsDefaultsCheckBox.setVisibility(View.GONE); - - AlertDialog.Builder alertDialog = new AlertDialog.Builder(getContext()); - - alertDialog.setIcon(R.drawable.ic_baseline_warning); - alertDialog.setTitle(R.string.playlist_update_info); - alertDialog.setView(dialogView); - alertDialog.setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() - { - @Override - public void onClick(DialogInterface dialog, int which) - { - new LoadingTask(getActivity(), refreshSharesListView, cancellationToken) - { - @Override - protected Void doInBackground() throws Throwable - { - long millis = timeSpanPicker.getTimeSpan().getTotalMilliseconds(); - - if (millis > 0) - { - millis = TimeSpan.getCurrentTime().add(millis).getTotalMilliseconds(); - } - - Editable shareDescriptionText = shareDescription.getText(); - String description = shareDescriptionText != null ? shareDescriptionText.toString() : null; - - MusicService musicService = MusicServiceFactory.getMusicService(); - musicService.updateShare(share.getId(), description, millis); - return null; - } - - @Override - protected void done(Void result) - { - load(true); - Util.toast(getContext(), getResources().getString(R.string.playlist_updated_info, share.getName())); - } - - @Override - protected void error(Throwable error) - { - String msg; - msg = error instanceof OfflineException || error instanceof ApiNotSupportedException ? getErrorMessage(error) : String.format("%s %s", getResources().getString(R.string.playlist_updated_info_error, share.getName()), getErrorMessage(error)); - - Util.toast(getContext(), msg, false); - } - }.execute(); - } - }); - - alertDialog.setNegativeButton(R.string.common_cancel, null); - alertDialog.show(); - } -} 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 d3ae8caf..a0c607c0 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt @@ -44,6 +44,7 @@ import com.google.android.material.navigation.NavigationView import io.reactivex.rxjava3.disposables.CompositeDisposable import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel +import org.moire.ultrasonic.NavigationGraphDirections import org.moire.ultrasonic.R import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.data.ActiveServerProvider @@ -76,6 +77,9 @@ class NavigationActivity : AppCompatActivity() { private var bookmarksMenuItem: MenuItem? = null private var sharesMenuItem: MenuItem? = null private var podcastsMenuItem: MenuItem? = null + private var playlistsMenuItem: MenuItem? = null + private var downloadsMenuItem: MenuItem? = null + private var nowPlayingView: FragmentContainerView? = null private var nowPlayingHidden = false private var navigationView: NavigationView? = null @@ -274,15 +278,25 @@ class NavigationActivity : AppCompatActivity() { private fun setupNavigationMenu(navController: NavController) { navigationView?.setupWithNavController(navController) - // The exit menu is handled here manually - val exitItem: MenuItem? = navigationView?.menu?.findItem(R.id.menu_exit) - exitItem?.setOnMenuItemClickListener { item -> - if (item.itemId == R.id.menu_exit) { - setResult(Constants.RESULT_CLOSE_ALL) - mediaPlayerController.stopJukeboxService() - finish() - exit() + // The fragments which expect SafeArgs need to be navigated to with SafeArgs (even when + // they are empty)! + navigationView?.setNavigationItemSelectedListener { + when (it.itemId) { + R.id.mediaLibraryFragment -> { + navController.navigate(NavigationGraphDirections.toMediaLibrary()) + } + R.id.bookmarksFragment -> { + navController.navigate(NavigationGraphDirections.toBookmarks()) + } + R.id.menu_exit -> { + setResult(Constants.RESULT_CLOSE_ALL) + mediaPlayerController.stopJukeboxService() + finish() + exit() + } + else -> navController.navigate(it.itemId) } + drawerLayout?.closeDrawer(GravityCompat.START) true } @@ -290,6 +304,9 @@ class NavigationActivity : AppCompatActivity() { bookmarksMenuItem = navigationView?.menu?.findItem(R.id.bookmarksFragment) sharesMenuItem = navigationView?.menu?.findItem(R.id.sharesFragment) podcastsMenuItem = navigationView?.menu?.findItem(R.id.podcastFragment) + playlistsMenuItem = navigationView?.menu?.findItem(R.id.playlistsFragment) + downloadsMenuItem = navigationView?.menu?.findItem(R.id.downloadsFragment) + selectServerButton = navigationView?.getHeaderView(0)?.findViewById(R.id.header_select_server) selectServerButton?.setOnClickListener { @@ -457,17 +474,17 @@ class NavigationActivity : AppCompatActivity() { } private fun setMenuForServerCapabilities() { - if (ActiveServerProvider.isOffline()) { - chatMenuItem?.isVisible = false - bookmarksMenuItem?.isVisible = false - sharesMenuItem?.isVisible = false - podcastsMenuItem?.isVisible = false - return - } + val isOnline = !ActiveServerProvider.isOffline() val activeServer = activeServerProvider.getActiveServer() + + // Note: Offline capabilities are defined in ActiveServerProvider, OFFLINE_DB. + // If you add Offline support for some of these features you need + // to switch the boolean to true there. chatMenuItem?.isVisible = activeServer.chatSupport != false bookmarksMenuItem?.isVisible = activeServer.bookmarkSupport != false sharesMenuItem?.isVisible = activeServer.shareSupport != false podcastsMenuItem?.isVisible = activeServer.podcastSupport != false + playlistsMenuItem?.isVisible = isOnline + downloadsMenuItem?.isVisible = isOnline } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt index d7b50e7a..bba08b76 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt @@ -207,7 +207,11 @@ class ActiveServerProvider( allowSelfSignedCertificate = false, ldapSupport = false, musicFolderId = "", - minimumApiVersion = null + minimumApiVersion = null, + bookmarkSupport = false, + podcastSupport = false, + shareSupport = false, + chatSupport = false ) /** 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 2557aea4..ea918f33 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt @@ -1,23 +1,28 @@ /* * AlbumListFragment.kt - * Copyright (C) 2009-2021 Ultrasonic developers + * Copyright (C) 2009-2022 Ultrasonic developers * * Distributed under terms of the GNU GPLv3 license. */ +@file:Suppress("NAME_SHADOWING") + package org.moire.ultrasonic.fragment import android.os.Bundle import android.view.View import androidx.fragment.app.viewModels import androidx.lifecycle.LiveData +import androidx.lifecycle.viewModelScope import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.RecyclerView +import kotlinx.coroutines.launch import org.moire.ultrasonic.R import org.moire.ultrasonic.adapters.AlbumRowBinder +import org.moire.ultrasonic.api.subsonic.models.AlbumListType import org.moire.ultrasonic.domain.Album import org.moire.ultrasonic.model.AlbumListModel -import org.moire.ultrasonic.util.Constants /** * Displays a list of Albums from the media library @@ -39,33 +44,58 @@ class AlbumListFragment : EntryListFragment() { */ override val refreshOnCreation: Boolean = false + private val navArgs: AlbumListFragmentArgs by navArgs() + /** * The central function to pass a query to the model and return a LiveData object */ override fun getLiveData( - args: Bundle?, refresh: Boolean ): LiveData> { - if (args == null) throw IllegalArgumentException("Required arguments are missing") + fetchAlbums(refresh) - val refresh2 = args.getBoolean(Constants.INTENT_REFRESH) || refresh - val append = args.getBoolean(Constants.INTENT_APPEND) - - return listModel.getAlbumList(refresh2 or append, refreshListView!!, args) + return listModel.list } + private fun fetchAlbums(refresh: Boolean = navArgs.refresh, append: Boolean = navArgs.append) { + val refresh = navArgs.refresh || refresh + + listModel.viewModelScope.launch(handler) { + refreshListView?.isRefreshing = true + + if (navArgs.type == AlbumListType.BY_ARTIST) { + listModel.getAlbumsOfArtist( + refresh = navArgs.refresh, + id = navArgs.id!!, + name = navArgs.title + ) + } else { + listModel.getAlbums( + albumListType = navArgs.type, + size = navArgs.size, + offset = navArgs.offset, + append = append, + refresh = refresh or append + ) + } + refreshListView?.isRefreshing = false + } + } + + // TODO: Make generic + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + setTitle(navArgs.title) + // Attach our onScrollListener listView = view.findViewById(recyclerViewId).apply { val scrollListener = object : EndlessScrollListener(viewManager) { 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 = getArgumentsClone() - appendArgs.putBoolean(Constants.INTENT_APPEND, true) - getLiveData(appendArgs) + fetchAlbums(append = true) } } addOnScrollListener(scrollListener) @@ -83,11 +113,12 @@ class AlbumListFragment : EntryListFragment() { } override fun onItemClick(item: Album) { - val bundle = Bundle() - bundle.putString(Constants.INTENT_ID, item.id) - bundle.putBoolean(Constants.INTENT_IS_ALBUM, item.isDirectory) - bundle.putString(Constants.INTENT_NAME, item.title) - bundle.putString(Constants.INTENT_PARENT_ID, item.parent) - findNavController().navigate(R.id.trackCollectionFragment, bundle) + val action = AlbumListFragmentDirections.albumListToTrackCollection( + item.id, + isAlbum = item.isDirectory, + name = item.title, + parentId = item.parent + ) + findNavController().navigate(action) } } 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 2446e05a..eef035d9 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt @@ -1,18 +1,25 @@ +/* + * ArtistListFragment.kt + * Copyright (C) 2009-2022 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + 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.NavController import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs import org.moire.ultrasonic.R import org.moire.ultrasonic.adapters.ArtistRowBinder +import org.moire.ultrasonic.api.subsonic.models.AlbumListType import org.moire.ultrasonic.domain.Artist import org.moire.ultrasonic.domain.ArtistOrIndex import org.moire.ultrasonic.domain.Index import org.moire.ultrasonic.model.ArtistListModel -import org.moire.ultrasonic.util.Constants /** * Displays the list of Artists or Indexes (folders) from the media library @@ -29,16 +36,18 @@ class ArtistListFragment : EntryListFragment() { */ override val mainLayout = R.layout.list_layout_generic + private val navArgs: ArtistListFragmentArgs by navArgs() + /** * The central function to pass a query to the model and return a LiveData object */ - override fun getLiveData(args: Bundle?, refresh: Boolean): LiveData> { - val refresh2 = args?.getBoolean(Constants.INTENT_REFRESH) ?: false || refresh - return listModel.getItems(refresh2, refreshListView!!) + override fun getLiveData(refresh: Boolean): LiveData> { + return listModel.getItems(navArgs.refresh || refresh, refreshListView!!) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + setTitle(navArgs.title) viewAdapter.register( ArtistRowBinder( @@ -55,29 +64,24 @@ class ArtistListFragment : EntryListFragment() { * If we are showing artists, we need to go to AlbumList */ override fun onItemClick(item: ArtistOrIndex) { - Companion.onItemClick(item, findNavController()) - } - - companion object { - fun onItemClick(item: ArtistOrIndex, navController: NavController) { - val bundle = Bundle() - - // Common arguments - bundle.putString(Constants.INTENT_ID, item.id) - bundle.putString(Constants.INTENT_NAME, item.name) - bundle.putString(Constants.INTENT_PARENT_ID, item.id) - bundle.putBoolean(Constants.INTENT_ARTIST, (item is Artist)) - - // Check type - if (item is Index) { - navController.navigate(R.id.artistsListToTrackCollection, bundle) - } else { - bundle.putString(Constants.INTENT_ALBUM_LIST_TYPE, Constants.ALBUMS_OF_ARTIST) - bundle.putString(Constants.INTENT_ALBUM_LIST_TITLE, item.name) - bundle.putInt(Constants.INTENT_ALBUM_LIST_SIZE, 1000) - bundle.putInt(Constants.INTENT_ALBUM_LIST_OFFSET, 0) - navController.navigate(R.id.artistsListToAlbumsList, bundle) - } + // Check type + val action = if (item is Index) { + ArtistListFragmentDirections.artistsListToTrackCollection( + id = item.id, + name = item.name, + parentId = item.id, + isArtist = (item is Artist) + ) + } else { + ArtistListFragmentDirections.artistsListToAlbumsList( + type = AlbumListType.BY_ARTIST, + id = item.id, + title = item.name, + size = 1000, + offset = 0 + ) } + + findNavController().navigate(action) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt index 25aa3b98..8cbf1a74 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt @@ -37,7 +37,6 @@ class BookmarksFragment : TrackCollectionFragment() { } override fun getLiveData( - args: Bundle?, refresh: Boolean ): LiveData> { listModel.viewModelScope.launch(handler) { 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 3b6b302b..8a93da28 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt @@ -41,7 +41,7 @@ class DownloadsFragment : MultiListFragment() { /** * The central function to pass a query to the model and return a LiveData object */ - override fun getLiveData(args: Bundle?, refresh: Boolean): LiveData> { + override fun getLiveData(refresh: Boolean): LiveData> { return listModel.getList() } 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 a527b6d8..ed66cef3 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt @@ -1,3 +1,10 @@ +/* + * EditServerFragment.kt + * Copyright (C) 2009-2022 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + package org.moire.ultrasonic.fragment import android.os.Bundle diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EndlessScrollListener.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EndlessScrollListener.kt index 4e12491a..4b8664c4 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EndlessScrollListener.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EndlessScrollListener.kt @@ -24,7 +24,7 @@ abstract class EndlessScrollListener : RecyclerView.OnScrollListener { // Sets the starting page index private val startingPageIndex = 0 - var thisManager: RecyclerView.LayoutManager + private var thisManager: RecyclerView.LayoutManager constructor(layoutManager: LinearLayoutManager) { thisManager = layoutManager diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt index 395d2d6c..0cd26e32 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt @@ -1,3 +1,10 @@ +/* + * EntryListFragment.kt + * Copyright (C) 2009-2022 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + package org.moire.ultrasonic.fragment import android.os.Bundle @@ -13,7 +20,6 @@ import org.moire.ultrasonic.domain.GenericEntry import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.subsonic.DownloadHandler -import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Settings /** @@ -26,9 +32,9 @@ abstract class EntryListFragment : MultiListFragment() { /** * Whether to show the folder selector */ - fun showFolderHeader(): Boolean { - return listModel.showSelectFolderHeader(arguments) && - !listModel.isOffline() && !Settings.shouldUseId3Tags + private fun showFolderHeader(): Boolean { + return listModel.showSelectFolderHeader() && !listModel.isOffline() && + !Settings.shouldUseId3Tags } override fun onContextMenuItemSelected(menuItem: MenuItem, item: T): Boolean { @@ -38,12 +44,14 @@ abstract class EntryListFragment : MultiListFragment() { } override fun onItemClick(item: T) { - val bundle = Bundle() - bundle.putString(Constants.INTENT_ID, item.id) - bundle.putString(Constants.INTENT_NAME, item.name) - bundle.putString(Constants.INTENT_PARENT_ID, item.id) - bundle.putBoolean(Constants.INTENT_ARTIST, (item is Artist)) - findNavController().navigate(R.id.trackCollectionFragment, bundle) + val action = EntryListFragmentDirections.entryListToTrackCollection( + id = item.id, + name = item.name, + parentId = item.id, + isArtist = (item is Artist), + ) + + findNavController().navigate(action) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -59,7 +67,7 @@ abstract class EntryListFragment : MultiListFragment() { currentSetting.musicFolderId = it serverSettingsModel.updateItem(currentSetting) } - listModel.refresh(refreshListView!!, arguments) + listModel.refresh(refreshListView!!) } viewAdapter.register( @@ -71,7 +79,7 @@ abstract class EntryListFragment : MultiListFragment() { * What to do when the list has changed */ override val defaultObserver: (List) -> Unit = { - emptyView.isVisible = it.isEmpty() && !(refreshListView?.isRefreshing?:false) + emptyView.isVisible = it.isEmpty() && !(refreshListView?.isRefreshing ?: false) if (showFolderHeader()) { val list = mutableListOf(folderHeader) @@ -92,12 +100,11 @@ abstract class EntryListFragment : MultiListFragment() { ) listModel.musicFolders.observe( - viewLifecycleOwner, - { - header.folders = it - viewAdapter.notifyItemChanged(0) - } - ) + viewLifecycleOwner + ) { + header.folders = it + viewAdapter.notifyItemChanged(0) + } header } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MainFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MainFragment.kt index caa1def0..75f07df5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MainFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MainFragment.kt @@ -1,3 +1,10 @@ +/* + * MainFragment.kt + * Copyright (C) 2009-2022 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + package org.moire.ultrasonic.fragment import android.os.Bundle @@ -7,12 +14,12 @@ import android.view.ViewGroup import android.widget.TextView import androidx.core.view.isVisible import androidx.fragment.app.Fragment -import androidx.navigation.Navigation +import androidx.navigation.fragment.findNavController import org.koin.core.component.KoinComponent import org.moire.ultrasonic.R +import org.moire.ultrasonic.api.subsonic.models.AlbumListType import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline import org.moire.ultrasonic.databinding.MainBinding -import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util @@ -118,7 +125,7 @@ class MainFragment : Fragment(), KoinComponent { musicTitle.isVisible = true artistsButton.isVisible = true albumsButton.isVisible = isOnline || useId3Offline - genresButton.isVisible = true + genresButton.isVisible = isOnline // Songs songsTitle.isVisible = true @@ -143,35 +150,35 @@ class MainFragment : Fragment(), KoinComponent { private fun setupClickListener() { albumsNewestButton.setOnClickListener { - showAlbumList("newest", R.string.main_albums_newest) + showAlbumList(AlbumListType.NEWEST, R.string.main_albums_newest) } albumsRandomButton.setOnClickListener { - showAlbumList("random", R.string.main_albums_random) + showAlbumList(AlbumListType.RANDOM, R.string.main_albums_random) } albumsHighestButton.setOnClickListener { - showAlbumList("highest", R.string.main_albums_highest) + showAlbumList(AlbumListType.HIGHEST, R.string.main_albums_highest) } albumsRecentButton.setOnClickListener { - showAlbumList("recent", R.string.main_albums_recent) + showAlbumList(AlbumListType.RECENT, R.string.main_albums_recent) } albumsFrequentButton.setOnClickListener { - showAlbumList("frequent", R.string.main_albums_frequent) + showAlbumList(AlbumListType.FREQUENT, R.string.main_albums_frequent) } albumsStarredButton.setOnClickListener { - showAlbumList(Constants.STARRED, R.string.main_albums_starred) + showAlbumList(AlbumListType.STARRED, R.string.main_albums_starred) } albumsAlphaByNameButton.setOnClickListener { - showAlbumList(Constants.ALPHABETICAL_BY_NAME, R.string.main_albums_alphaByName) + showAlbumList(AlbumListType.SORTED_BY_NAME, R.string.main_albums_alphaByName) } albumsAlphaByArtistButton.setOnClickListener { - showAlbumList("alphabeticalByArtist", R.string.main_albums_alphaByArtist) + showAlbumList(AlbumListType.SORTED_BY_ARTIST, R.string.main_albums_alphaByArtist) } songsStarredButton.setOnClickListener { @@ -183,7 +190,7 @@ class MainFragment : Fragment(), KoinComponent { } albumsButton.setOnClickListener { - showAlbumList(Constants.ALPHABETICAL_BY_NAME, R.string.main_albums_title) + showAlbumList(AlbumListType.SORTED_BY_NAME, R.string.main_albums_title) } randomSongsButton.setOnClickListener { @@ -200,45 +207,47 @@ class MainFragment : Fragment(), KoinComponent { } private fun showStarredSongs() { - val bundle = Bundle() - bundle.putInt(Constants.INTENT_STARRED, 1) - Navigation.findNavController(requireView()).navigate(R.id.mainToTrackCollection, bundle) + val action = MainFragmentDirections.mainToTrackCollection( + getStarred = true, + ) + findNavController().navigate(action) } private fun showRandomSongs() { - val bundle = Bundle() - bundle.putInt(Constants.INTENT_RANDOM, 1) - bundle.putInt(Constants.INTENT_ALBUM_LIST_SIZE, Settings.maxSongs) - Navigation.findNavController(requireView()).navigate(R.id.mainToTrackCollection, bundle) + val action = MainFragmentDirections.mainToTrackCollection( + getRandom = true, + size = Settings.maxSongs + ) + findNavController().navigate(action) } private fun showArtists() { - val bundle = Bundle() - bundle.putString( - Constants.INTENT_ALBUM_LIST_TITLE, - requireContext().resources.getString(R.string.main_artists_title) + val action = MainFragmentDirections.mainToArtistList( + title = requireContext().resources.getString(R.string.main_artists_title) ) - Navigation.findNavController(requireView()).navigate(R.id.mainToArtistList, bundle) + findNavController().navigate(action) } - private fun showAlbumList(type: String, titleIndex: Int) { - val bundle = Bundle() + private fun showAlbumList(type: AlbumListType, titleIndex: Int) { val title = requireContext().resources.getString(titleIndex, "") - bundle.putString(Constants.INTENT_ALBUM_LIST_TYPE, type) - bundle.putString(Constants.INTENT_ALBUM_LIST_TITLE, title) - bundle.putInt(Constants.INTENT_ALBUM_LIST_SIZE, Settings.maxAlbums) - bundle.putInt(Constants.INTENT_ALBUM_LIST_OFFSET, 0) - Navigation.findNavController(requireView()).navigate(R.id.mainToAlbumList, bundle) + val action = MainFragmentDirections.mainToAlbumList( + type = type, + title = title, + size = Settings.maxAlbums, + offset = 0 + ) + findNavController().navigate(action) } private fun showGenres() { - Navigation.findNavController(requireView()).navigate(R.id.mainToSelectGenre) + findNavController().navigate(R.id.mainToSelectGenre) } private fun showVideos() { - val bundle = Bundle() - bundle.putInt(Constants.INTENT_VIDEOS, 1) - Navigation.findNavController(requireView()).navigate(R.id.mainToTrackCollection, bundle) + val action = MainFragmentDirections.mainToTrackCollection( + getVideos = true, + ) + findNavController().navigate(action) } companion object { 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 fef74587..e1900806 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt @@ -8,6 +8,8 @@ package org.moire.ultrasonic.fragment import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.view.LayoutInflater import android.view.MenuItem import android.view.View @@ -22,6 +24,7 @@ import androidx.lifecycle.MutableLiveData import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import kotlinx.coroutines.CoroutineExceptionHandler import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel import org.moire.ultrasonic.R @@ -32,7 +35,7 @@ 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.CommunicationError import org.moire.ultrasonic.util.Util /** @@ -67,12 +70,12 @@ abstract class MultiListFragment : Fragment() { * The LiveData containing the list provided by the model * Implement this as a getter */ - internal lateinit var liveDataItems: LiveData> + private lateinit var liveDataItems: LiveData> /** * The central function to pass a query to the model and return a LiveData object */ - open fun getLiveData(args: Bundle? = null, refresh: Boolean = false): LiveData> { + open fun getLiveData(refresh: Boolean = false): LiveData> { return MutableLiveData() } @@ -94,6 +97,16 @@ abstract class MultiListFragment : Fragment() { */ open val refreshOnCreation: Boolean = true + /** + * The default Exception Handler for Coroutines + */ + val handler = CoroutineExceptionHandler { _, exception -> + Handler(Looper.getMainLooper()).post { + CommunicationError.handleError(exception, context) + } + refreshListView?.isRefreshing = false + } + open fun setTitle(title: String?) { if (title == null) { FragmentTitle.setTitle( @@ -118,17 +131,14 @@ abstract class MultiListFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - // Set the title if available - setTitle(arguments?.getString(Constants.INTENT_ALBUM_LIST_TITLE)) - // Setup refresh handler refreshListView = view.findViewById(refreshListId) refreshListView?.setOnRefreshListener { - listModel.refresh(refreshListView!!, arguments) + listModel.refresh(refreshListView!!) } // Populate the LiveData. This starts an API request in most cases - liveDataItems = getLiveData(arguments, refreshOnCreation) + liveDataItems = getLiveData(refreshOnCreation) // Link view to display text if the list is empty emptyView = view.findViewById(emptyViewId) @@ -165,16 +175,4 @@ 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 - } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt index 9f3254b5..e9b6ab8c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt @@ -17,15 +17,16 @@ import android.widget.ImageView import android.widget.TextView import androidx.fragment.app.Fragment import androidx.navigation.Navigation +import androidx.navigation.fragment.findNavController import io.reactivex.rxjava3.disposables.Disposable import java.lang.Exception import kotlin.math.abs import org.koin.android.ext.android.inject +import org.moire.ultrasonic.NavigationGraphDirections import org.moire.ultrasonic.R import org.moire.ultrasonic.service.MediaPlayerController import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.subsonic.ImageLoaderProvider -import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util.applyTheme import org.moire.ultrasonic.util.Util.getNotificationImageSize @@ -35,7 +36,6 @@ import timber.log.Timber /** * Contains the mini-now playing information box displayed at the bottom of the screen */ -@Suppress("unused") class NowPlayingFragment : Fragment() { private var downX = 0f @@ -107,21 +107,13 @@ class NowPlayingFragment : Fragment() { nowPlayingArtist!!.text = artist nowPlayingAlbumArtImage!!.setOnClickListener { - val bundle = Bundle() - - if (Settings.shouldUseId3Tags) { - bundle.putBoolean(Constants.INTENT_IS_ALBUM, true) - bundle.putString(Constants.INTENT_ID, file.albumId) - } else { - bundle.putBoolean(Constants.INTENT_IS_ALBUM, false) - bundle.putString(Constants.INTENT_ID, file.parent) - } - - bundle.putString(Constants.INTENT_NAME, file.album) - bundle.putString(Constants.INTENT_NAME, file.album) - - Navigation.findNavController(requireActivity(), R.id.nav_host_fragment) - .navigate(R.id.trackCollectionFragment, bundle) + val id3 = Settings.shouldUseId3Tags + val action = NavigationGraphDirections.toTrackCollection( + isAlbum = id3, + id = if (id3) file.albumId else file.parent, + name = file.album + ) + findNavController().navigate(action) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/OnBackPressedHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/OnBackPressedHandler.kt index cf7ecb32..c34ca2dd 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/OnBackPressedHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/OnBackPressedHandler.kt @@ -1,3 +1,10 @@ +/* + * OnBackPressedHandler.kt + * Copyright (C) 2009-2022 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + package org.moire.ultrasonic.fragment /** 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 598d46e2..06221a4b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -43,6 +43,7 @@ import androidx.media3.common.Player import androidx.media3.common.Timeline import androidx.media3.session.SessionResult import androidx.navigation.Navigation +import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper.ACTION_STATE_DRAG import androidx.recyclerview.widget.LinearLayoutManager @@ -160,7 +161,7 @@ class PlayerFragment : private val hollowStar = R.drawable.ic_star_hollow private val fullStar = R.drawable.ic_star_full - internal val viewAdapter: BaseAdapter by lazy { + private val viewAdapter: BaseAdapter by lazy { BaseAdapter() } @@ -205,7 +206,7 @@ class PlayerFragment : fiveStar5ImageView = view.findViewById(R.id.song_five_star_5) } - @Suppress("LongMethod") + @Suppress("LongMethod", "DEPRECATION") @SuppressLint("ClickableViewAccessibility") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { cancellationToken = CancellationToken() @@ -353,15 +354,6 @@ class PlayerFragment : registerForContextMenu(playlistView) - if (arguments != null && requireArguments().getBoolean( - Constants.INTENT_SHUFFLE, - false - ) - ) { - networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() - mediaPlayerController.isShufflePlayEnabled = true - } - visualizerViewLayout.isVisible = false VisualizerController.get().observe( requireActivity() @@ -641,11 +633,14 @@ class PlayerFragment : if (track == null) return false if (Settings.shouldUseId3Tags) { + PlayerFragmentDirections.playerToSelectAlbum( + id = track.artistId, + name = track.artist, + parentId = track.artistId, + isArtist = true, + ) bundle = Bundle() - bundle.putString(Constants.INTENT_ID, track.artistId) - bundle.putString(Constants.INTENT_NAME, track.artist) - bundle.putString(Constants.INTENT_PARENT_ID, track.artistId) - bundle.putBoolean(Constants.INTENT_ARTIST, true) + Navigation.findNavController(requireView()) .navigate(R.id.playerToSelectAlbum, bundle) } @@ -655,18 +650,19 @@ class PlayerFragment : if (track == null) return false val albumId = if (Settings.shouldUseId3Tags) track.albumId else track.parent - bundle = Bundle() - bundle.putString(Constants.INTENT_ID, albumId) - bundle.putString(Constants.INTENT_NAME, track.album) - bundle.putString(Constants.INTENT_PARENT_ID, track.parent) - bundle.putBoolean(Constants.INTENT_IS_ALBUM, true) - Navigation.findNavController(requireView()) - .navigate(R.id.playerToSelectAlbum, bundle) + + val action = PlayerFragmentDirections.playerToSelectAlbum( + id = albumId, + name = track.album, + parentId = track.parent, + isAlbum = true + ) + + findNavController().navigate(action) return true } R.id.menu_lyrics -> { if (track == null) return false - bundle = Bundle() bundle.putString(Constants.INTENT_ARTIST, track.artist) bundle.putString(Constants.INTENT_TITLE, track.title) @@ -815,7 +811,12 @@ class PlayerFragment : val playlistEntry = item.toTrack() tracks.add(playlistEntry) } - shareHandler.createShare(this, tracks, null, cancellationToken) + shareHandler.createShare( + this, + tracks = tracks, + swipe = null, + cancellationToken = cancellationToken, + ) return true } R.id.menu_item_share_song -> { @@ -824,7 +825,12 @@ class PlayerFragment : val tracks: MutableList = ArrayList() tracks.add(currentSong) - shareHandler.createShare(this, tracks, null, cancellationToken) + shareHandler.createShare( + this, + tracks, + swipe = null, + cancellationToken = cancellationToken + ) return true } else -> return false diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt index 2f6d8196..d3b5d7f2 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt @@ -1,3 +1,10 @@ +/* + * SearchFragment.kt + * Copyright (C) 2009-2022 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + package org.moire.ultrasonic.fragment import android.app.SearchManager @@ -11,7 +18,6 @@ import androidx.appcompat.widget.SearchView import androidx.core.view.isVisible import androidx.fragment.app.viewModels import androidx.lifecycle.viewModelScope -import androidx.navigation.Navigation import androidx.navigation.fragment.findNavController import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import kotlinx.coroutines.launch @@ -24,6 +30,7 @@ import org.moire.ultrasonic.adapters.DividerBinder import org.moire.ultrasonic.adapters.MoreButtonBinder import org.moire.ultrasonic.adapters.MoreButtonBinder.MoreButton import org.moire.ultrasonic.adapters.TrackViewBinder +import org.moire.ultrasonic.api.subsonic.models.AlbumListType import org.moire.ultrasonic.domain.Album import org.moire.ultrasonic.domain.Artist import org.moire.ultrasonic.domain.ArtistOrIndex @@ -47,6 +54,8 @@ import timber.log.Timber /** * Initiates a search on the media library and displays the results + * + * TODO: Move to SafeArgs */ class SearchFragment : MultiListFragment(), KoinComponent { private var searchResult: SearchResult? = null @@ -266,33 +275,37 @@ class SearchFragment : MultiListFragment(), KoinComponent { } private fun onArtistSelected(item: ArtistOrIndex) { - val bundle = Bundle() - - // Common arguments - bundle.putString(Constants.INTENT_ID, item.id) - bundle.putString(Constants.INTENT_NAME, item.name) - bundle.putString(Constants.INTENT_PARENT_ID, item.id) - bundle.putBoolean(Constants.INTENT_ARTIST, (item is Artist)) - - // Check type - if (item is Index) { - findNavController().navigate(R.id.searchToTrackCollection, bundle) + // Create action based on type + val action = if (item is Index) { + SearchFragmentDirections.searchToTrackCollection( + id = item.id, + name = item.name, + parentId = item.id, + isArtist = (item is Artist) + ) } else { - bundle.putString(Constants.INTENT_ALBUM_LIST_TYPE, Constants.ALBUMS_OF_ARTIST) - bundle.putString(Constants.INTENT_ALBUM_LIST_TITLE, item.name) - bundle.putInt(Constants.INTENT_ALBUM_LIST_SIZE, 1000) - bundle.putInt(Constants.INTENT_ALBUM_LIST_OFFSET, 0) - findNavController().navigate(R.id.searchToAlbumsList, bundle) + SearchFragmentDirections.searchToAlbumsList( + type = AlbumListType.BY_ARTIST, + id = item.id, + title = item.name, + size = 1000, + offset = 0 + ) } + + // Lets go! + findNavController().navigate(action) } private fun onAlbumSelected(album: Album, autoplay: Boolean) { - val bundle = Bundle() - bundle.putString(Constants.INTENT_ID, album.id) - bundle.putString(Constants.INTENT_NAME, album.title) - bundle.putBoolean(Constants.INTENT_IS_ALBUM, album.isDirectory) - bundle.putBoolean(Constants.INTENT_AUTOPLAY, autoplay) - Navigation.findNavController(requireView()).navigate(R.id.searchToTrackCollection, bundle) + + val action = SearchFragmentDirections.searchToTrackCollection( + id = album.id, + name = album.title, + autoPlay = autoplay, + isAlbum = true + ) + findNavController().navigate(action) } private fun onSongSelected(song: Track, append: Boolean) { @@ -366,7 +379,8 @@ class SearchFragment : MultiListFragment(), KoinComponent { autoPlay = true, playNext = false, shuffle = false, - songs = songs + songs = songs, + playlistName = null ) } R.id.song_menu_play_next -> { @@ -378,7 +392,8 @@ class SearchFragment : MultiListFragment(), KoinComponent { autoPlay = false, playNext = true, shuffle = false, - songs = songs + songs = songs, + playlistName = null ) } R.id.song_menu_play_last -> { @@ -390,7 +405,8 @@ class SearchFragment : MultiListFragment(), KoinComponent { autoPlay = false, playNext = false, shuffle = false, - songs = songs + songs = songs, + playlistName = null ) } R.id.song_menu_pin -> { @@ -431,7 +447,13 @@ class SearchFragment : MultiListFragment(), KoinComponent { } R.id.song_menu_share -> { songs.add(item) - shareHandler.createShare(this, songs, searchRefresh, cancellationToken!!) + shareHandler.createShare( + fragment = this, + tracks = songs, + swipe = searchRefresh, + cancellationToken = cancellationToken!!, + additionalId = null + ) } } 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 ddf9cc04..b4d9c021 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -8,8 +8,6 @@ package org.moire.ultrasonic.fragment import android.os.Bundle -import android.os.Handler -import android.os.Looper import android.view.Menu import android.view.MenuInflater import android.view.MenuItem @@ -19,11 +17,11 @@ import androidx.core.view.isVisible import androidx.fragment.app.viewModels import androidx.lifecycle.LiveData import androidx.lifecycle.viewModelScope -import androidx.navigation.Navigation +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs 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 import org.moire.ultrasonic.R @@ -45,9 +43,7 @@ import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker import org.moire.ultrasonic.subsonic.ShareHandler import org.moire.ultrasonic.subsonic.VideoPlayer import org.moire.ultrasonic.util.CancellationToken -import org.moire.ultrasonic.util.CommunicationError import org.moire.ultrasonic.util.ConfirmationDialog -import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.EntryByDiscAndTrackComparator import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util @@ -93,6 +89,8 @@ open class TrackCollectionFragment : MultiListFragment() { */ override val mainLayout: Int = R.layout.list_layout_track + private val navArgs: TrackCollectionFragmentArgs by navArgs() + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) cancellationToken = CancellationToken() @@ -102,7 +100,7 @@ open class TrackCollectionFragment : MultiListFragment() { // Setup refresh handler refreshListView = view.findViewById(refreshListId) refreshListView?.setOnRefreshListener { - getLiveData(arguments, true) + handleRefresh() } setupButtons(view) @@ -155,6 +153,10 @@ open class TrackCollectionFragment : MultiListFragment() { } } + internal open fun handleRefresh() { + getLiveData(true) + } + internal open fun setupButtons(view: View) { selectButton = view.findViewById(R.id.select_album_select) playNowButton = view.findViewById(R.id.select_album_play_now) @@ -178,7 +180,8 @@ open class TrackCollectionFragment : MultiListFragment() { downloadHandler.download( this@TrackCollectionFragment, append = true, save = false, autoPlay = false, playNext = true, shuffle = false, - songs = getSelectedSongs() + songs = getSelectedSongs(), + playlistName = navArgs.playlistName ) } @@ -219,13 +222,6 @@ open class TrackCollectionFragment : MultiListFragment() { } } - val handler = CoroutineExceptionHandler { _, exception -> - Handler(Looper.getMainLooper()).post { - CommunicationError.handleError(exception, context) - } - refreshListView?.isRefreshing = false - } - override fun onPrepareOptionsMenu(menu: Menu) { super.onPrepareOptionsMenu(menu) playAllButton = menu.findItem(R.id.select_album_play_all) @@ -254,7 +250,8 @@ open class TrackCollectionFragment : MultiListFragment() { } else if (itemId == R.id.menu_item_share) { shareHandler.createShare( this, getSelectedSongs(), - refreshListView, cancellationToken!! + refreshListView, cancellationToken!!, + navArgs.id ) return true } @@ -274,7 +271,7 @@ open class TrackCollectionFragment : MultiListFragment() { if (selectedSongs.isNotEmpty()) { downloadHandler.download( this, append, false, !append, playNext = false, - shuffle = false, songs = selectedSongs + shuffle = false, songs = selectedSongs, null ) } else { playAll(false, append) @@ -304,10 +301,10 @@ open class TrackCollectionFragment : MultiListFragment() { } } - val isArtist = arguments?.getBoolean(Constants.INTENT_ARTIST, false) ?: false - val id = arguments?.getString(Constants.INTENT_ID) + val isArtist = navArgs.isArtist + val id = navArgs.id - if (hasSubFolders && id != null) { + if (hasSubFolders) { downloadHandler.downloadRecursively( fragment = this, id = id, @@ -328,7 +325,8 @@ open class TrackCollectionFragment : MultiListFragment() { autoPlay = !append, playNext = false, shuffle = shuffle, - songs = getAllSongs() + songs = getAllSongs(), + playlistName = navArgs.playlistName ) } } @@ -465,7 +463,7 @@ open class TrackCollectionFragment : MultiListFragment() { } } - val listSize = arguments?.getInt(Constants.INTENT_ALBUM_LIST_SIZE, 0) ?: 0 + val listSize = navArgs.size // Hide select button for video lists and singular selection lists selectButton!!.isVisible = !allVideos && viewAdapter.hasMultipleSelection() && songCount > 0 @@ -475,9 +473,9 @@ open class TrackCollectionFragment : MultiListFragment() { moreButton!!.visibility = View.GONE } else { moreButton!!.visibility = View.VISIBLE - if ((arguments?.getInt(Constants.INTENT_RANDOM, 0) ?: 0) > 0) { + if (navArgs.getRandom) { moreRandomTracks() - } else if ((arguments?.getString(Constants.INTENT_GENRE_NAME, "") ?: "") != "") { + } else if (navArgs.genreName != null) { moreSongsForGenre() } } @@ -488,9 +486,7 @@ open class TrackCollectionFragment : MultiListFragment() { enableButtons() - val isAlbumList = arguments?.containsKey( - Constants.INTENT_ALBUM_LIST_TYPE - ) ?: false + val isAlbumList = (navArgs.albumListType != null) playAllButtonVisible = !(isAlbumList || entryList.isEmpty()) && !allVideos shareButtonVisible = !isOffline() && songCount > 0 @@ -499,7 +495,7 @@ open class TrackCollectionFragment : MultiListFragment() { shareButton?.isVisible = shareButtonVisible if (songCount > 0 && listModel.showHeader) { - val intentAlbumName = arguments?.getString(Constants.INTENT_NAME, "") + val intentAlbumName = navArgs.name val albumHeader = AlbumHeader(it, intentAlbumName) val mixedList: MutableList = mutableListOf(albumHeader) mixedList.addAll(entryList) @@ -508,11 +504,11 @@ open class TrackCollectionFragment : MultiListFragment() { viewAdapter.submitList(entryList) } - val playAll = arguments?.getBoolean(Constants.INTENT_AUTOPLAY, false) ?: false + val playAll = navArgs.autoPlay if (playAll && songCount > 0) { playAll( - arguments?.getBoolean(Constants.INTENT_SHUFFLE, false) ?: false, + navArgs.shuffle, false ) } @@ -522,37 +518,30 @@ open class TrackCollectionFragment : MultiListFragment() { Timber.i("Processed list") } - private fun moreSongsForGenre(args: Bundle = requireArguments()) { + private fun moreSongsForGenre() { moreButton!!.setOnClickListener { - val theGenre = args.getString(Constants.INTENT_GENRE_NAME) - val size = args.getInt(Constants.INTENT_ALBUM_LIST_SIZE, 0) - val theOffset = args.getInt( - Constants.INTENT_ALBUM_LIST_OFFSET, 0 - ) + size - val bundle = Bundle() - bundle.putString(Constants.INTENT_GENRE_NAME, theGenre) - bundle.putInt(Constants.INTENT_ALBUM_LIST_SIZE, size) - bundle.putInt(Constants.INTENT_ALBUM_LIST_OFFSET, theOffset) - - Navigation.findNavController(requireView()) - .navigate(R.id.trackCollectionFragment, bundle) + val action = TrackCollectionFragmentDirections.loadMoreTracks( + genreName = navArgs.genreName, + size = navArgs.size, + offset = navArgs.offset + navArgs.size + ) + findNavController().navigate(action) } } private fun moreRandomTracks() { - val listSize = arguments?.getInt(Constants.INTENT_ALBUM_LIST_SIZE, 0) ?: 0 + + val listSize = navArgs.size moreButton!!.setOnClickListener { - val offset = requireArguments().getInt( - Constants.INTENT_ALBUM_LIST_OFFSET, 0 - ) + listSize - val bundle = Bundle() - bundle.putInt(Constants.INTENT_RANDOM, 1) - bundle.putInt(Constants.INTENT_ALBUM_LIST_SIZE, listSize) - bundle.putInt(Constants.INTENT_ALBUM_LIST_OFFSET, offset) - Navigation.findNavController(requireView()).navigate( - R.id.trackCollectionFragment, bundle + val offset = navArgs.offset + listSize + + val action = TrackCollectionFragmentDirections.loadMoreTracks( + getRandom = true, + size = listSize, + offset = offset ) + findNavController().navigate(action) } } @@ -576,27 +565,25 @@ open class TrackCollectionFragment : MultiListFragment() { @Suppress("LongMethod") override fun getLiveData( - args: Bundle?, refresh: Boolean ): LiveData> { Timber.i("Starting gathering track collection data...") - if (args == null) return listModel.currentList - val id = args.getString(Constants.INTENT_ID) - val isAlbum = args.getBoolean(Constants.INTENT_IS_ALBUM, false) - val name = args.getString(Constants.INTENT_NAME) - val playlistId = args.getString(Constants.INTENT_PLAYLIST_ID) - val podcastChannelId = args.getString(Constants.INTENT_PODCAST_CHANNEL_ID) - val playlistName = args.getString(Constants.INTENT_PLAYLIST_NAME) - val shareId = args.getString(Constants.INTENT_SHARE_ID) - val shareName = args.getString(Constants.INTENT_SHARE_NAME) - val genreName = args.getString(Constants.INTENT_GENRE_NAME) + val id = navArgs.id + val isAlbum = navArgs.isAlbum + val name = navArgs.name + val playlistId = navArgs.playlistId + val podcastChannelId = navArgs.podcastChannelId + val playlistName = navArgs.playlistName + val shareId = navArgs.shareId + val shareName = navArgs.shareName + val genreName = navArgs.genreName - val getStarredTracks = args.getInt(Constants.INTENT_STARRED, 0) - val getVideos = args.getInt(Constants.INTENT_VIDEOS, 0) - val getRandomTracks = args.getInt(Constants.INTENT_RANDOM, 0) - val albumListSize = args.getInt(Constants.INTENT_ALBUM_LIST_SIZE, 0) - val albumListOffset = args.getInt(Constants.INTENT_ALBUM_LIST_OFFSET, 0) - val refresh2 = args.getBoolean(Constants.INTENT_REFRESH, true) || refresh + val getStarredTracks = navArgs.getStarred + val getVideos = navArgs.getVideos + val getRandomTracks = navArgs.getRandom + val albumListSize = navArgs.size + val albumListOffset = navArgs.offset + val refresh2 = navArgs.refresh || refresh listModel.viewModelScope.launch(handler) { refreshListView?.isRefreshing = true @@ -613,13 +600,13 @@ open class TrackCollectionFragment : MultiListFragment() { } else if (genreName != null) { setTitle(genreName) listModel.getSongsForGenre(genreName, albumListSize, albumListOffset) - } else if (getStarredTracks != 0) { + } else if (getStarredTracks) { setTitle(getString(R.string.main_songs_starred)) listModel.getStarred() - } else if (getVideos != 0) { + } else if (getVideos) { setTitle(R.string.main_videos) listModel.getVideos(refresh2) - } else if (getRandomTracks != 0) { + } else if (getRandomTracks) { setTitle(R.string.main_songs_random) listModel.getRandom(albumListSize) } else { @@ -659,7 +646,8 @@ open class TrackCollectionFragment : MultiListFragment() { autoPlay = false, playNext = true, shuffle = false, - songs = songs + songs = songs, + playlistName = navArgs.playlistName ) } R.id.song_menu_play_last -> { @@ -681,8 +669,11 @@ open class TrackCollectionFragment : MultiListFragment() { R.id.song_menu_share -> { if (item is Track) { shareHandler.createShare( - this, listOf(item), refreshListView, - cancellationToken!! + this, + tracks = listOf(item), + swipe = refreshListView, + cancellationToken = cancellationToken!!, + additionalId = navArgs.id ) } } @@ -706,15 +697,13 @@ open class TrackCollectionFragment : MultiListFragment() { override fun onItemClick(item: MusicDirectory.Child) { when { item.isDirectory -> { - val bundle = Bundle() - bundle.putString(Constants.INTENT_ID, item.id) - bundle.putBoolean(Constants.INTENT_IS_ALBUM, item.isDirectory) - bundle.putString(Constants.INTENT_NAME, item.title) - bundle.putString(Constants.INTENT_PARENT_ID, item.parent) - Navigation.findNavController(requireView()).navigate( - R.id.trackCollectionFragment, - bundle + val action = TrackCollectionFragmentDirections.loadMoreTracks( + id = item.id, + isAlbum = true, + name = item.title, + parentId = item.parent ) + findNavController().navigate(action) } item is Track && item.isVideo -> { VideoPlayer.playVideo(requireContext(), item) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PlaylistsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PlaylistsFragment.kt new file mode 100644 index 00000000..0bd9e4cd --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PlaylistsFragment.kt @@ -0,0 +1,345 @@ +/* + * PlaylistsFragment.kt + * Copyright (C) 2009-2022 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + +package org.moire.ultrasonic.fragment.legacy + +import android.annotation.SuppressLint +import android.app.AlertDialog +import android.os.Bundle +import android.text.Spannable +import android.text.SpannableString +import android.text.method.LinkMovementMethod +import android.text.util.Linkify +import android.view.ContextMenu +import android.view.ContextMenu.ContextMenuInfo +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView.AdapterContextMenuInfo +import android.widget.CheckBox +import android.widget.EditText +import android.widget.ListView +import android.widget.TextView +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import java.util.Locale +import org.koin.java.KoinJavaComponent.inject +import org.moire.ultrasonic.R +import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException +import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline +import org.moire.ultrasonic.domain.Playlist +import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle +import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService +import org.moire.ultrasonic.service.OfflineException +import org.moire.ultrasonic.subsonic.DownloadHandler +import org.moire.ultrasonic.util.BackgroundTask +import org.moire.ultrasonic.util.CacheCleaner +import org.moire.ultrasonic.util.CancellationToken +import org.moire.ultrasonic.util.FragmentBackgroundTask +import org.moire.ultrasonic.util.LoadingTask +import org.moire.ultrasonic.util.Util.applyTheme +import org.moire.ultrasonic.util.Util.toast +import org.moire.ultrasonic.view.PlaylistAdapter + +/** + * Displays the playlists stored on the server + * + * TODO: This file has been converted from Java, but not modernized yet. + */ +class PlaylistsFragment : Fragment() { + private var refreshPlaylistsListView: SwipeRefreshLayout? = null + private var playlistsListView: ListView? = null + private var emptyTextView: View? = null + private var playlistAdapter: PlaylistAdapter? = null + private val downloadHandler = inject( + DownloadHandler::class.java + ) + private var cancellationToken: CancellationToken? = null + override fun onCreate(savedInstanceState: Bundle?) { + applyTheme(this.context) + super.onCreate(savedInstanceState) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.select_playlist, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + cancellationToken = CancellationToken() + refreshPlaylistsListView = view.findViewById(R.id.select_playlist_refresh) + playlistsListView = view.findViewById(R.id.select_playlist_list) + refreshPlaylistsListView!!.setOnRefreshListener { load(true) } + emptyTextView = view.findViewById(R.id.select_playlist_empty) + playlistsListView!!.setOnItemClickListener { parent, _, position, _ -> + val (id1, name) = parent.getItemAtPosition(position) as Playlist + + val action = PlaylistsFragmentDirections.playlistsToTrackCollection( + id = id1, + playlistId = id1, + name = name, + playlistName = name, + ) + findNavController().navigate(action) + } + registerForContextMenu(playlistsListView!!) + setTitle(this, R.string.playlist_label) + load(false) + } + + override fun onDestroyView() { + cancellationToken!!.cancel() + super.onDestroyView() + } + + private fun load(refresh: Boolean) { + val task: BackgroundTask> = + object : FragmentBackgroundTask>( + activity, true, refreshPlaylistsListView, cancellationToken + ) { + @Throws(Throwable::class) + override fun doInBackground(): List { + val musicService = getMusicService() + val playlists = musicService.getPlaylists(refresh) + if (!isOffline()) CacheCleaner().cleanPlaylists(playlists) + return playlists + } + + override fun done(result: List) { + playlistsListView!!.adapter = + PlaylistAdapter(context, result).also { playlistAdapter = it } + emptyTextView!!.visibility = if (result.isEmpty()) View.VISIBLE else View.GONE + } + } + task.execute() + } + + override fun onCreateContextMenu(menu: ContextMenu, view: View, menuInfo: ContextMenuInfo?) { + super.onCreateContextMenu(menu, view, menuInfo) + val inflater = requireActivity().menuInflater + if (isOffline()) inflater.inflate( + R.menu.select_playlist_context_offline, + menu + ) else inflater.inflate(R.menu.select_playlist_context, menu) + val downloadMenuItem = menu.findItem(R.id.playlist_menu_download) + if (downloadMenuItem != null) { + downloadMenuItem.isVisible = !isOffline() + } + } + + override fun onContextItemSelected(menuItem: MenuItem): Boolean { + val info = menuItem.menuInfo as AdapterContextMenuInfo + val playlist = playlistsListView!!.getItemAtPosition(info.position) as Playlist + when (menuItem.itemId) { + R.id.playlist_menu_pin -> { + downloadHandler.value.downloadPlaylist( + this, + id = playlist.id, + name = playlist.name, + save = true, + append = true, + autoplay = false, + shuffle = false, + background = true, + playNext = false, + unpin = false + ) + } + R.id.playlist_menu_unpin -> { + downloadHandler.value.downloadPlaylist( + this, + id = playlist.id, + name = playlist.name, + save = false, + append = false, + autoplay = false, + shuffle = false, + background = true, + playNext = false, + unpin = true + ) + } + R.id.playlist_menu_download -> { + downloadHandler.value.downloadPlaylist( + this, + id = playlist.id, + name = playlist.name, + save = false, + append = false, + autoplay = false, + shuffle = false, + background = true, + playNext = false, + unpin = false + ) + } + R.id.playlist_menu_play_now -> { + val action = PlaylistsFragmentDirections.playlistsToTrackCollection( + playlistId = playlist.id, + playlistName = playlist.name, + autoPlay = true + ) + findNavController().navigate(action) + } + R.id.playlist_menu_play_shuffled -> { + val action = PlaylistsFragmentDirections.playlistsToTrackCollection( + playlistId = playlist.id, + playlistName = playlist.name, + autoPlay = true, + shuffle = true + ) + + findNavController().navigate(action) + } + R.id.playlist_menu_delete -> { + deletePlaylist(playlist) + } + R.id.playlist_info -> { + displayPlaylistInfo(playlist) + } + R.id.playlist_update_info -> { + updatePlaylistInfo(playlist) + } + else -> { + return super.onContextItemSelected(menuItem) + } + } + return true + } + + private fun deletePlaylist(playlist: Playlist) { + AlertDialog.Builder(context).setIcon(R.drawable.ic_baseline_warning) + .setTitle(R.string.common_confirm).setMessage( + resources.getString(R.string.delete_playlist, playlist.name) + ).setPositiveButton(R.string.common_ok) { _, _ -> + object : LoadingTask(activity, refreshPlaylistsListView, cancellationToken) { + @Throws(Throwable::class) + override fun doInBackground(): Any? { + val musicService = getMusicService() + musicService.deletePlaylist(playlist.id) + return null + } + + override fun done(result: Any?) { + playlistAdapter!!.remove(playlist) + playlistAdapter!!.notifyDataSetChanged() + toast( + context, + resources.getString(R.string.menu_deleted_playlist, playlist.name) + ) + } + + override fun error(error: Throwable) { + val msg: String = + if (error is OfflineException || error is ApiNotSupportedException) + getErrorMessage( + error + ) else String.format( + Locale.ROOT, + "%s %s", + resources.getString( + R.string.menu_deleted_playlist_error, + playlist.name + ), + getErrorMessage(error) + ) + toast(context, msg, false) + } + }.execute() + }.setNegativeButton(R.string.common_cancel, null).show() + } + + private fun displayPlaylistInfo(playlist: Playlist) { + val textView = TextView(context) + textView.setPadding(5, 5, 5, 5) + val message: Spannable = SpannableString( + """ + Owner: ${playlist.owner} + Comments: ${playlist.comment} + Song Count: ${playlist.songCount} + """.trimIndent() + + if (playlist.public == null) "" else """ + + Public: ${playlist.public} + """.trimIndent() + """ + + Creation Date: ${playlist.created.replace('T', ' ')} + """.trimIndent() + ) + Linkify.addLinks(message, Linkify.WEB_URLS) + textView.text = message + textView.movementMethod = LinkMovementMethod.getInstance() + AlertDialog.Builder(context).setTitle(playlist.name).setCancelable(true) + .setIcon(R.drawable.ic_baseline_info).setView(textView).show() + } + + @SuppressLint("InflateParams") + private fun updatePlaylistInfo(playlist: Playlist) { + val dialogView = layoutInflater.inflate(R.layout.update_playlist, null) ?: return + val nameBox = dialogView.findViewById(R.id.get_playlist_name) + val commentBox = dialogView.findViewById(R.id.get_playlist_comment) + val publicBox = dialogView.findViewById(R.id.get_playlist_public) + nameBox.setText(playlist.name) + commentBox.setText(playlist.comment) + val pub = playlist.public + if (pub == null) { + publicBox.isEnabled = false + } else { + publicBox.isChecked = pub + } + val alertDialog = AlertDialog.Builder(context) + alertDialog.setIcon(R.drawable.ic_baseline_warning) + alertDialog.setTitle(R.string.playlist_update_info) + alertDialog.setView(dialogView) + alertDialog.setPositiveButton(R.string.common_ok) { _, _ -> + object : LoadingTask(activity, refreshPlaylistsListView, cancellationToken) { + @Throws(Throwable::class) + override fun doInBackground(): Any? { + val nameBoxText = nameBox.text + val commentBoxText = commentBox.text + val name = nameBoxText?.toString() + val comment = commentBoxText?.toString() + val musicService = getMusicService() + musicService.updatePlaylist(playlist.id, name, comment, publicBox.isChecked) + return null + } + + override fun done(result: Any?) { + load(true) + toast( + context, + resources.getString(R.string.playlist_updated_info, playlist.name) + ) + } + + override fun error(error: Throwable) { + val msg: String = + if (error is OfflineException || error is ApiNotSupportedException) + getErrorMessage( + error + ) else String.format( + Locale.ROOT, + "%s %s", + resources.getString( + R.string.playlist_updated_info_error, + playlist.name + ), + getErrorMessage(error) + ) + toast(context, msg, false) + } + }.execute() + } + alertDialog.setNegativeButton(R.string.common_cancel, null) + alertDialog.show() + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PodcastFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PodcastFragment.kt new file mode 100644 index 00000000..b8fec24b --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PodcastFragment.kt @@ -0,0 +1,94 @@ +/* + * PodcastFragment.kt + * Copyright (C) 2009-2022 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + +package org.moire.ultrasonic.fragment.legacy + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ListView +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import org.moire.ultrasonic.R +import org.moire.ultrasonic.domain.PodcastsChannel +import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle +import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService +import org.moire.ultrasonic.util.BackgroundTask +import org.moire.ultrasonic.util.CancellationToken +import org.moire.ultrasonic.util.FragmentBackgroundTask +import org.moire.ultrasonic.util.Util.applyTheme +import org.moire.ultrasonic.view.PodcastsChannelsAdapter + +/** + * Displays the podcasts available on the server + * + * TODO: This file has been converted from Java, but not modernized yet. + */ +class PodcastFragment : Fragment() { + private var emptyTextView: View? = null + var channelItemsListView: ListView? = null + private var cancellationToken: CancellationToken? = null + private var swipeRefresh: SwipeRefreshLayout? = null + override fun onCreate(savedInstanceState: Bundle?) { + applyTheme(this.context) + super.onCreate(savedInstanceState) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.podcasts, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + cancellationToken = CancellationToken() + swipeRefresh = view.findViewById(R.id.podcasts_refresh) + swipeRefresh!!.setOnRefreshListener { load(view.context, true) } + setTitle(this, R.string.podcasts_label) + emptyTextView = view.findViewById(R.id.select_podcasts_empty) + channelItemsListView = view.findViewById(R.id.podcasts_channels_items_list) + channelItemsListView!!.setOnItemClickListener { parent, _, position, _ -> + val (id) = parent.getItemAtPosition(position) as PodcastsChannel + val action = PodcastFragmentDirections.podcastToTrackCollection( + podcastChannelId = id + ) + + findNavController().navigate(action) + } + load(view.context, false) + } + + override fun onDestroyView() { + cancellationToken!!.cancel() + super.onDestroyView() + } + + private fun load(context: Context, refresh: Boolean) { + val task: BackgroundTask> = + object : FragmentBackgroundTask>( + activity, true, swipeRefresh, cancellationToken + ) { + @Throws(Throwable::class) + override fun doInBackground(): List { + val musicService = getMusicService() + return musicService.getPodcastsChannels(refresh) + } + + override fun done(result: List) { + channelItemsListView!!.adapter = PodcastsChannelsAdapter(context, result) + emptyTextView!!.visibility = if (result.isEmpty()) View.VISIBLE else View.GONE + } + } + task.execute() + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SelectGenreFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SelectGenreFragment.kt new file mode 100644 index 00000000..9f038b30 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SelectGenreFragment.kt @@ -0,0 +1,107 @@ +/* + * SelectGenreFragment.kt + * Copyright (C) 2009-2022 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + +package org.moire.ultrasonic.fragment.legacy + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.ListView +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import org.moire.ultrasonic.R +import org.moire.ultrasonic.domain.Genre +import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle +import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService +import org.moire.ultrasonic.util.BackgroundTask +import org.moire.ultrasonic.util.CancellationToken +import org.moire.ultrasonic.util.FragmentBackgroundTask +import org.moire.ultrasonic.util.Settings.maxSongs +import org.moire.ultrasonic.util.Util.applyTheme +import org.moire.ultrasonic.view.GenreAdapter +import timber.log.Timber + +/** + * Displays the available genres in the media library + * + * TODO: This file has been converted from Java, but not modernized yet. + */ +class SelectGenreFragment : Fragment() { + private var refreshGenreListView: SwipeRefreshLayout? = null + private var genreListView: ListView? = null + private var emptyView: View? = null + private var cancellationToken: CancellationToken? = null + + override fun onCreate(savedInstanceState: Bundle?) { + applyTheme(this.context) + super.onCreate(savedInstanceState) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.select_genre, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + cancellationToken = CancellationToken() + refreshGenreListView = view.findViewById(R.id.select_genre_refresh) + genreListView = view.findViewById(R.id.select_genre_list) + refreshGenreListView!!.setOnRefreshListener { load(true) } + genreListView!!.setOnItemClickListener { parent: AdapterView<*>, + _: View?, + position: Int, + _: Long -> + val genre = parent.getItemAtPosition(position) as Genre + + val action = SelectGenreFragmentDirections.selectGenreToTrackCollection( + genreName = genre.name, + size = maxSongs, + offset = 0 + ) + findNavController().navigate(action) + } + emptyView = view.findViewById(R.id.select_genre_empty) + registerForContextMenu(genreListView!!) + setTitle(this, R.string.main_genres_title) + load(false) + } + + override fun onDestroyView() { + cancellationToken!!.cancel() + super.onDestroyView() + } + + private fun load(refresh: Boolean) { + val task: BackgroundTask> = object : FragmentBackgroundTask>( + activity, true, refreshGenreListView, cancellationToken + ) { + override fun doInBackground(): List { + val musicService = getMusicService() + var genres: List = ArrayList() + try { + genres = musicService.getGenres(refresh) + } catch (all: Exception) { + Timber.e(all, "Failed to load genres") + } + return genres + } + + override fun done(result: List) { + emptyView!!.isVisible = result.isEmpty() + genreListView!!.adapter = GenreAdapter(context, result) + } + } + task.execute() + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SharesFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SharesFragment.kt new file mode 100644 index 00000000..7753d079 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SharesFragment.kt @@ -0,0 +1,364 @@ +/* + * SharesFragment.kt + * Copyright (C) 2009-2022 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + +package org.moire.ultrasonic.fragment.legacy + +import android.annotation.SuppressLint +import android.app.AlertDialog +import android.os.Bundle +import android.text.Spannable +import android.text.SpannableString +import android.text.method.LinkMovementMethod +import android.text.util.Linkify +import android.view.ContextMenu +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.CheckBox +import android.widget.EditText +import android.widget.ListView +import android.widget.TextView +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import java.util.Locale +import org.koin.java.KoinJavaComponent +import org.moire.ultrasonic.R +import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException +import org.moire.ultrasonic.domain.Share +import org.moire.ultrasonic.fragment.FragmentTitle +import org.moire.ultrasonic.service.MusicServiceFactory +import org.moire.ultrasonic.service.OfflineException +import org.moire.ultrasonic.subsonic.DownloadHandler +import org.moire.ultrasonic.util.BackgroundTask +import org.moire.ultrasonic.util.CancellationToken +import org.moire.ultrasonic.util.FragmentBackgroundTask +import org.moire.ultrasonic.util.LoadingTask +import org.moire.ultrasonic.util.TimeSpan +import org.moire.ultrasonic.util.TimeSpanPicker +import org.moire.ultrasonic.util.Util +import org.moire.ultrasonic.view.ShareAdapter + +/** + * Displays the shares in the media library + * + * TODO: This file has been converted from Java, but not modernized yet. + */ +class SharesFragment : Fragment() { + private var refreshSharesListView: SwipeRefreshLayout? = null + private var sharesListView: ListView? = null + private var emptyTextView: View? = null + private var shareAdapter: ShareAdapter? = null + private val downloadHandler = KoinJavaComponent.inject( + DownloadHandler::class.java + ) + private var cancellationToken: CancellationToken? = null + 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(R.layout.select_share, container, false) + } + + @Suppress("NAME_SHADOWING") + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + cancellationToken = CancellationToken() + refreshSharesListView = view.findViewById(R.id.select_share_refresh) + sharesListView = view.findViewById(R.id.select_share_list) + refreshSharesListView!!.setOnRefreshListener { load(true) } + emptyTextView = view.findViewById(R.id.select_share_empty) + sharesListView!!.onItemClickListener = AdapterView.OnItemClickListener { + parent, _, position, _ -> + val share = parent.getItemAtPosition(position) as Share + + val action = SharesFragmentDirections.sharesToTrackCollection( + shareId = share.id, + shareName = share.name + ) + findNavController().navigate(action) + } + registerForContextMenu(sharesListView!!) + FragmentTitle.setTitle(this, R.string.button_bar_shares) + load(false) + } + + override fun onDestroyView() { + cancellationToken!!.cancel() + super.onDestroyView() + } + + private fun load(refresh: Boolean) { + val task: BackgroundTask> = object : FragmentBackgroundTask>( + activity, true, refreshSharesListView, cancellationToken + ) { + @Throws(Throwable::class) + override fun doInBackground(): List { + val musicService = MusicServiceFactory.getMusicService() + return musicService.getShares(refresh) + } + + override fun done(result: List) { + sharesListView!!.adapter = ShareAdapter(context, result).also { shareAdapter = it } + emptyTextView!!.visibility = if (result.isEmpty()) View.VISIBLE else View.GONE + } + } + task.execute() + } + + override fun onCreateContextMenu( + menu: ContextMenu, + view: View, + menuInfo: ContextMenu.ContextMenuInfo? + ) { + super.onCreateContextMenu(menu, view, menuInfo) + val inflater = requireActivity().menuInflater + inflater.inflate(R.menu.select_share_context, menu) + } + + override fun onContextItemSelected(menuItem: MenuItem): Boolean { + val info = menuItem.menuInfo as AdapterView.AdapterContextMenuInfo + val share = sharesListView!!.getItemAtPosition(info.position) as Share + when (menuItem.itemId) { + R.id.share_menu_pin -> { + downloadHandler.value.downloadShare( + this, + share.id, + share.name, + save = true, + append = true, + autoplay = false, + shuffle = false, + background = true, + playNext = false, + unpin = false + ) + } + R.id.share_menu_unpin -> { + downloadHandler.value.downloadShare( + this, + share.id, + share.name, + save = false, + append = false, + autoplay = false, + shuffle = false, + background = true, + playNext = false, + unpin = true + ) + } + R.id.share_menu_download -> { + downloadHandler.value.downloadShare( + this, + share.id, + share.name, + save = false, + append = false, + autoplay = false, + shuffle = false, + background = true, + playNext = false, + unpin = false + ) + } + R.id.share_menu_play_now -> { + downloadHandler.value.downloadShare( + this, + share.id, + share.name, + save = false, + append = false, + autoplay = true, + shuffle = false, + background = false, + playNext = false, + unpin = false + ) + } + R.id.share_menu_play_shuffled -> { + downloadHandler.value.downloadShare( + this, + share.id, + share.name, + save = false, + append = false, + autoplay = true, + shuffle = true, + background = false, + playNext = false, + unpin = false + ) + } + R.id.share_menu_delete -> { + deleteShare(share) + } + R.id.share_info -> { + displayShareInfo(share) + } + R.id.share_update_info -> { + updateShareInfo(share) + } + else -> { + return super.onContextItemSelected(menuItem) + } + } + return true + } + + private fun deleteShare(share: Share) { + AlertDialog.Builder(context).setIcon(R.drawable.ic_baseline_warning) + .setTitle(R.string.common_confirm).setMessage( + resources.getString(R.string.delete_playlist, share.name) + ).setPositiveButton(R.string.common_ok) { _, _ -> + object : LoadingTask(activity, refreshSharesListView, cancellationToken) { + @Throws(Throwable::class) + override fun doInBackground(): Any? { + val musicService = MusicServiceFactory.getMusicService() + musicService.deleteShare(share.id) + return null + } + + override fun done(result: Any?) { + shareAdapter!!.remove(share) + shareAdapter!!.notifyDataSetChanged() + Util.toast( + context, + resources.getString(R.string.menu_deleted_share, share.name) + ) + } + + override fun error(error: Throwable) { + val msg: String = + if (error is OfflineException || error is ApiNotSupportedException) { + getErrorMessage( + error + ) + } else { + String.format( + Locale.ROOT, + "%s %s", + resources.getString( + R.string.menu_deleted_share_error, + share.name + ), + getErrorMessage(error) + ) + } + Util.toast(context, msg, false) + } + }.execute() + }.setNegativeButton(R.string.common_cancel, null).show() + } + + private fun displayShareInfo(share: Share) { + val textView = TextView(context) + textView.setPadding(5, 5, 5, 5) + val message: Spannable = SpannableString( + """ + Owner: ${share.username} + Comments: ${if (share.description == null) "" else share.description} + URL: ${share.url} + Entry Count: ${share.getEntries().size} + Visit Count: ${share.visitCount} + """.trimIndent() + + ( + if (share.created == null) "" else """ + + Creation Date: ${share.created!!.replace('T', ' ')} + """.trimIndent() + ) + + ( + if (share.lastVisited == null) "" else """ + + Last Visited Date: ${share.lastVisited!!.replace('T', ' ')} + """.trimIndent() + ) + + if (share.expires == null) "" else """ + + Expiration Date: ${share.expires!!.replace('T', ' ')} + """.trimIndent() + ) + Linkify.addLinks(message, Linkify.WEB_URLS) + textView.text = message + textView.movementMethod = LinkMovementMethod.getInstance() + AlertDialog.Builder(context).setTitle("Share Details").setCancelable(true) + .setIcon(R.drawable.ic_baseline_info).setView(textView).show() + } + + @SuppressLint("InflateParams") + private fun updateShareInfo(share: Share) { + val dialogView = layoutInflater.inflate(R.layout.share_details, null) ?: return + val shareDescription = dialogView.findViewById(R.id.share_description) + val timeSpanPicker = dialogView.findViewById(R.id.date_picker) + shareDescription.setText(share.description) + val hideDialogCheckBox = dialogView.findViewById(R.id.hide_dialog) + val saveAsDefaultsCheckBox = dialogView.findViewById(R.id.save_as_defaults) + val noExpirationCheckBox = dialogView.findViewById(R.id.timeSpanDisableCheckBox) + noExpirationCheckBox.setOnCheckedChangeListener { _, b -> + timeSpanPicker.isEnabled = !b + } + noExpirationCheckBox.isChecked = true + timeSpanPicker.setTimeSpanDisableText(resources.getText(R.string.no_expiration)) + hideDialogCheckBox.visibility = View.GONE + saveAsDefaultsCheckBox.visibility = View.GONE + val alertDialog = AlertDialog.Builder(context) + alertDialog.setIcon(R.drawable.ic_baseline_warning) + alertDialog.setTitle(R.string.playlist_update_info) + alertDialog.setView(dialogView) + alertDialog.setPositiveButton(R.string.common_ok) { _, _ -> + object : LoadingTask(activity, refreshSharesListView, cancellationToken) { + @Throws(Throwable::class) + override fun doInBackground(): Any? { + var millis = timeSpanPicker.timeSpan.totalMilliseconds + if (millis > 0) { + millis = TimeSpan.getCurrentTime().add(millis).totalMilliseconds + } + val shareDescriptionText = shareDescription.text + val description = shareDescriptionText?.toString() + val musicService = MusicServiceFactory.getMusicService() + musicService.updateShare(share.id, description, millis) + return null + } + + override fun done(result: Any?) { + load(true) + Util.toast( + context, + resources.getString(R.string.playlist_updated_info, share.name) + ) + } + + override fun error(error: Throwable) { + val msg: String + msg = if (error is OfflineException || error is ApiNotSupportedException) { + getErrorMessage( + error + ) + } else { + String.format( + Locale.ROOT, + "%s %s", + resources.getString(R.string.playlist_updated_info_error, share.name), + getErrorMessage(error) + ) + } + Util.toast(context, msg, false) + } + }.execute() + } + alertDialog.setNegativeButton(R.string.common_cancel, null) + alertDialog.show() + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt index 95c6ff49..ae08cb55 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt @@ -1,124 +1,104 @@ +/* + * AlbumListModel.kt + * Copyright (C) 2009-2022 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + 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 kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import org.moire.ultrasonic.api.subsonic.models.AlbumListType import org.moire.ultrasonic.domain.Album -import org.moire.ultrasonic.service.MusicService -import org.moire.ultrasonic.util.Constants +import org.moire.ultrasonic.service.MusicServiceFactory import org.moire.ultrasonic.util.Settings class AlbumListModel(application: Application) : GenericListModel(application) { val list: MutableLiveData> = MutableLiveData() - var lastType: String? = null + private var lastType: AlbumListType? = null private var loadedUntil: Int = 0 - fun getAlbumList( - refresh: Boolean, - swipe: SwipeRefreshLayout, - args: Bundle - ): LiveData> { - // Don't reload the data if navigating back to the view that was active before. - // This way, we keep the scroll position - val albumListType = args.getString(Constants.INTENT_ALBUM_LIST_TYPE)!! - - if (refresh || list.value?.isEmpty() != false || albumListType != lastType) { - lastType = albumListType - backgroundLoadFromServer(refresh, swipe, args) - } - return list - } - - private fun getAlbumsOfArtist( - musicService: MusicService, + suspend fun getAlbumsOfArtist( refresh: Boolean, id: String, name: String? ) { - list.postValue(musicService.getAlbumsOfArtist(id, name, refresh)) + withContext(Dispatchers.IO) { + val service = MusicServiceFactory.getMusicService() + list.postValue(service.getAlbumsOfArtist(id, name, refresh)) + } } - override fun load( - isOffline: Boolean, - useId3Tags: Boolean, - musicService: MusicService, - refresh: Boolean, - args: Bundle + @Suppress("NAME_SHADOWING") + suspend fun getAlbums( + albumListType: AlbumListType, + size: Int = 0, + offset: Int = 0, + append: Boolean = false, + refresh: Boolean ) { - super.load(isOffline, useId3Tags, musicService, refresh, args) - - val albumListType = args.getString(Constants.INTENT_ALBUM_LIST_TYPE)!! - val size = args.getInt(Constants.INTENT_ALBUM_LIST_SIZE, 0) - var offset = args.getInt(Constants.INTENT_ALBUM_LIST_OFFSET, 0) - val append = args.getBoolean(Constants.INTENT_APPEND, false) - - val musicDirectory: List - val musicFolderId = if (showSelectFolderHeader(args)) { - activeServerProvider.getActiveServer().musicFolderId - } else { - null + // Don't reload the data if navigating back to the view that was active before. + // This way, we keep the scroll position + if ((!refresh && list.value?.isEmpty() == false && albumListType == lastType)) { + return } + lastType = albumListType - // If we are refreshing the random list, we want to avoid items moving across the screen, - // by clearing the list first - if (refresh && !append && albumListType == "random") { - list.postValue(listOf()) - } + withContext(Dispatchers.IO) { + val service = MusicServiceFactory.getMusicService() + var offset = offset - // Handle the logic for endless scrolling: - // If appending the existing list, set the offset from where to load - if (append) offset += (size + loadedUntil) + val musicDirectory: List + val musicFolderId = if (showSelectFolderHeader()) { + activeServerProvider.getActiveServer().musicFolderId + } else { + null + } - if (albumListType == Constants.ALBUMS_OF_ARTIST) { - return getAlbumsOfArtist( - musicService, - refresh, - args.getString(Constants.INTENT_ID, ""), - args.getString(Constants.INTENT_NAME, "") - ) - } + // If we are refreshing the random list, we want to avoid items moving across the screen, + // by clearing the list first + if (refresh && !append && albumListType == AlbumListType.RANDOM) { + list.postValue(listOf()) + } - val type = AlbumListType.fromName(albumListType) + // Handle the logic for endless scrolling: + // If appending the existing list, set the offset from where to load + if (append) offset += (size + loadedUntil) - if (useId3Tags) { - musicDirectory = - musicService.getAlbumList2( - type, size, + musicDirectory = if (Settings.shouldUseId3Tags) { + service.getAlbumList2( + albumListType, size, offset, musicFolderId ) - } else { - musicDirectory = musicService.getAlbumList( - type, size, - offset, musicFolderId - ) + } else { + service.getAlbumList( + albumListType, size, + offset, musicFolderId + ) + } + + currentListIsSortable = isCollectionSortable(albumListType) + + if (append && list.value != null) { + val newList = ArrayList() + newList.addAll(list.value!!) + newList.addAll(musicDirectory) + list.postValue(newList) + } else { + list.postValue(musicDirectory) + } + + loadedUntil = offset } - - currentListIsSortable = isCollectionSortable(type) - - if (append && list.value != null) { - val newList = ArrayList() - newList.addAll(list.value!!) - newList.addAll(musicDirectory) - list.postValue(newList) - } else { - list.postValue(musicDirectory) - } - - loadedUntil = offset } - override fun showSelectFolderHeader(args: Bundle?): Boolean { - if (args == null) return false - - // TODO: Use proper type here - val albumListType = args.getString(Constants.INTENT_ALBUM_LIST_TYPE)!! - - val isAlphabetical = (albumListType == AlbumListType.SORTED_BY_NAME.typeName) || - (albumListType == AlbumListType.SORTED_BY_ARTIST.typeName) + override fun showSelectFolderHeader(): Boolean { + val isAlphabetical = (lastType == AlbumListType.SORTED_BY_NAME) || + (lastType == AlbumListType.SORTED_BY_ARTIST) return !isOffline() && !Settings.shouldUseId3Tags && isAlphabetical } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ArtistListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ArtistListModel.kt index e41c6bbc..6dd042ce 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ArtistListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ArtistListModel.kt @@ -7,7 +7,6 @@ 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 @@ -38,10 +37,9 @@ class ArtistListModel(application: Application) : GenericListModel(application) isOffline: Boolean, useId3Tags: Boolean, musicService: MusicService, - refresh: Boolean, - args: Bundle + refresh: Boolean ) { - super.load(isOffline, useId3Tags, musicService, refresh, args) + super.load(isOffline, useId3Tags, musicService, refresh) val musicFolderId = activeServer.musicFolderId @@ -54,7 +52,7 @@ class ArtistListModel(application: Application) : GenericListModel(application) artists.postValue(result.toMutableList().sortedWith(comparator)) } - override fun showSelectFolderHeader(args: Bundle?): Boolean { + override fun showSelectFolderHeader(): Boolean { return true } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt index 2d4b4aea..813dcce8 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt @@ -1,8 +1,14 @@ +/* + * GenericListModel.kt + * Copyright (C) 2009-2022 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + package org.moire.ultrasonic.model import android.app.Application import android.content.Context -import android.os.Bundle import android.os.Handler import android.os.Looper import androidx.lifecycle.AndroidViewModel @@ -41,7 +47,7 @@ open class GenericListModel(application: Application) : val musicFolders: MutableLiveData> = MutableLiveData(listOf()) - open fun showSelectFolderHeader(args: Bundle?): Boolean { + open fun showSelectFolderHeader(): Boolean { return false } @@ -55,8 +61,8 @@ open class GenericListModel(application: Application) : /** * Refreshes the cached items from the server */ - fun refresh(swipe: SwipeRefreshLayout, bundle: Bundle?) { - backgroundLoadFromServer(true, swipe, bundle ?: Bundle()) + fun refresh(swipe: SwipeRefreshLayout) { + backgroundLoadFromServer(true, swipe) } /** @@ -64,12 +70,11 @@ open class GenericListModel(application: Application) : */ fun backgroundLoadFromServer( refresh: Boolean, - swipe: SwipeRefreshLayout, - bundle: Bundle = Bundle() + swipe: SwipeRefreshLayout ) { viewModelScope.launch { swipe.isRefreshing = true - loadFromServer(refresh, swipe, bundle) + loadFromServer(refresh, swipe) swipe.isRefreshing = false } } @@ -77,18 +82,22 @@ open class GenericListModel(application: Application) : /** * Calls the load() function with error handling */ - suspend fun loadFromServer(refresh: Boolean, swipe: SwipeRefreshLayout, bundle: Bundle) = + private suspend fun loadFromServer( + refresh: Boolean, + swipe: SwipeRefreshLayout + ) { withContext(Dispatchers.IO) { val musicService = MusicServiceFactory.getMusicService() val isOffline = ActiveServerProvider.isOffline() val useId3Tags = Settings.shouldUseId3Tags try { - load(isOffline, useId3Tags, musicService, refresh, bundle) + load(isOffline, useId3Tags, musicService, refresh) } catch (all: Exception) { handleException(all, swipe.context) } } + } private fun handleException(exception: Exception, context: Context) { Handler(Looper.getMainLooper()).post { @@ -103,12 +112,11 @@ open class GenericListModel(application: Application) : isOffline: Boolean, useId3Tags: Boolean, musicService: MusicService, - refresh: Boolean, - args: Bundle + refresh: Boolean ) { // Update the list of available folders if enabled @Suppress("ComplexCondition") - if (showSelectFolderHeader(args) && !isOffline && !useId3Tags && refresh) { + if (showSelectFolderHeader() && !isOffline && !useId3Tags && refresh) { musicFolders.postValue( musicService.getMusicFolders(refresh) ) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt index 252c48cb..84087b86 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt @@ -1,14 +1,12 @@ package org.moire.ultrasonic.model import android.app.Application -import android.os.Bundle import androidx.lifecycle.MutableLiveData import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext 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.Settings @@ -16,16 +14,6 @@ class SearchListModel(application: Application) : GenericListModel(application) var searchResult: MutableLiveData = MutableLiveData() - 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 diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/Plan.md b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/Plan.md deleted file mode 100644 index 0019ee0b..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/Plan.md +++ /dev/null @@ -1,17 +0,0 @@ - - - -UI: -[x] Display tracks -[x] On selection: Translate Tracks to MediaItems -[x] Move playlist val to Controller: Keep it around for easier migration!! -[x] Also make a LRU Cache to help with translation between MediaItem and DownloadFile -[x] Hand MediaItems to Service -[] If wanted also hand them to Downloader.kt -[x] Service plays MediaItem through OkHttp -[x] UI needs to receive info from service -[x] Create a Cache Layer -[] Translate AutoMediaBrowserService -[] Add new shuffle icon.... - -convertToPlaybackStateCompatState() 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 34af142b..d6312be7 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt @@ -1,6 +1,6 @@ /* * CachedMusicService.kt - * Copyright (C) 2009-2021 Ultrasonic developers + * Copyright (C) 2009-2022 Ultrasonic developers * * Distributed under terms of the GNU GPLv3 license. */ @@ -31,7 +31,6 @@ import org.moire.ultrasonic.domain.SearchResult import org.moire.ultrasonic.domain.Share import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.domain.UserInfo -import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.LRUCache import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.TimeLimitedCache @@ -393,7 +392,7 @@ class CachedMusicService(private val musicService: MusicService) : MusicService, var result = cachedGenres.get() if (result == null) { result = musicService.getGenres(refresh) - cachedGenres.set(result!!) + cachedGenres.set(result) } val sorted = result.toMutableList() @@ -443,7 +442,7 @@ class CachedMusicService(private val musicService: MusicService) : MusicService, override fun getVideos(refresh: Boolean): MusicDirectory? { checkSettingsChanged() var cache = - if (refresh) null else cachedMusicDirectories[Constants.INTENT_VIDEOS] + if (refresh) null else cachedMusicDirectories[CACHE_KEY_VIDEOS] var dir = cache?.get() if (dir == null) { dir = musicService.getVideos(refresh) @@ -451,7 +450,7 @@ class CachedMusicService(private val musicService: MusicService) : MusicService, Settings.directoryCacheTime.toLong(), TimeUnit.SECONDS ) cache.set(dir) - cachedMusicDirectories.put(Constants.INTENT_VIDEOS, cache) + cachedMusicDirectories.put(CACHE_KEY_VIDEOS, cache) } return dir } @@ -493,6 +492,7 @@ class CachedMusicService(private val musicService: MusicService) : MusicService, companion object { private const val MUSIC_DIR_CACHE_SIZE = 100 + const val CACHE_KEY_VIDEOS = "VIDEOS" } init { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt index 4c21c547..d1e0a3a3 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt @@ -231,7 +231,7 @@ class MediaPlayerController( private fun publishPlaybackState() { val newState = RxBus.StateWithTrack( - track = currentMediaItem?.let { it.toTrack() }, + track = currentMediaItem?.toTrack(), index = currentMediaItemIndex, isPlaying = isPlaying, state = playbackState 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 ea9ed883..eb3c8bb9 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt @@ -1,6 +1,6 @@ /* * MusicService.kt - * Copyright (C) 2009-2021 Ultrasonic developers + * Copyright (C) 2009-2022 Ultrasonic developers * * Distributed under terms of the GNU GPLv3 license. */ @@ -36,7 +36,7 @@ interface MusicService { fun isLicenseValid(): Boolean @Throws(Exception::class) - fun getGenres(refresh: Boolean): List? + fun getGenres(refresh: Boolean): List @Throws(Exception::class) fun star(id: String?, albumId: String?, artistId: 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 5c438218..039e1de1 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt @@ -376,7 +376,7 @@ class OfflineMusicService : MusicService, KoinComponent { } @Throws(Exception::class) - override fun getGenres(refresh: Boolean): List? { + override fun getGenres(refresh: Boolean): List { throw OfflineException("Getting Genres not available in offline mode") } @@ -572,7 +572,7 @@ class OfflineMusicService : MusicService, KoinComponent { title = name val albumArt = FileUtil.getAlbumArtFile(this) - if (albumArt != null && File(albumArt).exists()) { + if (File(albumArt).exists()) { coverArt = albumArt } } @@ -628,10 +628,11 @@ class OfflineMusicService : MusicService, KoinComponent { if (string == null) return null val slashIndex = string.indexOf('/') - if (slashIndex > 0) - return string.substring(0, slashIndex).toIntOrNull() + + return if (slashIndex > 0) + string.substring(0, slashIndex).toIntOrNull() else - return string.toIntOrNull() + string.toIntOrNull() } /* @@ -642,10 +643,10 @@ class OfflineMusicService : MusicService, KoinComponent { val duration: Long? = string.toLongOrNull() - if (duration != null) - return TimeUnit.MILLISECONDS.toSeconds(duration).toInt() + return if (duration != null) + TimeUnit.MILLISECONDS.toSeconds(duration).toInt() else - return null + null } // TODO: Simplify this deeply nested and complicated function diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt index 5223c641..a6e2e034 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt @@ -574,7 +574,7 @@ open class RESTMusicService( @Throws(Exception::class) override fun getGenres( refresh: Boolean - ): List? { + ): List { val response = API.getGenres().execute().throwOnFailure() return response.body()!!.genresList.toDomainEntityList() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt index d5f0d14a..ff531995 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt @@ -1,3 +1,10 @@ +/* + * DownloadHandler.kt + * Copyright (C) 2009-2022 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + package org.moire.ultrasonic.subsonic import android.app.Activity @@ -11,7 +18,6 @@ import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.service.MediaPlayerController import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService -import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.EntryByDiscAndTrackComparator import org.moire.ultrasonic.util.ModalBackgroundTask import org.moire.ultrasonic.util.Settings @@ -35,6 +41,7 @@ class DownloadHandler( playNext: Boolean, shuffle: Boolean, songs: List, + playlistName: String?, ) { val onValid = Runnable { // TODO: The logic here is different than in the controller... @@ -52,9 +59,7 @@ class DownloadHandler( shuffle, insertionMode ) - val playlistName: String? = fragment.arguments?.getString( - Constants.INTENT_PLAYLIST_NAME - ) + if (playlistName != null) { mediaPlayerController.suggestedPlaylistName = playlistName } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ShareHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ShareHandler.kt index d0f8f596..f60ef15f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ShareHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ShareHandler.kt @@ -1,3 +1,10 @@ +/* + * ShareHandler.kt + * Copyright (C) 2009-2022 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + package org.moire.ultrasonic.subsonic import android.app.AlertDialog @@ -20,7 +27,6 @@ import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService import org.moire.ultrasonic.util.BackgroundTask import org.moire.ultrasonic.util.CancellationToken -import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.FragmentBackgroundTask import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.ShareDetails @@ -42,31 +48,12 @@ class ShareHandler(val context: Context) { private var textViewExpiration: TextView? = null private val pattern = Pattern.compile(":") - fun createShare( - fragment: Fragment, - tracks: List?, - swipe: SwipeRefreshLayout?, - cancellationToken: CancellationToken - ) { - val askForDetails = Settings.shouldAskForShareDetails - val shareDetails = ShareDetails() - shareDetails.Entries = tracks - if (askForDetails) { - showDialog(fragment, shareDetails, swipe, cancellationToken) - } else { - shareDetails.Description = Settings.defaultShareDescription - shareDetails.Expiration = TimeSpan.getCurrentTime().add( - Settings.defaultShareExpirationInMillis - ).totalMilliseconds - share(fragment, shareDetails, swipe, cancellationToken) - } - } - fun share( fragment: Fragment, shareDetails: ShareDetails, swipe: SwipeRefreshLayout?, - cancellationToken: CancellationToken + cancellationToken: CancellationToken, + additionalId: String? ) { val task: BackgroundTask = object : FragmentBackgroundTask( fragment.requireActivity(), @@ -80,7 +67,7 @@ class ShareHandler(val context: Context) { if (!shareDetails.ShareOnServer && shareDetails.Entries.size == 1) return null if (shareDetails.Entries.isEmpty()) { - fragment.arguments?.getString(Constants.INTENT_ID).ifNotNull { + additionalId.ifNotNull { ids.add(it) } } else { @@ -144,11 +131,33 @@ class ShareHandler(val context: Context) { task.execute() } + fun createShare( + fragment: Fragment, + tracks: List?, + swipe: SwipeRefreshLayout?, + cancellationToken: CancellationToken, + additionalId: String? = null + ) { + val askForDetails = Settings.shouldAskForShareDetails + val shareDetails = ShareDetails() + shareDetails.Entries = tracks + if (askForDetails) { + showDialog(fragment, shareDetails, swipe, cancellationToken, additionalId) + } else { + shareDetails.Description = Settings.defaultShareDescription + shareDetails.Expiration = TimeSpan.getCurrentTime().add( + Settings.defaultShareExpirationInMillis + ).totalMilliseconds + share(fragment, shareDetails, swipe, cancellationToken, additionalId) + } + } + private fun showDialog( fragment: Fragment, shareDetails: ShareDetails, swipe: SwipeRefreshLayout?, - cancellationToken: CancellationToken + cancellationToken: CancellationToken, + additionalId: String? ) { val layout = LayoutInflater.from(fragment.context).inflate(R.layout.share_details, null) @@ -205,7 +214,7 @@ class ShareHandler(val context: Context) { Settings.shareOnServer = shareDetails.ShareOnServer } - share(fragment, shareDetails, swipe, cancellationToken) + share(fragment, shareDetails, swipe, cancellationToken, additionalId) } builder.setNegativeButton(R.string.common_cancel) { dialog, _ -> @@ -216,9 +225,7 @@ class ShareHandler(val context: Context) { builder.setCancelable(true) timeSpanPicker!!.setTimeSpanDisableText(context.resources.getString(R.string.no_expiration)) - noExpirationCheckBox!!.setOnCheckedChangeListener { - _, - b -> + noExpirationCheckBox!!.setOnCheckedChangeListener { _, b -> timeSpanPicker!!.isEnabled = !b } 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 b8b178fc..a1135e6b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt @@ -1,6 +1,6 @@ /* * Constants.kt - * Copyright (C) 2009-2021 Ultrasonic developers + * Copyright (C) 2009-2022 Ultrasonic developers * * Distributed under terms of the GNU GPLv3 license. */ @@ -15,32 +15,13 @@ object Constants { const val REST_PROTOCOL_VERSION = "1.7.0" const val REST_CLIENT_ID = "Ultrasonic" - // Names for intent extras. - const val INTENT_ID = "subsonic.id" - const val INTENT_NAME = "subsonic.name" + // Legacy names for intent extras, in those fragments which don't use SafeArgs yet. const val INTENT_ARTIST = "subsonic.artist" const val INTENT_TITLE = "subsonic.title" const val INTENT_AUTOPLAY = "subsonic.playall" const val INTENT_QUERY = "subsonic.query" - const val INTENT_PLAYLIST_ID = "subsonic.playlist.id" - const val INTENT_PODCAST_CHANNEL_ID = "subsonic.podcastChannel.id" - const val INTENT_PARENT_ID = "subsonic.parent.id" - const val INTENT_PLAYLIST_NAME = "subsonic.playlist.name" - const val INTENT_SHARE_ID = "subsonic.share.id" - const val INTENT_SHARE_NAME = "subsonic.share.name" const val INTENT_ALBUM_LIST_TYPE = "subsonic.albumlisttype" - const val INTENT_ALBUM_LIST_TITLE = "subsonic.albumlisttitle" - const val INTENT_ALBUM_LIST_SIZE = "subsonic.albumlistsize" - const val INTENT_ALBUM_LIST_OFFSET = "subsonic.albumlistoffset" - const val INTENT_SHUFFLE = "subsonic.shuffle" - const val INTENT_REFRESH = "subsonic.refresh" - const val INTENT_STARRED = "subsonic.starred" - const val INTENT_RANDOM = "subsonic.random" - const val INTENT_GENRE_NAME = "subsonic.genre" - const val INTENT_IS_ALBUM = "subsonic.isalbum" - const val INTENT_VIDEOS = "subsonic.videos" const val INTENT_SHOW_PLAYER = "subsonic.showplayer" - const val INTENT_APPEND = "subsonic.append" // Names for Intent Actions const val CMD_PROCESS_KEYCODE = "org.moire.ultrasonic.CMD_PROCESS_KEYCODE" @@ -62,8 +43,5 @@ object Constants { const val FILENAME_PLAYLIST_SER = "downloadstate.ser" 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/res/navigation/navigation_graph.xml b/ultrasonic/src/main/res/navigation/navigation_graph.xml index 90f978a6..a00ade3a 100644 --- a/ultrasonic/src/main/res/navigation/navigation_graph.xml +++ b/ultrasonic/src/main/res/navigation/navigation_graph.xml @@ -3,6 +3,13 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/navigation_graph" app:startDestination="@id/mainFragment"> + + + + + + + + @@ -40,21 +58,150 @@ + + + android:defaultValue="false"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + android:name="org.moire.ultrasonic.fragment.legacy.PlaylistsFragment" > + android:name="org.moire.ultrasonic.fragment.legacy.SharesFragment" > + android:name="org.moire.ultrasonic.fragment.legacy.PodcastFragment" > + android:name="org.moire.ultrasonic.fragment.legacy.SelectGenreFragment"> + +