diff --git a/src/App.vue b/src/App.vue
index 10c3e02b7b..4b30c10c57 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -90,7 +90,7 @@ onMounted(() => {
darkModePreference.addEventListener("change", handlePreferredColorSchemeChange);
if ("indexedDB" in window) {
- const request = indexedDB.open("piped-db", 6);
+ const request = indexedDB.open("piped-db", 7);
request.onupgradeneeded = ev => {
const db = request.result;
console.log("Upgrading object store.");
@@ -118,6 +118,10 @@ onMounted(() => {
const playlistVideosStore = db.createObjectStore("playlist_videos", { keyPath: "videoId" });
playlistVideosStore.createIndex("videoId", "videoId", { unique: true });
}
+ if (!db.objectStoreNames.contains("blocked_channels")) {
+ const store = db.createObjectStore("blocked_channels", { keyPath: "channelId" });
+ store.createIndex("channelId", "channelId", { unique: true });
+ }
// migration to fix an invalid previous length of channel ids: 11 -> 24
(async () => {
if (ev.oldVersion < 6) {
diff --git a/src/components/ChannelPage.vue b/src/components/ChannelPage.vue
index 646a2deedf..8dd6dbf96e 100644
--- a/src/components/ChannelPage.vue
+++ b/src/components/ChannelPage.vue
@@ -18,6 +18,12 @@
+
+
+
+
+
+
+ |
+ |
+
+
+
+
+ |
+
+ |
+
+
+ |
+
+
+
@@ -472,6 +496,7 @@ import {
usePreferenceString,
} from "@/composables/usePreferences";
import { fetchJson, apiUrl, authApiUrl, getAuthToken, hashCode, isAuthenticated } from "@/composables/useApi";
+import { getBlockedChannels, removeBlockedChannel } from "@/composables/useChannels.js";
import { getCustomInstances } from "@/composables/useCustomInstances";
import { download } from "@/composables/useMisc";
import { getDefaultLanguage } from "@/composables/useFormatting";
@@ -502,6 +527,7 @@ const authInstance = usePreferenceBoolean("authInstance", false);
const selectedAuthInstance = usePreferenceString("auth_instance_url", selectedInstance.value);
const customInstances = ref([]);
const publicInstances = ref([]);
+const blockedChannels = ref([]);
const sponsorBlock = usePreferenceBoolean("sponsorblock", true);
const skipOptionsStorage = usePreferenceJSON("skipOptions", null);
const skipOptions = ref(createDefaultSkipOptions());
@@ -667,6 +693,7 @@ onMounted(async () => {
if (Object.keys(route.query).length > 0) router.replace({ query: {} });
fetchInstances();
+ loadBlockedChannels();
if (testLocalStorage()) {
skipOptions.value = normalizeSkipOptions(skipOptionsStorage.value);
@@ -715,6 +742,15 @@ async function fetchInstances() {
});
}
+async function loadBlockedChannels() {
+ blockedChannels.value = await getBlockedChannels();
+}
+
+async function unblockChannel(channelId) {
+ removeBlockedChannel(channelId);
+ blockedChannels.value = blockedChannels.value.filter(channel => channel.channelId !== channelId);
+}
+
function sslScore(url) {
return "https://www.ssllabs.com/ssltest/analyze.html?d=" + new URL(url).host + "&latest";
}
diff --git a/src/components/VideoItem.vue b/src/components/VideoItem.vue
index 8c2c619184..01b9210130 100644
--- a/src/components/VideoItem.vue
+++ b/src/components/VideoItem.vue
@@ -134,6 +134,7 @@ import PlaylistAddModal from "./PlaylistAddModal.vue";
import ShareModal from "./ShareModal.vue";
import ConfirmModal from "./ConfirmModal.vue";
import VideoThumbnail from "./VideoThumbnail.vue";
+import { isChannelBlocked } from "@/composables/useChannels.js";
import { numberFormat, timeAgo } from "@/composables/useFormatting.js";
import { getPreferenceBoolean } from "@/composables/usePreferences.js";
import { removeVideoFromPlaylist } from "@/composables/usePlaylists.js";
@@ -180,6 +181,14 @@ function removeVideo() {
}
function shouldShowVideo() {
+ if (window.location.pathname !== props.item.uploaderUrl) {
+ isChannelBlocked(props.item.uploaderUrl).then(blocked => {
+ if (blocked) {
+ showVideo.value = false;
+ }
+ });
+ }
+
if (!props.isFeed || !getPreferenceBoolean("hideWatched", false)) return;
const objectStore = window.db.transaction("watch_history", "readonly").objectStore("watch_history");
diff --git a/src/composables/useChannels.js b/src/composables/useChannels.js
new file mode 100644
index 0000000000..790afa81bd
--- /dev/null
+++ b/src/composables/useChannels.js
@@ -0,0 +1,61 @@
+function normalizeId(channelId) {
+ return channelId.replace("/channel/", "");
+}
+
+export function isChannelBlocked(channelUrl) {
+ return new Promise(resolve => {
+ var tx = window.db.transaction("blocked_channels", "readonly");
+ var store = tx.objectStore("blocked_channels");
+ store.count(normalizeId(channelUrl)).onsuccess = e => {
+ const result = e.target.result;
+ resolve(result > 0);
+ };
+ });
+}
+
+export function addBlockedChannel(channelId, name) {
+ var tx = window.db.transaction("blocked_channels", "readwrite");
+ var store = tx.objectStore("blocked_channels");
+ store.add({ channelId, name });
+}
+
+export function removeBlockedChannel(channelId) {
+ var tx = window.db.transaction("blocked_channels", "readwrite");
+ var store = tx.objectStore("blocked_channels");
+ store.delete(channelId);
+}
+
+export function getBlockedChannels() {
+ return new Promise(resolve => {
+ let blockedChannels = [];
+ var tx = window.db.transaction("blocked_channels", "readonly");
+ var store = tx.objectStore("blocked_channels");
+ const cursor = store.index("channelId").openCursor();
+ cursor.onsuccess = e => {
+ const cursor = e.target.result;
+ if (cursor) {
+ blockedChannels.push(cursor.value);
+ cursor.continue();
+ } else {
+ resolve(blockedChannels);
+ }
+ };
+ });
+}
+
+export async function toggleChannelBlock(channelUrl, name) {
+ const channelId = normalizeId(channelUrl);
+ return new Promise(resolve => {
+ var tx = window.db.transaction("blocked_channels", "readwrite");
+ var store = tx.objectStore("blocked_channels");
+ store.count(channelId).onsuccess = e => {
+ const result = e.target.result;
+ if (result > 0) {
+ store.delete(channelId);
+ } else {
+ store.add({ channelId, name });
+ }
+ resolve(result === 0);
+ };
+ });
+}
diff --git a/src/locales/en.json b/src/locales/en.json
index 4db4740fd2..10e01d5430 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -17,7 +17,8 @@
"bookmarks": "Bookmarks",
"channel_groups": "Channel groups",
"dearrow": "DeArrow",
- "custom_instances": "Custom instances"
+ "custom_instances": "Custom instances",
+ "blocked_channels": "Blocked Channels"
},
"player": {
"watch_on": "View on {0}",
@@ -177,7 +178,10 @@
"show_password": "Show password",
"hide_password": "Hide password",
"rss_feed": "RSS feed",
- "playlist_rss_feed": "Playlist RSS feed"
+ "playlist_rss_feed": "Playlist RSS feed",
+ "blocked_channels": "Blocked channels",
+ "block": "Block",
+ "unblock": "Unblock"
},
"comment": {
"pinned_by": "Pinned by {author}",
@@ -194,7 +198,8 @@
"up_to_date": "Up to date?",
"ssl_score": "SSL Score",
"uptime_30d": "Uptime (30d)",
- "api_url": "API URL"
+ "api_url": "API URL",
+ "channel": "Channel"
},
"login": {
"username": "Username",