Merge 60cb6a11a823c1c5c221fb9a07ee29644a32a46b into 4481dd7fe6dd8c9bd116c391aed544de6239c640

This commit is contained in:
Syed Mutaib 2026-02-21 05:31:25 -08:00 committed by GitHub
commit 473eada494
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 366 additions and 3 deletions

View File

@ -32,12 +32,14 @@ import org.schabi.newpipe.info_list.holder.StreamGridInfoItemHolder;
import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder;
import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.BlockedChannelsManager;
import org.schabi.newpipe.util.FallbackViewHolder;
import org.schabi.newpipe.util.OnClickGesture;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;
/*
* Created by Christian Schabesberger on 01.08.16.
@ -132,8 +134,25 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
+ infoItemList.size() + ", data.size() = " + data.size());
}
// Filter out items from blocked channels
final List<? extends InfoItem> filteredData = data.stream()
.filter(item -> {
if (item instanceof StreamInfoItem) {
final StreamInfoItem streamItem = (StreamInfoItem) item;
final String uploaderUrl = streamItem.getUploaderUrl();
return !BlockedChannelsManager.INSTANCE.isChannelBlocked(
infoItemBuilder.getContext(), uploaderUrl);
}
return true; // Keep non-stream items (channels, playlists, etc.)
})
.collect(Collectors.toList());
if (filteredData.isEmpty()) {
return;
}
final int offsetStart = sizeConsideringHeaderOffset();
infoItemList.addAll(data);
infoItemList.addAll(filteredData);
if (DEBUG) {
Log.d(TAG, "addInfoItemList() after > offsetStart = " + offsetStart + ", "
@ -141,7 +160,7 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
+ "hasHeader = " + hasHeader() + ", "
+ "showFooter = " + showFooter);
}
notifyItemRangeInserted(offsetStart, data.size());
notifyItemRangeInserted(offsetStart, filteredData.size());
if (showFooter) {
final int footerNow = sizeConsideringHeaderOffset();

View File

@ -329,6 +329,7 @@ public final class InfoItemDialog {
);
addPlayWithKodiEntryIfNeeded();
addMarkAsWatchedEntryIfNeeded();
addEntry(StreamDialogDefaultEntry.BLOCK_CHANNEL);
addEntry(StreamDialogDefaultEntry.SHOW_CHANNEL_DETAILS);
return this;
}

View File

@ -10,6 +10,8 @@ import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import com.google.android.material.snackbar.Snackbar;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.download.DownloadDialog;
@ -19,6 +21,7 @@ import org.schabi.newpipe.error.UserAction;
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.BlockedChannelsManager;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils;
@ -148,7 +151,34 @@ public enum StreamDialogDefaultEntry {
.onErrorComplete()
.observeOn(AndroidSchedulers.mainThread())
.subscribe()
);
),
BLOCK_CHANNEL(R.string.block_channel, (fragment, item) -> {
final String uploaderUrl = item.getUploaderUrl();
final String uploaderName = item.getUploaderName();
if (uploaderUrl != null && !uploaderUrl.isEmpty()) {
// Block the channel
BlockedChannelsManager.INSTANCE.blockChannel(
fragment.requireContext(), uploaderUrl, uploaderName);
// Show snackbar with undo action
final Snackbar snackbar = Snackbar.make(
fragment.requireActivity().findViewById(android.R.id.content),
fragment.getString(R.string.channel_blocked,
uploaderName != null ? uploaderName : ""),
Snackbar.LENGTH_LONG
);
snackbar.setAction(R.string.undo, v -> {
// Unblock the channel
BlockedChannelsManager.INSTANCE.unblockChannel(
fragment.requireContext(), uploaderUrl);
});
snackbar.show();
}
});
@StringRes

View File

@ -0,0 +1,129 @@
package org.schabi.newpipe.settings
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.Snackbar
import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.FragmentBlockedChannelsBinding
import org.schabi.newpipe.util.BlockedChannelsManager
/**
* Fragment to display and manage blocked channels
*/
class BlockedChannelsFragment : Fragment() {
private var _binding: FragmentBlockedChannelsBinding? = null
private val binding get() = _binding!!
private lateinit var adapter: BlockedChannelsAdapter
private val blockedChannels = mutableListOf<String>()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentBlockedChannelsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
loadBlockedChannels()
}
private fun setupRecyclerView() {
adapter = BlockedChannelsAdapter(blockedChannels) { channelUrl ->
showUnblockDialog(channelUrl)
}
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
binding.recyclerView.adapter = adapter
}
private fun loadBlockedChannels() {
blockedChannels.clear()
blockedChannels.addAll(BlockedChannelsManager.getBlockedChannelsList(requireContext()))
adapter.notifyDataSetChanged()
updateEmptyView()
}
private fun updateEmptyView() {
if (blockedChannels.isEmpty()) {
binding.emptyView.visibility = View.VISIBLE
binding.recyclerView.visibility = View.GONE
} else {
binding.emptyView.visibility = View.GONE
binding.recyclerView.visibility = View.VISIBLE
}
}
private fun showUnblockDialog(channelUrl: String) {
AlertDialog.Builder(requireContext())
.setTitle(R.string.unblock_channel)
.setMessage(R.string.unblock_channel_confirmation)
.setPositiveButton(R.string.unblock_channel) { _, _ ->
unblockChannel(channelUrl)
}
.setNegativeButton(R.string.cancel, null)
.show()
}
private fun unblockChannel(channelUrl: String) {
BlockedChannelsManager.unblockChannel(requireContext(), channelUrl)
loadBlockedChannels()
Snackbar.make(
binding.root,
R.string.channel_unblocked,
Snackbar.LENGTH_SHORT
).show()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
/**
* RecyclerView adapter for blocked channels
*/
private class BlockedChannelsAdapter(
private val channels: List<String>,
private val onUnblockClick: (String) -> Unit
) : RecyclerView.Adapter<BlockedChannelsAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_blocked_channel, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val channelUrl = channels[position]
holder.bind(channelUrl, onUnblockClick)
}
override fun getItemCount(): Int = channels.size
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val channelUrlText: android.widget.TextView = itemView.findViewById(R.id.channel_url)
private val unblockButton: android.widget.Button = itemView.findViewById(R.id.unblock_button)
fun bind(channelUrl: String, onUnblockClick: (String) -> Unit) {
channelUrlText.text = channelUrl
unblockButton.setOnClickListener {
onUnblockClick(channelUrl)
}
}
}
}
}

View File

@ -0,0 +1,104 @@
package org.schabi.newpipe.util
import android.content.Context
import android.content.SharedPreferences
import androidx.preference.PreferenceManager
/**
* Manager for handling blocked channels.
* Stores blocked channel IDs in SharedPreferences and provides methods to add, remove, and check blocked channels.
*/
object BlockedChannelsManager {
private const val BLOCKED_CHANNELS_KEY = "blocked_channels"
private const val DELIMITER = ","
/**
* Get the SharedPreferences instance
*/
private fun getPreferences(context: Context): SharedPreferences {
return PreferenceManager.getDefaultSharedPreferences(context)
}
/**
* Get the set of blocked channel IDs
*/
fun getBlockedChannelIds(context: Context): Set<String> {
val prefs = getPreferences(context)
val blockedString = prefs.getString(BLOCKED_CHANNELS_KEY, "") ?: ""
return if (blockedString.isEmpty()) {
emptySet()
} else {
blockedString.split(DELIMITER).toSet()
}
}
/**
* Check if a channel is blocked
*
* @param context Application context
* @param channelUrl The channel URL to check
* @return true if the channel is blocked, false otherwise
*/
fun isChannelBlocked(context: Context, channelUrl: String?): Boolean {
if (channelUrl.isNullOrEmpty()) {
return false
}
return getBlockedChannelIds(context).contains(channelUrl)
}
/**
* Block a channel
*
* @param context Application context
* @param channelUrl The channel URL to block
* @param channelName The channel name (for logging/debugging)
*/
fun blockChannel(context: Context, channelUrl: String, channelName: String? = null) {
val blockedChannels = getBlockedChannelIds(context).toMutableSet()
blockedChannels.add(channelUrl)
saveBlockedChannels(context, blockedChannels)
}
/**
* Unblock a channel
*
* @param context Application context
* @param channelUrl The channel URL to unblock
*/
fun unblockChannel(context: Context, channelUrl: String) {
val blockedChannels = getBlockedChannelIds(context).toMutableSet()
blockedChannels.remove(channelUrl)
saveBlockedChannels(context, blockedChannels)
}
/**
* Get all blocked channels as a list of channel URLs
*
* @param context Application context
* @return List of blocked channel URLs
*/
fun getBlockedChannelsList(context: Context): List<String> {
return getBlockedChannelIds(context).toList()
}
/**
* Clear all blocked channels
*
* @param context Application context
*/
fun clearAllBlockedChannels(context: Context) {
getPreferences(context).edit()
.remove(BLOCKED_CHANNELS_KEY)
.apply()
}
/**
* Save the blocked channels set to SharedPreferences
*/
private fun saveBlockedChannels(context: Context, blockedChannels: Set<String>) {
val blockedString = blockedChannels.joinToString(DELIMITER)
getPreferences(context).edit()
.putString(BLOCKED_CHANNELS_KEY, blockedString)
.apply()
}
}

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingTop="8dp"
android:paddingBottom="8dp" />
<TextView
android:id="@+id/emptyView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/no_blocked_channels"
android:textAppearance="?android:attr/textAppearanceMedium"
android:visibility="gone" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="4dp"
app:cardCornerRadius="4dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="12dp">
<TextView
android:id="@+id/channel_url"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="2"
android:textAppearance="?android:attr/textAppearanceMedium" />
<Button
android:id="@+id/unblock_button"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="@string/unblock_channel" />
</LinearLayout>
</androidx.cardview.widget.CardView>

View File

@ -436,6 +436,15 @@
<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="block_channel">Block channel</string>
<string name="unblock_channel">Unblock channel</string>
<string name="unblock_channel_confirmation">Are you sure you want to unblock this channel?</string>
<string name="channel_blocked">Channel blocked: %s</string>
<string name="channel_unblocked">Channel unblocked</string>
<string name="blocked_channels">Blocked channels</string>
<string name="blocked_channels_title">Blocked channels</string>
<string name="blocked_channels_summary">Manage blocked channels</string>
<string name="no_blocked_channels">No blocked channels</string>
<string name="enqueue_stream">Enqueue</string>
<string name="enqueued">Enqueued</string>
<string name="enqueue_next_stream">Enqueue next</string>

View File

@ -82,6 +82,14 @@
app:singleLineTitle="false"
app:iconSpaceReserved="false" />
<PreferenceScreen
android:fragment="org.schabi.newpipe.settings.BlockedChannelsFragment"
android:key="blocked_channels"
android:summary="@string/blocked_channels_summary"
android:title="@string/blocked_channels_title"
app:singleLineTitle="false"
app:iconSpaceReserved="false" />
<MultiSelectListPreference
android:key="@string/show_search_suggestions_key"
android:summary="@string/show_search_suggestions_summary"

View File

@ -1,5 +1,6 @@
android.useAndroidX=true
org.gradle.jvmargs=-Xmx2048M --add-opens jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED
org.gradle.java.home=/usr/local/sdkman/candidates/java/17.0.15-ms
systemProp.file.encoding=utf-8
# https://docs.gradle.org/current/userguide/configuration_cache.html