Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@
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.
Expand Down Expand Up @@ -132,16 +134,33 @@ public void addInfoItemList(@Nullable final List<? extends InfoItem> data) {
+ 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 + ", "
+ "infoItemList.size() = " + infoItemList.size() + ", "
+ "hasHeader = " + hasHeader() + ", "
+ "showFooter = " + showFooter);
}
notifyItemRangeInserted(offsetStart, data.size());
notifyItemRangeInserted(offsetStart, filteredData.size());

if (showFooter) {
final int footerNow = sizeConsideringHeaderOffset();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ public Builder addDefaultEndEntries() {
);
addPlayWithKodiEntryIfNeeded();
addMarkAsWatchedEntryIfNeeded();
addEntry(StreamDialogDefaultEntry.BLOCK_CHANNEL);
addEntry(StreamDialogDefaultEntry.SHOW_CHANNEL_DETAILS);
return this;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@
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;
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;
Expand Down Expand Up @@ -135,7 +138,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);
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.setActionTextColor(Color.YELLOW)


snackbar.show();
}
});


@StringRes
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
}
}
104 changes: 104 additions & 0 deletions app/src/main/java/org/schabi/newpipe/util/BlockedChannelsManager.kt
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocked channels should be stored in their own DB table with url and name so that the name can be displayed to the user.

Original file line number Diff line number Diff line change
@@ -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()
}
}
24 changes: 24 additions & 0 deletions app/src/main/res/layout/fragment_blocked_channels.xml
Original file line number Diff line number Diff line change
@@ -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>
Loading