Youtube polymer engine fixes

Some fixes for Youtube polymer engine

当前为 2021-08-09 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Youtube polymer engine fixes
// @description  Some fixes for Youtube polymer engine
// @namespace    bo.gd.an[at]rambler.ru
// @version      2.10.1
// @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      For personal use only
// @require      https://greasyfork.org/scripts/427473-object-setdefault/code/object_setDefault.js?version=937466
// ==/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 (!settings)
			settings = GM_getValue ('settings', {});
		else {
			saver ();
			ls.removeItem ('__fix__settings__');
			}
		settings.storage = 'GM_*Value';
		}
	else if (typeof (GM) !== 'undefined' && GM && GM.getValue && GM.setValue) {
		saver = function () {
			(async () => await GM.setValue ('settings', settings)) ();
			};
		if (!settings)
			settings = (async () => await GM.getValue ('settings', {})) ();
		else {
			saver ();
			ls.removeItem ('__fix__settings__');
			}
		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;
		}
	if ("wide_description" in settings) {	// удалено в 2.9.1
		settings.description_width = settings.wide_description ? 1 : 0;
		delete settings.wide_description;
		}
	// 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;
		settings.setDefault ("inst_ver", fix_version);
		}
	settings.setDefault ("align_player", 0);
	settings.setDefault ("default_player", 0);
	settings.setDefault ("hide_guide", true);
	settings.setDefault ("hide_yt_suggested_blocks", true);
	settings.setDefault ("logo_target", "");
	settings.setDefault ("fix_removed_placeholder", true);
	settings.setDefault ("theater_player", 0);
	settings.setDefault ("thumbnail_size", 2);
	settings.setDefault ("thumbnail_size_m", 720);
	settings.setDefault ("unfix_header", true);
	settings.setDefault ("search_thumbnail", 0);
	settings.setDefault ("clear_search", false);
	settings.setDefault ("channel_top", 0);
	settings.setDefault ("try_load_more", false);
	settings.setDefault ("unbound_video_title", false);
	settings.setDefault ("video_quality", 0);
	settings.setDefault ("no_resume_time", false);
	settings.setDefault ("remove_yt_redirect", false);
	settings.setDefault ("exact_view_count", false);
	settings.setDefault ("resume_bar_handling", settings.no_resume_time ? 1 : 0);
	settings.setDefault ("watched_grayscale", 0);
	settings.setDefault ("watched_blur", 0);
	settings.setDefault ("disable_player_click_overlay", false);
	settings.setDefault ("description_width", 0);
	settings.setDefault ("simpler_fullscreen", false);
	settings.setDefault ('clear_link_pp', 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_line fieldset {border:1px solid #ccc;padding:0 0.5em}
			.ytfix_line legend {padding:0 0.5em}
			.ytfix_line img {margin:0 1em}
			.ytfix_hide{display:none}
			.ytfixback{position:absolute;left:0;top:0;right:0;height:100%;background:#eee}
			.ytfix_slide_base {position:relative;margin-right:1em;display:inline-block;height:1em}
			.ytfix_slide_bar {position:absolute;left:0;right:0;top:0.3em;bottom:0.3em;background:#ddd;border:1px solid #ccc}
			.ytfix_slide_stroke {position:absolute;width:1px;top:0.1em;bottom:0.1em;background:#ccc}
			.ytfix_slide_arrow {position:absolute;width:9px;bottom:0;top:0;background:#ddd;border:1px solid #aaa;border-radius:0.5em}
			.ytfix_tabs {position:relative}
			.ytfix_tabs > div {display:none;border:1px solid #ccc}
			.ytfix_tabs > input {display:none}
			.ytfix_tabs > label {display:inline-block;background:#ddd;border:1px solid #ccc;padding:0.5em 1em;position:relative;top:1px}
			.ytfix_tabs > label ~ label {border-left:none}
			.ytfix_tabs > input:checked + label {background-color:#eee;border-bottom:1px solid #eee}
			`;
		plane.appendChild (style);
		function AddLine (plane) {
			const q = document.createElement ('div');
			q.className = 'ytfix_line';
			for (let i = 1, L = arguments.length; i < L; ++i)
				q.appendChild (arguments [i]);
			plane.appendChild (q);
			return q;
			}
		let e1, e2;
		e1 = document.createElement ('b');
		e1.appendChild (document.createTextNode ('YouTube Polymer Fixes: Settings'));
		AddLine (plane, e1);
		if (fix_version) {
			e1 = document.createElement ('b');
			e1.appendChild (document.createTextNode (`Version: ${fix_version}`));
			AddLine (plane, e1);
			}
		if (!saver) {
			e1 = document.createElement ('span');
			e1.style = 'color:red';
			e1.appendChild (document.createTextNode ('Cannot edit settings: no access to any storage.'));
			AddLine (plane, e1);
			e1 = document.createElement ('span');
			e1.appendChild (document.createTextNode ('If you are using Firefox, allow cookies for this site.'));
			AddLine (plane, e1);
			}
		else {
			const ess = {};
			function MakeDesc (desc, extra) {
				const e = document.createElement ('span');
				e.appendChild (document.createTextNode (desc));
				if (extra) {
					if (extra.style)
						e.style = style;
					if (extra.note) {
						const n = document.createElement ('sup');
						n.appendChild (document.createTextNode (extra.note));
						e.appendChild (n);
						}
					}
				return e;
				}
			function MakeNote (num, desc, extra) {
				const e = document.createElement ('span');
				if (num) {
					const n = document.createElement ('sup');
					n.appendChild (document.createTextNode (num));
					e.appendChild (n);
					}
				e.appendChild (document.createTextNode (desc));
				if (extra && extra.style)
					e.style = 'font-size:0.75em;' + extra.style;
				else
					e.style = 'font-size:0.75em';
				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 MakeSlider (nm, width, snap, steps) {
				let desc = { value : -1, mouse : false };
				const e = document.createElement ('div');
				e.className = 'ytfix_slide_base';
				e.style.width = `${width*snap*steps+1}px`;
				const b = document.createElement ('div');
				b.className = 'ytfix_slide_bar';
				e.appendChild (b);
				for (let x = width * snap * steps; x >= 0; x -= width * snap) {
					const s = document.createElement ('div');
					s.className = 'ytfix_slide_stroke';
					s.style.left = `${x}px`;
					e.appendChild (s);
					}
				const a = document.createElement ('div');
				a.className = 'ytfix_slide_arrow';
				e.appendChild (a);
				const i = document.createElement ('input');
				i.className = 'ytfix_field';
				i.type = 'number';
				i.style.width = `${(snap*steps).toString().length+2}em`;
				i.min = 0;
				i.max = snap * steps;
				i.step = 1;
				function UpdateValue (newvalue) {
					if (newvalue < 0)
						newvalue = 0;
					else if (newvalue > snap * steps)
						newvalue = snap * steps;
					if (newvalue == desc.value)
						return;
					desc.value = newvalue;
					a.style.left = `${desc.value*width-5}px`;
					i.value = desc.value;
					if (desc.callback)
						desc.callback (desc);
					}
				UpdateValue (settings [nm]);
				e.addEventListener ('mousedown', function (ev) {
					if (ev.buttons != 1)
						return;
					desc.mouse = ev.target === a;
					if (desc.mouse)
						return;
					let sliderRect = a.getBoundingClientRect ();
					if (ev.clientX <= sliderRect.left)
						UpdateValue (desc.value - snap);
					else if (ev.clientX > sliderRect.right)
						UpdateValue (desc.value + snap);
					});
				e.addEventListener ('mousemove', function (ev) {
					if (ev.buttons != 1 || !desc.mouse)
						return;
					let mx = ev.clientX - e.getBoundingClientRect ().left;
					mx += (width * snap) >> 1;
					mx -= mx % (width * snap);
					UpdateValue (mx / width);
					});
				i.addEventListener ('input', function () {
					if (/^\d+$/.test (i.value))
						UpdateValue (parseInt (i.value));
					});
				desc.base = e;
				desc.input = i;
				ess [nm] = desc;
				return desc;
				}
			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;
				}
			const tabs_data = [];
			function MakeTab (name, text, checked) {
				const inp = document.createElement ('input');
				inp.type = 'radio';
				inp.id = name;
				inp.name = 'tabs';
				if (checked)
					inp.setAttribute ('checked', '');
				const lbl = document.createElement ('label');
				lbl.setAttribute ('for', name);
				lbl.appendChild (document.createTextNode (text));
				const cont = document.createElement ('div');
				cont.id = name + '_cont';
				style.innerHTML += `.ytfix_tabs > input#${name}:checked ~ div#${name}_cont {display:block}`;
				tabs_data.push ({ inp: inp, lbl: lbl, cont: cont });
				return cont;
				}
			const tab_gen = MakeTab ('tab_gen', 'General', true);
			const tab_front = MakeTab ('tab_front', 'Front page', false);
			const tab_search = MakeTab ('tab_search', 'Search', false);
			const tab_video = MakeTab ('tab_video', 'Video', false);
			const tab_channel = MakeTab ('tab_channel', 'Channel', false);
			const tabs_data_2 = [plane];
			tabs_data.forEach ((x) => tabs_data_2.push (x.inp, x.lbl));
			const tabbf = document.createElement ('div');
			tabbf.style = 'display:block;width:1px;height:2px;border-width:0 0 0 1px;position:absolute';
			tabs_data_2.push (tabbf);
			tabs_data.forEach ((x) => tabs_data_2.push (x.cont));
			const tabs = AddLine.apply (this, tabs_data_2);
			tabs.className += ' ytfix_tabs';

			AddLine (tab_gen, MakeBoolElement ("hide_guide"), MakeDesc ('Hide "Guide" menu when page opens'));
			AddLine (tab_video, 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 (tab_front, MakeDesc ('Set thumbnails width'), tsi, tsm);
			AddLine (tab_search, MakeDesc ('Set thumbnails width'), MakeListElement ("search_thumbnail", ['default', '240px', '360px']));
			AddLine (tab_video, MakeDesc ("Set player height in default mode"), MakeListElement ("default_player", ['default', '144px', '240px', '360px', '480px', '720px']));
			AddLine (tab_video, MakeDesc ("Set player height in theater mode"), MakeListElement ("theater_player", ['default', '144px', '240px', '360px', '480px', '720px']));
			AddLine (tab_front, MakeBoolElement ("hide_yt_suggested_blocks"), MakeDesc ('Hide suggestions blocks (recommended playlists, latest posts, etc.)'));
			AddLine (tab_search, MakeBoolElement ("clear_search"), MakeDesc ("Hide suggestions blocks (for you, people also watched, etc.)"));
			AddLine (tab_gen, MakeBoolElement ("unfix_header"), MakeDesc ("Unstick header bar from top of the screen"));
			AddLine (tab_video, MakeDesc ("Align resized player into it's container (normal and theater modes)"), MakeListElement ("align_player", ['center', 'left', 'right']));
			AddLine (tab_channel, MakeDesc ("Channel banner behaviour"), MakeListElement ('channel_top', ['default', 'hide banner with scrolling', 'hide banner entirely']));
			AddLine (tab_gen, MakeBoolElement ('try_load_more'), MakeDesc ('Add button to try loading more content on pages with dynamic content load'));
			AddLine (tab_gen, MakeBoolElement ('unbound_video_title'), MakeDesc ('Remove size limit for video titles'));
			AddLine (tab_gen, MakeDesc ("Change YT logo target to https://www.youtube.com/..."), MakeTextElement ("logo_target"));
			AddLine (tab_gen, MakeBoolElement ("remove_yt_redirect"), MakeDesc ('Remove YT tracking from links (/redirect?...)'));
			AddLine (tab_gen, MakeBoolElement ("no_resume_time"), MakeDesc ('Remove resume time from the video links (&t=...)'));
			AddLine (tab_gen, MakeDesc ('Video resume bar (red)'), MakeListElement ("resume_bar_handling", ['depending on resule time (default)', 'full width of thumbnail', 'hide']));
			const wwfs = document.createElement ('fieldset');
			const wwl = wwfs.appendChild (document.createElement ('legend'));
			wwl.appendChild (document.createTextNode ('Watched video thumbnails modification'));
			const wwt = wwfs.appendChild (document.createElement ('table')).appendChild (document.createElement ('tr'));
			const wwc1 = wwt.appendChild (document.createElement ('td'));
			const wwgs = MakeSlider ('watched_grayscale', 2, 10, 10);
			AddLine (wwc1, MakeDesc ('Grayscale, %'), wwgs.base, wwgs.input);
			const wwb = MakeSlider ('watched_blur', 50, 1, 4);
			AddLine (wwc1, MakeDesc ('Blur, px'), wwb.base, wwb.input);
			AddLine (wwc1, MakeNote (0, 'Options require user to be logged into YT account'));
			AddLine (wwc1, MakeNote (0, 'Sample image taken from https://unsplash.com/photos/n6TWNDfyPwk'));
			const wwc2 = wwt.appendChild (document.createElement ('td'));
			wwc2.style.textAlign = 'center';
			wwc2.appendChild (document.createTextNode ('Example'));
			wwc2.appendChild (document.createElement ('br'));
			wwc2.appendChild (document.createElement ('img')).src = 'https://picsum.photos/id/197/267/178';
			const wwc3 = wwt.appendChild (document.createElement ('td'));
			wwc3.style.textAlign = 'center';
			wwc3.appendChild (document.createTextNode ('Modified example'));
			wwc3.appendChild (document.createElement ('br'));
			const wwc3i = wwc3.appendChild (document.createElement ('img'));
			wwc3i.src = 'https://picsum.photos/id/197/267/178';
			function UpdateFilters () {
				wwc3i.style.filter = `grayscale(${wwgs.value}%)blur(${wwb.value}px)`;
				}
			UpdateFilters ();
			wwgs.callback = UpdateFilters;
			wwb.callback = UpdateFilters;
			AddLine (tab_gen, wwfs);
			AddLine (tab_video, MakeDesc ('Starting video quality'), MakeListElement ('video_quality', ['Auto (default)', '2160p (4K)', '1440p (HD)', '1080p (HD)', '720p', '480p', '360p', '240p', '144p']));
			AddLine (tab_gen, MakeBoolElement ("exact_view_count"), MakeDesc ('Show exact view counts in video descriptions'));
			AddLine (tab_video, MakeBoolElement ("disable_player_click_overlay"), MakeDesc ('Remove rewinding overlay'));
			AddLine (tab_video, MakeDesc ("Video description width (including suggested videos column)"), MakeListElement ("description_width", ['default', 'stretch', '1200px', '1280px', '1360px', '1440px', '1520px', '1600px', '1680px', '1760px', '1840px', '1920px']));
			AddLine (tab_video, MakeBoolElement ("simpler_fullscreen"), MakeDesc ("Simplify fullscreen (no video description, comments, etc.)"));
			AddLine (tab_gen, MakeBoolElement ("clear_link_pp"), MakeDesc ('Remove &pp= from links'));
			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;
				settings.exact_view_count = ess.exact_view_count.checked;
				settings.resume_bar_handling = ess.resume_bar_handling.selectedIndex;
				settings.watched_grayscale = ess.watched_grayscale.value;
				settings.watched_blur = ess.watched_blur.value;
				settings.disable_player_click_overlay = ess.disable_player_click_overlay.checked;
				settings.description_width = ess.description_width.selectedIndex;
				settings.simpler_fullscreen = ess.simpler_fullscreen.checked;
				settings.clear_link_pp = ess.clear_link_pp.checked;
				saver ();
				alert ('Settings saved');
				history.back ();
				});
			e2 = MakeButton ('Return to YouTube without saving', function () {
				history.back ();
				});
			AddLine (plane, 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 (plane, 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:not([fullscreen]):not([simple-fullscreen]):not([theater]){--ytd-watch-flexy-min-player-height:${size_norm}px!important;--ytd-watch-flexy-max-player-height:${size_norm}px!important;--ytd-watch-flexy-min-player-width:calc(${size_norm}px*var(--ytd-watch-flexy-width-ratio)/var(--ytd-watch-flexy-height-ratio))!important;--ytd-watch-flexy-max-player-width:var(--ytd-watch-flexy-min-player-width)!important}
			ytd-watch-flexy:not([fullscreen]):not([simple-fullscreen]):not([theater]) div#player-container-outer.ytd-watch-flexy{height:var(--ytd-watch-flexy-max-player-height);min-width:calc(${size_norm}px*var(--ytd-watch-flexy-width-ratio)/var(--ytd-watch-flexy-height-ratio))!important}
			`;
	const size_theater = sizes [settings.theater_player];
	if (size_theater)
		styles += `
			ytd-watch-flexy:not([fullscreen]):not([simple-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}
			ytd-watch-flexy:not([fullscreen]):not([simple-fullscreen])[theater] div#movie_player{height:${size_theater}px;width:calc(${size_theater}px*var(--ytd-watch-flexy-width-ratio)/var(--ytd-watch-flexy-height-ratio))}
			`;
	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;-webkit-line-clamp: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.resume_bar_handling)
		styles += [
			'',
			'div.ytd-thumbnail-overlay-resume-playback-renderer{width:100%!important}',
			'ytd-thumbnail-overlay-resume-playback-renderer{display:none!important}'
			] [settings.resume_bar_handling];
	let watched_filter = '';
	if (settings.watched_grayscale)
		watched_filter += `grayscale(${settings.watched_grayscale}%)`;
	if (settings.watched_blur)
		watched_filter += `blur(${settings.watched_blur}px)`;
	if (watched_filter)
		styles += `a[href*="/watch?"]:not([href^="/watch?"]).ytd-thumbnail img {filter:${watched_filter}}`;
	if (settings.no_resume_time || watched_filter) {
		function removeTimes (replace) {
			function removeTimesClearer (l) {
				while (l && l.tagName != 'A')
					l = l.parentNode;
				if (l)
					l.href = l.href.replace (/&t=\d+s?/, replace);
				}
			document.querySelectorAll ('a[href^="/watch?"] div.ytd-thumbnail-overlay-resume-playback-renderer').forEach (removeTimesClearer);	// основное применение
			document.querySelectorAll ('a[href^="/watch?"][href*="&t="]').forEach (removeTimesClearer);	// на случай прочих ссылок
			}
		inject_func.push (removeTimes);
		inject_ints.push ({ call: removeTimes, params: [settings.no_resume_time ? '' : '$1'] });
		}
	if (settings.remove_yt_redirect) {
		function removeRedirectClearer (l) {
			l.href = decodeURIComponent (l.href.replace (/^.*\?(.*&)q=([^&]+)(&.*)?$/, '$2'));
			if (l.data && l.data.urlEndpoint)
				l.data.urlEndpoint.url = l.href;
			}
		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 });
		}
	if (settings.exact_view_count) {
		function getCounterText (x) {
			try { return x.__data.data.viewCountText.simpleText; } catch (ex) { }
			try { return x.__data.data.content.videoRenderer.viewCountText.simpleText; } catch (ex) { }
			//console.log ('no viewers text found for:', x);
			return undefined;
			}
		function replaceCountersText (x) {
			const par = x.parentNode.__ytfix_parent;
			if (!par)
				return;
			const tgt = getCounterText (par);
			if (tgt && x.textContent != tgt)
				x.textContent = tgt;
			}
		function replaceCountersCallback (mm) {
			for (let i = mm.length; --i >= 0; ) {
				const m = mm [i];
				if (m.type == 'characterData')
					replaceCountersText (m.target);
				}
			}
		function replaceCounters (state) {
			if (!state.m) {
				state.m = new MutationObserver (replaceCountersCallback);
				state.opt = { subtree: true, characterData: true };
				}
			function replaceCountersEach (x) {
				x.setAttribute ('ytfix', '');
				const ee = x.querySelectorAll ('#metadata-line span');
				if (ee.length != 2)
					return;
				const e = ee [0];
				e.__ytfix_parent = x;
				replaceCountersText (e.firstChild);
				state.m.observe (e, state.opt);
				}
			document.querySelectorAll ('ytd-compact-video-renderer:not([ytfix])').forEach (replaceCountersEach);
			document.querySelectorAll ('ytd-grid-video-renderer:not([ytfix])').forEach (replaceCountersEach);
			document.querySelectorAll ('ytd-rich-item-renderer:not([ytfix])').forEach (replaceCountersEach);
			document.querySelectorAll ('ytd-video-renderer:not([ytfix])').forEach (replaceCountersEach);
			}
		inject_func.push (getCounterText);
		inject_func.push (replaceCountersText);
		inject_func.push (replaceCountersCallback);
		inject_func.push (replaceCounters);
		inject_ints.push ({ call: replaceCounters, params: [{}] });
		}
	if (settings.disable_player_click_overlay)
		styles += 'div.ytp-doubletap-ui,div.ytp-doubletap-ui-legacy {display:none}';
	if (settings.description_width) {
		const w = [0, '100vw', '1200px', '1280px', '1360px', '1440px', '1520px', '1600px', '1680px', '1760px', '1840px', '1920px'] [settings.description_width];
		styles += `
			ytd-app:not([mini-guide-visible_]) ytd-page-manager {--ytf-width:calc(${w} - 20px);min-width:100%}
			ytd-app[mini-guide-visible_]  ytd-page-manager {--ytf-width:calc(${w} - 92px);min-width:calc(100% - 92px)}
			ytd-watch-flexy[flexy][fullscreen] {min-width:100%!important}
			ytd-watch-flexy[flexy] #columns {--ytd-watch-flexy-min-player-width:calc(var(--ytf-width) - var(--ytd-watch-flexy-sidebar-width) - 3 * var(--ytd-margin-6x));min-width:var(--ytf-width)!important;max-width:var(--ytf-width)!important}
			`;
		}
	if (settings.simpler_fullscreen) {
		styles += 'button.ytp-fullerscreen-edu-button{display:none!important}';
		function InitFullscreenData (state) {
			let dd = false;
			if (document.exitFullscreen)
				dd = document.fullscreenEnabled && { enter: 'requestFullscreen', exit: 'exitFullscreen', check: 'fullscreenElement', event: 'fullscreenchange', type: 'standart' };
			else if (document.mozCancelFullScreen)
				dd = document.mozFullScreenEnabled && { enter: 'mozRequestFullScreen', exit: 'mozCancelFullScreen', check: 'mozFullScreenElement', event: 'mozfullscreenchange', type: 'mozilla' };
			else if (document.webkitExitFullscreen)
				dd = document.webkitFullscreenEnabled && { enter: 'webkitRequestFullscreen', exit: 'webkitExitFullscreen', check: 'webkitFullscreenElement', event: 'webkitfullscreenchange', type: 'webkit' };
			else if (document.msExitFullscreen)
				dd = document.msFullscreenEnabled && { enter: 'msRequestFullscreen', exit: 'msExitFullscreen', check: 'msFullscreenElement', event: 'MSFullscreenChange', type: 'microsoft' };
			if (!dd)
				return console.log ('unable to determine fullscreen API prefix or fullscreen API disabled');
			console.log ('detected fullscreen API type:', dd.type);
			state.keys = dd;
			state.s = 1;
			document.addEventListener (dd.event, function () {
				const enter = !!document [dd.check];
				document.getElementById ("movie_player").setFauxFullscreen (enter);
				for (const q of document.querySelectorAll ('ytd-watch-flexy'))
					q [enter ? 'setAttribute' : 'removeAttribute'] ('simple-fullscreen', '');
				});
			}
		function CatchFullscreenButton (state) {
			if (state.s != 1)
				return;
			for (const p of document.querySelectorAll ('button.ytp-fullscreen-button:not([ytfix])')) {
				const q = document.createElement ('button');
				q.className = p.className;
				p.parentNode.appendChild (q);
				q.appendChild (p.querySelector ('svg'));
				q.setAttribute ('title', 'Full screen');
				p.style.display = 'none';
				p.disabled = true;
				p.setAttribute ('ytfix', '');
				q.setAttribute ('ytfix', '');
				q.addEventListener ('mousedown', function (event) {
					event.preventDefault ();
					});
				q.addEventListener ('click', function (event) {
					if (document [state.keys.check])
						document [state.keys.exit] ();
					else {
						let w = event.target;
						while (w && w.tagName != "YTD-PLAYER")
							w = w.parentNode;
						if (w)
							w [state.keys.enter] ();
						}
					});
				}
			}
		inject_func.push (InitFullscreenData);
		inject_func.push (CatchFullscreenButton);
		inject_ints.push ({ init: InitFullscreenData, call: CatchFullscreenButton, params: [{ s: 0 }] });
		}
	if (settings.clear_link_pp) {
		function clearLinkPP (replace) {
			function clearLink (l) {
				l.setAttribute ('href', l.getAttribute ('href').replace (/&pp=[^&]*/, ''));
				}
			document.querySelectorAll ('a[href*="/watch?"][href*="&pp="]').forEach (clearLink);
			}
		inject_func.push (clearLinkPP);
		inject_ints.push ({ call: clearLinkPP });
		}
	// "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 InitInterval () {
			for (let i = ints.length; --i >= 0; ) {
				const Q = ints [i];
				if (!Q.init)
					continue;
				try { Q.init.apply (this, Q.params); }
				catch (e) { }
				}
			}
		function InjectInterval () {
			for (let i = ints.length; --i >= 0; ) {
				const Q = ints [i];
				if (!Q.call)
					continue;
				try { Q.call.apply (this, Q.params); }
				catch (e) { console.log (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];
			if (Q.call && Q.init)
				ss += `{ init: ${Q.init.name}, call: ${Q.call.name}, params: ${JSON.stringify (Q.params || [])} },\n`;
			else if (Q.call)
				ss += `{ call: ${Q.call.name}, params: ${JSON.stringify (Q.params || [])} },\n`;
			else if (Q.init)
				ss += `{ init: ${Q.init.name}, params: ${JSON.stringify (Q.params || [])} },\n`;
			}
		ss += `];\n(${InitInterval})();\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');
	}) ();