Merge pull request #7570 from TeamNewPipe/improvement/infoItemDialogBuilder

Refactor generating InfoItemDialog's
This commit is contained in:
litetex 2022-02-26 16:18:39 +01:00 committed by GitHub
commit 37517c7dd1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 691 additions and 597 deletions

View File

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

View File

@ -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<I, N> extends BaseStateFragment<I>
implements ListViewContract<I, N>, StateSaver.WriteRead,
SharedPreferences.OnSharedPreferenceChangeListener {
@ -268,7 +260,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
@Override
public void held(final StreamInfoItem selectedItem) {
showStreamDialog(selectedItem);
showInfoItemDialog(selectedItem);
}
});
@ -409,55 +401,12 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
}
}
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<StreamDialogEntry> 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();
}
/*//////////////////////////////////////////////////////////////////////////

View File

@ -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<StreamInfoItem, Playl
}
@Override
protected void showStreamDialog(final StreamInfoItem item) {
protected void showInfoItemDialog(final StreamInfoItem item) {
final Context context = getContext();
final Activity activity = getActivity();
if (context == null || context.getResources() == null || activity == null) {
return;
try {
final InfoItemDialog.Builder dialogBuilder =
new InfoItemDialog.Builder(getActivity(), context, this, item);
dialogBuilder
.setAction(
StreamDialogDefaultEntry.START_HERE_ON_BACKGROUND,
(f, infoItem) -> NavigationHelper.playOnBackgroundPlayer(
context, getPlayQueueStartingAt(infoItem), true))
.create()
.show();
} catch (final IllegalArgumentException e) {
InfoItemDialog.Builder.reportErrorDuringInitialization(e, item);
}
final ArrayList<StreamDialogEntry> 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

View File

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

View File

@ -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<StreamDialogEntry> 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();
}
/**
* <p>Builder to generate a {@link InfoItemDialog} for a {@link StreamInfoItem}.</p>
* Use {@link #addEntry(StreamDialogDefaultEntry)}
* and {@link #addAllEntries(StreamDialogDefaultEntry...)} to add options to the dialog.
* <br>
* 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<StreamDialogEntry> entries = new ArrayList<>();
private final boolean addDefaultEntriesAutomatically;
/**
* <p>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.</p>
* The dialog has the following structure:
* <pre>
* + - - - - - - - - - - - - - - - - - - - - - -+
* | 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 |
* + - - - - - - - - - - - - - - - - - - - - - -+
* </pre>
* 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 <code>activity, context</code>
* or resources is <code>null</code>
*/
public Builder(final Activity activity,
final Context context,
@NonNull final Fragment fragment,
@NonNull final StreamInfoItem infoItem) {
this(activity, context, fragment, infoItem, true);
}
/**
* <p>Create an instance of this {@link Builder} for a {@link StreamInfoItem}.</p>
* <p>If {@code addDefaultEntriesAutomatically} is set to {@code true},
* some default entries are added to the top and bottom of the dialog.</p>
* The dialog has the following structure:
* <pre>
* + - - - - - - - - - - - - - - - - - - - - - -+
* | 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 |
* + - - - - - - - - - - - - - - - - - - - - - -+
* </pre>
* 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}.
* <br/>
* Entries added with {@link #addEntry(StreamDialogDefaultEntry)} and
* {@link #addAllEntries(StreamDialogDefaultEntry...)} are added in between.
* @throws IllegalArgumentException if <code>activity, context</code>
* or resources is <code>null</code>
*/
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;
}
/**
* <p>Change an entries' action that is called when the entry is selected.</p>
* <p><strong>Warning:</strong> 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.</p>
* @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.
* <br/>
* 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()));
}
}
}

View File

@ -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;
/**
* <p>
* This enum provides entries that are accepted
* by the {@link InfoItemDialog.Builder}.
* </p>
* <p>
* 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 <code>onClick()</code>).
* <br/>
* They action can be overridden by using the Builder's
* {@link InfoItemDialog.Builder#setAction(
* StreamDialogDefaultEntry, StreamDialogEntry.StreamDialogEntryAction)}
* method.
* </p>
*/
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);
}
}

View File

@ -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.
* <br />
* 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<SinglePlayQueue> 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));
}
}
}

View File

@ -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<FeedState>() {
@ -356,53 +352,12 @@ class FeedFragment : BaseStateFragment<FeedState>() {
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<StreamDialogEntry>()
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<FeedState>() {
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

View File

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

View File

@ -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<List<PlaylistStreamEntry>, 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<List<PlaylistSt
@Override
public void held(final LocalItem selectedItem) {
if (selectedItem instanceof PlaylistStreamEntry) {
showStreamItemDialog((PlaylistStreamEntry) selectedItem);
showInfoItemDialog((PlaylistStreamEntry) selectedItem);
}
}
@ -743,70 +738,39 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
return getPlayQueue(Math.max(itemListAdapter.getItemsList().indexOf(infoItem), 0));
}
protected void showStreamItemDialog(final PlaylistStreamEntry item) {
final Context context = getContext();
final Activity activity = getActivity();
if (context == null || context.getResources() == null || activity == null) {
return;
}
protected void showInfoItemDialog(final PlaylistStreamEntry item) {
final StreamInfoItem infoItem = item.toStreamInfoItem();
final ArrayList<StreamDialogEntry> 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) {

View File

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

View File

@ -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.<br>
* <br>
* 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<StreamDialogEntry> 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<SinglePlayQueue> 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));
}
}
}

View File

@ -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 <a href="https://kodi.tv/">Kodi website</a>
*/
public final class KoreUtils {
private KoreUtils() { }