diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..40b878db5 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ \ No newline at end of file diff --git a/js&css/extension/init.js b/js&css/extension/init.js index a5899608d..1ffb0c0a0 100644 --- a/js&css/extension/init.js +++ b/js&css/extension/init.js @@ -52,7 +52,9 @@ extension.events.on('init', function () { extension.features.openNewTab(); extension.features.removeListParamOnNewTab(); extension.features.removeMemberOnly(); - // extension.features.hideSponsoredVideosOnHome?.(); + // extension.features.hideSponsoredVideosOnHome?.(); + extension.features.channelBlocker?.(); + bodyReady(); }); diff --git a/js&css/extension/www.youtube.com/features/channel-blocker.js b/js&css/extension/www.youtube.com/features/channel-blocker.js new file mode 100644 index 000000000..e6904c301 --- /dev/null +++ b/js&css/extension/www.youtube.com/features/channel-blocker.js @@ -0,0 +1,427 @@ +(function () { + console.log(">>> Channel Blocker: UI PRO (TOGGLE SWITCH) <<<"); + + const STORAGE_KEY = "yt-blocker-header-aggressive"; + let channelsMap = {}; + let isFilterEnabled = false; + let safetyInterval = null; + + // STORAGE + function loadSettings() { + try { + const stored = localStorage.getItem(STORAGE_KEY); + if (stored) channelsMap = JSON.parse(stored); + } catch (e) { + console.error("Settings error", e); + channelsMap = {}; + } + } + + function saveSettings() { + localStorage.setItem(STORAGE_KEY, JSON.stringify(channelsMap)); + } + + // DATA FETCHING + function findAllChannelRenderers(obj, results = []) { + if (!obj || typeof obj !== 'object') return results; + if (obj.channelRenderer) results.push(obj.channelRenderer); + Object.values(obj).forEach(value => { + if (typeof value === 'object') findAllChannelRenderers(value, results); + }); + return results; + } + + async function fetchOfficialSubs() { + const statusBtn = document.getElementById("cb-rescan"); + if(statusBtn) statusBtn.innerText = "⏳ ..."; + + try { + const response = await fetch("https://www.youtube.com/feed/channels"); + const text = await response.text(); + let foundChannels = []; + + const jsonMatch = text.match(/var ytInitialData = ({.*?});/); + if (jsonMatch) { + try { + const data = JSON.parse(jsonMatch[1]); + const renderers = findAllChannelRenderers(data); + renderers.forEach(c => { + const name = c.title?.simpleText || c.title?.runs?.[0]?.text; + let url = c.navigationEndpoint?.commandMetadata?.webCommandMetadata?.url; + if (name && url) foundChannels.push({ name: name, url: url }); + }); + } catch (e) { console.error("JSON error", e); } + } + + if (foundChannels.length === 0) { + const parser = new DOMParser(); + const doc = parser.parseFromString(text, "text/html"); + const items = doc.querySelectorAll("ytd-channel-renderer"); + items.forEach(item => { + const link = item.querySelector("a#main-link"); + const textEl = item.querySelector("#text"); + if (link && textEl) { + foundChannels.push({ name: textEl.innerText.trim(), url: link.getAttribute("href") }); + } + }); + } + + let count = 0; + let newMap = {}; + foundChannels.forEach(item => { + if (item.url.includes("/feed/") || item.url.includes("UC-9-kyTW8ZkZNDHQJ6FgpwQ")) return; + let cleanUrl = item.url; + if (!cleanUrl.startsWith("/")) cleanUrl = "/" + cleanUrl; + + const existing = channelsMap[cleanUrl]; + newMap[cleanUrl] = { + name: item.name, + allowed: existing ? existing.allowed : false + }; + count++; + }); + + if (count > 0) { + channelsMap = newMap; + saveSettings(); + renderList(); + if(statusBtn) statusBtn.innerText = `✅ ${count}`; + setTimeout(() => { if(statusBtn) statusBtn.innerText = "↻ Refresh"; }, 2000); + } else { + if(statusBtn) statusBtn.innerText = "⚠️ 0"; + } + + } catch (e) { + console.error(e); + if(statusBtn) statusBtn.innerText = "❌"; + } + } + + // UI GENERATION (TOGGLE STYLE) + function createUI() { + if (document.getElementById("cb-container")) return; + + const headerEnd = document.querySelector("ytd-masthead #end"); + if (!headerEnd) return; + + const container = document.createElement("div"); + container.id = "cb-container"; + // Aligned nicely in header + Object.assign(container.style, { + display: "flex", alignItems: "center", marginRight: "8px", position: "relative" + }); + + // The Main "Pill" Container (The gray box) + const mainPill = document.createElement("div"); + Object.assign(mainPill.style, { + display: "flex", + alignItems: "center", + height: "36px", + borderRadius: "18px", + overflow: "hidden", + // NATIVE YOUTUBE COLORS + backgroundColor: "var(--yt-spec-badge-chip-background, rgba(0, 0, 0, 0.05))", + border: "1px solid var(--yt-spec-10-percent-layer, transparent)", + cursor: "pointer" + }); + + // The Clickable Toggle Section + const toggleSection = document.createElement("div"); + toggleSection.id = "cb-toggle-section"; + Object.assign(toggleSection.style, { + display: "flex", alignItems: "center", padding: "0 12px", height: "100%", + gap: "10px" + }); + + // Text Label + const labelText = document.createElement("span"); + labelText.innerText = "Block"; + Object.assign(labelText.style, { + fontSize: "14px", + fontWeight: "500", + fontFamily: "Roboto, Arial, sans-serif", + color: "var(--yt-spec-text-primary, #0f0f0f)" + }); + + // The Visual Toggle Switch + const toggleSwitch = document.createElement("div"); + Object.assign(toggleSwitch.style, { + position: "relative", + width: "34px", + height: "18px", + borderRadius: "10px", + backgroundColor: "#909090", // Default Gray (OFF) + transition: "background-color 0.2s ease" + }); + + const toggleKnob = document.createElement("div"); + Object.assign(toggleKnob.style, { + position: "absolute", + top: "2px", + left: "2px", + width: "14px", + height: "14px", + borderRadius: "50%", + backgroundColor: "#fff", + boxShadow: "0 1px 3px rgba(0,0,0,0.4)", + transition: "transform 0.2s ease" + }); + + toggleSwitch.appendChild(toggleKnob); + toggleSection.appendChild(labelText); + toggleSection.appendChild(toggleSwitch); + + // Toggle Logic + toggleSection.onclick = () => { + isFilterEnabled = !isFilterEnabled; + updateToggleVisuals(toggleSwitch, toggleKnob); + if (isFilterEnabled) { + startAggressiveLoop(); + } else { + stopAggressiveLoop(); + showAllVideos(); + } + }; + + // The Arrow Button (Divider) + const divider = document.createElement("div"); + Object.assign(divider.style, { + width: "1px", height: "20px", + backgroundColor: "var(--yt-spec-text-secondary, #ccc)", + opacity: "0.3" + }); + + const arrowBtn = document.createElement("div"); + arrowBtn.innerText = "▼"; + Object.assign(arrowBtn.style, { + display: "flex", alignItems: "center", justifyContent: "center", + padding: "0 8px", height: "100%", cursor: "pointer", + fontSize: "10px", color: "var(--yt-spec-text-secondary, #606060)" + }); + + arrowBtn.onmouseover = () => arrowBtn.style.backgroundColor = "rgba(0,0,0,0.05)"; + arrowBtn.onmouseout = () => arrowBtn.style.backgroundColor = "transparent"; + + arrowBtn.onclick = (e) => { + e.stopPropagation(); + const list = document.getElementById("cb-list-container"); + const isHidden = list.style.display === "none"; + list.style.display = isHidden ? "block" : "none"; + if (isHidden) { + renderList(); + if (Object.keys(channelsMap).length === 0) fetchOfficialSubs(); + } + }; + + // Assemble Pill + mainPill.appendChild(toggleSection); + mainPill.appendChild(divider); + mainPill.appendChild(arrowBtn); + + // The Dropdown List + const listContainer = document.createElement("div"); + listContainer.id = "cb-list-container"; + Object.assign(listContainer.style, { + display: "none", position: "absolute", top: "45px", right: "0px", + width: "300px", maxHeight: "400px", overflowY: "auto", + borderRadius: "12px", padding: "10px", + boxShadow: "0 4px 16px rgba(0,0,0,0.3)", zIndex: "9999", + backgroundColor: "var(--yt-spec-menu-background, #fff)", + border: "1px solid var(--yt-spec-10-percent-layer, #ccc)", + color: "var(--yt-spec-text-primary, #000)" + }); + + listContainer.innerHTML = ` +