Merge ab114d2bdbdd733a7fb98dbc0cd2cb45fadd1cd2 into 8b28bd1a184e912b55847b4c31bd6c3e1af4049c

This commit is contained in:
Aryan Choudhary 2026-02-19 06:02:38 -08:00 committed by GitHub
commit e3a7989139
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 228 additions and 59 deletions

View File

@ -13,6 +13,7 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.App;
@ -73,7 +74,7 @@ public final class InfoItemDialog {
// 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);
entries.get(index).action.onClick((FragmentActivity) activity, info);
dialog = new AlertDialog.Builder(activity)
.setCustomTitle(bannerView)

View File

@ -9,6 +9,7 @@ import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.fragment.app.FragmentManager;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.stream.model.StreamEntity;
@ -44,44 +45,43 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
* </p>
*/
public enum StreamDialogDefaultEntry {
SHOW_CHANNEL_DETAILS(R.string.show_channel_details, (fragment, item) -> {
final var activity = fragment.requireActivity();
fetchUploaderUrlIfSparse(activity, item.getServiceId(), item.getUrl(),
item.getUploaderUrl(), url -> openChannelFragment(activity, item, url));
}),
SHOW_CHANNEL_DETAILS(R.string.show_channel_details, (fragmentActivity, item) ->
fetchUploaderUrlIfSparse(fragmentActivity, item.getServiceId(), item.getUrl(),
item.getUploaderUrl(), url -> openChannelFragment(fragmentActivity, item, url))
),
/**
* 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))
ENQUEUE(R.string.enqueue_stream, (fragmentActivity, item) ->
fetchItemInfoIfSparse(fragmentActivity, item, singlePlayQueue ->
NavigationHelper.enqueueOnPlayer(fragmentActivity, 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))
ENQUEUE_NEXT(R.string.enqueue_next_stream, (fragmentActivity, item) ->
fetchItemInfoIfSparse(fragmentActivity, item, singlePlayQueue ->
NavigationHelper.enqueueNextOnPlayer(fragmentActivity, singlePlayQueue))
),
START_HERE_ON_BACKGROUND(R.string.start_here_on_background, (fragment, item) ->
fetchItemInfoIfSparse(fragment.requireContext(), item, singlePlayQueue ->
START_HERE_ON_BACKGROUND(R.string.start_here_on_background, (fragmentActivity, item) ->
fetchItemInfoIfSparse(fragmentActivity, item, singlePlayQueue ->
NavigationHelper.playOnBackgroundPlayer(
fragment.getContext(), singlePlayQueue, true))),
fragmentActivity, 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))),
START_HERE_ON_POPUP(R.string.start_here_on_popup, (fragmentActivity, item) ->
fetchItemInfoIfSparse(fragmentActivity, item, singlePlayQueue ->
NavigationHelper.playOnPopupPlayer(fragmentActivity, singlePlayQueue, true))),
SET_AS_PLAYLIST_THUMBNAIL(R.string.set_as_playlist_thumbnail, (fragment, item) -> {
SET_AS_PLAYLIST_THUMBNAIL(R.string.set_as_playlist_thumbnail, (fragmentActivity, item) -> {
throw new UnsupportedOperationException("This needs to be implemented manually "
+ "by using InfoItemDialog.Builder.setAction()");
}),
DELETE(R.string.delete, (fragment, item) -> {
DELETE(R.string.delete, (fragmentActivity, item) -> {
throw new UnsupportedOperationException("This needs to be implemented manually "
+ "by using InfoItemDialog.Builder.setAction()");
}),
@ -90,12 +90,12 @@ public enum StreamDialogDefaultEntry {
* 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) ->
APPEND_PLAYLIST(R.string.add_to_playlist, (fragmentActivity, item) ->
PlaylistDialog.createCorrespondingDialog(
fragment.getContext(),
fragmentActivity,
List.of(new StreamEntity(item)),
dialog -> dialog.show(
fragment.getParentFragmentManager(),
fragmentActivity.getSupportFragmentManager(),
"StreamDialogEntry@"
+ (dialog instanceof PlaylistAppendDialog ? "append" : "create")
+ "_playlist"
@ -103,49 +103,50 @@ public enum StreamDialogDefaultEntry {
)
),
PLAY_WITH_KODI(R.string.play_with_kodi_title, (fragment, item) ->
KoreUtils.playWithKore(fragment.requireContext(), Uri.parse(item.getUrl()))),
PLAY_WITH_KODI(R.string.play_with_kodi_title, (fragmentActivity, item) ->
KoreUtils.playWithKore(fragmentActivity, Uri.parse(item.getUrl()))),
SHARE(R.string.share, (fragment, item) ->
ShareUtils.shareText(fragment.requireContext(), item.getName(), item.getUrl(),
SHARE(R.string.share, (fragmentActivity, item) ->
ShareUtils.shareText(fragmentActivity, item.getName(), item.getUrl(),
item.getThumbnails())),
/**
* Opens a {@link DownloadDialog} after fetching some stream info.
* If the user quits the current fragment, it will not open a DownloadDialog.
* If the user quits the current fragmentActivity, it will not open a DownloadDialog.
*/
DOWNLOAD(R.string.download, (fragment, item) ->
fetchStreamInfoAndSaveToDatabase(fragment.requireContext(), item.getServiceId(),
DOWNLOAD(R.string.download, (fragmentActivity, item) ->
fetchStreamInfoAndSaveToDatabase(fragmentActivity, item.getServiceId(),
item.getUrl(), info -> {
// Ensure the fragment is attached and its state hasn't been saved to avoid
// Ensure the fragment in the activity is attached
// and its state hasn't been saved to avoid
// showing dialog during lifecycle changes or when the activity is paused,
// e.g. by selecting the download option and opening a different fragment.
if (fragment.isAdded() && !fragment.isStateSaved()) {
final FragmentManager fm = fragmentActivity.getSupportFragmentManager();
if (!fm.isStateSaved()) {
final DownloadDialog downloadDialog =
new DownloadDialog(fragment.requireContext(), info);
downloadDialog.show(fragment.getChildFragmentManager(),
"downloadDialog");
new DownloadDialog(fragmentActivity, info);
downloadDialog.show(fm, "downloadDialog");
}
})
),
OPEN_IN_BROWSER(R.string.open_in_browser, (fragment, item) ->
ShareUtils.openUrlInBrowser(fragment.requireContext(), item.getUrl())),
OPEN_IN_BROWSER(R.string.open_in_browser, (fragmentActivity, item) ->
ShareUtils.openUrlInBrowser(fragmentActivity, item.getUrl())),
MARK_AS_WATCHED(R.string.mark_as_watched, (fragment, item) ->
new HistoryRecordManager(fragment.getContext())
MARK_AS_WATCHED(R.string.mark_as_watched, (fragmentActivity, item) ->
new HistoryRecordManager(fragmentActivity)
.markAsWatched(item)
.doOnError(error -> {
.doOnError(error ->
ErrorUtil.showSnackbar(
fragment.requireContext(),
fragmentActivity,
new ErrorInfo(
error,
UserAction.OPEN_INFO_ITEM_DIALOG,
"Got an error when trying to mark as watched"
)
);
})
)
)
.onErrorComplete()
.observeOn(AndroidSchedulers.mainThread())
.subscribe()

View File

@ -4,7 +4,7 @@ import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
@ -26,6 +26,6 @@ public class StreamDialogEntry {
}
public interface StreamDialogEntryAction {
void onClick(Fragment fragment, StreamInfoItem infoItem);
void onClick(FragmentActivity fragmentActivity, StreamInfoItem infoItem);
}
}

View File

@ -1,22 +1,34 @@
package org.schabi.newpipe.info_list.holder;
import android.content.Context;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import androidx.core.view.AccessibilityDelegateCompat;
import androidx.core.view.ViewCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.fragment.app.FragmentActivity;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
import org.schabi.newpipe.ktx.ViewUtils;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.util.DependentPreferenceHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.image.CoilHelper;
import org.schabi.newpipe.views.AnimatedProgressBar;
@ -57,7 +69,8 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
if (item.getDuration() > 0) {
itemDurationView.setText(Localization.getDurationString(item.getDuration()));
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
itemDurationView.setBackgroundColor(ContextCompat.getColor(
itemBuilder.getContext(),
R.color.duration_background_color));
itemDurationView.setVisibility(View.VISIBLE);
@ -76,7 +89,8 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
}
} else if (StreamTypeUtil.isLiveStream(item.getStreamType())) {
itemDurationView.setText(R.string.duration_live);
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
itemDurationView.setBackgroundColor(ContextCompat.getColor(
itemBuilder.getContext(),
R.color.live_duration_background_color));
itemDurationView.setVisibility(View.VISIBLE);
itemProgressView.setVisibility(View.GONE);
@ -145,10 +159,142 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
}
return true;
});
updateAccessibilityActions(item);
}
private void updateAccessibilityActions(final StreamInfoItem item) {
ViewCompat.setAccessibilityDelegate(itemView, new AccessibilityDelegateCompat() {
@Override
public void onInitializeAccessibilityNodeInfo(
@NonNull final View host,
@NonNull final AccessibilityNodeInfoCompat info) {
super.onInitializeAccessibilityNodeInfo(host, info);
final Context context = itemBuilder.getContext();
if (context == null) {
return;
}
final PlayerHolder holder = PlayerHolder.INSTANCE;
if (holder.isPlayQueueReady()) {
info.addAction(new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
R.id.accessibility_action_enqueue,
context.getString(R.string.enqueue_stream)));
if (holder.getQueuePosition() < holder.getQueueSize() - 1) {
info.addAction(new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
R.id.accessibility_action_enqueue_next,
context.getString(R.string.enqueue_next_stream)));
}
}
info.addAction(new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
R.id.accessibility_action_background,
context.getString(R.string.start_here_on_background)));
if (!StreamTypeUtil.isAudio(item.getStreamType())) {
info.addAction(new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
R.id.accessibility_action_popup,
context.getString(R.string.start_here_on_popup)));
}
if (context instanceof FragmentActivity) {
info.addAction(new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
R.id.accessibility_action_download,
context.getString(R.string.download)));
info.addAction(new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
R.id.accessibility_action_playlist,
context.getString(R.string.add_to_playlist)));
}
info.addAction(new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
R.id.accessibility_action_share,
context.getString(R.string.share)));
info.addAction(new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
R.id.accessibility_action_browser,
context.getString(R.string.open_in_browser)));
if (KoreUtils.shouldShowPlayWithKodi(context, item.getServiceId())) {
info.addAction(new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
R.id.accessibility_action_kodi,
context.getString(R.string.play_with_kodi_title)));
}
final boolean isWatchHistoryEnabled = PreferenceManager
.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.enable_watch_history_key), false);
if (isWatchHistoryEnabled && !StreamTypeUtil.isLiveStream(item.getStreamType())) {
info.addAction(new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
R.id.accessibility_action_mark_watched,
context.getString(R.string.mark_as_watched)));
}
if (context instanceof AppCompatActivity) {
info.addAction(new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
R.id.accessibility_action_channel_details,
context.getString(R.string.accessibility_show_channel_details,
item.getUploaderName())));
}
info.addAction(new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
R.id.accessibility_action_show_options,
context.getString(R.string.more_options)));
}
@Override
public boolean performAccessibilityAction(@NonNull final View host, final int action,
final Bundle args) {
final Context context = itemBuilder.getContext();
final FragmentActivity fActivity = ((FragmentActivity) context);
if (context == null) {
return super.performAccessibilityAction(host, action, args);
}
if (action == R.id.accessibility_action_show_options) {
// display stream dialog
if (itemBuilder.getOnStreamSelectedListener() != null) {
itemBuilder.getOnStreamSelectedListener().held(item);
}
} else if (action == R.id.accessibility_action_enqueue) {
StreamDialogDefaultEntry.ENQUEUE.action.onClick(fActivity, item);
} else if (action == R.id.accessibility_action_enqueue_next) {
StreamDialogDefaultEntry.ENQUEUE_NEXT.action.onClick(fActivity, item);
} else if (action == R.id.accessibility_action_background) {
StreamDialogDefaultEntry.START_HERE_ON_BACKGROUND.action.onClick(
fActivity, item);
} else if (action == R.id.accessibility_action_popup) {
StreamDialogDefaultEntry.START_HERE_ON_POPUP.action.onClick(fActivity, item);
} else if (action == R.id.accessibility_action_download) {
StreamDialogDefaultEntry.DOWNLOAD.action.onClick(fActivity, item);
} else if (action == R.id.accessibility_action_playlist) {
StreamDialogDefaultEntry.APPEND_PLAYLIST.action.onClick(fActivity, item);
} else if (action == R.id.accessibility_action_share) {
StreamDialogDefaultEntry.SHARE.action.onClick(fActivity, item);
} else if (action == R.id.accessibility_action_browser) {
StreamDialogDefaultEntry.OPEN_IN_BROWSER.action.onClick(fActivity, item);
} else if (action == R.id.accessibility_action_kodi) {
StreamDialogDefaultEntry.PLAY_WITH_KODI.action.onClick(fActivity, item);
} else if (action == R.id.accessibility_action_mark_watched) {
StreamDialogDefaultEntry.MARK_AS_WATCHED.action.onClick(fActivity, item);
} else if (action == R.id.accessibility_action_channel_details) {
StreamDialogDefaultEntry.SHOW_CHANNEL_DETAILS.action.onClick(fActivity, item);
} else {
return super.performAccessibilityAction(host, action, args);
}
// return true if the action was handled
return true;
}
});
}
private void disableLongClick() {
itemView.setLongClickable(false);
itemView.setOnLongClickListener(null);
ViewCompat.setAccessibilityDelegate(itemView, null);
}
}

View File

@ -176,7 +176,7 @@
android:scaleType="fitXY"
android:src="@drawable/ic_repeat"
android:tint="?attr/colorAccent"
tools:ignore="ContentDescription" />
android:contentDescription="@string/notification_action_repeat" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/control_backward"
@ -191,7 +191,7 @@
android:scaleType="fitCenter"
android:src="@drawable/ic_previous"
android:tint="?attr/colorAccent"
tools:ignore="ContentDescription" />
android:contentDescription="@string/previous_stream" />
<ImageButton
android:id="@+id/control_fast_rewind"
@ -205,7 +205,8 @@
android:focusable="true"
android:scaleType="fitCenter"
android:src="@drawable/exo_controls_rewind"
android:tint="?attr/colorAccent" />
android:tint="?attr/colorAccent"
android:contentDescription="@string/rewind" />
<ImageButton
android:id="@+id/control_play_pause"
@ -221,7 +222,7 @@
android:scaleType="fitCenter"
android:src="@drawable/ic_pause"
android:tint="?attr/colorAccent"
tools:ignore="ContentDescription" />
android:contentDescription="@string/play" />
<ProgressBar
android:id="@+id/control_progress_bar"
@ -255,7 +256,8 @@
android:focusable="true"
android:scaleType="fitCenter"
android:src="@drawable/exo_controls_fastforward"
android:tint="?attr/colorAccent" />
android:tint="?attr/colorAccent"
android:contentDescription="@string/forward" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/control_forward"
@ -270,7 +272,7 @@
android:scaleType="fitCenter"
android:src="@drawable/ic_next"
android:tint="?attr/colorAccent"
tools:ignore="ContentDescription" />
android:contentDescription="@string/next_stream" />
<ImageButton
android:id="@+id/control_shuffle"
@ -286,7 +288,7 @@
android:scaleType="fitXY"
android:src="@drawable/ic_shuffle"
android:tint="?attr/colorAccent"
tools:ignore="ContentDescription" />
android:contentDescription="@string/notification_action_shuffle" />
</RelativeLayout>
</RelativeLayout>

View File

@ -28,7 +28,7 @@
android:scaleType="centerInside"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription"
android:contentDescription="@string/select_icon"
tools:src="@drawable/ic_asterisk" />
<com.google.android.material.textfield.TextInputLayout
@ -195,7 +195,7 @@
android:scaleType="centerInside"
android:src="@drawable/ic_delete"
android:visibility="gone"
tools:ignore="ContentDescription"
android:contentDescription="@string/delete"
tools:visibility="visible" />
<Button

View File

@ -56,8 +56,8 @@
android:layout_gravity="center"
android:background="@android:color/transparent"
android:src="@drawable/ic_play_arrow_shadow"
android:contentDescription="@string/play"
android:visibility="invisible"
tools:ignore="ContentDescription"
tools:visibility="visible" />
<org.schabi.newpipe.views.NewPipeTextView
@ -188,7 +188,7 @@
android:layout_marginTop="11dp"
android:layout_marginEnd="10dp"
android:src="@drawable/ic_expand_more"
tools:ignore="ContentDescription" />
android:contentDescription="@string/show_more" />
</FrameLayout>
@ -614,7 +614,7 @@
android:paddingLeft="@dimen/video_item_search_padding"
android:paddingRight="@dimen/video_item_search_padding"
android:scaleType="fitCenter"
tools:ignore="ContentDescription" />
android:contentDescription="@string/switch_to_main" />
<LinearLayout
android:id="@+id/overlay_metadata_layout"

View File

@ -15,6 +15,7 @@
android:layout_width="@dimen/video_item_search_thumbnail_image_width"
android:layout_height="@dimen/video_item_search_thumbnail_image_height"
android:scaleType="fitCenter"
android:importantForAccessibility="no"
android:src="@drawable/placeholder_thumbnail_video"
app:layout_constraintBottom_toTopOf="@+id/itemProgressView"
app:layout_constraintStart_toStartOf="parent"

View File

@ -18,6 +18,7 @@
android:layout_alignParentTop="true"
android:layout_marginRight="@dimen/video_item_search_image_right_margin"
android:scaleType="fitCenter"
android:importantForAccessibility="no"
android:src="@drawable/placeholder_thumbnail_video"
tools:ignore="RtlHardcoded" />

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="accessibility_action_show_options" type="id"/>
<item name="accessibility_action_enqueue" type="id"/>
<item name="accessibility_action_enqueue_next" type="id"/>
<item name="accessibility_action_background" type="id"/>
<item name="accessibility_action_popup" type="id"/>
<item name="accessibility_action_download" type="id"/>
<item name="accessibility_action_playlist" type="id"/>
<item name="accessibility_action_share" type="id"/>
<item name="accessibility_action_browser" type="id"/>
<item name="accessibility_action_kodi" type="id"/>
<item name="accessibility_action_mark_watched" type="id"/>
<item name="accessibility_action_channel_details" type="id"/>
</resources>

View File

@ -436,6 +436,7 @@
<string name="audio_track">Audio track</string>
<string name="hold_to_append">Hold to enqueue</string>
<string name="show_channel_details">Show channel details</string>
<string name="accessibility_show_channel_details">Show channel details for %1$s</string>
<string name="enqueue_stream">Enqueue</string>
<string name="enqueued">Enqueued</string>
<string name="enqueue_next_stream">Enqueue next</string>
@ -581,6 +582,7 @@
<string name="minimize_on_exit_none_description">None</string>
<string name="minimize_on_exit_background_description">Minimize to background player</string>
<string name="minimize_on_exit_popup_description">Minimize to popup player</string>
<string name="select_icon">Select icon</string>
<!-- Autoplay behavior -->
<string name="autoplay_summary">Start playback automatically — %s</string>
<string name="wifi_only">Only on Wi-Fi</string>