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",