summaryrefslogtreecommitdiff
path: root/home-manager/features/gui/apps/qutebrowser
diff options
context:
space:
mode:
authortriethyl <triethylammonium@pm.me>2025-09-02 10:48:21 -0400
committertriethyl <triethylammonium@pm.me>2025-09-02 10:48:21 -0400
commit31c316d19cd974bb81a5d6de62142ff24db1c78e (patch)
treecb941422c76cb8953830a8d58c8e14dca1a10319 /home-manager/features/gui/apps/qutebrowser
parent1c21018347aa277caba74e554cb8d1b1e7fc6bed (diff)
reorganized directory structure
Diffstat (limited to 'home-manager/features/gui/apps/qutebrowser')
-rw-r--r--home-manager/features/gui/apps/qutebrowser/default.nix81
-rw-r--r--home-manager/features/gui/apps/qutebrowser/scripts/yt-ad-skip.js29
-rw-r--r--home-manager/features/gui/apps/qutebrowser/scripts/yt-dislike-viewer.js704
-rw-r--r--home-manager/features/gui/apps/qutebrowser/scripts/yt-shorts-blocker.js23
-rw-r--r--home-manager/features/gui/apps/qutebrowser/scripts/yt-sponsor-skip.js51
5 files changed, 888 insertions, 0 deletions
diff --git a/home-manager/features/gui/apps/qutebrowser/default.nix b/home-manager/features/gui/apps/qutebrowser/default.nix
new file mode 100644
index 0000000..e12c9f3
--- /dev/null
+++ b/home-manager/features/gui/apps/qutebrowser/default.nix
@@ -0,0 +1,81 @@
+{ config, pkgs, lib, ... }: let
+ cfg = config.features.gui.apps.qutebrowser;
+in {
+ options.features.gui.apps.qutebrowser.enable = lib.mkEnableOption "qutebrowser";
+ config = lib.mkIf cfg.enable {
+ programs.qutebrowser = {
+ enable = true;
+ settings = {
+ content = {
+ autoplay = false;
+ tls.certificate_errors = "ask-block-thirdparty";
+ };
+ scrolling.bar = "never";
+ window.transparent = true;
+ keyhint.delay = 0;
+ tabs = {
+ position = "left";
+ width = "10%";
+ };
+ url = {
+ default_page = "https://web.tabliss.io";
+ start_pages = "https://web.tabliss.io";
+ };
+ input.insert_mode.auto_load = true;
+ };
+ keyBindings = {
+ normal = {
+ # Tab Movement Keys
+ "<Ctrl-j>" = "tab-move +";
+ "<Ctrl-k>" = "tab-move -";
+
+ # Universal Scrolling Keys
+ "<Alt-j>" = "scroll-px 0 50";
+ "<Alt-k>" = "scroll-px 0 -50";
+
+ # Open Tab Relatively
+ "<Ctrl-o>" = "cmd-set-text -s :open -tr";
+
+ # Move Tab to Another Window
+ "gc" = "cmd-set-text -s :tab-give";
+
+ # Hide UI
+ "z" = lib.mkMerge [
+ "config-cycle tabs.show never always"
+ "config-cycle statusbar.show in-mode always"
+ "config-cycle scrolling.bar never always"
+ ];
+ };
+ };
+ searchEngines = {
+ # Default Search Engine
+ DEFAULT = "https://search.brave.com/search?q={}";
+
+ # General Search Engines
+ g = "https://www.google.com/search?q={}";
+ b = "https://www.bing.com/search?q={}";
+
+ # Other Search Engines
+ w = "https://en.wikipedia.org/wiki/Special:Search?search={}&amp;go=Go&amp;ns0=1";
+ y = "https://youtube.com/results?search_query={}";
+ t = "https://www.wordreference.com/es/translation.asp?tranword={}";
+ p = "https://thangs.com/search/{}?scope=all";
+
+ # Nix Search Engines
+ n = "https://mynixos.com/search?q={}";
+ nw = "https://wiki.nixos.org/index.php?search={}";
+ np = "https://search.nixos.org/packages?channel=unstable&from=0&size=50&sort=relevance&type=packages&query={}";
+ no = "https://search.nixos.org/options?channel=unstable&size=50&sort=relevance&type=packages&query={}";
+ nh = "https://home-manager-options.extranix.com/?query={}&release=master";
+ };
+ greasemonkey = let
+ mkGmScript = name: ( pkgs.writeText "${name}.js" (builtins.readFile ./scripts/${name}.js) );
+ in [
+ (mkGmScript "yt-ad-skip")
+ (mkGmScript "yt-sponsor-skip")
+ (mkGmScript "yt-shorts-blocker")
+ (mkGmScript "yt-dislike-viewer")
+ ];
+ };
+ };
+}
diff --git a/home-manager/features/gui/apps/qutebrowser/scripts/yt-ad-skip.js b/home-manager/features/gui/apps/qutebrowser/scripts/yt-ad-skip.js
new file mode 100644
index 0000000..56336df
--- /dev/null
+++ b/home-manager/features/gui/apps/qutebrowser/scripts/yt-ad-skip.js
@@ -0,0 +1,29 @@
+// ==UserScript==
+// @name Youtube Ad Skip
+// @version 0.0.7
+// @description Make Youtube more tolerable by automatically skipping ads
+// @author Adcott
+// @match *://*.youtube.com/*
+// ==/UserScript==
+
+GM_addStyle(`
+#player-ads,
+.adDisplay,
+.ad-container,
+.ytd-display-ad-renderer,
+.video-ads,
+ytd-rich-item-renderer:has(ytd-ad-slot-renderer),
+ytd-ad-slot-renderer,
+#masthead-ad,
+*[class^="ytd-ad-"],
+#panels.ytd-watch-flexy {
+ display: none !important;
+}`);
+
+document.addEventListener('load', () => {
+ let ad = document.querySelector('.ad-showing:has(.ytp-ad-persistent-progress-bar-container) video');
+ let skipButton = document.querySelector('.ytp-ad-skip-button');
+
+ if (ad) ad.currentTime = 99999;
+ if (skipButton) skipButton.click();
+}, true);
diff --git a/home-manager/features/gui/apps/qutebrowser/scripts/yt-dislike-viewer.js b/home-manager/features/gui/apps/qutebrowser/scripts/yt-dislike-viewer.js
new file mode 100644
index 0000000..50f3eff
--- /dev/null
+++ b/home-manager/features/gui/apps/qutebrowser/scripts/yt-dislike-viewer.js
@@ -0,0 +1,704 @@
+// ==UserScript==
+// @name Return YouTube Dislike
+// @namespace https://www.returnyoutubedislike.com/
+// @homepage https://www.returnyoutubedislike.com/
+// @version 3.1.5
+// @encoding utf-8
+// @description Return of the YouTube Dislike, Based off https://www.returnyoutubedislike.com/
+// @icon https://github.com/Anarios/return-youtube-dislike/raw/main/Icons/Return%20Youtube%20Dislike%20-%20Transparent.png
+// @author Anarios & JRWR
+// @match *://*.youtube.com/*
+// @exclude *://music.youtube.com/*
+// @exclude *://*.music.youtube.com/*
+// @compatible chrome
+// @compatible firefox
+// @compatible opera
+// @compatible safari
+// @compatible edge
+// @grant GM.xmlHttpRequest
+// @connect youtube.com
+// @grant GM_addStyle
+// @run-at document-end
+// @downloadURL https://update.greasyfork.org/scripts/436115/Return%20YouTube%20Dislike.user.js
+// @updateURL https://update.greasyfork.org/scripts/436115/Return%20YouTube%20Dislike.meta.js
+// ==/UserScript==
+
+const extConfig = {
+ // BEGIN USER OPTIONS
+ // You may change the following variables to allowed values listed in the corresponding brackets (* means default). Keep the style and keywords intact.
+ showUpdatePopup: false, // [true, false*] Show a popup tab after extension update (See what's new)
+ disableVoteSubmission: false, // [true, false*] Disable like/dislike submission (Stops counting your likes and dislikes)
+ disableLogging: true, // [true*, false] Disable Logging API Response in JavaScript Console.
+ coloredThumbs: false, // [true, false*] Colorize thumbs (Use custom colors for thumb icons)
+ coloredBar: false, // [true, false*] Colorize ratio bar (Use custom colors for ratio bar)
+ colorTheme: "classic", // [classic*, accessible, neon] Color theme (red/green, blue/yellow, pink/cyan)
+ numberDisplayFormat: "compactShort", // [compactShort*, compactLong, standard] Number format (For non-English locale users, you may be able to improve appearance with a different option. Please file a feature request if your locale is not covered)
+ numberDisplayRoundDown: true, // [true*, false] Round down numbers (Show rounded down numbers)
+ tooltipPercentageMode: "none", // [none*, dash_like, dash_dislike, both, only_like, only_dislike] Mode of showing percentage in like/dislike bar tooltip.
+ numberDisplayReformatLikes: false, // [true, false*] Re-format like numbers (Make likes and dislikes format consistent)
+ rateBarEnabled: false, // [true, false*] Enables ratio bar under like/dislike buttons
+ // END USER OPTIONS
+};
+
+const LIKED_STATE = "LIKED_STATE";
+const DISLIKED_STATE = "DISLIKED_STATE";
+const NEUTRAL_STATE = "NEUTRAL_STATE";
+let previousState = 3; //1=LIKED, 2=DISLIKED, 3=NEUTRAL
+let likesvalue = 0;
+let dislikesvalue = 0;
+let preNavigateLikeButton = null;
+
+let isMobile = location.hostname == "m.youtube.com";
+let isShorts = () => location.pathname.startsWith("/shorts");
+let mobileDislikes = 0;
+function cLog(text, subtext = "") {
+ if (!extConfig.disableLogging) {
+ subtext = subtext.trim() === "" ? "" : `(${subtext})`;
+ console.log(`[Return YouTube Dislikes] ${text} ${subtext}`);
+ }
+}
+
+function isInViewport(element) {
+ const rect = element.getBoundingClientRect();
+ const height = innerHeight || document.documentElement.clientHeight;
+ const width = innerWidth || document.documentElement.clientWidth;
+ return (
+ // When short (channel) is ignored, the element (like/dislike AND short itself) is
+ // hidden with a 0 DOMRect. In this case, consider it outside of Viewport
+ !(rect.top == 0 && rect.left == 0 && rect.bottom == 0 && rect.right == 0) &&
+ rect.top >= 0 &&
+ rect.left >= 0 &&
+ rect.bottom <= height &&
+ rect.right <= width
+ );
+}
+
+function getButtons() {
+ if (isShorts()) {
+ let elements = document.querySelectorAll(
+ isMobile ? "ytm-like-button-renderer" : "#like-button > ytd-like-button-renderer",
+ );
+ for (let element of elements) {
+ if (isInViewport(element)) {
+ return element;
+ }
+ }
+ }
+ if (isMobile) {
+ return (
+ document.querySelector(".slim-video-action-bar-actions .segmented-buttons") ??
+ document.querySelector(".slim-video-action-bar-actions")
+ );
+ }
+ if (document.getElementById("menu-container")?.offsetParent === null) {
+ return (
+ document.querySelector("ytd-menu-renderer.ytd-watch-metadata > div") ??
+ document.querySelector("ytd-menu-renderer.ytd-video-primary-info-renderer > div")
+ );
+ } else {
+ return document.getElementById("menu-container")?.querySelector("#top-level-buttons-computed");
+ }
+}
+
+function getDislikeButton() {
+ if (getButtons().children[0].tagName === "YTD-SEGMENTED-LIKE-DISLIKE-BUTTON-RENDERER") {
+ if (getButtons().children[0].children[1] === undefined) {
+ return document.querySelector("#segmented-dislike-button");
+ } else {
+ return getButtons().children[0].children[1];
+ }
+ } else {
+ if (getButtons().querySelector("segmented-like-dislike-button-view-model")) {
+ const dislikeViewModel = getButtons().querySelector("dislike-button-view-model");
+ if (!dislikeViewModel) cLog("Dislike button wasn't added to DOM yet...");
+ return dislikeViewModel;
+ } else {
+ return getButtons().children[1];
+ }
+ }
+}
+
+function getLikeButton() {
+ return getButtons().children[0].tagName === "YTD-SEGMENTED-LIKE-DISLIKE-BUTTON-RENDERER"
+ ? document.querySelector("#segmented-like-button") !== null
+ ? document.querySelector("#segmented-like-button")
+ : getButtons().children[0].children[0]
+ : getButtons().querySelector("like-button-view-model") ?? getButtons().children[0];
+}
+
+function getLikeTextContainer() {
+ return (
+ getLikeButton().querySelector("#text") ??
+ getLikeButton().getElementsByTagName("yt-formatted-string")[0] ??
+ getLikeButton().querySelector("span[role='text']")
+ );
+}
+
+function getDislikeTextContainer() {
+ const dislikeButton = getDislikeButton();
+ let result =
+ dislikeButton?.querySelector("#text") ??
+ dislikeButton?.getElementsByTagName("yt-formatted-string")[0] ??
+ dislikeButton?.querySelector("span[role='text']");
+ if (result === null) {
+ let textSpan = document.createElement("span");
+ textSpan.id = "text";
+ textSpan.style.marginLeft = "6px";
+ dislikeButton?.querySelector("button").appendChild(textSpan);
+ if (dislikeButton) dislikeButton.querySelector("button").style.width = "auto";
+ result = textSpan;
+ }
+ return result;
+}
+
+function createObserver(options, callback) {
+ const observerWrapper = new Object();
+ observerWrapper.options = options;
+ observerWrapper.observer = new MutationObserver(callback);
+ observerWrapper.observe = function (element) {
+ this.observer.observe(element, this.options);
+ };
+ observerWrapper.disconnect = function () {
+ this.observer.disconnect();
+ };
+ return observerWrapper;
+}
+
+let shortsObserver = null;
+
+if (isShorts() && !shortsObserver) {
+ cLog("Initializing shorts mutation observer");
+ shortsObserver = createObserver(
+ {
+ attributes: true,
+ },
+ (mutationList) => {
+ mutationList.forEach((mutation) => {
+ if (
+ mutation.type === "attributes" &&
+ mutation.target.nodeName === "TP-YT-PAPER-BUTTON" &&
+ mutation.target.id === "button"
+ ) {
+ cLog("Short thumb button status changed");
+ if (mutation.target.getAttribute("aria-pressed") === "true") {
+ mutation.target.style.color =
+ mutation.target.parentElement.parentElement.id === "like-button"
+ ? getColorFromTheme(true)
+ : getColorFromTheme(false);
+ } else {
+ mutation.target.style.color = "unset";
+ }
+ return;
+ }
+ cLog("Unexpected mutation observer event: " + mutation.target + mutation.type);
+ });
+ },
+ );
+}
+
+function isVideoLiked() {
+ if (isMobile) {
+ return getLikeButton().querySelector("button").getAttribute("aria-label") == "true";
+ }
+ return getLikeButton().classList.contains("style-default-active");
+}
+
+function isVideoDisliked() {
+ if (isMobile) {
+ return getDislikeButton()?.querySelector("button").getAttribute("aria-label") == "true";
+ }
+ return getDislikeButton()?.classList.contains("style-default-active");
+}
+
+function isVideoNotLiked() {
+ if (isMobile) {
+ return !isVideoLiked();
+ }
+ return getLikeButton().classList.contains("style-text");
+}
+
+function isVideoNotDisliked() {
+ if (isMobile) {
+ return !isVideoDisliked();
+ }
+ return getDislikeButton()?.classList.contains("style-text");
+}
+
+function checkForUserAvatarButton() {
+ if (isMobile) {
+ return;
+ }
+ if (document.querySelector("#avatar-btn")) {
+ return true;
+ } else {
+ return false;
+ }
+}
+
+function getState() {
+ if (isVideoLiked()) {
+ return LIKED_STATE;
+ }
+ if (isVideoDisliked()) {
+ return DISLIKED_STATE;
+ }
+ return NEUTRAL_STATE;
+}
+
+function setLikes(likesCount) {
+ if (isMobile) {
+ getButtons().children[0].querySelector(".button-renderer-text").innerText = likesCount;
+ return;
+ }
+ getLikeTextContainer().innerText = likesCount;
+}
+
+function setDislikes(dislikesCount) {
+ if (isMobile) {
+ mobileDislikes = dislikesCount;
+ return;
+ }
+
+ const _container = getDislikeTextContainer();
+ _container?.removeAttribute("is-empty");
+ if (_container?.innerText !== dislikesCount) {
+ _container.innerText = dislikesCount;
+ }
+}
+
+function getLikeCountFromButton() {
+ try {
+ if (isShorts()) {
+ //Youtube Shorts don't work with this query. It's not necessary; we can skip it and still see the results.
+ //It should be possible to fix this function, but it's not critical to showing the dislike count.
+ return false;
+ }
+ let likeButton =
+ getLikeButton().querySelector("yt-formatted-string#text") ?? getLikeButton().querySelector("button");
+
+ let likesStr = likeButton.getAttribute("aria-label").replace(/\D/g, "");
+ return likesStr.length > 0 ? parseInt(likesStr) : false;
+ } catch {
+ return false;
+ }
+}
+
+(typeof GM_addStyle != "undefined"
+ ? GM_addStyle
+ : (styles) => {
+ let styleNode = document.createElement("style");
+ styleNode.type = "text/css";
+ styleNode.innerText = styles;
+ document.head.appendChild(styleNode);
+ })(`
+ #return-youtube-dislike-bar-container {
+ background: var(--yt-spec-icon-disabled);
+ border-radius: 2px;
+ }
+
+ #return-youtube-dislike-bar {
+ background: var(--yt-spec-text-primary);
+ border-radius: 2px;
+ transition: all 0.15s ease-in-out;
+ }
+
+ .ryd-tooltip {
+ position: absolute;
+ display: block;
+ height: 2px;
+ bottom: -10px;
+ }
+
+ .ryd-tooltip-bar-container {
+ width: 100%;
+ height: 2px;
+ position: absolute;
+ padding-top: 6px;
+ padding-bottom: 12px;
+ top: -6px;
+ }
+
+ ytd-menu-renderer.ytd-watch-metadata {
+ overflow-y: visible !important;
+ }
+
+ #top-level-buttons-computed {
+ position: relative !important;
+ }
+ `);
+
+function createRateBar(likes, dislikes) {
+ if (isMobile || !extConfig.rateBarEnabled) {
+ return;
+ }
+ let rateBar = document.getElementById("return-youtube-dislike-bar-container");
+
+ const widthPx = getLikeButton().clientWidth + (getDislikeButton()?.clientWidth ?? 52);
+
+ const widthPercent = likes + dislikes > 0 ? (likes / (likes + dislikes)) * 100 : 50;
+
+ var likePercentage = parseFloat(widthPercent.toFixed(1));
+ const dislikePercentage = (100 - likePercentage).toLocaleString();
+ likePercentage = likePercentage.toLocaleString();
+
+ var tooltipInnerHTML;
+ switch (extConfig.tooltipPercentageMode) {
+ case "dash_like":
+ tooltipInnerHTML = `${likes.toLocaleString()}&nbsp;/&nbsp;${dislikes.toLocaleString()}&nbsp;&nbsp;-&nbsp;&nbsp;${likePercentage}%`;
+ break;
+ case "dash_dislike":
+ tooltipInnerHTML = `${likes.toLocaleString()}&nbsp;/&nbsp;${dislikes.toLocaleString()}&nbsp;&nbsp;-&nbsp;&nbsp;${dislikePercentage}%`;
+ break;
+ case "both":
+ tooltipInnerHTML = `${likePercentage}%&nbsp;/&nbsp;${dislikePercentage}%`;
+ break;
+ case "only_like":
+ tooltipInnerHTML = `${likePercentage}%`;
+ break;
+ case "only_dislike":
+ tooltipInnerHTML = `${dislikePercentage}%`;
+ break;
+ default:
+ tooltipInnerHTML = `${likes.toLocaleString()}&nbsp;/&nbsp;${dislikes.toLocaleString()}`;
+ }
+
+ if (!rateBar && !isMobile) {
+ let colorLikeStyle = "";
+ let colorDislikeStyle = "";
+ if (extConfig.coloredBar) {
+ colorLikeStyle = "; background-color: " + getColorFromTheme(true);
+ colorDislikeStyle = "; background-color: " + getColorFromTheme(false);
+ }
+
+ getButtons().insertAdjacentHTML(
+ "beforeend",
+ `
+ <div class="ryd-tooltip" style="width: ${widthPx}px">
+ <div class="ryd-tooltip-bar-container">
+ <div
+ id="return-youtube-dislike-bar-container"
+ style="width: 100%; height: 2px;${colorDislikeStyle}"
+ >
+ <div
+ id="return-youtube-dislike-bar"
+ style="width: ${widthPercent}%; height: 100%${colorDislikeStyle}"
+ ></div>
+ </div>
+ </div>
+ <tp-yt-paper-tooltip position="top" id="ryd-dislike-tooltip" class="style-scope ytd-sentiment-bar-renderer" role="tooltip" tabindex="-1">
+ <!--css-build:shady-->${tooltipInnerHTML}
+ </tp-yt-paper-tooltip>
+ </div>
+`,
+ );
+ let descriptionAndActionsElement = document.getElementById("top-row");
+ descriptionAndActionsElement.style.borderBottom = "1px solid var(--yt-spec-10-percent-layer)";
+ descriptionAndActionsElement.style.paddingBottom = "10px";
+ } else {
+ document.querySelector(".ryd-tooltip").style.width = widthPx + "px";
+ document.getElementById("return-youtube-dislike-bar").style.width = widthPercent + "%";
+
+ if (extConfig.coloredBar) {
+ document.getElementById("return-youtube-dislike-bar-container").style.backgroundColor = getColorFromTheme(false);
+ document.getElementById("return-youtube-dislike-bar").style.backgroundColor = getColorFromTheme(true);
+ }
+ }
+}
+
+function setState() {
+ cLog("Fetching votes...");
+ let statsSet = false;
+
+ fetch(`https://returnyoutubedislikeapi.com/votes?videoId=${getVideoId()}`).then((response) => {
+ response.json().then((json) => {
+ if (json && !("traceId" in response) && !statsSet) {
+ const { dislikes, likes } = json;
+ cLog(`Received count: ${dislikes}`);
+ likesvalue = likes;
+ dislikesvalue = dislikes;
+ setDislikes(numberFormat(dislikes));
+ if (extConfig.numberDisplayReformatLikes === true) {
+ const nativeLikes = getLikeCountFromButton();
+ if (nativeLikes !== false) {
+ setLikes(numberFormat(nativeLikes));
+ }
+ }
+ createRateBar(likes, dislikes);
+ if (extConfig.coloredThumbs === true) {
+ const dislikeButton = getDislikeButton();
+ if (isShorts()) {
+ // for shorts, leave deactived buttons in default color
+ const shortLikeButton = getLikeButton().querySelector("tp-yt-paper-button#button");
+ const shortDislikeButton = dislikeButton?.querySelector("tp-yt-paper-button#button");
+ if (shortLikeButton.getAttribute("aria-pressed") === "true") {
+ shortLikeButton.style.color = getColorFromTheme(true);
+ }
+ if (shortDislikeButton && shortDislikeButton.getAttribute("aria-pressed") === "true") {
+ shortDislikeButton.style.color = getColorFromTheme(false);
+ }
+ shortsObserver.observe(shortLikeButton);
+ shortsObserver.observe(shortDislikeButton);
+ } else {
+ getLikeButton().style.color = getColorFromTheme(true);
+ if (dislikeButton) dislikeButton.style.color = getColorFromTheme(false);
+ }
+ }
+ }
+ });
+ });
+}
+
+function updateDOMDislikes() {
+ setDislikes(numberFormat(dislikesvalue));
+ createRateBar(likesvalue, dislikesvalue);
+}
+
+function likeClicked() {
+ if (checkForUserAvatarButton() == true) {
+ if (previousState == 1) {
+ likesvalue--;
+ updateDOMDislikes();
+ previousState = 3;
+ } else if (previousState == 2) {
+ likesvalue++;
+ dislikesvalue--;
+ updateDOMDislikes();
+ previousState = 1;
+ } else if (previousState == 3) {
+ likesvalue++;
+ updateDOMDislikes();
+ previousState = 1;
+ }
+ if (extConfig.numberDisplayReformatLikes === true) {
+ const nativeLikes = getLikeCountFromButton();
+ if (nativeLikes !== false) {
+ setLikes(numberFormat(nativeLikes));
+ }
+ }
+ }
+}
+
+function dislikeClicked() {
+ if (checkForUserAvatarButton() == true) {
+ if (previousState == 3) {
+ dislikesvalue++;
+ updateDOMDislikes();
+ previousState = 2;
+ } else if (previousState == 2) {
+ dislikesvalue--;
+ updateDOMDislikes();
+ previousState = 3;
+ } else if (previousState == 1) {
+ likesvalue--;
+ dislikesvalue++;
+ updateDOMDislikes();
+ previousState = 2;
+ if (extConfig.numberDisplayReformatLikes === true) {
+ const nativeLikes = getLikeCountFromButton();
+ if (nativeLikes !== false) {
+ setLikes(numberFormat(nativeLikes));
+ }
+ }
+ }
+ }
+}
+
+function setInitialState() {
+ setState();
+}
+
+function getVideoId() {
+ const urlObject = new URL(window.location.href);
+ const pathname = urlObject.pathname;
+ if (pathname.startsWith("/clip")) {
+ return (document.querySelector("meta[itemprop='videoId']") || document.querySelector("meta[itemprop='identifier']")).content;
+ } else {
+ if (pathname.startsWith("/shorts")) {
+ return pathname.slice(8);
+ }
+ return urlObject.searchParams.get("v");
+ }
+}
+
+function isVideoLoaded() {
+ if (isMobile) {
+ return document.getElementById("player").getAttribute("loading") == "false";
+ }
+ const videoId = getVideoId();
+
+ return (
+ // desktop: spring 2024 UI
+ document.querySelector(`ytd-watch-grid[video-id='${videoId}']`) !== null ||
+ // desktop: older UI
+ document.querySelector(`ytd-watch-flexy[video-id='${videoId}']`) !== null ||
+ // mobile: no video-id attribute
+ document.querySelector('#player[loading="false"]:not([hidden])') !== null
+ );
+}
+
+function roundDown(num) {
+ if (num < 1000) return num;
+ const int = Math.floor(Math.log10(num) - 2);
+ const decimal = int + (int % 3 ? 1 : 0);
+ const value = Math.floor(num / 10 ** decimal);
+ return value * 10 ** decimal;
+}
+
+function numberFormat(numberState) {
+ let numberDisplay;
+ if (extConfig.numberDisplayRoundDown === false) {
+ numberDisplay = numberState;
+ } else {
+ numberDisplay = roundDown(numberState);
+ }
+ return getNumberFormatter(extConfig.numberDisplayFormat).format(numberDisplay);
+}
+
+function getNumberFormatter(optionSelect) {
+ let userLocales;
+ if (document.documentElement.lang) {
+ userLocales = document.documentElement.lang;
+ } else if (navigator.language) {
+ userLocales = navigator.language;
+ } else {
+ try {
+ userLocales = new URL(
+ Array.from(document.querySelectorAll("head > link[rel='search']"))
+ ?.find((n) => n?.getAttribute("href")?.includes("?locale="))
+ ?.getAttribute("href"),
+ )?.searchParams?.get("locale");
+ } catch {
+ cLog("Cannot find browser locale. Use en as default for number formatting.");
+ userLocales = "en";
+ }
+ }
+
+ let formatterNotation;
+ let formatterCompactDisplay;
+ switch (optionSelect) {
+ case "compactLong":
+ formatterNotation = "compact";
+ formatterCompactDisplay = "long";
+ break;
+ case "standard":
+ formatterNotation = "standard";
+ formatterCompactDisplay = "short";
+ break;
+ case "compactShort":
+ default:
+ formatterNotation = "compact";
+ formatterCompactDisplay = "short";
+ }
+
+ const formatter = Intl.NumberFormat(userLocales, {
+ notation: formatterNotation,
+ compactDisplay: formatterCompactDisplay,
+ });
+ return formatter;
+}
+
+function getColorFromTheme(voteIsLike) {
+ let colorString;
+ switch (extConfig.colorTheme) {
+ case "accessible":
+ if (voteIsLike === true) {
+ colorString = "dodgerblue";
+ } else {
+ colorString = "gold";
+ }
+ break;
+ case "neon":
+ if (voteIsLike === true) {
+ colorString = "aqua";
+ } else {
+ colorString = "magenta";
+ }
+ break;
+ case "classic":
+ default:
+ if (voteIsLike === true) {
+ colorString = "lime";
+ } else {
+ colorString = "red";
+ }
+ }
+ return colorString;
+}
+
+let smartimationObserver = null;
+
+function setEventListeners(evt) {
+ let jsInitChecktimer;
+
+ function checkForJS_Finish() {
+ //console.log();
+ if (isShorts() || (getButtons()?.offsetParent && isVideoLoaded())) {
+ const buttons = getButtons();
+ const dislikeButton = getDislikeButton();
+
+ if (preNavigateLikeButton !== getLikeButton() && dislikeButton) {
+ cLog("Registering button listeners...");
+ try {
+ getLikeButton().addEventListener("click", likeClicked);
+ dislikeButton?.addEventListener("click", dislikeClicked);
+ getLikeButton().addEventListener("touchstart", likeClicked);
+ dislikeButton?.addEventListener("touchstart", dislikeClicked);
+ dislikeButton?.addEventListener("focusin", updateDOMDislikes);
+ dislikeButton?.addEventListener("focusout", updateDOMDislikes);
+ preNavigateLikeButton = getLikeButton();
+
+ if (!smartimationObserver) {
+ smartimationObserver = createObserver(
+ {
+ attributes: true,
+ subtree: true,
+ childList: true,
+ },
+ updateDOMDislikes,
+ );
+ smartimationObserver.container = null;
+ }
+
+ const smartimationContainer = buttons.querySelector("yt-smartimation");
+ if (smartimationContainer && smartimationObserver.container != smartimationContainer) {
+ cLog("Initializing smartimation mutation observer");
+ smartimationObserver.disconnect();
+ smartimationObserver.observe(smartimationContainer);
+ smartimationObserver.container = smartimationContainer;
+ }
+ } catch {
+ return;
+ } //Don't spam errors into the console
+ }
+ if (dislikeButton) {
+ setInitialState();
+ clearInterval(jsInitChecktimer);
+ }
+ }
+ }
+
+ cLog("Setting up...");
+ jsInitChecktimer = setInterval(checkForJS_Finish, 111);
+}
+
+(function () {
+ "use strict";
+ window.addEventListener("yt-navigate-finish", setEventListeners, true);
+ setEventListeners();
+})();
+if (isMobile) {
+ let originalPush = history.pushState;
+ history.pushState = function (...args) {
+ window.returnDislikeButtonlistenersSet = false;
+ setEventListeners(args[2]);
+ return originalPush.apply(history, args);
+ };
+ setInterval(() => {
+ const dislikeButton = getDislikeButton();
+ if (dislikeButton?.querySelector(".button-renderer-text") === null) {
+ getDislikeTextContainer().innerText = mobileDislikes;
+ } else {
+ if (dislikeButton) dislikeButton.querySelector(".button-renderer-text").innerText = mobileDislikes;
+ }
+ }, 1000);
+}
diff --git a/home-manager/features/gui/apps/qutebrowser/scripts/yt-shorts-blocker.js b/home-manager/features/gui/apps/qutebrowser/scripts/yt-shorts-blocker.js
new file mode 100644
index 0000000..383fec3
--- /dev/null
+++ b/home-manager/features/gui/apps/qutebrowser/scripts/yt-shorts-blocker.js
@@ -0,0 +1,23 @@
+// ==UserScript==
+// @name YouTube Shorts Blocker
+// @namespace http://tampermonkey.net/
+// @version 0.1.2
+// @description Blocks the YouTube shorts from appearing.
+// @author Aiden Charles
+// @license MIT
+// @match https://www.youtube.com/*
+// @require https://code.jquery.com/jquery-3.4.1.slim.min.js
+// @grant none
+// ==/UserScript==
+
+(function() {
+ console.log("YouTube Shorts blocker script is running!");
+
+ setInterval(function() {
+ $("ytd-reel-shelf-renderer").hide();
+ $("a[title='Shorts']").hide();
+ $('a[href^="/shorts/"]').closest('ytd-video-renderer').hide();
+ $('span:contains("Shorts")').closest('#content.ytd-rich-section-renderer').hide();
+ }, 1000);
+})();
+
diff --git a/home-manager/features/gui/apps/qutebrowser/scripts/yt-sponsor-skip.js b/home-manager/features/gui/apps/qutebrowser/scripts/yt-sponsor-skip.js
new file mode 100644
index 0000000..3779cbf
--- /dev/null
+++ b/home-manager/features/gui/apps/qutebrowser/scripts/yt-sponsor-skip.js
@@ -0,0 +1,51 @@
+// ==UserScript==
+// @name Sponsorblock
+// @version 1.1.0
+// @description Skip sponsor segments automatically
+// @author afreakk
+// @author vongaisberg
+// @match *://*.youtube.com/*
+// @exclude *://*.youtube.com/subscribe_embed?*
+// ==/UserScript==
+const delay = 1000;
+
+const tryFetchSkipSegments = (videoID) =>
+
+ fetch(`https://sponsor.ajay.app/api/skipSegments?videoID=${videoID}`)
+ .then((r) => r.json())
+ .then((rJson) =>
+ rJson.filter((a) => a.actionType === 'skip').map((a) => a.segment)
+ )
+ .catch(
+ (e) =>
+ console.log(
+ `Sponsorblock: failed fetching skipSegments for ${videoID}, reason: ${e}`
+ ) || []
+ );
+
+const skipSegments = async () => {
+ const videoID = new URL(document.location).searchParams.get('v');
+ if (!videoID) {
+ return;
+ }
+ const key = `segmentsToSkip-${videoID}`;
+ window[key] = window[key] || (await tryFetchSkipSegments(videoID));
+ for (const v of document.querySelectorAll('video')) {
+ if (Number.isNaN(v.duration)) continue;
+ for (const [start, end] of window[key]) {
+ if (v.currentTime < end && v.currentTime >= start) {
+ console.log(`Sponsorblock: skipped video @${v.currentTime} from ${start} to ${end}`);
+ v.currentTime = end;
+ return
+ }
+ const timeToSponsor = (start - v.currentTime) / v.playbackRate;
+ if (v.currentTime < start && timeToSponsor < (delay / 1000)) {
+ console.log(`Sponsorblock: Almost at sponsor segment, sleep for ${timeToSponsor * 1000}ms`);
+ setTimeout(skipSegments, timeToSponsor * 1000);
+ }
+ }
+ }
+};
+if (!window.skipSegmentsIntervalID) {
+ window.skipSegmentsIntervalID = setInterval(skipSegments, delay);
+}