Merge 60cb6a11a823c1c5c221fb9a07ee29644a32a46b into 4481dd7fe6dd8c9bd116c391aed544de6239c640
This commit is contained in:
commit
473eada494
@ -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();
|
||||
|
||||
@ -329,6 +329,7 @@ public final class InfoItemDialog {
|
||||
);
|
||||
addPlayWithKodiEntryIfNeeded();
|
||||
addMarkAsWatchedEntryIfNeeded();
|
||||
addEntry(StreamDialogDefaultEntry.BLOCK_CHANNEL);
|
||||
addEntry(StreamDialogDefaultEntry.SHOW_CHANNEL_DETAILS);
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
app/src/main/res/layout/fragment_blocked_channels.xml
Normal file
24
app/src/main/res/layout/fragment_blocked_channels.xml
Normal 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>
|
||||
38
app/src/main/res/layout/item_blocked_channel.xml
Normal file
38
app/src/main/res/layout/item_blocked_channel.xml
Normal 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>
|
||||
@ -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>
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user