// ==UserScript==
// @name Youtube Quality HD
// @version 1.9.0
// @description Automatically select your desired video quality and select premium when posibble. (Support YouTube Desktop, Music & Mobile)
// @run-at document-body
// @inject-into content
// @match https://www.youtube.com/*
// @match https://m.youtube.com/*
// @match https://music.youtube.com/*
// @exclude https://*.youtube.com/live_chat*
// @exclude https://*.youtube.com/tv*
// @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @grant GM.getValue
// @grant GM.setValue
// @grant unsafeWindow
// @author Fznhq
// @namespace https://github.com/fznhq
// @homepageURL https://github.com/fznhq/userscript-collection
// @license GNU GPLv3
// ==/UserScript==
// Icons provided by https://uxwing.com/
(async function () {
"use strict";
const unsafeWindowExists = typeof unsafeWindow != "undefined";
/** @type {Window} */
const win = unsafeWindowExists ? unsafeWindow.window : window;
const body = document.body;
const head = document.head;
const $host = win.location.hostname;
const isMobile = $host.includes("m.youtube");
const isMusic = $host.includes("music.youtube");
const listQuality = [144, 240, 360, 480, 720, 1080, 1440, 2160, 2880, 4320];
const defaultQuality = 1080;
let manualOverride = false;
let isUpdated = false;
let settingsClicked = false;
let maybeChangeQuality = false;
/** @namespace */
const options = {
preferred_quality: defaultQuality,
preferred_premium: true,
updated_id: "",
};
const icons = {
premium: `{"svg":{"viewBox":"-12 -12 147 119"},"path":{"d":"M1 28 20 1a3 3 0 0 1 3-1h77a3 3 0 0 1 3 1l19 27a3 3 0 0 1 1 2 3 3 0 0 1-1 2L64 94a3 3 0 0 1-4 0L1 32a3 3 0 0 1-1-1 3 3 0 0 1 1-3m44 5 17 51 17-51Zm39 0L68 82l46-49ZM56 82 39 33H9zM28 5l13 20L56 5Zm39 0 15 20L95 5Zm33 2L87 27h28zM77 27 61 7 47 27Zm-41 0L22 7 8 27Z"}}`,
quality: `{"svg":{"viewBox":"-12 -12 147 131"},"path":{"fill-rule":"evenodd","d":"M113 57a4 4 0 0 1 2 1l3 4a5 5 0 0 1 1 2 4 4 0 0 1 0 1 4 4 0 0 1 0 2 4 4 0 0 1-1 1l-3 2v1l1 1v2h3a4 4 0 0 1 3 1 4 4 0 0 1 1 2 4 4 0 0 1 0 1l-1 6a4 4 0 0 1-1 3 4 4 0 0 1-3 1h-3l-1 1-1 1v1l2 2a4 4 0 0 1 1 1 4 4 0 0 1-1 3 4 4 0 0 1-1 2l-4 3a4 4 0 0 1-1 1 4 4 0 0 1-2 0 5 5 0 0 1-1 0 4 4 0 0 1-2-1l-2-3a1 1 0 0 1 0 1h-3v3a4 4 0 0 1-1 2 4 4 0 0 1-1 1 4 4 0 0 1-1 1 4 4 0 0 1-2 0h-5a4 4 0 0 1-4-5v-3l-2-1-1-1-2 2a4 4 0 0 1-2 1 4 4 0 0 1-1 0 4 4 0 0 1-2 0 4 4 0 0 1-1-1l-4-4a5 5 0 0 1 0-2 4 4 0 0 1-1-2 4 4 0 0 1 2-3l2-2v-1l-1-2h-2a4 4 0 0 1-2-1 4 4 0 0 1-1-1 4 4 0 0 1-1-1 4 4 0 0 1 0-2v-5a4 4 0 0 1 1-2 5 5 0 0 1 1-1 4 4 0 0 1 1-1 4 4 0 0 1 2 0h3l1-1v-2l-1-2a4 4 0 0 1-1-1 4 4 0 0 1 0-2 4 4 0 0 1 0-2 4 4 0 0 1 1-1l4-3a5 5 0 0 1 2-1 4 4 0 0 1 1-1 4 4 0 0 1 2 1 4 4 0 0 1 1 1l2 2h2l1-1 1-2a4 4 0 0 1 0-2 4 4 0 0 1 1-1 4 4 0 0 1 2-1 4 4 0 0 1 1 0h6a5 5 0 0 1 1 1 4 4 0 0 1 2 1 4 4 0 0 1 0 1 4 4 0 0 1 1 2l-1 3h1l1 1 1 1 3-2a4 4 0 0 1 1-1 4 4 0 0 1 2 0 4 4 0 0 1 1 0M11 0h82a11 11 0 0 1 11 11v30h-1a11 11 0 0 0-2-1h-2V21H5v49h51a12 12 0 0 0 0 2v4h-1v11h4l1 1h1l-1 1a12 12 0 0 0 0 2v1H11A11 11 0 0 1 0 81V11A11 11 0 0 1 11 0m35 31 19 13a3 3 0 0 1 0 4L47 61a3 3 0 0 1-2 0 3 3 0 0 1-3-2V33l1-1a3 3 0 0 1 3-1m4 56V76H29v11ZM24 76H5v5a6 6 0 0 0 6 6h13zm52-60V5H55v11Zm5-11v11h18v-5a6 6 0 0 0-6-6ZM50 16V5H29v11Zm-26 0V5H11a6 6 0 0 0-6 6v5Zm70 56a6 6 0 1 1-6 7 6 6 0 0 1 6-7m-1-8a14 14 0 1 1-13 16 14 14 0 0 1 13-16"}}`,
check_mark: `{"svg":{"viewBox":"-32 -32 186.9 153.8"},"path":{"d":"M1.2 55.5a3.7 3.7 0 0 1 5-5.5l34.1 30.9 76.1-79.7a3.8 3.8 0 0 1 5.4 5.1L43.2 88.7a3.7 3.7 0 0 1-5.2.2L1.2 55.5z"}}`,
arrow: `{"svg":{"class":"transform-icon-svg","viewBox":"-80 -80 227 283","fill":"#aaa"},"path":{"d":"M2 111a7 7 0 1 0 10 10l53-55-5-5 5 5c3-3 3-7 0-10L12 2A7 7 0 1 0 2 12l48 49z"}}`,
};
/**
* @param {string} name
* @param {boolean} value
* @returns {boolean}
*/
function saveOption(name, value) {
GM.setValue(name, value);
return (options[name] = value);
}
for (const name in options) {
const saved_option = await GM.getValue(name);
if (saved_option === undefined) {
saveOption(name, options[name]);
} else {
options[name] = saved_option;
}
}
/**
* @param {number} length
* @returns {string}
*/
function generateId(length = 12) {
let out = "id";
while (--length > 1) out += Math.floor(Math.random() * 36).toString(36);
return out;
}
const bridgeName = "main-bridge-" + generateId();
const bridgeMain = function () {
function handleBridge(ev) {
const data = ev.detail.split("|");
const [uniqueId, type] = data.splice(0, 2);
const handlers = {
api() {
const [id, fnName, ...args] = data;
const player = document.getElementById(id);
return player ? player[fnName](...args) : "";
},
create() {
const [tagName, id] = data;
const element = document.createElement(tagName);
element.id = id;
element.setAttribute("data-custom-item-bridge", "");
return document.body.append(element), id;
},
};
const detail = handlers[type]();
window.dispatchEvent(new CustomEvent(uniqueId, { detail }));
}
function spoofData(ev) {
const item = ev.target.closest("[data-custom-item-bridge]");
if (item) item.data = {};
}
window.addEventListener("bridgeName", handleBridge);
window.addEventListener("click", spoofData, true);
}.toString();
const script = head.appendChild(document.createElement("script"));
script.textContent = `(${bridgeMain.replace("bridgeName", bridgeName)})();`;
/**
* @returns {Promise<any>}
*/
function sendToMain() {
const uniqueId = generateId(64);
const detail = [uniqueId, ...arguments].join("|");
return new Promise((resolve) => {
function callback(ev) {
resolve(ev.detail);
win.removeEventListener(uniqueId, callback);
}
win.addEventListener(uniqueId, callback);
win.dispatchEvent(new CustomEvent(bridgeName, { detail }));
});
}
/**
* @param {string} id
* @param {'getPlaybackQualityLabel' | 'getAvailableQualityData' | 'setPlaybackQualityRange' | 'playVideo'} name
* @param {string[]} args
* @returns {Promise<string>}
*/
function API(id, name, ...args) {
return sendToMain("api", id, name, ...args);
}
/**
* @param {string} tagName
* @returns {Promise<HTMLElement>}
*/
async function createInMain(tagName) {
const id = await sendToMain("create", tagName, generateId());
const element = find(document, tagName + "#" + id);
return element.removeAttribute("id"), element.remove(), element;
}
/**
* @param {string} name
* @param {object} attributes
* @param {Array} append
* @returns {SVGElement}
*/
function createNS(name, attributes = {}, append = []) {
const el = document.createElementNS("http://www.w3.org/2000/svg", name);
for (const k in attributes) el.setAttributeNS(null, k, attributes[k]);
return el.append(...append), el;
}
for (const name in icons) {
const icon = JSON.parse(icons[name]);
icon.svg = { ...icon.svg, width: "100%", height: "100%" };
icons[name] = createNS("svg", icon.svg, [createNS("path", icon.path)]);
}
/**
* @param {Document | HTMLElement} context
* @param {string} query
* @param {boolean} all
* @returns {HTMLElement | NodeListOf<HTMLElement> | null}
*/
function find(context, query, all = false) {
const property = "querySelector" + (all ? "All" : "");
return context[property](query);
}
/**
* @param {string} query
* @param {boolean} cache
* @returns {() => HTMLElement | null}
*/
function $(query, cache = true) {
let element = null;
return () => (cache && element) || (element = find(document, query));
}
const cachePlayers = {};
const cacheTextQuality = new Set();
const query = {
movie_player: "#movie_player",
short_player: "#shorts-player",
m_menu_item: "[role='menuitem'], ytm-menu-service-item-renderer",
m_settings_btn: `player-top-controls .player-settings-icon, shorts-video ytm-bottom-sheet-renderer`,
};
const allowedPlayerIds = [query.movie_player, query.short_player];
const element = {
settings: $(".ytp-settings-menu"),
panel_settings: $(".ytp-settings-menu .ytp-panel-menu"),
quality_menu: $(".ytp-quality-menu", false),
movie_player: $(query.movie_player, !isMobile),
short_player: $(query.short_player),
popup_menu: $("ytd-popup-container ytd-menu-service-item-renderer"),
m_bottom_container: $("bottom-sheet-container:not(:empty)", false),
m_settings: $(query.m_settings_btn, false),
music_popup: $("ytmusic-menu-popup-renderer"),
// Reserve Element
premium_menu: document.createElement("div"),
option_text: document.createTextNode(""),
};
const style = head.appendChild(document.createElement("style"));
style.textContent = /*css*/ `
[dir='rtl'] svg.transform-icon-svg {
transform: scale(-1, 1);
}
`;
/**
* @param {MutationCallback} callback
* @param {Node} target
* @param {MutationObserverInit | undefined} options
*/
function observer(callback, target = body, options) {
const mutation = new MutationObserver(callback);
mutation.observe(target, options || { subtree: true, childList: true });
}
/**
* @param {string} label
* @returns {number}
*/
function parseQualityLabel(label) {
return parseInt(label.replace(/^(\D+)/, "").slice(0, 4), 10);
}
/**
* @typedef {object} QualityData
* @property {any} formatId
* @property {string} qualityLabel
* @property {string} quality
* @property {boolean} isPlayable
*/
/**
* @param {number[]} numberQualityData
* @returns {number}
*/
function getPreferredQuality(numberQualityData) {
const currentMaxQuality = Math.max(...numberQualityData);
return !isFinite(currentMaxQuality) ||
currentMaxQuality > options.preferred_quality
? options.preferred_quality
: currentMaxQuality;
}
/**
* @param {string} id
* @param {QualityData[]} qualityData
* @returns {QualityData | null}
*/
function getQuality(id, qualityData) {
const q = { premium: null, normal: null };
const short = id.includes("short");
const numQD = qualityData.map((d) => parseQualityLabel(d.qualityLabel));
if (!numQD.some((quality) => quality)) return null;
let preferred = getPreferredQuality(numQD);
let indexQuality = listQuality.indexOf(preferred);
while (!numQD.includes(preferred) && indexQuality-- > 0) {
preferred = listQuality[indexQuality];
}
qualityData.forEach((data) => {
const label = data.qualityLabel.toLowerCase();
if (parseQualityLabel(label) == preferred && data.isPlayable) {
if (label.includes("premium")) q.premium = data;
else q.normal = data;
}
});
return (options.preferred_premium && !short && q.premium) || q.normal;
}
async function setVideoQuality() {
if (manualOverride) return;
if (isUpdated) return (isUpdated = false);
const id = this.id;
const label = await API(id, "getPlaybackQualityLabel");
const quality = parseQualityLabel(label);
const qualityData = await API(id, "getAvailableQualityData");
const selected = getQuality(id, qualityData || []);
if (
quality &&
selected &&
listQuality.includes(quality) &&
(isUpdated = selected.qualityLabel != label)
) {
API(
id,
"setPlaybackQualityRange",
selected.quality,
selected.quality,
selected.formatId
);
}
}
function triggerSyncOptions() {
const id = generateId();
GM.setValue("updated_id", id).then(() => (options.updated_id = id));
}
/**
*
* @param {keyof options} optionName
* @param {any} newValue
* @param {HTMLElement} player
* @returns {any}
*/
function savePreferred(optionName, newValue, player) {
saveOption(optionName, newValue);
triggerSyncOptions();
setVideoQuality.call(player, (isUpdated = false));
return newValue;
}
/**
* @param {string} className
* @param {Array} append
* @returns {HTMLDivElement}
*/
function itemElement(className = "", append = []) {
const el = document.createElement("div");
el.className = "ytp-menuitem" + (className ? "-" + className : "");
return el.append(...append), el;
}
/**
* @param {SVGSVGElement} svgIcon
* @param {string} textLabel
* @param {boolean} checkbox
* @returns {{item: HTMLDivElement, content: HTMLDivElement}}
*/
function createMenuItem(svgIcon, textLabel, checkbox = false) {
const inner = checkbox ? [itemElement("toggle-checkbox")] : [];
const content = itemElement("content", inner);
const icon = itemElement("icon", [svgIcon.cloneNode(true)]);
const label = itemElement("label", [textLabel]);
const item = itemElement("", [icon, label, content]);
return (icon.style.fill = "currentColor"), { item, content };
}
/**
* @param {HTMLElement} element
* @param {boolean} state
*/
function setChecked(element, state) {
element.setAttribute("aria-checked", state);
}
function premiumMenu() {
const menu = createMenuItem(icons.premium, "Preferred Premium", true);
const name = "preferred_premium";
setChecked(menu.item, options[name]);
menu.item.addEventListener("click", function () {
const player = element.movie_player();
setChecked(this, savePreferred(name, !options[name], player));
});
return (element.premium_menu = menu.item);
}
/**
* @param {string} value
* @param {Text | undefined} text
*/
function setTextQuality(value, text) {
if (text) cacheTextQuality.add(text);
cacheTextQuality.forEach((qualityText) => {
qualityText.textContent = value + "p";
});
}
/**
* @param {HTMLElement} element
* @returns {DOMRect}
*/
function getRect(element) {
return element.getBoundingClientRect();
}
/**
* @param {HTMLElement} content
* @param {HTMLElement} player
*/
function qualityOption(content, player) {
const name = "preferred_quality";
const text = document.createTextNode("");
setTextQuality(options[name], text);
content.style.cursor = "pointer";
content.style.fontWeight = 500;
content.style.wordSpacing = "2rem";
content.append("< ", text, " >");
content.addEventListener("click", function (ev) {
const threshold = this.clientWidth / 2;
const clickPos = ev.clientX - getRect(this).left;
let pos = listQuality.indexOf(options[name]);
if (
(clickPos < threshold && pos > 0 && pos--) ||
(clickPos > threshold && pos < listQuality.length - 1 && ++pos)
) {
manualOverride = false;
setTextQuality(savePreferred(name, listQuality[pos], player));
}
});
}
function qualityMenu() {
const menu = createMenuItem(icons.quality, "Preferred Quality");
menu.item.style.cursor = "default";
menu.content.style.fontSize = "130%";
qualityOption(menu.content, element.movie_player());
return menu.item;
}
/**
* @param {Element[]} elements
*/
function removeAttributes(elements) {
elements.forEach((element) => {
element.textContent = "";
Array.from(element.attributes).forEach((attr) => {
if (attr.name != "class") element.removeAttribute(attr.name);
});
});
}
/**
* @param {NodeListOf<Element>} element
* @returns {HTMLElement}
*/
function firstOnly(element) {
for (let i = 1; i < element.length; i++) element[i].remove();
return element[0];
}
/**
* @param {HTMLElement} element
* @param {SVGSVGElement | undefined} icon
* @param {string} label
* @param {Text | string} selectedLabel
*/
function parseItem(element, icon, label, selectedLabel) {
const item = body.appendChild(element.cloneNode(true));
const iIcon = firstOnly(find(item, "c3-icon, yt-icon", true));
const iText = firstOnly(
find(item, "[role='text'], yt-formatted-string", true)
);
const optionLabel = iText.cloneNode();
const optionIcon = iIcon.cloneNode();
const wrapperIcon = (icon) => {
return itemElement(" yt-icon-shape yt-spec-icon-shape", [icon]);
};
iText.after(optionLabel, optionIcon);
removeAttributes([iIcon, iText, optionIcon, optionLabel]);
if (selectedLabel) {
optionIcon.append(wrapperIcon(icons.arrow));
optionLabel.style.marginInline = "auto 0";
optionLabel.style.color = "#aaa";
optionLabel.append(selectedLabel);
} else optionIcon.remove();
if (icon) iIcon.append(wrapperIcon(icon.cloneNode(true)));
iText.textContent = label;
return item;
}
/**
* @param {HTMLElement} itemElement
* @returns {HTMLElement}
*/
function shortQualityMenu(itemElement) {
const item = parseItem(itemElement, icons.quality, "Preferred Quality");
const container = find(item, "yt-formatted-string:last-of-type");
const option = document.createElement("div");
item.setAttribute("use-icons", "");
item.style.userSelect = "none";
item.style.cursor = "default";
container.append(option);
container.style.minWidth = "130px";
option.style.margin = container.style.margin = "0 auto";
option.style.width = "fit-content";
option.style.paddingInline = "12px";
qualityOption(option, element.short_player());
return item;
}
/**
* @param {MouseEvent} ev
*/
function setOverride(ev) {
if (manualOverride) return;
const menu = element.quality_menu();
const quality = parseQualityLabel(ev.target.textContent);
if (menu && listQuality.includes(quality)) manualOverride = true;
}
/**
* @param {HTMLElement} player
*/
function addVideoListener(player) {
const cache = cachePlayers[player.id];
const video = find(player, "video");
if (!cache || cache[1] !== video) {
cachePlayers[player.id] = [player, video];
const fn = setVideoQuality.bind(player);
video.addEventListener("play", () => setTimeout(fn, 200));
video.addEventListener("resize", fn);
}
}
/**
* @param {'watch' | 'short'} type
* @returns {boolean}
*/
function isVideoPage(type) {
const path = win.location.pathname;
const types = type ? [type] : ["watch", "short"];
return types.some((type) => path.startsWith("/" + type));
}
function resetState() {
isUpdated = false;
manualOverride = false;
}
/**
* @param {CustomEvent} ev
*/
function playerUpdated(ev) {
let player = null;
if (
isVideoPage() &&
allowedPlayerIds.some((id) => (player = find(ev.target, id)))
) {
resetState();
addVideoListener(player);
}
}
async function syncOptions() {
if ((await GM.getValue("updated_id")) != options.updated_id) {
isUpdated = false;
for (const name in options) options[name] = await GM.getValue(name);
setChecked(element.premium_menu, options.preferred_premium);
setTextQuality(options.preferred_quality);
for (const id in cachePlayers) {
const [player, video] = cachePlayers[id];
if (!video.paused) setVideoQuality.call(player);
}
}
}
(function checkOptions() {
setTimeout(() => syncOptions().then(checkOptions), 1e3);
})();
/**
* @param {HTMLElement} menuItem
* @param {string} noteText
* @returns {{preferred: HTMLElement, items: HTMLElement[]}}
*/
function listQualityToItem(menuItem, noteText = "") {
const preferredIndex = listQuality.indexOf(options.preferred_quality);
const video = element.movie_player();
const items = listQuality.map((quality, i) => {
const note = quality == defaultQuality ? noteText : "";
const icon = preferredIndex == i && icons.check_mark;
const item = parseItem(menuItem, icon, `${quality}p ${note}`);
item.addEventListener("click", () => {
body.click(), (manualOverride = false);
savePreferred("preferred_quality", quality, video);
});
return item;
});
return { preferred: items[preferredIndex], items: items.reverse() };
}
function musicPopupStyle() {
style.textContent += /*css*/ `
ytmusic-menu-popup-renderer {
min-width: 268.5px !important;
}
#items.ytmusic-menu-popup-renderer {
width: 250px !important;
}
`;
}
/**
* @param {HTMLElement} itemElement
* @param {HTMLElement} popup
*/
function musicPopupObserver(itemElement, popup) {
const dropdown = popup.closest("tp-yt-iron-dropdown");
const menu = find(popup, "#items");
const item = parseItem(
itemElement,
icons.quality,
"Preferred Quality",
element.option_text
);
const optionIcon = find(item, "yt-formatted-string + yt-icon");
optionIcon.style.marginInline = 0;
item.className = "ytmusic-menu-popup-renderer";
item.addEventListener("click", () => {
menu.textContent = "";
menu.append(...listQualityToItem(item).items);
win.dispatchEvent(new Event("resize"));
});
function addMenuItem() {
if (dropdown.getAttribute("aria-hidden") !== "true") {
menu.append(item);
setTextQuality(options.preferred_quality, element.option_text);
}
}
observer(addMenuItem, dropdown, { attributeFilter: ["aria-hidden"] });
dropdown.setAttribute("aria-hidden", false);
}
if (isMusic) {
return observer((_, observe) => {
const player = element.movie_player();
const popup = element.music_popup();
if (player && !cachePlayers[player.id]) addVideoListener(player);
if (popup) {
observe.disconnect();
createInMain("ytmusic-menu-service-item-renderer").then(
(element) => {
musicPopupStyle();
musicPopupObserver(element, popup);
}
);
}
});
}
/** @type {HTMLElement} */
let listCustomMenuItem = null;
const customMenuHashId = "custom-bottom-menu";
/**
* @param {HTMLElement} container
*/
function listCustomMenu(container) {
win.location.replace("#" + customMenuHashId);
listCustomMenuItem = container.cloneNode(true);
const item = find(listCustomMenuItem, query.m_menu_item);
const menu = item.parentElement;
const header = find(listCustomMenuItem, "#header-wrapper");
const content = find(listCustomMenuItem, "#content-wrapper");
const listQualityItems = listQualityToItem(item, "(Recommended)");
menu.textContent = "";
menu.append(...listQualityItems.items);
header.remove();
content.style.maxHeight = "250px";
body.style.overflow = "hidden";
container.parentElement.parentElement.append(listCustomMenuItem);
const contentTop = getRect(content).top;
const preferredQualityRect = getRect(listQualityItems.preferred);
const realTop = preferredQualityRect.top - contentTop;
content.scrollTo(0, realTop - preferredQualityRect.height * 2);
listCustomMenuItem.addEventListener("click", () => win.history.back());
}
function mobileQualityMenu() {
const container = element.m_bottom_container();
if (container) {
settingsClicked = false;
maybeChangeQuality = false;
const item = find(container, query.m_menu_item);
const menu = item.parentElement;
const menuItem = parseItem(
item,
icons.quality,
"Preferred Quality",
element.option_text
);
setTextQuality(options.preferred_quality, element.option_text);
menu.append(menuItem);
menu.addEventListener("click", () => (maybeChangeQuality = true));
menuItem.addEventListener("click", () => listCustomMenu(container));
}
}
/**
* @param {MouseEvent} ev
*/
function mobileSetSettingsClicked(ev) {
if (isVideoPage()) {
const settings = element.m_settings();
settingsClicked = settings && settings.contains(ev.target);
}
}
/**
* @param {MouseEvent} ev
*/
function mobileSetOverride(ev) {
if (listCustomMenuItem) return (maybeChangeQuality = false);
if (manualOverride || !maybeChangeQuality) return;
const container = element.m_bottom_container();
if (container && find(container, query.m_menu_item)) {
const quality = parseQualityLabel(ev.target.textContent);
if (listQuality.includes(quality)) manualOverride = true;
maybeChangeQuality = false;
}
}
/**
* @param {CustomEvent} ev
*/
function mobilePlayerUpdated(ev) {
if (isVideoPage() && ev.detail.type == "newdata") resetState();
}
/**
* @param {HashChangeEvent} ev
*/
function mobileHandlePressBack(ev) {
if (listCustomMenuItem && ev.oldURL.includes(customMenuHashId)) {
listCustomMenuItem = listCustomMenuItem.remove();
body.style.overflow = "";
}
}
if (isMobile) {
win.addEventListener("click", mobileSetSettingsClicked, true);
win.addEventListener("click", mobileSetOverride, true);
win.addEventListener("hashchange", mobileHandlePressBack);
document.addEventListener("video-data-change", mobilePlayerUpdated);
return observer(() => {
const player = element.movie_player();
if (player) {
addVideoListener(player);
const unstarted = player.className.includes("unstarted-mode");
if (unstarted && isVideoPage()) API(player.id, "playVideo");
}
if (settingsClicked) mobileQualityMenu();
});
}
function initShortMenu() {
let menu = null;
if (isVideoPage("short") && (menu = element.popup_menu())) {
win.removeEventListener("click", initShortMenu);
createInMain("ytd-menu-service-item-renderer").then((element) => {
menu.parentElement.append(shortQualityMenu(element));
});
}
}
win.addEventListener("click", initShortMenu);
observer((_, observe) => {
const movie_player = element.movie_player();
const short_player = element.short_player();
if (short_player) addVideoListener(short_player);
if (movie_player) {
addVideoListener(movie_player);
element.panel_settings().append(premiumMenu(), qualityMenu());
element.settings().addEventListener("click", setOverride, true);
document.addEventListener("yt-player-updated", playerUpdated);
observe.disconnect();
}
});
})();