Youtube polymer engine fixes

Some fixes for Youtube polymer engine

目前为 2021-04-24 提交的版本。查看 最新版本

// ==UserScript==
// @name         Youtube polymer engine fixes
// @description  Some fixes for Youtube polymer engine
// @namespace    bo.gd.an[at]rambler.ru
// @version      2.6.0
// @match        https://www.youtube.com/*
// @compatible   firefox 56
// @author       Bogudan
// @grant        GM_info
// @grant        GM.info
// @grant        GM_getValue
// @grant        GM.getValue
// @grant        GM_setValue
// @grant        GM.setValue
// @noframes
// @run-at       document-start
// @license      CC-BY-NC-ND-4.0
// ==/UserScript==

(function () {
    'use strict';
	if (document.location.pathname == '/error')	// нам нечего делать на страницах с ошибками
		return;
	// test local storage availability and load settings from there first
	let settings, ls, saver;
	try {
		function lsTest (st, v) {
			st.setItem ('__fix_test__', v);
			return st.getItem ('__fix_test__') == v;
			};
		const _s = window.localStorage;
		if (lsTest (_s, 'qwe') && lsTest (_s, 'rty')) { // do 2 times just in case LS stored value once, but does not let change it later
			ls = _s;
			ls.removeItem ('__fix_test__');
			settings = JSON.parse (ls.getItem ('__fix__settings__'));
			}
		}
	catch (e) { }
	// select storage: GM_*/GM.* or local storage (in case userscript manager does not grant us GMs)
	if (typeof (GM_getValue) !== 'undefined' && typeof (GM_setValue) !== 'undefined' && GM_getValue && GM_setValue) {
		saver = function () {
			GM_setValue ('settings', settings);
			if (ls)
				ls.removeItem ('__fix__settings__');
			}
		if (!settings)
			settings = GM_getValue ('settings', {});
		else
			saver ();
		settings.storage = 'GM_*Value';
		}
	else if (typeof (GM) !== 'undefined' && GM && GM.getValue && GM.setValue) {
		saver = function () {
			(async () => await GM.setValue ('settings', settings)) ();
			if (ls)
				ls.removeItem ('__fix__settings__');
			};
		if (!settings)
			settings = (async () => await GM.getValue ('settings', {})) ();
		else
			saver ();
		settings.storage = 'GM.*Value';
		}
	else if (ls) {
		if (!settings)
			settings = {};
		saver = function () {
			ls.setItem ('__fix__settings__', JSON.stringify (settings));
			};
		settings.storage = 'window.localStorage.*Item';
		}
	else
		settings = {};
	// delete old settings
	if ("default_player_640" in settings) {	// удалено в 0.5
		settings.default_player = settings.default_player_640 ? 3 : 0;
		delete settings.default_player_640;
		}
	if ("reduce_thumbnail" in settings) {	// удалено в 0.6.0
		settings.thumbnail_size = settings.reduce_thumbnail ? 2 : 0;
		delete settings.reduce_thumbnail;
		}
	if ("reduce_font" in settings) {	// удалено в 2.5.8: размеры текста уменьшились на стороне YT
		settings.fix_removed_placeholder = settings.reduce_font;
		delete settings.reduce_font;
		}
	// set default values
	const gminfo = typeof (GM_info) !== 'undefined' && GM_info || typeof (GM) !== 'undefined' && GM && GM.info;
	const fix_version = gminfo && gminfo.script && gminfo.script.version;
	if (fix_version) {
		settings.version = fix_version;
		if (!("inst_ver" in settings))
			settings.inst_ver = fix_version;
		}
	if (!("align_player" in settings))
		settings.align_player = 0;
	if (!("default_player" in settings))
		settings.default_player = 0;
	if (!("hide_guide" in settings))
		settings.hide_guide = true;
	if (!("hide_yt_suggested_blocks" in settings))
		settings.hide_yt_suggested_blocks = true;
	if (!("logo_target" in settings))
		settings.logo_target = "";
	if (!("fix_removed_placeholder" in settings))
		settings.fix_removed_placeholder = true;
	if (!("theater_player" in settings))
		settings.theater_player = 0;
	if (!("thumbnail_size" in settings))
		settings.thumbnail_size = 2;
	if (!("thumbnail_size_m" in settings))
		settings.thumbnail_size_m = 720;
	if (!("unfix_header" in settings))
		settings.unfix_header = true;
	if (!("search_thumbnail" in settings))
		settings.search_thumbnail = 0;
	if (!("clear_search" in settings))
		settings.clear_search = false;
	if (!("channel_top" in settings))
		settings.channel_top = 0;
	if (!("try_load_more" in settings))
		settings.try_load_more = false;
	if (!("unbound_video_title" in settings))
		settings.unbound_video_title = false;
	if (!("video_quality" in settings))
		settings.video_quality = 0;
	if (!("no_resume_time" in settings))
		settings.no_resume_time = false;
	if (!("remove_yt_redirect" in settings))
		settings.remove_yt_redirect = false;
	console.log ('fix settings:', settings);
	// catch "settings" page
	if (document.location.pathname == '/fix-settings') {
		document.title = "YouTube Polymer Fixes: Settings";
		const back = document.createElement ('div');
		back.className = 'ytfixback';
		const plane = document.createElement ('div');
		plane.className = 'ytfix';
		const style = document.createElement ('style');
		style.type = 'text/css';
		style.innerHTML = `
			.ytfix{position:absolute;left:0;top:0;right:0;background:#eee;padding:3em}
			.ytfix_line{margin:1em}
			.ytfix_line span,.ytfix_line input,.ytfix_line select{margin-right:1em}
			.ytfix_field{padding:0.2em;border:1px solid #888}
			.ytfix_button{padding:0.4em;border:1px solid #888}
			.ytfix_hide{display:none}
			.ytfixback{position:absolute;left:0;top:0;right:0;height:100%;background:#eee}
			`;
		plane.appendChild (style);
		function AddLine () {
			const q = document.createElement ('div');
			q.className = 'ytfix_line';
			for (let i = 0, L = arguments.length; i < L; ++i)
				q.appendChild (arguments [i]);
			plane.appendChild (q);
			}
		let e1, e2;
		e1 = document.createElement ('b');
		e1.appendChild (document.createTextNode ('YouTube Polymer Fixes: Settings'));
		AddLine (e1);
		if (fix_version) {
			e1 = document.createElement ('b');
			e1.appendChild (document.createTextNode (`Version: ${fix_version}`));
			AddLine (e1);
			}
		if (!saver) {
			e1 = document.createElement ('span');
			e1.style = 'color:red';
			e1.appendChild (document.createTextNode ('Cannot edit settings: no access to any storage.'));
			AddLine (e1);
			e1 = document.createElement ('span');
			e1.appendChild (document.createTextNode ('If you are using Firefox, allow cookies for this site.'));
			AddLine (e1);
			}
		else {
			const ess = {};
			function MakeDesc (desc) {
				const e = document.createElement ('span');
				e.appendChild (document.createTextNode (desc));
				return e;
				}
			function MakeBoolElement (nm) {
				const e = document.createElement ('input');
				e.type = 'checkbox';
				e.checked = settings [nm];
				ess [nm] = e;
				return e;
				}
			function MakeListElement (nm, opts) {
				const e = document.createElement ('select');
				e.className = 'ytfix_field';
				ess [nm] = e;
				for (let i = 0, L = opts.length; i < L; ++i) {
					const o = document.createElement ('option');
					o.appendChild (document.createTextNode (opts [i]));
					e.appendChild (o);
					}
				e.selectedIndex = settings [nm];
				return e;
				}
			function MakeTextElement (nm) {
				const e = document.createElement ('input');
				e.className = 'ytfix_field';
				e.value = settings [nm];
				ess [nm] = e;
				return e;
				}
			function MakeButton (text, click) {
				const e = document.createElement ('input');
				e.type = 'button';
				e.className = 'ytfix_button';
				e.value = text;
				e.addEventListener ('click', click);
				return e;
				}
			AddLine (MakeBoolElement ("hide_guide"), MakeDesc ('Hide "Guide" menu when page opens'));
			AddLine (MakeBoolElement ("fix_removed_placeholder"), MakeDesc ('Make size of "Video removed" placeholder about the same as removed video description'));
			const tsm = MakeTextElement ("thumbnail_size_m");
			tsm.className = settings.thumbnail_size == 5 ? 'ytfix_field' : 'ytfix_hide';
			const tsi = MakeListElement ("thumbnail_size", ['default', '180px', '240px', '360px', '480px', 'manual']);
			tsi.addEventListener ('change', function () {
				ess.thumbnail_size_m.className = ess.thumbnail_size.selectedIndex == 5 ? 'ytfix_field' : 'ytfix_hide';
				});
			AddLine (MakeDesc ('Set thumbnails width for front page'), tsi, tsm);
			AddLine (MakeDesc ('Set thumbnails width for search page'), MakeListElement ("search_thumbnail", ['default', '240px', '360px']));
			AddLine (MakeDesc ("Set player height in default mode"), MakeListElement ("default_player", ['default', '144px', '240px', '360px', '480px', '720px']));
			AddLine (MakeDesc ("Set player height in theater mode"), MakeListElement ("theater_player", ['default', '144px', '240px', '360px', '480px', '720px']));
			AddLine (MakeBoolElement ("hide_yt_suggested_blocks"), MakeDesc ('Hide suggestions blocks on main page (recommended playlists, latest posts, etc.)'));
			AddLine (MakeBoolElement ("clear_search"), MakeDesc ("Hide suggestions blocks in search (for you, people also watched, etc.)"));
			AddLine (MakeBoolElement ("unfix_header"), MakeDesc ("Unstick header bar from top of the screen"));
			AddLine (MakeDesc ("Align resized player into it's container (normal and theater modes)"), MakeListElement ("align_player", ['center', 'left', 'right']));
			AddLine (MakeDesc ("Modify channels' pages behaviour"), MakeListElement ('channel_top', ['default', 'hide banner with scrolling', 'hide banner on load']));
			AddLine (MakeBoolElement ('try_load_more'), MakeDesc ('Add button to try loading more content on pages with dynamic content load'));
			AddLine (MakeBoolElement ('unbound_video_title'), MakeDesc ('Remove size limit for video titles'));
			AddLine (MakeDesc ("Change YT logo target to https://www.youtube.com/..."), MakeTextElement ("logo_target"));
			AddLine (MakeBoolElement ("no_resume_time"), MakeDesc ('Remove resume time from the video links (&t=...)'));
			AddLine (MakeBoolElement ("remove_yt_redirect"), MakeDesc ('Remove YT tracking from links (/redirect?...)'));
			AddLine (MakeDesc ('Starting video quality'), MakeListElement ('video_quality', ['Auto (default)', '2160p (4K)', '1440p (HD)', '1080p (HD)', '720p', '480p', '360p', '240p', '144p']));
			e1 = MakeButton ('Save settings and return to YouTube', function () {
				settings.hide_guide = ess.hide_guide.checked;
				settings.fix_removed_placeholder = ess.fix_removed_placeholder.checked;
				settings.thumbnail_size = ess.thumbnail_size.selectedIndex;
				if (settings.thumbnail_size == 5) {
					const v = ess.thumbnail_size_m.value;
					if (!/^\d+$/.test (v)) {
						alert ('Error: invalid value for thumbnails size');
						return;
						}
					settings.thumbnail_size_m = parseInt (v);
					}
				settings.search_thumbnail = ess.search_thumbnail.selectedIndex;
				settings.default_player = ess.default_player.selectedIndex;
				settings.theater_player = ess.theater_player.selectedIndex;
				settings.hide_yt_suggested_blocks = ess.hide_yt_suggested_blocks.checked;
				settings.unfix_header = ess.unfix_header.checked;
				settings.align_player = ess.align_player.selectedIndex;
				settings.channel_top = ess.channel_top.selectedIndex;
				settings.logo_target = ess.logo_target.value;
				settings.clear_search = ess.clear_search.checked;
				settings.try_load_more = ess.try_load_more.checked;
				settings.unbound_video_title = ess.unbound_video_title.checked;
				settings.video_quality = ess.video_quality.selectedIndex;
				settings.no_resume_time = ess.no_resume_time.checked;
				settings.remove_yt_redirect = ess.remove_yt_redirect.checked;
				saver ();
				alert ('Settings saved');
				history.back ();
				});
			e2 = MakeButton ('Return to YouTube without saving', function () {
				history.back ();
				});
			AddLine (e1, e2);
			e1 = MakeButton ('Export settings', function () {
				const d = document.createElement ('a');
				d.style.display = 'none';
				d.setAttribute ('download', 'ytfix_settings.json');
				d.setAttribute ('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent (JSON.stringify (settings)));
				document.body.appendChild (d);
				d.click ();
				document.body.removeChild (d);
				});
			e2 = MakeButton ('Import settings', function () {
				const f = document.createElement ('input');
				f.type = 'file';
				f.style.display = 'none';
				f.addEventListener ('change', function () {
					if (f.files.length != 1)
						return;
					const rdr = new FileReader ();
					rdr.addEventListener ('load', function () {
						try {
							settings = JSON.parse (rdr.result);
							saver ();
							alert ('Settings imported');
							document.location.reload ();
							}
						catch (ex) {
							alert ('Error parsing settings\n' + ex);
							}
						});
					rdr.addEventListener ('error', () => alert ('Error loading file\n' + rdr.error));
					rdr.readAsText (f.files [0]);
					});
				document.body.appendChild (f);
				f.click ();
				document.body.removeChild (f);
				});
			AddLine (e1, e2);
			}
		let int = setInterval (function () {
			if (!document.body)
				return;
			document.body.appendChild (back);
			document.body.appendChild (plane);
			clearInterval (int);
			}, 1);
		console.log ('Settings page created');
		return;
		}
	// apply settings
	let styles = '';
	const inject_func = [], inject_ints = [];
	if (settings.hide_guide) {
		function HideGuideTimer (info) {
			if (info.act == 0 && document.location.toString () != info.url)	// обнаружение смены адреса
				info.act = 1;
			if (info.act == 1) {	// wait for sorp page load completion
				const Q = document.getElementsByTagName ('yt-page-navigation-progress');
				if (!Q.length || !Q [0].hasAttribute ('hidden'))
					return;
				info.act = 2;
				}
			if (info.act == 2) {	// wait for button and press it if necessary
				const guide_button = document.getElementById ('guide-button');
				if (!guide_button)
					return;
				let tmp = guide_button.getElementsByTagName ('button');
				if (!tmp.length)
					return;
				tmp = tmp [0];
				if (!tmp.hasAttribute ('aria-pressed'))
					return;
				if (tmp.attributes ['aria-pressed'].value == 'true')
					guide_button.click ();
				else {
					info.url = document.location.toString ();
					info.act = 0;
					window.dispatchEvent (new Event ('resize'));
					}
				}
			}
		inject_func.push (HideGuideTimer);
		inject_ints.push ({ call: HideGuideTimer, params: [{ act: 2 }] });
		}
	if (settings.fix_removed_placeholder)
		styles += 'paper-button.style-blue-text,tp-yt-paper-button.style-blue-text{padding:0!important}';
	if (settings.thumbnail_size)
		styles += 'ytd-rich-item-renderer{width:' + [0, 180, 240, 360, 480, settings.thumbnail_size_m] [settings.thumbnail_size] + 'px!important}';
	if (settings.hide_yt_suggested_blocks)
		styles += 'div#contents.ytd-rich-grid-renderer ytd-rich-section-renderer{display:none!important}';
	if (settings.unfix_header)
		styles += `
			div#masthead-container.ytd-app,ytd-mini-guide-renderer.ytd-app,app-drawer#guide{position:absolute!important}
			ytd-feed-filter-chip-bar-renderer{position:relative!important}
			div#chips-wrapper{position:absolute!important;top:0!important}
			`;
	if (settings.search_thumbnail) {
		const sz = [0, 240, 360] [settings.search_thumbnail] + 'px!important';
		// min-width defaults to 240px, max-width defaults to 360px
		// sizes for: videos, playlists, channels, mixes
		styles += `ytd-video-renderer[use-prominent-thumbs] ytd-thumbnail.ytd-video-renderer,ytd-playlist-renderer[use-prominent-thumbs] ytd-playlist-thumbnail.ytd-playlist-renderer,ytd-channel-renderer[use-prominent-thumbs] #avatar-section.ytd-channel-renderer,ytd-radio-renderer[use-prominent-thumbs] ytd-thumbnail.ytd-radio-renderer{min-width:${sz};max-width:${sz}}`;
		}
	if (settings.clear_search)
		styles += 'ytd-two-column-search-results-renderer ytd-shelf-renderer.style-scope.ytd-item-section-renderer,ytd-two-column-search-results-renderer ytd-horizontal-card-list-renderer.style-scope.ytd-item-section-renderer{display:none!important}';
	styles += [
		'#player-theater-container{margin-left:auto!important;margin-right:auto!important}',
		'#player-container-outer{margin-left:0!important}',
		'#player-container-outer{margin-right:0!important}#player-theater-container{margin-left:auto!important}',
		] [settings.align_player];
	const sizes = [0, 144, 240, 360, 480, 720];
	const size_norm = sizes [settings.default_player];
	if (size_norm)
		styles += `
			ytd-watch-flexy{--ytd-watch-flexy-min-player-height:${size_norm}px!important;--ytd-watch-flexy-max-player-height:${size_norm}px!important;--ytd-watch-flexy-max-player-width:var(--ytd-watch-flexy-min-player-width)!important}
			#primary{min-width:calc(max(var(--ytd-watch-flexy-min-player-height)*16/9,var(--ytd-watch-flexy-min-player-width)))!important;max-width:calc(max(var(--ytd-watch-flexy-min-player-height)*16/9,var(--ytd-watch-flexy-min-player-width)))!important}
			`;
	const size_theater = sizes [settings.theater_player];
	if (size_theater)
		styles += `ytd-watch-flexy:not([fullscreen])[theater] #player-theater-container{min-width:calc(${size_theater}px*var(--ytd-watch-flexy-width-ratio)/var(--ytd-watch-flexy-height-ratio))!important;max-width:calc(${size_theater}px*var(--ytd-watch-flexy-width-ratio)/var(--ytd-watch-flexy-height-ratio))!important;min-height:${size_theater}px!important;max-height:${size_theater}px!important;height:${size_theater}px!important}`;
	if (size_norm || size_theater) {
		function SetPlayerSize (sn, st) {
			const eq = document.getElementsByTagName ("ytd-watch-flexy");
			if (!eq.length)
				return;
			const s = eq [0].hasAttribute ('theater') ? st : sn;
			if (!s)
				return;
			const ep = document.getElementById ("movie_player");
			if (ep && ep.setInternalSize && ep.isFullscreen && ep.getPlayerSize && !ep.isFullscreen () && ep.getPlayerSize ().height != s)
				ep.setInternalSize ();
		    }
		inject_func.push (SetPlayerSize);
		inject_ints.push ({ call: SetPlayerSize, params: [size_norm, size_theater] });
		}
	if (settings.logo_target) {
		let url = settings.logo_target;
		if (url [0] != '/')
			url = '/' + url;
		url = document.location.origin + url;
		function SetLogoURL (url) {
			const l = document.querySelectorAll ('a#logo');
			for (let i = l.length; --i >= 0; ) {
				const Q = l [i];
				const D = Q.data;
				if (D && D.commandMetadata && Q.href != url) {
					Q.href = url;
					D.commandMetadata.webCommandMetadata.url = url;
				    }
				}
		    }
		inject_func.push (SetLogoURL);
		inject_ints.push ({ call: SetLogoURL, params: [url] });
		}
	if (settings.channel_top)
		styles += 'app-header#header.style-scope.ytd-c4-tabbed-header-renderer{transform:none!important;position:absolute;left:0px!important;top:0px;margin-top:0px}';
	if (settings.channel_top > 1)
		styles += `
			div#contentContainer.style-scope.app-header-layout{padding-top:148px!important}
			div#contentContainer.style-scope.app-header{height:148px!important}
			div.banner-visible-area.style-scope.ytd-c4-tabbed-header-renderer{display:none!important}
			`;
	if (settings.try_load_more) {
		function TryLoadMore () {
			const l = document.querySelectorAll ('#show-more-button');
			let i = l.length;
			if (--i >= 0 && l [i].hasAttribute ('hidden')) {
				l [i].removeAttribute ('hidden');
				l [i].innerText = 'TRY LOAD MORE';
				}
			while (--i >= 0)
				l [i].parentNode.removeChild (l [i]);
			}
		inject_func.push (TryLoadMore);
		inject_ints.push ({ call: TryLoadMore });
		styles += '#show-more-button{color:var(--yt-spec-call-to-action);width:100%;text-align:center;border:1px solid;padding:1em;cursor:pointer}';
		}
	if (settings.unbound_video_title)
		styles += '#video-title{max-height:none!important}';
	if (settings.video_quality) {
		function TryQuality (quality, qq, ep) {
			return qq.includes (quality) && (ep.setPlaybackQualityRange (quality, quality) || true);
			}
		function UpdateVideoQuality (det, st) {
			const ep = document.getElementById ("movie_player");
			if (!ep || !ep.getPreferredQuality || !ep.getAvailableQualityLevels || !ep.setPlaybackQualityRange || !ep.getVideoData || ep.getPreferredQuality () != 'auto')
				return;
			const vid = ep.getVideoData ().video_id;
			if (st.fail == vid)	// данное видео уже обработано
				return;
			const qq = ep.getAvailableQualityLevels ();
			if (!qq || !qq.length)
				return;
			switch (det) {	// intentional no breaks here
				case 1: if (TryQuality ('hd2160', qq, ep)) return;
				case 2: if (TryQuality ('hd1440', qq, ep)) return;
				case 3: if (TryQuality ('hd1080', qq, ep)) return;
				case 4: if (TryQuality ('hd720', qq, ep)) return;
				case 5: if (TryQuality ('large', qq, ep)) return;
				case 6: if (TryQuality ('medium', qq, ep)) return;
				case 7: if (TryQuality ('small', qq, ep)) return;
				case 8: if (TryQuality ('tiny', qq, ep)) return;
				}
			console.log ('Unknown video qualities in list: ', qq);
			st.fail = vid;
			};
		inject_func.push (TryQuality);
		inject_func.push (UpdateVideoQuality);
		inject_ints.push ({ call: UpdateVideoQuality, params: [settings.video_quality, {}] });
		}
	if (settings.no_resume_time) {
		styles += 'div.ytd-thumbnail-overlay-resume-playback-renderer{width:100%!important}';
		function removeTimesClearer (l) {
			l.href = l.href.replace (/&t=\d+s?/, '');
			}
		function removeTimes () {
			document.querySelectorAll ('a[href^="/watch?"][href*="&t="]').forEach (removeTimesClearer);
			}
		inject_func.push (removeTimesClearer);
		inject_func.push (removeTimes);
		inject_ints.push ({ call: removeTimes });
		}
	if (settings.remove_yt_redirect) {
		function removeRedirectClearer (l) {
			l.href = decodeURIComponent (l.href.replace (/^.*\?(.*&)q=([^&]+)(&.*)?$/, '$2'));
			}
		function removeRedirect () {
			document.querySelectorAll ('a[href^="https://www.youtube.com/redirect?"]').forEach (removeRedirectClearer);
			}
		inject_func.push (removeRedirectClearer);
		inject_func.push (removeRedirect);
		inject_ints.push ({ call: removeRedirect });
		}
	// "settings" button
	// can't store created button: Polymer overrides it's content on soft reload leaving tags in place
	// but can store element that Polymer does not know how to deal with and just drops
	function createSettingsButton (fix_version, st) {
		if (st.mark && st.mark.parentNode)
			return;
		let toolBar = document.getElementsByTagName ('ytd-topbar-menu-button-renderer');
		if (!toolBar.length)
			return;
		toolBar = toolBar [0];
		if (!toolBar)
			return;
		toolBar = toolBar.parentNode;
		const sb = document.createElement ('ytd-topbar-menu-button-renderer');	// ytd-notification-topbar-button-renderer
		sb.className = 'style-scope ytd-masthead style-default';				// style-scope ytd-masthead notification-button-style-type-default
		sb.setAttribute ('use-keyboard-focused', '');
		sb.setAttribute ('is-icon-button', '');
		sb.setAttribute ('has-no-text', '');
		toolBar.insertBefore (sb, toolBar.childNodes [0]);
		// div[id=notification-count][class=style-scope ytd-notification-topbar-button-renderer][innerHTML=...]
		const mark = document.createElement ('fix-settings-mark');
		mark.style = 'display:none';
		toolBar.insertBefore (mark, sb); // must be added to parent node of buttons in order to Polymer dropped it on soft reload
		st.mark = mark;
		const icb = document.createElement ('yt-icon-button');
		icb.id = 'button';
		icb.className = 'style-scope ytd-topbar-menu-button-renderer style-default';
		const tt = document.createElement ('tp-yt-paper-tooltip');
		tt.className = 'style-scope ytd-topbar-menu-button-renderer';
		tt.setAttribute ('role', 'tooltip');
		tt.setAttribute ('tabindex', '-1');
		tt.style = 'right:auto;bottom:auto';
		tt.appendChild (document.createTextNode ('YT fixes ' + fix_version));	// YT wraps content into DIV element
		const aa = document.createElement ('a');
		aa.className = 'yt-simple-endpoint style-scope ytd-topbar-menu-button-renderer';
		aa.setAttribute ('tabindex', '-1');
		aa.href = '/fix-settings';
		aa.appendChild (icb);
		aa.appendChild (tt);
		sb.getElementsByTagName ('div') [0].appendChild (aa); // created by YT scripts
		const bb = icb.getElementsByTagName ('button') [0]; // created by YT scripts
		bb.setAttribute ('aria-label', 'fixes settings');
		const ic = document.createElement ('yt-icon');
		ic.className = 'style-scope ytd-topbar-menu-button-renderer';
		bb.appendChild (ic);
		const gpath = document.createElementNS ('http://www.w3.org/2000/svg', 'path');
		gpath.className.baseVal = 'style-scope yt-icon';
		gpath.setAttribute ('d', 'M1 20l6-6h2l11-11v-1l2-1 1 1-1 2h-1l-11 11v2l-6 6h-1l-2-2zM2 20v1l1 1h1l5-5v-2h-2zM13 15l2-2 8 8v1l-1 1h-1zM15 14l-1 1 7 7h1v-1zM9 11l2-2-2-2 1.5-3-3-3h-2l3 3-1.5 3-3 1.5-3-3v2l3 3 3-1.5zM9 10l-2-2 1-1 2 2z');
		const svgg = document.createElementNS ('http://www.w3.org/2000/svg', 'g');
		svgg.className.baseVal = 'style-scope yt-icon';
		svgg.appendChild (gpath);
		const svg = document.createElementNS ('http://www.w3.org/2000/svg', 'svg');
		svg.className.baseVal = 'style-scope yt-icon';
		svg.setAttributeNS (null, 'viewBox', '0 0 24 24');
		svg.setAttributeNS (null, 'preserveAspectRatio', 'xMidYMid meet');
		svg.setAttribute ('focusable', 'false');
		svg.setAttribute ('style', 'pointer-events: none; display: block; width: 100%; height: 100%;');
		svg.appendChild (svgg);
		ic.appendChild (svg); // YT clears *ic
		}
	inject_func.push (createSettingsButton);
	inject_ints.push ({ call: createSettingsButton, params: [fix_version, {}] });
	// styles
	function AddStyles () {
		if (styles.length == 0)
			return;
		if (!document.head)
			return setTimeout (AddStyles, 1);
		const style_element = document.createElement ('style');
		style_element.setAttribute ('type', 'text/css');
		style_element.setAttribute ('id', 'ytfixstyle');
		style_element.innerHTML = styles;
		document.head.appendChild (style_element);
		}
	AddStyles ();
	// injection
	function InjectScripts () {
		if (inject_ints.length == 0)
			return;
		if (!document.head)
			return setTimeout (InjectScripts, 1);
		function InjectInterval () {
			for (let i = ints.length; --i >= 0; ) {
				const Q = ints [i];
				try { Q.call.apply (this, Q.params); }
				catch (e) { }
				}
			}
		let ss = '(function () {\n';
		for (let i = inject_func.length; --i >= 0; )
			ss += `${inject_func [i]}\n`;
		ss += 'let ints = [\n';
		for (let i = inject_ints.length; --i >= 0; ) {
			const Q = inject_ints [i];
			ss += `{ call: ${Q.call.name}, params: ${JSON.stringify (Q.params || [])} },\n`;
			}
		ss += `];\n${InjectInterval}\nsetInterval (InjectInterval, 1000);\nconsole.log ("Fixes injected");\n}) ();`;
		const sse = document.createElement ('script');
		sse.setAttribute ('id', 'ytfixscript');
		sse.appendChild (document.createTextNode (ss));
		document.head.appendChild (sse);
		}
	InjectScripts ();
	console.log ('Fixes loaded');
	}) ();