diff --git a/app/src/main/java/org/schabi/newpipe/error/UserAction.java b/app/src/main/java/org/schabi/newpipe/error/UserAction.java index e8dec9556..d291f0491 100644 --- a/app/src/main/java/org/schabi/newpipe/error/UserAction.java +++ b/app/src/main/java/org/schabi/newpipe/error/UserAction.java @@ -28,8 +28,8 @@ public enum UserAction { DOWNLOAD_FAILED("download failed"), PREFERENCES_MIGRATION("migration of preferences"), SHARE_TO_NEWPIPE("share to newpipe"), - CHECK_FOR_NEW_APP_VERSION("check for new app version"); - + CHECK_FOR_NEW_APP_VERSION("check for new app version"), + OPEN_INFO_ITEM_DIALOG("open info item dialog"); private final String message; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java index 987d8c21f..72b91fd17 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java @@ -1,6 +1,8 @@ package org.schabi.newpipe.fragments.list; -import android.app.Activity; +import static org.schabi.newpipe.ktx.ViewUtils.animate; +import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling; + import android.content.Context; import android.content.SharedPreferences; import android.content.res.Configuration; @@ -25,29 +27,19 @@ import org.schabi.newpipe.extractor.channel.ChannelInfoItem; import org.schabi.newpipe.extractor.comments.CommentsInfoItem; import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; -import org.schabi.newpipe.info_list.InfoItemDialog; +import org.schabi.newpipe.info_list.dialog.InfoItemDialog; import org.schabi.newpipe.info_list.InfoListAdapter; -import org.schabi.newpipe.player.helper.PlayerHolder; -import org.schabi.newpipe.util.external_communication.KoreUtils; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.StateSaver; -import org.schabi.newpipe.util.StreamDialogEntry; import org.schabi.newpipe.views.SuperScrollLayoutManager; -import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Queue; import java.util.function.Supplier; -import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; -import static org.schabi.newpipe.ktx.ViewUtils.animate; -import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling; - public abstract class BaseListFragment extends BaseStateFragment implements ListViewContract, StateSaver.WriteRead, SharedPreferences.OnSharedPreferenceChangeListener { @@ -268,7 +260,7 @@ public abstract class BaseListFragment extends BaseStateFragment @Override public void held(final StreamInfoItem selectedItem) { - showStreamDialog(selectedItem); + showInfoItemDialog(selectedItem); } }); @@ -409,55 +401,12 @@ public abstract class BaseListFragment extends BaseStateFragment } } - protected void showStreamDialog(final StreamInfoItem item) { - final Context context = getContext(); - final Activity activity = getActivity(); - if (context == null || context.getResources() == null || activity == null) { - return; + protected void showInfoItemDialog(final StreamInfoItem item) { + try { + new InfoItemDialog.Builder(getActivity(), getContext(), this, item).create().show(); + } catch (final IllegalArgumentException e) { + InfoItemDialog.Builder.reportErrorDuringInitialization(e, item); } - final List entries = new ArrayList<>(); - - if (PlayerHolder.getInstance().isPlayQueueReady()) { - entries.add(StreamDialogEntry.enqueue); - - if (PlayerHolder.getInstance().getQueueSize() > 1) { - entries.add(StreamDialogEntry.enqueue_next); - } - } - - if (item.getStreamType() == StreamType.AUDIO_STREAM) { - entries.addAll(Arrays.asList( - StreamDialogEntry.start_here_on_background, - StreamDialogEntry.append_playlist, - StreamDialogEntry.share - )); - } else { - entries.addAll(Arrays.asList( - StreamDialogEntry.start_here_on_background, - StreamDialogEntry.start_here_on_popup, - StreamDialogEntry.append_playlist, - StreamDialogEntry.share - )); - } - entries.add(StreamDialogEntry.open_in_browser); - if (KoreUtils.shouldShowPlayWithKodi(context, item.getServiceId())) { - entries.add(StreamDialogEntry.play_with_kodi); - } - - // show "mark as watched" only when watch history is enabled - if (StreamDialogEntry.shouldAddMarkAsWatched(item.getStreamType(), context)) { - entries.add( - StreamDialogEntry.mark_as_watched - ); - } - if (!isNullOrEmpty(item.getUploaderUrl())) { - entries.add(StreamDialogEntry.show_channel_details); - } - - StreamDialogEntry.setEnabledEntries(entries); - - new InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context), - (dialog, which) -> StreamDialogEntry.clickOn(which, this, item)).show(); } /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java index ebfa9db7d..5bf20c144 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java @@ -1,10 +1,8 @@ package org.schabi.newpipe.fragments.list.playlist; -import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; import static org.schabi.newpipe.ktx.ViewUtils.animate; import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling; -import android.app.Activity; import android.content.Context; import android.content.res.ColorStateList; import android.os.Bundle; @@ -41,24 +39,20 @@ import org.schabi.newpipe.extractor.ServiceList; import org.schabi.newpipe.extractor.playlist.PlaylistInfo; import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper; import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.fragments.list.BaseListInfoFragment; -import org.schabi.newpipe.info_list.InfoItemDialog; +import org.schabi.newpipe.info_list.dialog.InfoItemDialog; import org.schabi.newpipe.local.playlist.RemotePlaylistManager; import org.schabi.newpipe.player.MainPlayer.PlayerType; -import org.schabi.newpipe.player.helper.PlayerHolder; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PicassoHelper; -import org.schabi.newpipe.util.StreamDialogEntry; -import org.schabi.newpipe.util.external_communication.KoreUtils; +import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry; import org.schabi.newpipe.util.external_communication.ShareUtils; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; @@ -145,60 +139,22 @@ public class PlaylistFragment extends BaseListInfoFragment NavigationHelper.playOnBackgroundPlayer( + context, getPlayQueueStartingAt(infoItem), true)) + .create() + .show(); + } catch (final IllegalArgumentException e) { + InfoItemDialog.Builder.reportErrorDuringInitialization(e, item); } - - final ArrayList entries = new ArrayList<>(); - - if (PlayerHolder.getInstance().isPlayQueueReady()) { - entries.add(StreamDialogEntry.enqueue); - - if (PlayerHolder.getInstance().getQueueSize() > 1) { - entries.add(StreamDialogEntry.enqueue_next); - } - } - - if (item.getStreamType() == StreamType.AUDIO_STREAM) { - entries.addAll(Arrays.asList( - StreamDialogEntry.start_here_on_background, - StreamDialogEntry.append_playlist, - StreamDialogEntry.share - )); - } else { - entries.addAll(Arrays.asList( - StreamDialogEntry.start_here_on_background, - StreamDialogEntry.start_here_on_popup, - StreamDialogEntry.append_playlist, - StreamDialogEntry.share - )); - } - entries.add(StreamDialogEntry.open_in_browser); - if (KoreUtils.shouldShowPlayWithKodi(context, item.getServiceId())) { - entries.add(StreamDialogEntry.play_with_kodi); - } - - // show "mark as watched" only when watch history is enabled - if (StreamDialogEntry.shouldAddMarkAsWatched(item.getStreamType(), context)) { - entries.add( - StreamDialogEntry.mark_as_watched - ); - } - if (!isNullOrEmpty(item.getUploaderUrl())) { - entries.add(StreamDialogEntry.show_channel_details); - } - - StreamDialogEntry.setEnabledEntries(entries); - - StreamDialogEntry.start_here_on_background.setCustomAction((fragment, infoItem) -> - NavigationHelper.playOnBackgroundPlayer(context, - getPlayQueueStartingAt(infoItem), true)); - - new InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context), - (dialog, which) -> StreamDialogEntry.clickOn(which, this, item)).show(); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemDialog.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemDialog.java deleted file mode 100644 index c485337f0..000000000 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemDialog.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.schabi.newpipe.info_list; - -import android.app.Activity; -import android.content.DialogInterface; -import android.view.View; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.stream.StreamInfoItem; - -public class InfoItemDialog { - private final AlertDialog dialog; - - public InfoItemDialog(@NonNull final Activity activity, - @NonNull final StreamInfoItem info, - @NonNull final String[] commands, - @NonNull final DialogInterface.OnClickListener actions) { - this(activity, commands, actions, info.getName(), info.getUploaderName()); - } - - public InfoItemDialog(@NonNull final Activity activity, - @NonNull final String[] commands, - @NonNull final DialogInterface.OnClickListener actions, - @NonNull final String title, - @Nullable final String additionalDetail) { - - final View bannerView = View.inflate(activity, R.layout.dialog_title, null); - bannerView.setSelected(true); - - final TextView titleView = bannerView.findViewById(R.id.itemTitleView); - titleView.setText(title); - - final TextView detailsView = bannerView.findViewById(R.id.itemAdditionalDetails); - if (additionalDetail != null) { - detailsView.setText(additionalDetail); - detailsView.setVisibility(View.VISIBLE); - } else { - detailsView.setVisibility(View.GONE); - } - - dialog = new AlertDialog.Builder(activity) - .setCustomTitle(bannerView) - .setItems(commands, actions) - .create(); - } - - public void show() { - dialog.show(); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/dialog/InfoItemDialog.java b/app/src/main/java/org/schabi/newpipe/info_list/dialog/InfoItemDialog.java new file mode 100644 index 000000000..2264ab370 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/info_list/dialog/InfoItemDialog.java @@ -0,0 +1,356 @@ +package org.schabi.newpipe.info_list.dialog; + +import static org.schabi.newpipe.MainActivity.DEBUG; + +import android.app.Activity; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Build; +import android.util.Log; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; +import androidx.preference.PreferenceManager; + +import org.schabi.newpipe.App; +import org.schabi.newpipe.R; +import org.schabi.newpipe.error.ErrorInfo; +import org.schabi.newpipe.error.ErrorUtil; +import org.schabi.newpipe.error.UserAction; +import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.extractor.stream.StreamType; +import org.schabi.newpipe.player.helper.PlayerHolder; +import org.schabi.newpipe.util.external_communication.KoreUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +/** + * Dialog for a {@link StreamInfoItem}. + * The dialog's content are actions that can be performed on the {@link StreamInfoItem}. + * This dialog is mostly used for longpress context menus. + */ +public final class InfoItemDialog { + private static final String TAG = Build.class.getSimpleName(); + /** + * Ideally, {@link InfoItemDialog} would extend {@link AlertDialog}. + * However, extending {@link AlertDialog} requires many additional lines + * and brings more complexity to this class, especially the constructor. + * To circumvent this, an {@link AlertDialog.Builder} is used in the constructor. + * Its result is stored in this class variable to allow access via the {@link #show()} method. + */ + private final AlertDialog dialog; + + private InfoItemDialog(@NonNull final Activity activity, + @NonNull final Fragment fragment, + @NonNull final StreamInfoItem info, + @NonNull final List entries) { + + // Create the dialog's title + final View bannerView = View.inflate(activity, R.layout.dialog_title, null); + bannerView.setSelected(true); + + final TextView titleView = bannerView.findViewById(R.id.itemTitleView); + titleView.setText(info.getName()); + + final TextView detailsView = bannerView.findViewById(R.id.itemAdditionalDetails); + if (info.getUploaderName() != null) { + detailsView.setText(info.getUploaderName()); + detailsView.setVisibility(View.VISIBLE); + } else { + detailsView.setVisibility(View.GONE); + } + + // Get the entry's descriptions which are displayed in the dialog + final String[] items = entries.stream() + .map(entry -> entry.getString(activity)).toArray(String[]::new); + + // Call an entry's action / onClick method when the entry is selected. + final DialogInterface.OnClickListener action = (d, index) -> + entries.get(index).action.onClick(fragment, info); + + dialog = new AlertDialog.Builder(activity) + .setCustomTitle(bannerView) + .setItems(items, action) + .create(); + + } + + public void show() { + dialog.show(); + } + + /** + *

Builder to generate a {@link InfoItemDialog} for a {@link StreamInfoItem}.

+ * Use {@link #addEntry(StreamDialogDefaultEntry)} + * and {@link #addAllEntries(StreamDialogDefaultEntry...)} to add options to the dialog. + *
+ * Custom actions for entries can be set using + * {@link #setAction(StreamDialogDefaultEntry, StreamDialogEntry.StreamDialogEntryAction)}. + */ + public static class Builder { + @NonNull private final Activity activity; + @NonNull private final Context context; + @NonNull private final StreamInfoItem infoItem; + @NonNull private final Fragment fragment; + @NonNull private final List entries = new ArrayList<>(); + private final boolean addDefaultEntriesAutomatically; + + /** + *

Create a {@link Builder builder} instance for a {@link StreamInfoItem} + * that automatically adds the some default entries + * at the top and bottom of the dialog.

+ * The dialog has the following structure: + *
+         *     + - - - - - - - - - - - - - - - - - - - - - -+
+         *     | ENQUEUE                                    |
+         *     | ENQUEUE_NEXT                               |
+         *     | START_ON_BACKGROUND                        |
+         *     | START_ON_POPUP                             |
+         *     + - - - - - - - - - - - - - - - - - - - - - -+
+         *     | entries added manually with                |
+         *     | addEntry() and addAllEntries()             |
+         *     + - - - - - - - - - - - - - - - - - - - - - -+
+         *     | APPEND_PLAYLIST                            |
+         *     | SHARE                                      |
+         *     | OPEN_IN_BROWSER                            |
+         *     | PLAY_WITH_KODI                             |
+         *     | MARK_AS_WATCHED                            |
+         *     | SHOW_CHANNEL_DETAILS                       |
+         *     + - - - - - - - - - - - - - - - - - - - - - -+
+         * 
+ * Please note that some entries are not added depending on the user's preferences, + * the item's {@link StreamType} and the current player state. + * + * @param activity + * @param context + * @param fragment + * @param infoItem the item for this dialog; all entries and their actions work with + * this {@link StreamInfoItem} + * @throws IllegalArgumentException if activity, context + * or resources is null + */ + public Builder(final Activity activity, + final Context context, + @NonNull final Fragment fragment, + @NonNull final StreamInfoItem infoItem) { + this(activity, context, fragment, infoItem, true); + } + + /** + *

Create an instance of this {@link Builder} for a {@link StreamInfoItem}.

+ *

If {@code addDefaultEntriesAutomatically} is set to {@code true}, + * some default entries are added to the top and bottom of the dialog.

+ * The dialog has the following structure: + *
+         *     + - - - - - - - - - - - - - - - - - - - - - -+
+         *     | ENQUEUE                                    |
+         *     | ENQUEUE_NEXT                               |
+         *     | START_ON_BACKGROUND                        |
+         *     | START_ON_POPUP                             |
+         *     + - - - - - - - - - - - - - - - - - - - - - -+
+         *     | entries added manually with                |
+         *     | addEntry() and addAllEntries()             |
+         *     + - - - - - - - - - - - - - - - - - - - - - -+
+         *     | APPEND_PLAYLIST                            |
+         *     | SHARE                                      |
+         *     | OPEN_IN_BROWSER                            |
+         *     | PLAY_WITH_KODI                             |
+         *     | MARK_AS_WATCHED                            |
+         *     | SHOW_CHANNEL_DETAILS                       |
+         *     + - - - - - - - - - - - - - - - - - - - - - -+
+         * 
+ * Please note that some entries are not added depending on the user's preferences, + * the item's {@link StreamType} and the current player state. + * + * @param activity + * @param context + * @param fragment + * @param infoItem + * @param addDefaultEntriesAutomatically + * whether default entries added with {@link #addDefaultBeginningEntries()} + * and {@link #addDefaultEndEntries()} are added automatically when generating + * the {@link InfoItemDialog}. + *
+ * Entries added with {@link #addEntry(StreamDialogDefaultEntry)} and + * {@link #addAllEntries(StreamDialogDefaultEntry...)} are added in between. + * @throws IllegalArgumentException if activity, context + * or resources is null + */ + public Builder(final Activity activity, + final Context context, + @NonNull final Fragment fragment, + @NonNull final StreamInfoItem infoItem, + final boolean addDefaultEntriesAutomatically) { + if (activity == null || context == null || context.getResources() == null) { + if (DEBUG) { + Log.d(TAG, "activity, context or resources is null: activity = " + + activity + ", context = " + context); + } + throw new IllegalArgumentException("activity, context or resources is null"); + } + this.activity = activity; + this.context = context; + this.fragment = fragment; + this.infoItem = infoItem; + this.addDefaultEntriesAutomatically = addDefaultEntriesAutomatically; + if (addDefaultEntriesAutomatically) { + addDefaultBeginningEntries(); + } + } + + /** + * Adds a new entry and appends it to the current entry list. + * @param entry the entry to add + * @return the current {@link Builder} instance + */ + public Builder addEntry(@NonNull final StreamDialogDefaultEntry entry) { + entries.add(entry.toStreamDialogEntry()); + return this; + } + + /** + * Adds new entries. These are appended to the current entry list. + * @param newEntries the entries to add + * @return the current {@link Builder} instance + */ + public Builder addAllEntries(@NonNull final StreamDialogDefaultEntry... newEntries) { + Stream.of(newEntries).forEach(this::addEntry); + return this; + } + + /** + *

Change an entries' action that is called when the entry is selected.

+ *

Warning: Only use this method when the entry has been already added. + * Changing the action of an entry which has not been added to the Builder yet + * does not have an effect.

+ * @param entry the entry to change + * @param action the action to perform when the entry is selected + * @return the current {@link Builder} instance + */ + public Builder setAction(@NonNull final StreamDialogDefaultEntry entry, + @NonNull final StreamDialogEntry.StreamDialogEntryAction action) { + for (int i = 0; i < entries.size(); i++) { + if (entries.get(i).resource == entry.resource) { + entries.set(i, new StreamDialogEntry(entry.resource, action)); + return this; + } + } + return this; + } + + /** + * Adds {@link StreamDialogDefaultEntry#ENQUEUE} if the player is open and + * {@link StreamDialogDefaultEntry#ENQUEUE_NEXT} if there are multiple streams + * in the play queue. + * @return the current {@link Builder} instance + */ + public Builder addEnqueueEntriesIfNeeded() { + if (PlayerHolder.getInstance().isPlayQueueReady()) { + addEntry(StreamDialogDefaultEntry.ENQUEUE); + + if (PlayerHolder.getInstance().getQueueSize() > 1) { + addEntry(StreamDialogDefaultEntry.ENQUEUE_NEXT); + } + } + return this; + } + + /** + * Adds the {@link StreamDialogDefaultEntry#START_HERE_ON_BACKGROUND}. + * If the {@link #infoItem} is not a pure audio (live) stream, + * {@link StreamDialogDefaultEntry#START_HERE_ON_POPUP} is added, too. + * @return the current {@link Builder} instance + */ + public Builder addStartHereEntries() { + addEntry(StreamDialogDefaultEntry.START_HERE_ON_BACKGROUND); + if (infoItem.getStreamType() != StreamType.AUDIO_STREAM + && infoItem.getStreamType() != StreamType.AUDIO_LIVE_STREAM) { + addEntry(StreamDialogDefaultEntry.START_HERE_ON_POPUP); + } + return this; + } + + /** + * Adds {@link StreamDialogDefaultEntry.MARK_AS_WATCHED} if the watch history is enabled + * and the stream is not a livestream. + * @return the current {@link Builder} instance + */ + public Builder addMarkAsWatchedEntryIfNeeded() { + final boolean isWatchHistoryEnabled = PreferenceManager + .getDefaultSharedPreferences(context) + .getBoolean(context.getString(R.string.enable_watch_history_key), false); + if (isWatchHistoryEnabled + && infoItem.getStreamType() != StreamType.LIVE_STREAM + && infoItem.getStreamType() != StreamType.AUDIO_LIVE_STREAM) { + addEntry(StreamDialogDefaultEntry.MARK_AS_WATCHED); + } + return this; + } + + /** + * Adds the {@link StreamDialogDefaultEntry.PLAY_WITH_KODI} entry if it is needed. + * @return the current {@link Builder} instance + */ + public Builder addPlayWithKodiEntryIfNeeded() { + if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) { + addEntry(StreamDialogDefaultEntry.PLAY_WITH_KODI); + } + return this; + } + + /** + * Add the entries which are usually at the top of the action list. + *
+ * This method adds the "enqueue" (see {@link #addEnqueueEntriesIfNeeded()}) + * and "start here" (see {@link #addStartHereEntries()} entries. + * @return the current {@link Builder} instance + */ + public Builder addDefaultBeginningEntries() { + addEnqueueEntriesIfNeeded(); + addStartHereEntries(); + return this; + } + + /** + * Add the entries which are usually at the bottom of the action list. + * @return the current {@link Builder} instance + */ + public Builder addDefaultEndEntries() { + addAllEntries( + StreamDialogDefaultEntry.APPEND_PLAYLIST, + StreamDialogDefaultEntry.SHARE, + StreamDialogDefaultEntry.OPEN_IN_BROWSER + ); + addPlayWithKodiEntryIfNeeded(); + addMarkAsWatchedEntryIfNeeded(); + addEntry(StreamDialogDefaultEntry.SHOW_CHANNEL_DETAILS); + return this; + } + + /** + * Creates the {@link InfoItemDialog}. + * @return a new instance of {@link InfoItemDialog} + */ + public InfoItemDialog create() { + if (addDefaultEntriesAutomatically) { + addDefaultEndEntries(); + } + return new InfoItemDialog(this.activity, this.fragment, this.infoItem, this.entries); + } + + public static void reportErrorDuringInitialization(final Throwable throwable, + final InfoItem item) { + ErrorUtil.showSnackbar(App.getApp().getBaseContext(), new ErrorInfo( + throwable, + UserAction.OPEN_INFO_ITEM_DIALOG, + "none", + item.getServiceId())); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogDefaultEntry.java b/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogDefaultEntry.java new file mode 100644 index 000000000..eda9e19bc --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogDefaultEntry.java @@ -0,0 +1,142 @@ +package org.schabi.newpipe.info_list.dialog; + +import static org.schabi.newpipe.info_list.dialog.StreamDialogEntry.fetchItemInfoIfSparse; +import static org.schabi.newpipe.util.NavigationHelper.openChannelFragment; + +import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.local.dialog.PlaylistAppendDialog; +import org.schabi.newpipe.local.dialog.PlaylistDialog; +import org.schabi.newpipe.local.history.HistoryRecordManager; +import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.SaveUploaderUrlHelper; +import org.schabi.newpipe.util.external_communication.KoreUtils; +import org.schabi.newpipe.util.external_communication.ShareUtils; + +import java.util.Collections; + +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; + +/** + *

+ * This enum provides entries that are accepted + * by the {@link InfoItemDialog.Builder}. + *

+ *

+ * These entries contain a String {@link #resource} which is displayed in the dialog and + * a default {@link #action} that is executed + * when the entry is selected (via onClick()). + *
+ * They action can be overridden by using the Builder's + * {@link InfoItemDialog.Builder#setAction( + * StreamDialogDefaultEntry, StreamDialogEntry.StreamDialogEntryAction)} + * method. + *

+ */ +public enum StreamDialogDefaultEntry { + SHOW_CHANNEL_DETAILS(R.string.show_channel_details, (fragment, item) -> + SaveUploaderUrlHelper.saveUploaderUrlIfNeeded(fragment, item, + uploaderUrl -> openChannelFragment(fragment, item, uploaderUrl)) + ), + + /** + * Enqueues the stream automatically to the current PlayerType. + */ + ENQUEUE(R.string.enqueue_stream, (fragment, item) -> + fetchItemInfoIfSparse(fragment.requireContext(), item, singlePlayQueue -> + NavigationHelper.enqueueOnPlayer(fragment.getContext(), singlePlayQueue)) + ), + + /** + * Enqueues the stream automatically to the current PlayerType + * after the currently playing stream. + */ + ENQUEUE_NEXT(R.string.enqueue_next_stream, (fragment, item) -> + fetchItemInfoIfSparse(fragment.requireContext(), item, singlePlayQueue -> + NavigationHelper.enqueueNextOnPlayer(fragment.getContext(), singlePlayQueue)) + ), + + START_HERE_ON_BACKGROUND(R.string.start_here_on_background, (fragment, item) -> + fetchItemInfoIfSparse(fragment.requireContext(), item, singlePlayQueue -> + NavigationHelper.playOnBackgroundPlayer( + fragment.getContext(), singlePlayQueue, true))), + + START_HERE_ON_POPUP(R.string.start_here_on_popup, (fragment, item) -> + fetchItemInfoIfSparse(fragment.requireContext(), item, singlePlayQueue -> + NavigationHelper.playOnPopupPlayer(fragment.getContext(), singlePlayQueue, true))), + + SET_AS_PLAYLIST_THUMBNAIL(R.string.set_as_playlist_thumbnail, (fragment, item) -> { + throw new UnsupportedOperationException("This needs to be implemented manually " + + "by using InfoItemDialog.Builder.setAction()"); + }), + + DELETE(R.string.delete, (fragment, item) -> { + throw new UnsupportedOperationException("This needs to be implemented manually " + + "by using InfoItemDialog.Builder.setAction()"); + }), + + /** + * Opens a {@link PlaylistDialog} to either append the stream to a playlist + * or create a new playlist if there are no local playlists. + */ + APPEND_PLAYLIST(R.string.add_to_playlist, (fragment, item) -> + PlaylistDialog.createCorrespondingDialog( + fragment.getContext(), + Collections.singletonList(new StreamEntity(item)), + dialog -> dialog.show( + fragment.getParentFragmentManager(), + "StreamDialogEntry@" + + (dialog instanceof PlaylistAppendDialog ? "append" : "create") + + "_playlist" + ) + ) + ), + + PLAY_WITH_KODI(R.string.play_with_kodi_title, (fragment, item) -> { + final Uri videoUrl = Uri.parse(item.getUrl()); + try { + NavigationHelper.playWithKore(fragment.requireContext(), videoUrl); + } catch (final Exception e) { + KoreUtils.showInstallKoreDialog(fragment.requireActivity()); + } + }), + + SHARE(R.string.share, (fragment, item) -> + ShareUtils.shareText(fragment.requireContext(), item.getName(), item.getUrl(), + item.getThumbnailUrl())), + + OPEN_IN_BROWSER(R.string.open_in_browser, (fragment, item) -> + ShareUtils.openUrlInBrowser(fragment.requireContext(), item.getUrl())), + + + MARK_AS_WATCHED(R.string.mark_as_watched, (fragment, item) -> + new HistoryRecordManager(fragment.getContext()) + .markAsWatched(item) + .onErrorComplete() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe() + ); + + + @StringRes + public final int resource; + @NonNull + public final StreamDialogEntry.StreamDialogEntryAction action; + + StreamDialogDefaultEntry(@StringRes final int resource, + @NonNull final StreamDialogEntry.StreamDialogEntryAction action) { + this.resource = resource; + this.action = action; + } + + @NonNull + public StreamDialogEntry toStreamDialogEntry() { + return new StreamDialogEntry(resource, action); + } + +} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogEntry.java b/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogEntry.java new file mode 100644 index 000000000..a8d361447 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogEntry.java @@ -0,0 +1,91 @@ +package org.schabi.newpipe.info_list.dialog; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; +import androidx.fragment.app.Fragment; + +import org.schabi.newpipe.error.ErrorInfo; +import org.schabi.newpipe.error.ErrorUtil; +import org.schabi.newpipe.error.UserAction; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.extractor.stream.StreamType; +import org.schabi.newpipe.local.history.HistoryRecordManager; +import org.schabi.newpipe.player.playqueue.SinglePlayQueue; +import org.schabi.newpipe.util.ExtractorHelper; + +import java.util.function.Consumer; + +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.schedulers.Schedulers; + +public class StreamDialogEntry { + + @StringRes + public final int resource; + @NonNull + public final StreamDialogEntryAction action; + + public StreamDialogEntry(@StringRes final int resource, + @NonNull final StreamDialogEntryAction action) { + this.resource = resource; + this.action = action; + } + + public String getString(@NonNull final Context context) { + return context.getString(resource); + } + + public interface StreamDialogEntryAction { + void onClick(Fragment fragment, StreamInfoItem infoItem); + } + + /** + * Fetches a {@link StreamInfoItem} if it is incomplete and executes the callback. + *
+ * This method is required if the info has been fetched + * via a {@link org.schabi.newpipe.extractor.feed.FeedExtractor}. + * FeedExtractors provide a fast and lightweight method to fetch info, + * but the info might be incomplete + * (see {@link org.schabi.newpipe.local.feed.service.FeedLoadService} for more details). + * @param context + * @param item the item which is checked and eventually loaded completely + * @param callback + */ + public static void fetchItemInfoIfSparse(@NonNull final Context context, + @NonNull final StreamInfoItem item, + @NonNull final Consumer callback) { + if (!(item.getStreamType() == StreamType.LIVE_STREAM + || item.getStreamType() == StreamType.AUDIO_LIVE_STREAM) + && item.getDuration() < 0) { + // Sparse item: fetched by fast fetch + ExtractorHelper.getStreamInfo( + item.getServiceId(), + item.getUrl(), + false + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(result -> { + final HistoryRecordManager recordManager = + new HistoryRecordManager(context); + recordManager.saveStreamState(result, 0) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnError(throwable -> ErrorUtil.showSnackbar( + context, + new ErrorInfo(throwable, UserAction.REQUESTED_STREAM, + item.getUrl(), item.getServiceId()))) + .subscribe(); + + callback.accept(new SinglePlayQueue(result)); + }, throwable -> ErrorUtil.createNotification(context, + new ErrorInfo(throwable, UserAction.REQUESTED_CHANNEL, + "Could not fetch missing stream info"))); + } else { + callback.accept(new SinglePlayQueue(item)); + } + } + +} diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt index e6da0d545..8285d21e6 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt @@ -68,25 +68,21 @@ import org.schabi.newpipe.error.UserAction import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException import org.schabi.newpipe.extractor.stream.StreamInfoItem -import org.schabi.newpipe.extractor.stream.StreamType import org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty import org.schabi.newpipe.fragments.BaseStateFragment -import org.schabi.newpipe.info_list.InfoItemDialog +import org.schabi.newpipe.info_list.dialog.InfoItemDialog import org.schabi.newpipe.ktx.animate import org.schabi.newpipe.ktx.animateHideRecyclerViewAllowingScrolling import org.schabi.newpipe.ktx.slideUp import org.schabi.newpipe.local.feed.item.StreamItem import org.schabi.newpipe.local.feed.service.FeedLoadService import org.schabi.newpipe.local.subscription.SubscriptionManager -import org.schabi.newpipe.player.helper.PlayerHolder import org.schabi.newpipe.util.DeviceUtils import org.schabi.newpipe.util.Localization import org.schabi.newpipe.util.NavigationHelper -import org.schabi.newpipe.util.StreamDialogEntry import org.schabi.newpipe.util.ThemeHelper.getGridSpanCountStreams import org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout import java.time.OffsetDateTime -import java.util.ArrayList import java.util.function.Consumer class FeedFragment : BaseStateFragment() { @@ -356,53 +352,12 @@ class FeedFragment : BaseStateFragment() { feedBinding.loadingProgressBar.max = progressState.maxProgress } - private fun showStreamDialog(item: StreamInfoItem) { + private fun showInfoItemDialog(item: StreamInfoItem) { val context = context val activity: Activity? = getActivity() if (context == null || context.resources == null || activity == null) return - val entries = ArrayList() - if (PlayerHolder.getInstance().isPlayQueueReady) { - entries.add(StreamDialogEntry.enqueue) - - if (PlayerHolder.getInstance().queueSize > 1) { - entries.add(StreamDialogEntry.enqueue_next) - } - } - - if (item.streamType == StreamType.AUDIO_STREAM) { - entries.addAll( - listOf( - StreamDialogEntry.start_here_on_background, - StreamDialogEntry.append_playlist, - StreamDialogEntry.share, - StreamDialogEntry.open_in_browser - ) - ) - } else { - entries.addAll( - listOf( - StreamDialogEntry.start_here_on_background, - StreamDialogEntry.start_here_on_popup, - StreamDialogEntry.append_playlist, - StreamDialogEntry.share, - StreamDialogEntry.open_in_browser - ) - ) - } - - // show "mark as watched" only when watch history is enabled - if (StreamDialogEntry.shouldAddMarkAsWatched(item.streamType, context)) { - entries.add( - StreamDialogEntry.mark_as_watched - ) - } - entries.add(StreamDialogEntry.show_channel_details) - - StreamDialogEntry.setEnabledEntries(entries) - InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context)) { _, which -> - StreamDialogEntry.clickOn(which, this, item) - }.show() + InfoItemDialog.Builder(activity, context, this, item).create().show() } private val listenerStreamItem = object : OnItemClickListener, OnItemLongClickListener { @@ -418,7 +373,7 @@ class FeedFragment : BaseStateFragment() { override fun onItemLongClick(item: Item<*>, view: View): Boolean { if (item is StreamItem && !isRefreshing) { - showStreamDialog(item.streamWithState.stream.toStreamInfoItem()) + showInfoItemDialog(item.streamWithState.stream.toStreamInfoItem()) return true } return false diff --git a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java index 73682d5d5..01df34292 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java @@ -1,6 +1,5 @@ package org.schabi.newpipe.local.history; -import android.app.Activity; import android.content.Context; import android.os.Bundle; import android.os.Parcelable; @@ -29,20 +28,16 @@ import org.schabi.newpipe.databinding.StatisticPlaylistControlBinding; import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.extractor.stream.StreamType; -import org.schabi.newpipe.info_list.InfoItemDialog; +import org.schabi.newpipe.info_list.dialog.InfoItemDialog; import org.schabi.newpipe.local.BaseLocalListFragment; -import org.schabi.newpipe.player.helper.PlayerHolder; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.settings.HistorySettingsFragment; -import org.schabi.newpipe.util.external_communication.KoreUtils; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; -import org.schabi.newpipe.util.StreamDialogEntry; +import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; @@ -154,7 +149,7 @@ public class StatisticsPlaylistFragment @Override public void held(final LocalItem selectedItem) { if (selectedItem instanceof StreamStatisticsEntry) { - showStreamDialog((StreamStatisticsEntry) selectedItem); + showInfoItemDialog((StreamStatisticsEntry) selectedItem); } } }); @@ -328,66 +323,30 @@ public class StatisticsPlaylistFragment return getPlayQueue(Math.max(itemListAdapter.getItemsList().indexOf(infoItem), 0)); } - private void showStreamDialog(final StreamStatisticsEntry item) { + private void showInfoItemDialog(final StreamStatisticsEntry item) { final Context context = getContext(); - final Activity activity = getActivity(); - if (context == null || context.getResources() == null || activity == null) { - return; - } final StreamInfoItem infoItem = item.toStreamInfoItem(); - final ArrayList entries = new ArrayList<>(); + try { + final InfoItemDialog.Builder dialogBuilder = + new InfoItemDialog.Builder(getActivity(), context, this, infoItem); - if (PlayerHolder.getInstance().isPlayQueueReady()) { - entries.add(StreamDialogEntry.enqueue); - - if (PlayerHolder.getInstance().getQueueSize() > 1) { - entries.add(StreamDialogEntry.enqueue_next); - } + // set entries in the middle; the others are added automatically + dialogBuilder + .addEntry(StreamDialogDefaultEntry.DELETE) + .setAction( + StreamDialogDefaultEntry.DELETE, + (f, i) -> deleteEntry( + Math.max(itemListAdapter.getItemsList().indexOf(item), 0))) + .setAction( + StreamDialogDefaultEntry.START_HERE_ON_BACKGROUND, + (f, i) -> NavigationHelper.playOnBackgroundPlayer( + context, getPlayQueueStartingAt(item), true)) + .create() + .show(); + } catch (final IllegalArgumentException e) { + InfoItemDialog.Builder.reportErrorDuringInitialization(e, infoItem); } - - if (infoItem.getStreamType() == StreamType.AUDIO_STREAM) { - entries.addAll(Arrays.asList( - StreamDialogEntry.start_here_on_background, - StreamDialogEntry.delete, - StreamDialogEntry.append_playlist, - StreamDialogEntry.share - )); - } else { - entries.addAll(Arrays.asList( - StreamDialogEntry.start_here_on_background, - StreamDialogEntry.start_here_on_popup, - StreamDialogEntry.delete, - StreamDialogEntry.append_playlist, - StreamDialogEntry.share - )); - } - entries.add(StreamDialogEntry.open_in_browser); - if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) { - entries.add(StreamDialogEntry.play_with_kodi); - } - - // show "mark as watched" only when watch history is enabled - if (StreamDialogEntry.shouldAddMarkAsWatched( - item.getStreamEntity().getStreamType(), - context - )) { - entries.add( - StreamDialogEntry.mark_as_watched - ); - } - entries.add(StreamDialogEntry.show_channel_details); - - StreamDialogEntry.setEnabledEntries(entries); - - StreamDialogEntry.start_here_on_background.setCustomAction((fragment, infoItemDuplicate) -> - NavigationHelper - .playOnBackgroundPlayer(context, getPlayQueueStartingAt(item), true)); - StreamDialogEntry.delete.setCustomAction((fragment, infoItemDuplicate) -> - deleteEntry(Math.max(itemListAdapter.getItemsList().indexOf(item), 0))); - - new InfoItemDialog(activity, infoItem, StreamDialogEntry.getCommands(context), - (dialog, which) -> StreamDialogEntry.clickOn(which, this, infoItem)).show(); } private void deleteEntry(final int index) { diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java index feb5b2f96..9ea6c020d 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java @@ -1,6 +1,8 @@ package org.schabi.newpipe.local.playlist; -import android.app.Activity; +import static org.schabi.newpipe.ktx.ViewUtils.animate; +import static org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout; + import android.content.Context; import android.content.DialogInterface; import android.os.Bundle; @@ -38,22 +40,18 @@ import org.schabi.newpipe.databinding.PlaylistControlBinding; import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.extractor.stream.StreamType; -import org.schabi.newpipe.info_list.InfoItemDialog; +import org.schabi.newpipe.info_list.dialog.InfoItemDialog; import org.schabi.newpipe.local.BaseLocalListFragment; import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.player.MainPlayer.PlayerType; -import org.schabi.newpipe.player.helper.PlayerHolder; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; -import org.schabi.newpipe.util.external_communication.KoreUtils; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; -import org.schabi.newpipe.util.StreamDialogEntry; +import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.List; @@ -68,9 +66,6 @@ import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.schedulers.Schedulers; import io.reactivex.rxjava3.subjects.PublishSubject; -import static org.schabi.newpipe.ktx.ViewUtils.animate; -import static org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout; - public class LocalPlaylistFragment extends BaseLocalListFragment, Void> { // Save the list 10 seconds after the last change occurred private static final long SAVE_DEBOUNCE_MILLIS = 10000; @@ -182,7 +177,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment entries = new ArrayList<>(); + try { + final Context context = getContext(); + final InfoItemDialog.Builder dialogBuilder = + new InfoItemDialog.Builder(getActivity(), context, this, infoItem); - if (PlayerHolder.getInstance().isPlayQueueReady()) { - entries.add(StreamDialogEntry.enqueue); - - if (PlayerHolder.getInstance().getQueueSize() > 1) { - entries.add(StreamDialogEntry.enqueue_next); - } - } - if (infoItem.getStreamType() == StreamType.AUDIO_STREAM) { - entries.addAll(Arrays.asList( - StreamDialogEntry.start_here_on_background, - StreamDialogEntry.set_as_playlist_thumbnail, - StreamDialogEntry.delete, - StreamDialogEntry.append_playlist, - StreamDialogEntry.share - )); - } else { - entries.addAll(Arrays.asList( - StreamDialogEntry.start_here_on_background, - StreamDialogEntry.start_here_on_popup, - StreamDialogEntry.set_as_playlist_thumbnail, - StreamDialogEntry.delete, - StreamDialogEntry.append_playlist, - StreamDialogEntry.share - )); - } - entries.add(StreamDialogEntry.open_in_browser); - if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) { - entries.add(StreamDialogEntry.play_with_kodi); - } - - // show "mark as watched" only when watch history is enabled - if (StreamDialogEntry.shouldAddMarkAsWatched( - item.getStreamEntity().getStreamType(), - context - )) { - entries.add( - StreamDialogEntry.mark_as_watched + // add entries in the middle + dialogBuilder.addAllEntries( + StreamDialogDefaultEntry.SET_AS_PLAYLIST_THUMBNAIL, + StreamDialogDefaultEntry.DELETE ); + + // set custom actions + // all entries modified below have already been added within the builder + dialogBuilder + .setAction( + StreamDialogDefaultEntry.START_HERE_ON_BACKGROUND, + (f, i) -> NavigationHelper.playOnBackgroundPlayer( + context, getPlayQueueStartingAt(item), true)) + .setAction( + StreamDialogDefaultEntry.SET_AS_PLAYLIST_THUMBNAIL, + (f, i) -> + changeThumbnailUrl(item.getStreamEntity().getThumbnailUrl())) + .setAction( + StreamDialogDefaultEntry.DELETE, + (f, i) -> deleteItem(item)) + .create() + .show(); + } catch (final IllegalArgumentException e) { + InfoItemDialog.Builder.reportErrorDuringInitialization(e, infoItem); } - entries.add(StreamDialogEntry.show_channel_details); - - StreamDialogEntry.setEnabledEntries(entries); - - StreamDialogEntry.start_here_on_background.setCustomAction((fragment, infoItemDuplicate) -> - NavigationHelper.playOnBackgroundPlayer(context, - getPlayQueueStartingAt(item), true)); - StreamDialogEntry.set_as_playlist_thumbnail.setCustomAction( - (fragment, infoItemDuplicate) -> - changeThumbnailUrl(item.getStreamEntity().getThumbnailUrl())); - StreamDialogEntry.delete.setCustomAction((fragment, infoItemDuplicate) -> - deleteItem(item)); - - new InfoItemDialog(activity, infoItem, StreamDialogEntry.getCommands(context), - (dialog, which) -> StreamDialogEntry.clickOn(which, this, infoItem)).show(); } private void setInitialData(final long pid, final String title) { diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index 49ee49668..a9faa8c42 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -31,6 +31,7 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.Stream; import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.fragments.MainFragment; import org.schabi.newpipe.fragments.detail.VideoDetailFragment; @@ -408,6 +409,15 @@ public final class NavigationHelper { .commit(); } + public static void openChannelFragment(@NonNull final Fragment fragment, + @NonNull final StreamInfoItem item, + final String uploaderUrl) { + // For some reason `getParentFragmentManager()` doesn't work, but this does. + openChannelFragment( + fragment.requireActivity().getSupportFragmentManager(), + item.getServiceId(), uploaderUrl, item.getUploaderName()); + } + public static void openPlaylistFragment(final FragmentManager fragmentManager, final int serviceId, final String url, @NonNull final String name) { diff --git a/app/src/main/java/org/schabi/newpipe/util/StreamDialogEntry.java b/app/src/main/java/org/schabi/newpipe/util/StreamDialogEntry.java deleted file mode 100644 index 1b4c8046c..000000000 --- a/app/src/main/java/org/schabi/newpipe/util/StreamDialogEntry.java +++ /dev/null @@ -1,238 +0,0 @@ -package org.schabi.newpipe.util; - -import android.content.Context; -import android.net.Uri; -import android.util.Log; - -import androidx.fragment.app.Fragment; -import androidx.preference.PreferenceManager; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.extractor.stream.StreamType; -import org.schabi.newpipe.local.dialog.PlaylistAppendDialog; -import org.schabi.newpipe.local.dialog.PlaylistDialog; -import org.schabi.newpipe.local.history.HistoryRecordManager; -import org.schabi.newpipe.player.playqueue.SinglePlayQueue; -import org.schabi.newpipe.util.external_communication.KoreUtils; -import org.schabi.newpipe.util.external_communication.ShareUtils; - -import java.util.Collections; -import java.util.List; -import java.util.function.Consumer; - -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; -import io.reactivex.rxjava3.schedulers.Schedulers; - -public enum StreamDialogEntry { - ////////////////////////////////////// - // enum values with DEFAULT actions // - ////////////////////////////////////// - - show_channel_details(R.string.show_channel_details, (fragment, item) -> { - SaveUploaderUrlHelper.saveUploaderUrlIfNeeded(fragment, item, - uploaderUrl -> openChannelFragment(fragment, item, uploaderUrl)); - }), - - /** - * Enqueues the stream automatically to the current PlayerType.
- *
- * Info: Add this entry within showStreamDialog. - */ - enqueue(R.string.enqueue_stream, (fragment, item) -> { - fetchItemInfoIfSparse(fragment, item, fullItem -> - NavigationHelper.enqueueOnPlayer(fragment.getContext(), fullItem)); - }), - - enqueue_next(R.string.enqueue_next_stream, (fragment, item) -> { - fetchItemInfoIfSparse(fragment, item, fullItem -> - NavigationHelper.enqueueNextOnPlayer(fragment.getContext(), fullItem)); - }), - - start_here_on_background(R.string.start_here_on_background, (fragment, item) -> { - fetchItemInfoIfSparse(fragment, item, fullItem -> - NavigationHelper.playOnBackgroundPlayer(fragment.getContext(), fullItem, true)); - }), - - start_here_on_popup(R.string.start_here_on_popup, (fragment, item) -> { - fetchItemInfoIfSparse(fragment, item, fullItem -> - NavigationHelper.playOnPopupPlayer(fragment.getContext(), fullItem, true)); - }), - - set_as_playlist_thumbnail(R.string.set_as_playlist_thumbnail, (fragment, item) -> { - }), // has to be set manually - - delete(R.string.delete, (fragment, item) -> { - }), // has to be set manually - - append_playlist(R.string.add_to_playlist, (fragment, item) -> { - PlaylistDialog.createCorrespondingDialog( - fragment.getContext(), - Collections.singletonList(new StreamEntity(item)), - dialog -> dialog.show( - fragment.getParentFragmentManager(), - "StreamDialogEntry@" - + (dialog instanceof PlaylistAppendDialog ? "append" : "create") - + "_playlist" - ) - ); - }), - - play_with_kodi(R.string.play_with_kodi_title, (fragment, item) -> { - final Uri videoUrl = Uri.parse(item.getUrl()); - try { - NavigationHelper.playWithKore(fragment.requireContext(), videoUrl); - } catch (final Exception e) { - KoreUtils.showInstallKoreDialog(fragment.requireActivity()); - } - }), - - share(R.string.share, (fragment, item) -> - ShareUtils.shareText(fragment.requireContext(), item.getName(), item.getUrl(), - item.getThumbnailUrl())), - - open_in_browser(R.string.open_in_browser, (fragment, item) -> - ShareUtils.openUrlInBrowser(fragment.requireContext(), item.getUrl())), - - - mark_as_watched(R.string.mark_as_watched, (fragment, item) -> { - new HistoryRecordManager(fragment.getContext()) - .markAsWatched(item) - .onErrorComplete() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(); - }); - - /////////////// - // variables // - /////////////// - - private static StreamDialogEntry[] enabledEntries; - private final int resource; - private final StreamDialogEntryAction defaultAction; - private StreamDialogEntryAction customAction; - - StreamDialogEntry(final int resource, final StreamDialogEntryAction defaultAction) { - this.resource = resource; - this.defaultAction = defaultAction; - this.customAction = null; - } - - - /////////////////////////////////////////////////////// - // non-static methods to initialize and edit entries // - /////////////////////////////////////////////////////// - - public static void setEnabledEntries(final List entries) { - setEnabledEntries(entries.toArray(new StreamDialogEntry[0])); - } - - /** - * To be called before using {@link #setCustomAction(StreamDialogEntryAction)}. - * - * @param entries the entries to be enabled - */ - public static void setEnabledEntries(final StreamDialogEntry... entries) { - // cleanup from last time StreamDialogEntry was used - for (final StreamDialogEntry streamDialogEntry : values()) { - streamDialogEntry.customAction = null; - } - - enabledEntries = entries; - } - - public static String[] getCommands(final Context context) { - final String[] commands = new String[enabledEntries.length]; - for (int i = 0; i != enabledEntries.length; ++i) { - commands[i] = context.getResources().getString(enabledEntries[i].resource); - } - - return commands; - } - - - //////////////////////////////////////////////// - // static methods that act on enabled entries // - //////////////////////////////////////////////// - - public static void clickOn(final int which, final Fragment fragment, - final StreamInfoItem infoItem) { - if (enabledEntries[which].customAction == null) { - enabledEntries[which].defaultAction.onClick(fragment, infoItem); - } else { - enabledEntries[which].customAction.onClick(fragment, infoItem); - } - } - - /** - * Can be used after {@link #setEnabledEntries(StreamDialogEntry...)} has been called. - * - * @param action the action to be set - */ - public void setCustomAction(final StreamDialogEntryAction action) { - this.customAction = action; - } - - public interface StreamDialogEntryAction { - void onClick(Fragment fragment, StreamInfoItem infoItem); - } - - public static boolean shouldAddMarkAsWatched(final StreamType streamType, - final Context context) { - final boolean isWatchHistoryEnabled = PreferenceManager - .getDefaultSharedPreferences(context) - .getBoolean(context.getString(R.string.enable_watch_history_key), false); - return streamType != StreamType.AUDIO_LIVE_STREAM - && streamType != StreamType.LIVE_STREAM - && isWatchHistoryEnabled; - } - - ///////////////////////////////////////////// - // private method to open channel fragment // - ///////////////////////////////////////////// - - private static void openChannelFragment(final Fragment fragment, - final StreamInfoItem item, - final String uploaderUrl) { - // For some reason `getParentFragmentManager()` doesn't work, but this does. - NavigationHelper.openChannelFragment( - fragment.requireActivity().getSupportFragmentManager(), - item.getServiceId(), uploaderUrl, item.getUploaderName()); - } - - ///////////////////////////////////////////// - // helper functions // - ///////////////////////////////////////////// - - private static void fetchItemInfoIfSparse(final Fragment fragment, - final StreamInfoItem item, - final Consumer callback) { - if (!(item.getStreamType() == StreamType.LIVE_STREAM - || item.getStreamType() == StreamType.AUDIO_LIVE_STREAM) - && item.getDuration() < 0) { - // Sparse item: fetched by fast fetch - ExtractorHelper.getStreamInfo( - item.getServiceId(), - item.getUrl(), - false - ) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(result -> { - final HistoryRecordManager recordManager = - new HistoryRecordManager(fragment.getContext()); - recordManager.saveStreamState(result, 0) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .doOnError(throwable -> Log.e("StreamDialogEntry", - throwable.toString())) - .subscribe(); - - callback.accept(new SinglePlayQueue(result)); - }, throwable -> Log.e("StreamDialogEntry", throwable.toString())); - } else { - callback.accept(new SinglePlayQueue(item)); - } - } -} diff --git a/app/src/main/java/org/schabi/newpipe/util/external_communication/KoreUtils.java b/app/src/main/java/org/schabi/newpipe/util/external_communication/KoreUtils.java index 6801f24ef..0df579d88 100644 --- a/app/src/main/java/org/schabi/newpipe/util/external_communication/KoreUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/external_communication/KoreUtils.java @@ -10,6 +10,10 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.ServiceList; import org.schabi.newpipe.util.NavigationHelper; +/** + * Util class that provides methods which are related to the Kodi Media Center and its Kore app. + * @see Kodi website + */ public final class KoreUtils { private KoreUtils() { }