Add YouTube Video Progress

Adds a progress bars (or dots) at bottom of video, a progress text which includes video quality and subtitle indicators, and a chapter title box on the YouTube video page.

  1. // ==UserScript==
  2. // @name Add YouTube Video Progress
  3. // @namespace https://gf.qytechs.cn/en/users/85671-jcunews
  4. // @version 1.20.139
  5. // @license GNU AGPLv3
  6. // @author jcunews
  7. // @description Adds a progress bars (or dots) at bottom of video, a progress text which includes video quality and subtitle indicators, and a chapter title box on the YouTube video page.
  8. // @match https://www.youtube.com/*
  9. // @grant GM_getValue
  10. // @grant GM_registerMenuCommand
  11. // @grant GM_setValue
  12. // @grant unsafeWindow
  13. // @inject-into page
  14. // @run-at document-start
  15. // ==/UserScript==
  16.  
  17. /*
  18. Changelog (this version only. Full list is gf.qytechs.cn):
  19. - Updated media formats support.
  20.  
  21. Notes:
  22. - On the progress text, the current video quality will have a "+" suffix if there's a higher one available.
  23. - Hovering the mouse cursor onto the video quality text will show the current and available video quality IDs and short description.
  24. - A "[C]" text may be shown to indicate subtitle availability, and hovering the mouse over it will show the available subtitle languages.
  25. - For videos which have 60fps and/or HDR video qualities, the listed available video qualities only includes the highest quality ones.
  26. The video player may present a lower video quality depending on the network speed, web browser capability, and user account setting.
  27. - Hovering the chapter box will display the previous and/or next chapter titles.
  28. - Hovering the mouse over the publication date will show a tooltip containing the complete publication date in local time, considering
  29. that YouTube may only shows how long the video was published. If the video was live streamed, it will contain the starting and
  30. ending recording time.
  31. - For new YouTube layout only.
  32. - TamperMonkey users should change the TamperMonkey's "Inject Mode" setting to "Instant" for more reliable script result.
  33. */
  34.  
  35. (window => {
  36.  
  37. //===== CONFIGURATION BEGIN =====
  38.  
  39. //*** Default settings (configurable via GUI) ***
  40. var progressbarAutohide = true; //autohide progressbar when YouTube video controls is visible
  41. var progressbarBelowVideo = false; //show progressbar below video frame instead of within video frame
  42. var progressbarDotStyle = false; //show graphical progress as dots instead of bars
  43. var progressbarHeight = 3; //in pixels
  44. var progressbarColor = "rgb(0,0,0,0.2)"; //e.g. opaque: "#fff" or "#e0e0e0" or "cyan"; or semi-transparent: "rgb(0,0,0,0.3)" (i.e. 30% opaque)
  45. var progressbarElapsedColor = "rgb(255,0,0,0.7)";
  46. var progressbarBufferColor = "rgb(51,119,255,0.7)";
  47. var progressbarDotStyleSize = 4; //Dot size (for width & height) in pixels if dot style is enabled
  48. var progressTextEnabled = true; //enable showing progress text including chapter title
  49. var progressTextInDocTitle = false; //also include progress text in document (browser tab) title
  50. var subtitleLanguageCode = "en"; //2-letters language code for select preferred subtitle language hotkey
  51.  
  52. //*** Internal settings (not configurable via GUI) ***
  53. var contentLoadProcessDelay = 0; //number of milliseconds before processing dynamically loaded contents (increase if slow network/browser)
  54. var progressTextStyles = "display:inline-block;vertical-align:top;border:1px solid #ccc;border-radius:4px;padding:2px;background:#eee;text-align:center;white-space:nowrap;font-size:9pt;line-height:normal";
  55. //styles override for progress text in YouTube Dark Mode
  56. var progressTextStylesDark = "border:1px solid #bbb;background:#111;color:#bbb";
  57. //style rules override for configuration GUI in YouTube Dark Mode
  58. var configGuiStylesDark = `
  59. #ayvpPopup{border-color:#bbb;background:#111;color:#bbb}
  60. #ayvpCaption{background:#333;color:#ddd;font-weight:bold}
  61. #ayvpContent input,
  62. #ayvpControls button{background:#333;color:#bbb}
  63. `;
  64. //swap functionality of Click and Ctrl+Click on subtitle indicator
  65. //false: Click = toggle subtitle; Ctrl+Click = use preferred subtitle
  66. //true : Click = use preferred subtitle; Ctrl+Click = toggle subtitle
  67. var swapSubtitleFunction = true;
  68.  
  69. //===== CONFIGURATION END =====
  70.  
  71. var timerWaitInfo, timerProgressMonitor, timerWaitPlayer, timerDoubleCheck, vplayer, eleProgressText, eleChapter, fmtMaps = {};
  72. var resNums = {
  73. "light" : "144p", //(old ID)
  74. "tiny" : "144p",
  75. "small" : "240p",
  76. "medium" : "360p", //nHD
  77. "large" : "480p", //WNTSC
  78. "hd720" : "720p", //HD 1K
  79. "hd1080" : "1080p", //FHD 2K
  80. "hd1440" : "1440p", //QHD
  81. "hd2160" : "2160p", //UHD 4K
  82. "hd2880" : "2880p", //UHD+ 5K
  83. "highres": "4320p", //FUHD 8K (YouTube's highest resolution [2019 April])
  84. "hd6480" : "6480p", //(fictional ID for 12K. Just in case...)
  85. "hd8640" : "8640p" //(fictional ID for QUHD 16K. Just in case...)
  86. };
  87. var resDescs = {
  88. "light" : "light\xa0(144p\xa0~QCIF)", //(old ID)
  89. "tiny" : "tiny\xa0(144p\xa0~QCIF)",
  90. "small" : "small\xa0(240p\xa0~SIF)",
  91. "medium" : "medium\xa0(360p\xa0nHD)",
  92. "large" : "large\xa0(480p\xa0WNTSC)",
  93. "hd720" : "hd720\xa0(720p\xa0HD\xa01K)",
  94. "hd1080" : "hd1080\xa0(1080p\xa0FHD\xa02K)",
  95. "hd1440" : "hd1440\xa0(1440p\xa0QHD)",
  96. "hd2160" : "hd2160\xa0(2160p\xa0UHD\xa04K)",
  97. "hd2880" : "hd2880\xa0(2880p\xa0UHD+\xa05K)",
  98. "highres": "highres\xa0(4320p\xa0FUHD 8K)", //YouTube's highest resolution [2019 April]
  99. "hd6480" : "hd6480\xa0(6480p\xa012K)", //fictional ID for 12K. Just in case...
  100. "hd8640" : "hd8640\xa0(8640p\xa0QUHD\xa016K)" //fictional ID for QUHD 16K. Just in case...
  101. };
  102. var fmts = [
  103. ['3GP', 'MP4V', [13,17,36]],
  104. ['FLV', 'H263', [5,6]],
  105. ['FLV', 'H264', [34,35]],
  106. ['MP4', 'H264', [18,22,37,38,59,78,82,83,84,85,91,92,93,94,95,96,132,133,134,135,136,137,138,151,160,212,216,229,230,231,232,264,266,269,270,298,299,304,305,311,312,597]],
  107. ['MP4', 'AV1', [394,395,396,397,398,399,400,401,402,571,694,695,696,697,698,699,700,701,702]],
  108. ['WebM', 'VP8', [43,44,45,46,100,101,102,167,168,169,170,218,219]],
  109. ['WebM', 'VP9', [242,243,244,245,246,247,248,271,272,278,302,303,308,313,315,598,602,603,604,605,606,609,612,614,616,617,620,623,625,628]],
  110. ['WebM', 'VP9.2', [330,331,332,333,334,335,336,337,338,339]], //HDR. 388 & 339 for 2880p & 4320p are assumed. may actually be incorrect.
  111. ['M4A', 'AAC', [139,140,141,256,258]],
  112. ['M4A', 'DTS-ES', [325]],
  113. ['M4A', 'AC-3', [328]],
  114. ['WebM', 'Vorbis', [171,172]],
  115. ['WebM', 'Opus', [249,250,251]]
  116. ];
  117. fmts.forEach(a => a[2].forEach(f => fmtMaps[f] = [a[0], a[1]]));
  118.  
  119. var langList = ("\
  120. Afrikaans,af;Albanian,sq;Amharic,am;Arabic,ar;Armenian,hy;Azerbaijani,az;Bangla,bn;Basque,eu;Belarusian,be;Bosnian,bs;\
  121. Bulgarian,bg;Burmese,my;Catalan,ca;Cebuano,ceb;Chinese (Simplified),zh-Hans;Chinese (Traditional),zh-Hant;Corsican,co;\
  122. Croatian,hr;Czech,cs;Danish,da;Dutch,nl;English,en;Esperanto,eo;Estonian,et;Filipino,fil;Finnish,fi;French,fr;Galician,gl;\
  123. Georgian,ka;German,de;Greek,el;Gujarati,gu;Haitian Creole,ht;Hausa,ha;Hawaiian,haw;Hebrew,iw;Hindi,hi;Hmong,hmn;Hungarian,hu;\
  124. Icelandic,is;Igbo,ig;Indonesian,id;Irish,ga;Italian,it;Japanese,ja;Javanese,jv;Kannada,kn;Kazakh,kk;Khmer,km;Kinyarwanda,rw;\
  125. Korean,ko;Kurdish,ku;Kyrgyz,ky;Lao,lo;Latin,la;Latvian,lv;Lithuanian,lt;Luxembourgish,lb;Macedonian,mk;Malagasy,mg;Malay,ms;\
  126. Malayalam,ml;Maltese,mt;Maori,mi;Marathi,mr;Mongolian,mn;Nepali,ne;Norwegian,no;Nyanja,ny;Odia,or;Pashto,ps;Persian,fa;\
  127. Polish,pl;Portuguese,pt;Punjabi,pa;Romanian,ro;Russian,ru;Samoan,sm;Scottish Gaelic,gd;Serbian,sr;Shona,sn;Sindhi,sd;\
  128. Sinhala,si;Slovak,sk;Slovenian,sl;Somali,so;Southern Sotho,st;Spanish,es;Sundanese,su;Swahili,sw;Swedish,sv;Tajik,tg;\
  129. Tamil,ta;Tatar,tt;Telugu,te;Thai,th;Turkish,tr;Turkmen,tk;Ukrainian,uk;Urdu,ur;Uyghur,ug;Uzbek,uz;Vietnamese,vi;Welsh,cy;\
  130. Western Frisian,fy;Xhosa,xh;Yiddish,yi;Yoruba,yo;Zulu,zu").split(";").map(a => a.split(","));
  131.  
  132. var subtitleTooltips = {"false": "Use preferred subtitle language", "true": "Toggle subtitle"};
  133.  
  134. function loadConfig(r, d, z) {
  135. d = {
  136. id: "ayvp", ver: 1, pbAutohide: progressbarAutohide, pbPosBelow: progressbarBelowVideo, pbDotStyle: progressbarDotStyle, pbHeight: progressbarHeight,
  137. pbColor: progressbarColor, pbElapsedColor: progressbarElapsedColor, pbBufferColor: progressbarBufferColor,
  138. pbDotStyleSize: progressbarDotStyleSize, ptEnabled: progressTextEnabled, ptDocTitle: progressTextInDocTitle, subLangCode: subtitleLanguageCode
  139. };
  140. r = GM_getValue("cfg", "");
  141. if (r) {
  142. try {
  143. r = JSON.parse(r);
  144. } catch(z) {
  145. r = {};
  146. }
  147. if (r.id === "ayvp") {
  148. Object.keys(d).forEach(k => {
  149. if (!(k in r)) r[k] = d[k];
  150. });
  151. } else {
  152. GM_setValue("cfg", JSON.stringify(d));
  153. alert("YouTube Video Progressbar configuration has been reset to default due to data corruption.");
  154. }
  155. } else {
  156. GM_setValue("cfg", JSON.stringify(r = d));
  157. }
  158. return r;
  159. }
  160.  
  161. var cfg = loadConfig();
  162.  
  163. function updProgressTextPos(a) {
  164. window["info-text"].classList.remove("floatingProgress");
  165. if (eleProgressText.offsetTop) window["info-text"].classList.add("floatingProgress");
  166. }
  167.  
  168. var ytpr, ql, resDescs2, orgTitle, chaps;
  169.  
  170. function getPlayer() {
  171. if (ytpr && window.movie_player) {
  172. return vplayer = window.movie_player;
  173. } else return (vplayer = document.querySelector(".html5-video-player"));
  174. }
  175.  
  176. function selectCaption(ev, v, o, c, a) {
  177. if ((v = getPlayer()) && (o = v.getPlayerResponse().captions) && (o = o.playerCaptionsTracklistRenderer) && (o = o.captionTracks)) {
  178. if (ev && (ev.ctrlKey === swapSubtitleFunction)) {
  179. ev.preventDefault();
  180. ev.stopPropagation();
  181. ev.stopImmediatePropagation();
  182. v.toggleSubtitles();
  183. return;
  184. }
  185. if ((c = v.getOption("captions", "track")) && c.vss_id) {
  186. if (c.vss_id === ("." + cfg.subLangCode)) {
  187. a = o.find(ct => ct.vssId === ("a." + cfg.subLangCode));
  188. if (!a) a = o.find(ct => ct.isTranslatable && (ct.vssId[0] === ".") && (ct.vssId.substr(1) !== cfg.subLangCode));
  189. if (!a) a = o.find(ct => ct.isTranslatable && (ct.vssId[1] === ".") && (ct.vssId.substr(2) !== cfg.subLangCode));
  190. }
  191. if (!a && (c.vss_id === ("a." + cfg.subLangCode))) {
  192. a = o.find(ct => ct.isTranslatable && (ct.vssId[0] === ".") && (ct.vssId.substr(1) !== cfg.subLangCode));
  193. if (!a) a = o.find(ct => ct.isTranslatable && (ct.vssId[1] === ".") && (ct.vssId.substr(2) !== cfg.subLangCode));
  194. }
  195. if (!a && c.is_translateable && (c.vss_id[0] === ".") && (c.vss_id.substr(1) !== cfg.subLangCode)) {
  196. a = o.find(ct => ct.isTranslatable && (ct.vssId[1] === ".") && (ct.vssId.substr(2) !== cfg.subLangCode));
  197. }
  198. }
  199. if (!a) {
  200. a = o.find(ct => ct.vssId === ("." + cfg.subLangCode));
  201. if (!a) a = o.find(ct => ct.vssId === ("a." + cfg.subLangCode));
  202. if (!a) a = o.find(ct => ct.isTranslatable && (ct.vssId[0] === ".") && (ct.vssId.substr(1) !== cfg.subLangCode));
  203. if (!a) a = o.find(ct => ct.isTranslatable && (ct.vssId[1] === ".") && (ct.vssId.substr(2) !== cfg.subLangCode));
  204. if (!a) {
  205. a = o.find(ct => ct.isTranslatable && (
  206. ((ct.vssId[0] === ".") && (ct.vssId.substr(1) !== cfg.subLangCode)) ||
  207. ((ct.vssId[1] === ".") && (ct.vssId.substr(2) !== cfg.subLangCode))
  208. ));
  209. }
  210. if (!a) return;
  211. }
  212. a = {languageCode: a.languageCode, vss_id: a.vssId};
  213. if (a.languageCode !== cfg.subLangCode) {
  214. v.getPlayerResponse().captions.playerCaptionsTracklistRenderer.translationLanguages.some(l => {
  215. if (l.languageCode === cfg.subLangCode) {
  216. a.translationLanguage = {languageCode: cfg.subLangCode};
  217. a.translationLanguage.languageName = l.languageName.simpleText;
  218. return true;
  219. }
  220. });
  221. }
  222. if (!c.languageCode) v.toggleSubtitles();
  223. v.setOption("captions", "track", a);
  224. }
  225. }
  226.  
  227. var to = {createHTML: s => s}, tp = window.trustedTypes?.createPolicy ? trustedTypes.createPolicy("", to) : to, html = s => tp.createHTML(s);
  228.  
  229. function processInfo(ev) {
  230. ql = null;
  231. if (window.vidprogress || !/^\/watch|\/shorts\//.test(location.pathname)) return;
  232. clearTimeout(timerWaitInfo);
  233. if (!eleProgressText) {
  234. eleProgressText = document.createElement("DIV");
  235. eleProgressText.id = "vidprogress";
  236. eleProgressText.style.cssText = `display:${cfg.ptEnabled ? "inline-block" : "none"}!important`;
  237. eleProgressText.innerHTML = html(`<span id="curq" style="font-weight:500"></span>
  238. <span id="subs" style="display:none"></span><span id="curtime" style="display:inline-block;margin-left:1ex"></span>
  239. <style>
  240. ytd-watch-grid[theater]{--ytd-watch-flexy-sidebar-width:36vw!important}
  241. #full-bleed-container.ytd-watch-flexy{z-index:2}
  242. #ytd-player{overflow:visible!important}
  243. .html5-video-player{overflow:visible}
  244. video[style*="top: -"]{top:-9999px!important}
  245. ytd-watch-flexy #below.ytd-watch-flexy{position:relative}
  246. ytd-watch-flexy ytd-watch-metadata{padding-top:.6em}
  247. ytd-watch-grid ytd-watch-metadata[title-headline-m] h1.ytd-watch-metadata{margin-top:-.2em;font-size:2.3em;line-height:normal}
  248. ytd-watch-grid ytd-watch-metadata[skinny-mode] #ytd-watch-info-text.ytd-watch-metadata{margin-bottom:0}
  249. ytd-watch-metadata :is(#sponsor-button,#subscribe-button,#actions-inner){align-self:end}
  250. ytd-watch-metadata :is(#owner,#actions-inner) .yt-spec-button-shape-next--size-m,
  251. ytd-watch-metadata yt-icon-button.ytd-subscription-notification-toggle-button-renderer{height:18pt;line-height:18pt}
  252. ytd-watch-metadata yt-icon-button.ytd-subscription-notification-toggle-button-renderer{padding-top:0;padding-bottom:0}
  253. ytd-segmented-like-dislike-button-renderer .yt-spec-button-shape-next--size-m.yt-spec-button-shape-next--segmented-start::after{top:0;height:18pt;line-height:18pt}
  254. ytd-watch-metadata #snippet{max-height:4rem!important}
  255. ytd-watch-metadata #plain-snippet-text{display:none}
  256. ytd-watch-metadata #snippet-text>yt-formatted-string,
  257. ytd-watch-metadata #snippet-text>yt-formatted-string>.yt-formatted-string.style-scope:nth-child(5){display:block!important}
  258. ytd-watch-metadata #description-inline-expander>yt-formatted-string>.yt-formatted-string.style-scope:nth-child(5){display:block!important}
  259. #info :is(ytd-toggle-button-renderer,ytd-button-renderer):not(:first-child),
  260. yt-icon-button:is(.ytd-toggle-button-renderer,.ytd-button-renderer):not(:first-child){margin-left:8px}
  261. #info ytd-toggle-button-renderer, #info ytd-button-renderer{font-size:9pt!important}
  262. #info ytd-toggle-button-renderer a.ytd-toggle-button-renderer,
  263. #info ytd-button-renderer a.ytd-button-renderer,
  264. ytd-button-renderer.force-icon-button a.ytd-button-renderer,
  265. ytd-button-renderer.force-icon-button a.ytd-button-renderer{padding-right:0}
  266. #menu-container{white-space:nowrap}
  267. #menu-container>#menu{display:inline-block}
  268. #description-and-actions #description,
  269. #description-and-actions #actions{min-width:0!important}
  270. #description-and-actions #actions{flex-grow:unset}
  271. #info yt-icon-button.ytd-toggle-button-renderer,
  272. #info yt-icon-button.ytd-button-renderer,
  273. :is(#description-and-actions,#actions) yt-icon-button:is(.ytd-toggle-button-renderer,.ytd-button-renderer){
  274. width:calc(var(--yt-button-icon-size,var(--yt-icon-width,40px)) - 10px)!important;
  275. height:calc(var(--yt-button-icon-size,var(--yt-icon-height,40px)) - 10px)!important;
  276. padding:0 4px 0 0!important;
  277. }
  278. #actions ytd-menu-renderer{padding-bottom:0}
  279. #description-and-actions .ytd-menu-renderer[style-target="button"]{width:calc(var(--yt-icon-width) - 14px);height:calc(var(--yt-icon-height) - 14px)}
  280. #actions ytd-toggle-button-renderer.force-icon-button a.ytd-toggle-button-renderer,
  281. #actions ytd-button-renderer.force-icon-button a.ytd-button-renderer{padding-right:0}
  282. #actions yt-icon-button.ytd-toggle-button-renderer,
  283. #actions yt-icon-button.ytd-button-renderer{padding:8px 0;width:24px}
  284. #vidprogress{${progressTextStyles};margin-left:10px;margin-right:1em;min-width:27ex;max-height:1.2em}
  285. #menu-container #vidprogress{margin-top:.6em}
  286. ytd-watch-metadata #vidprogress{position:absolute;z-index:2;left:50%;transform:translate(-50%,.6em)}
  287. ytd-watch-grid ytd-watch-metadata #vidprogress{margin-top:0}
  288. ytd-watch-metadata[skinny-mode] #vidprogress{margin-top:-.5em}
  289. ytd-watch-grid[swatcheroo-binary-layout] ytd-watch-metadata #vidprogress{transform:translate(-50%,-2em)}
  290. #description-and-actions #vidprogress{position:static;margin-left:0;margin-top:.9em}
  291. html[dark] #vidprogress{${progressTextStylesDark}}
  292. #info-text{position:relative;margin-top:1rem}
  293. #info-text.floatingProgress{overflow:visible!important}
  294. #info-text.floatingProgress>#vidprogress{position:absolute;right:0;bottom:1.7rem}
  295. #vidprogress #subs{cursor:pointer}
  296. #vidprogress2b,#vidprogress2c{transition:width 100ms linear}
  297. #vidchap{position:relative;z-index:1}
  298. ytd-watch-flexy[theater][is-two-columns_] #vidchap{top:-1.2em}
  299. #chapwrap{
  300. ${progressTextStyles};display:none;position:absolute;right:0;border:none;margin-left:0;
  301. padding:0;height:valv(1.2em + 6px);background:transparent;overflow:hidden;
  302. }
  303. #chap{
  304. ${progressTextStyles};position:relative;margin-left:0;margin-top:.4em;border:none;padding:0;transition:margin-top 200ms linear;
  305. }
  306. ytd-watch-grid[swatcheroo-binary-layout] #chap{margin-top:0}
  307. #chapprogresswrap{position:absolute;z-index:0;left:1px;top:1px;width:calc(100% - 2px);height:calc(100% - 2px)}
  308. #chapprogress{
  309. ${progressTextStyles};display:block;margin-left:0;border:none;border-radius:0;padding:0;
  310. height:100%;background:rgb(0,192,0,0.3);pointer-events:none;transition:width 100ms linear;
  311. }
  312. #chaptext{${progressTextStyles};position:relative;z-index:1;min-width:4em;margin-left:0;background:transparent}
  313. html[dark] #chap{${progressTextStylesDark};border:none}
  314. #chap.chide{margin-top:-2em}
  315. </style>`);
  316. }
  317. (function waitInfo(a, b, c, d) {
  318. a = (a = document.querySelector("ytd-watch-metadata")) && (getComputedStyle(a).display !== "none") ?
  319. "ytd-watch-metadata #description-content,ytd-watch-metadata #actions" : "#info-text #date,#menu-container #menu";
  320. if (b = document.querySelector(a)) {
  321. if (
  322. b.matches("ytd-watch-metadata #description") && (c = b.querySelector("#plain-snippet-text")) && !c.tcf && c.firstChild &&
  323. (d = c.firstChild.data.match(/(^.*?\u2022.*?\u2022\s+)(.*)/))
  324. ) {
  325. c.firstChild.data = `${d[1]}\n${d[2]}`;
  326. c.tcf = true;
  327. }
  328. if ((c = b.closest('#secondary')) && (c = c.querySelector('ytd-watch-metadata[skinny-mode] #owner'))) {
  329. c.parentNode.insertBefore(eleProgressText, c);
  330. } else b.parentNode.insertBefore(eleProgressText, b);
  331. subs.addEventListener("click", selectCaption);
  332. addEventListener("resize", updProgressTextPos);
  333. addEventListener("yt-navigate-finish", updProgressTextPos);
  334. updProgressTextPos();
  335. eleChapter = document.createElement("DIV");
  336. eleChapter.id = "vidchap";
  337. eleChapter.innerHTML = html(`<div id="chapwrap" style="display:${cfg.ptEnabled ? "block" : "none"}"><div id="chap" class="chide">
  338. <div id="chapprogresswrap"><div id="chapprogress"></div></div><div id="chaptext"></div>
  339. </div></div>`);
  340. (b = document.querySelector("#primary #player")).parentNode.insertBefore(eleChapter, b.nextSibling);
  341. setTimeout(updProgressTextPos, 0);
  342. } else timerWaitInfo = setTimeout(waitInfo, 200);
  343. })();
  344. }
  345.  
  346. function processPlayer(ev) {
  347. function zerolead(n){
  348. return n > 9 ? n : "0" + n;
  349. }
  350.  
  351. function sec2hms(sec) {
  352. var c = sec % 60, d = Math.floor(sec / 60);
  353. return (d >= 60 ? zerolead(Math.floor(d / 60)) + ":" : "") + zerolead(d % 60) + ":" + zerolead(c);
  354. }
  355.  
  356. function dateElapsed(c, t, r, a, b) {
  357. r = Math.floor((((t = new Date).getFullYear() * 12 + t.getMonth()) - (c.getFullYear() * 12 + c.getMonth())) / 12);
  358. if (!r) {
  359. r = Math.floor(t.getTime() / 86400000) - Math.floor(c.getTime() / 86400000);
  360. if (!(b = Math.floor(r / 30))) {
  361. if (!r) {
  362. return "today"
  363. } else if (b = Math.floor(r / 7)) {
  364. return b > 1 ? b + " weeks ago" : "a week ago"
  365. } else return r > 1 ? r + " days ago" : "a day ago"
  366. } else return b > 1 ? b + " months ago" : "a month ago"
  367. } else return r > 1 ? r + " years ago" : "a year ago"
  368. }
  369.  
  370. function updProgress(a, b, c, d, e, f, g, h, l){
  371. a = getPlayer();
  372. if (a && window.vidprogress2b && a.getCurrentTime) try {
  373. if (window.curtime) try {
  374. b = a.getPlaybackQuality();
  375. if (!ql) {
  376. if (window["page-manager"] && (c = window["page-manager"].getCurrentData()) &&
  377. (d = c.playerResponse) && (!ytpr || (d.trackingParams !== ytpr.trackingParams))) {
  378. dispatchEvent(new Event("resize"));
  379. setTimeout(() => dispatchEvent(new Event("resize")), 200);
  380. orgTitle = document.title;
  381. ytpr = (
  382. c.player && c.player.args && (
  383. (c.player.args.player_response && JSON.parse_(c.player.args.player_response)) || c.player.args.raw_player_response
  384. )
  385. ) || d;
  386. if (window.date) window.date.title = "";
  387. chaps = [];
  388. chaps.last = 0;
  389. chapwrap.style.display = "";
  390. chap.className = "chide"
  391. if (ytpr && ytpr.videoDetails && ytpr.videoDetails.shortDescription) {
  392. if (h = ytpr.videoDetails.shortDescription.match(/^(?:\s*\d+\.)?\s*(\d{1,2}:)?\d{1,2}:\d{1,2}\s+\S+.*/gm)) {
  393. chaps = h.map(s => {
  394. s = s.match(/^(?:\s*\d+\.)?\s*(\d{1,2}:)?(\d{1,2}):(\d{1,2})\s+(?:(?:-|\u2013|\u2014|\u2015)\s+(?:\d{1,2}:)?\d{1,2}:\d{1,2}\s+)?(?:(?:-|\u2013|\u2014|\u2015)\s+)?(.*?)\s*$/u);
  395. s[1] = s[1] ? parseInt(s[1]) : 0;
  396. s[2] = s[2] ? parseInt(s[2]) : 0;
  397. s[3] = s[3] ? parseInt(s[3]) : 0;
  398. return {time: (s[1] * 3600) + (s[2] * 60) + s[3], txt: s[4]}
  399. });
  400. chaps.some((o, i) => {
  401. if ((i > 0) && (o.time < chaps[i - 1].time)) {
  402. chaps.splice(i);
  403. return true
  404. }
  405. });
  406. }
  407. }
  408. if (!chaps.length) {
  409. if (
  410. (h = c) && (h = h.response) && (h = h.playerOverlays) && (h = h.playerOverlayRenderer) && (h = h.decoratedPlayerBarRenderer) &&
  411. (h = h.decoratedPlayerBarRenderer) && (h = h.playerBar) && (h = h.multiMarkersPlayerBarRenderer) && (h = h.markersMap)
  412. ) {
  413. h.some(m => {
  414. if (["AUTO_CHAPTERS", "DESCRIPTION_CHAPTERS"].includes(m.key)) {
  415. if ((m = m.value) && (m = m.chapters) && m.length && m[0] && m[0].chapterRenderer && m[0].chapterRenderer) {
  416. chaps = m.map(a => ({time: Math.floor(a.chapterRenderer.timeRangeStartMillis / 1000), txt: a.chapterRenderer.title.simpleText}))
  417. }
  418. return true
  419. }
  420. })
  421. }
  422. }
  423. if (chaps.length && chaps[0].time) chaps.unshift({time: 0, txt: "(Untitled)"});
  424. }
  425. if (ytpr) {
  426. if ((h = document.querySelector("#info-strings>yt-formatted-string")) && !h.title && d && (e = d.microformat) && (e = e.playerMicroformatRenderer)) {
  427. if ((f = e.liveBroadcastDetails) && f.startTimestamp) {
  428. g = "Started: " + (c = new Date(f.startTimestamp)).toLocaleString() + " (" + dateElapsed(c) + ")";
  429. if (f.endTimestamp) g += "; Ended: " + new Date(f.endTimestamp).toLocaleString();
  430. } else g = "";
  431. if (!g) {
  432. g = ((c = new Date(e.publishDate || e.uploadDate)).toLocaleDateString());
  433. if (!h.textContent.includes("ago")) g += " (" + dateElapsed(c) + ")";
  434. }
  435. h.title = g;
  436. }
  437. if (ytpr.streamingData.adaptiveFormats) {
  438. ql = {};
  439. resDescs2 = {};
  440. ytpr.streamingData.adaptiveFormats.forEach((o, i) => {
  441. if (!o.audioQuality) ql[o.quality] = [o.quality, o.qualityLabel, o.bitrate];
  442. });
  443. Object.keys(ql).forEach(k => {
  444. resDescs2[k] = resDescs[k].replace("(" + resNums[k], "(" + (ql[k] = (ql[k][1] || ql[k][0])));
  445. });
  446. }
  447. }
  448. }
  449. if (ql) {
  450. c = ql[b] || b;
  451. } else c = resNums[b] || b;
  452. (d = a.getAvailableQualityLevels()).pop();
  453. curq.textContent = c + (d.indexOf(b) > 0 ? "+" : "");
  454. c = e = a.getVideoStats();
  455. g = fmtMaps[e.afmt] || ("a" + e.afmt);
  456. if (e.fmt) { //has video
  457. if (f = fmtMaps[e.fmt]) {
  458. f = `${f[0]} ${f[1]}`;
  459. } else f = "vid" + e.fmt;
  460. if (e.afmt) { //video & audio
  461. if (g = fmtMaps[e.afmt]) {
  462. e = ` [${f} ${g[1]}]`;
  463. } else e = ` [${f} aud${e.afmt}]`;
  464. } else { //no audio. video only
  465. e = ` [${f}]`;
  466. }
  467. } else if (e.afmt) { //no video. audio only
  468. if (f = fmtMaps[e.afmt]) {
  469. e = ` [${f[0]} ${f[1]}]`;
  470. } else e = ` [aud${e.afmt}]`;
  471. } else e = "";
  472. f = a.querySelector("video");
  473. g = ql ? resDescs2 : resDescs;
  474. if ((h = a.getPlayerResponse()?.streamingData?.adaptiveFormats)?.forEach) {
  475. curq.title = `Current: ${g[b] || b}${e} (${f.videoWidth}x${f.videoHeight} video, ${f.offsetWidth}x${f.offsetHeight} viewport, ${
  476. ((f, t) => {
  477. f = [];
  478. c.fmt && f.push(parseInt(c.fmt));
  479. c.afmt && f.push(parseInt(c.afmt));
  480. t = 0;
  481. h.forEach((o, i) => f.includes(o.itag) && !o.isDrc && (t += parseInt(o.contentLength)));
  482. return parseFloat((t / 1048576).toFixed(2))
  483. })()
  484. }MB estimated size)\nAvailable: ${d.map(b => g[b] || b).join(", ")}`
  485. } else curq.title = `Current: ${g[b] || b}${e} (${f.videoWidth}x${f.videoHeight} video, ${f.offsetWidth}x${f.offsetHeight} viewport)\nAvailable: ${d.map(b => g[b] || b).join(", ")}`;
  486. } catch(b) {
  487. curq.textContent = "???";
  488. curq.title = b.message;
  489. }
  490. if (window.subs) {
  491. if (ytpr && ytpr.captions && ytpr.captions.playerCaptionsTracklistRenderer && ytpr.captions.playerCaptionsTracklistRenderer.captionTracks) {
  492. b = (d = ytpr.captions.playerCaptionsTracklistRenderer.captionTracks).map(v => v.name.simpleText);
  493. if (b.length) {
  494. c = a.getOption("captions", "track");
  495. b = [
  496. c.vss_id &&
  497. (c.vss_id.substr(0, 2) === "a.") ? "[c]" : ((d.length === 1) && (d[0].vssId[1] === ".") ? "[c]" : "[C]"),
  498. `Current subtitle: ${
  499. c && c.languageCode ?
  500. c.languageName + (c.translationLanguage && c.translationLanguage.languageName ? " >> " + c.translationLanguage.languageName : "") :
  501. "(none)"
  502. }\nAvailable subtitles: ${b.join(", ")}\nClick: ${subtitleTooltips[!swapSubtitleFunction]}\nCtrl+Click: ${subtitleTooltips[swapSubtitleFunction]}`,
  503. `display:inline-block${c && c.languageCode ? ";font-weight:bold" : ""}`
  504. ];
  505. } else b = ["", "", ""];
  506. } else b = ["", "", ""];
  507. subs.textContent = b[0];
  508. subs.title = b[1];
  509. subs.style.cssText = b[2];
  510. }
  511. b = a.getCurrentTime();
  512. if (b >= 0) {
  513. l = a.getDuration();
  514. if (!a.getVideoData().isLive) {
  515. if (window.curtime) {
  516. curtime.textContent = sec2hms(Math.floor(b)) + " / " + sec2hms(Math.floor(l)) + " (" + Math.floor(parseFloat((b * 100 / l).toFixed(3))) + "%)";
  517. if (chaps.length) {
  518. chaps.some((c, i, d) => {
  519. if ((b >= c.time) && (((i + 1) === chaps.length) || (b < chaps[i + 1].time))) {
  520. d = (i < (chaps.length - 1) ? chaps[i + 1].time : l) - c.time;
  521. chapprogress.style.width = ((b - c.time) * 100 / d) + "%";
  522. if (i !== chaps.last) {
  523. function doshow(d) {
  524. chap.ontransitionend = null;
  525. chaptext.textContent = c.txt;
  526. chap.title = `Duration: ${sec2hms(Math.floor(((i + 1) < chaps.length ? chaps[i + 1].time : l) - c.time))}\n` +
  527. (i ? `Previous: ${chaps[i - 1].txt}` : "") + ((i + 1) < chaps.length ? (i ? "\n" : "") + `Next: ${chaps[i + 1].txt}` : "");
  528. chapwrap.style.display = "block";
  529. chap.className = "";
  530. }
  531. chaps.last = i;
  532. if (chap.className) {
  533. doshow();
  534. } else {
  535. chap.ontransitionend = doshow;
  536. chap.className = "chide";
  537. }
  538. return true;
  539. }
  540. }
  541. });
  542. } else {
  543. chapwrap.style.display = "";
  544. chap.ontransitionend = null;
  545. chap.className = "chide"
  546. }
  547. }
  548. if (cfg.pbDotStyle) {
  549. vidprogress2b.style.left = ((b / l) * vidprogress2.offsetWidth) + "px";
  550. vidprogress2c.style.left = ((a.getVideoBytesLoaded() / a.getVideoBytesTotal()) * vidprogress2.offsetWidth) + "px";
  551. } else {
  552. vidprogress2b.style.width = ((b / l) * vidprogress2.offsetWidth) + "px";
  553. vidprogress2c.style.width = ((a.getVideoBytesLoaded() / a.getVideoBytesTotal()) * vidprogress2.offsetWidth) + "px";
  554. }
  555. } else {
  556. if (window.curtime) curtime.textContent = "LIVE";
  557. if (cfg.pbDotStyle) {
  558. vidprogress2b.style.left = "100%";
  559. } else vidprogress2b.style.width = "100%";
  560. }
  561. } else throw 0;
  562. vidprogress2.style.display = cfg.pbAutohide && !a.classList.contains("ytp-autohide") ? "none" : "";
  563. vidprogress2.style.transform = cfg.pbPosBelow && (a = document.querySelector("ytd-watch-flexy,ytd-watch-grid")) && !a.attributes.fullscreen ? "translateY(100%)" : "";
  564. if (cfg.ptDocTitle) document.title = `${curq.textContent}${subs.textContent ? " " + subs.textContent : ""} ${curtime.textContent} ${orgTitle}`;
  565. } catch(a) {
  566. if (window.curtime) {
  567. if (cfg.ptDocTitle) document.title = "??? " + orgTitle;
  568. curtime.textContent = "???";
  569. }
  570. if (cfg.pbDotStyle) {
  571. vidprogress2b.style.left = "0px";
  572. vidprogress2c.style.left = "0px";
  573. } else {
  574. vidprogress2b.style.width = "0px";
  575. vidprogress2c.style.width = "0px";
  576. }
  577. if (window.chapwrap) {
  578. chapwrap.style.display = "";
  579. chap.ontransitionend = null;
  580. chap.className = "chide"
  581. }
  582. }
  583. }
  584.  
  585. function resumeProgressMonitor() {
  586. if (timerProgressMonitor) return;
  587. // dispatchEvent(new Event("resize"));qwe
  588. updProgress();
  589. timerProgressMonitor = setInterval(updProgress, 200);
  590. }
  591.  
  592. function pauseProgressMonitor() {
  593. clearInterval(timerProgressMonitor);
  594. timerProgressMonitor = 0;
  595. updProgress();
  596. }
  597.  
  598. clearInterval(timerProgressMonitor);
  599. timerProgressMonitor = 0;
  600. clearTimeout(timerWaitPlayer);
  601. timerWaitPlayer = 0;
  602. clearInterval(timerDoubleCheck);
  603. timerDoubleCheck = 0;
  604. (function waitPlayer(v) {
  605. if (!window.vidprogress2 && getPlayer() && (vplayer.id !== "c4-player") && (a = vplayer.parentNode.querySelector("video"))) {
  606. b = document.createElement("DIV");
  607. b.id = "vidprogress2";
  608. b.style.cssText = `position:absolute;z-index:50;bottom:0;width:100%;height:${
  609. cfg.pbDotStyle ? cfg.pbDotStyleSize : cfg.pbHeight}px;background:${cfg.pbColor}`;
  610. v = cfg.pbDotStyle ? "width:" + cfg.pbDotStyleSize + "px;margin-left:-" + Math.floor(cfg.pbDotStyleSize / 2) + "px;" : "";
  611. b.innerHTML = html(`<div id="vidprogress2c" style="position:absolute;${v}height:100%;background:${cfg.pbBufferColor}"></div>
  612. <div id="vidprogress2b" style="position:absolute;${v}height:100%;background:${cfg.pbElapsedColor}"></div>`);
  613. vplayer.appendChild(b);
  614. if (vplayer.getPlayerState() === 1) resumeProgressMonitor();
  615. //useful: onLoadedMetadata(), onStateChange(state), onPlayVideo(info), onReady(playerApi), onVideoAreaChange(), onVideoDataChange(info)
  616. //states: -1=notReady, 0=ended, 1=playing, 2=paused, 3=ready, 4=???, 5=notAvailable?
  617. (v = vplayer.querySelector("video")).addEventListener("loadedmetadata", resumeProgressMonitor);
  618. v.addEventListener("play", resumeProgressMonitor);
  619. v.addEventListener("pause", pauseProgressMonitor);
  620. } else timerWaitPlayer = setTimeout(waitPlayer, 200);
  621. })();
  622.  
  623. function doubleCheck() {
  624. if (getPlayer() && vplayer.getPlayerState) {
  625. if (vplayer.getPlayerState() === 1) {
  626. resumeProgressMonitor();
  627. } else pauseProgressMonitor();
  628. }
  629. }
  630. if (!timerDoubleCheck) timerDoubleCheck = setInterval(doubleCheck, 500);
  631. }
  632.  
  633. function init() {
  634. orgTitle = document.title;
  635. processInfo();
  636. processPlayer();
  637. }
  638.  
  639. function parseColor(c, e) {
  640. (e = document.createElement("DIV")).style.color = c;
  641. c = getComputedStyle(document.documentElement.appendChild(e)).color;
  642. e.remove();
  643. if (e = c.match(/(\d+), (\d+), (\d+)(?:, (\d+\.\d+))?/)) {
  644. return [("0" + parseInt(e[1]).toString(16).toUpperCase()).substr(-2) + ("0" + parseInt(e[2]).toString(16).toUpperCase()).substr(-2) +
  645. ("0" + parseInt(e[3]).toString(16).toUpperCase()).substr(-2), e[4] ? parseFloat(e[4]).toFixed(1) : "1"];
  646. }
  647. return ["000000", "1"];
  648. }
  649.  
  650. function toRgba(rgb, opacity) {
  651. return rgb + ("0" + Math.floor(opacity * 255).toString(16)).substr(-2);
  652. }
  653.  
  654. GM_registerMenuCommand("Configuration", (elePop, a) => {
  655. if (window.ayvpConfig) return;
  656. (elePop = document.createElement("DIV")).id = "ayvpConfig";
  657. elePop.innerHTML = html(`<style>
  658. #ayvpConfig{
  659. position:fixed;z-index:999999999;left:0;top:0;right:0;bottom:0;background:rgb(0,0,0,0.5);
  660. color:#000;font:normal normal normal 10pt/normal sans-serif;cursor:pointer
  661. }
  662. #ayvpPopup{position:fixed;top:.5em;right:.5em;border:.1em solid #000;border-radius:.5em;padding:.3em;background:#fff;cursor:auto}
  663. #ayvpCaption{border-radius:.3em;padding:.2em .3em;background:#444;color:#fff;font-weight:bold}
  664. #ayvpContent{margin-top:1em}
  665. #ayvpContent tr+tr td{padding-top:.5em}
  666. #ayvpContent td+td{padding-left:.5em}
  667. #ayvpContent .radio:last-child,
  668. #ayvpContent .radio:last-child{margin-left:1em}
  669. #ayvpContent .radio input{vertical-align:middle;margin-top:-.1em}
  670. #ayvpPbh,
  671. #ayvpPbds{margin-left:.5em;width:4em}
  672. #ayvpContent .preview{display:inline-block;margin-left:.5em;white-space:nowrap}
  673. #ayvpContent .preview .barContainer{
  674. display:inline-block;position:relative;vertical-align:bottom;width:70pt;height:13pt;overflow:hidden;background:#000;
  675. white-space:normal;word-break:break-all;color:#fff;font-size:7pt;line-height:1em;
  676. }
  677. #ayvpContent .preview .barBase{position:absolute;left:0;right:0;bottom:0}
  678. #ayvpContent .preview .barBase *{height:100%}
  679. #ayvpContent .preview .barBuf,
  680. #ayvpContent .preview .barElap{width:66%}
  681. #ayvpContent .hrgb{width:4em;text-transform:uppercase}
  682. #ayvpContent .picker{margin-right:1em;vertical-align:top;width:2em;height:1.5em;padding:0}
  683. #ayvpContent .opacity{width:3em}
  684. #ayvpContent #ayvpPslc{margin-left:1ex}
  685. #ayvpControls{margin:1em 0 .5em 0;text-align:center}
  686. #ayvpControls button{margin:0 2em;width:5em}
  687. #ayvpControls #ayvpExp{float:left;margin-left:.3em}
  688. #ayvpControls #ayvpImp{float:right;margin-right:.3em}
  689. #ayvpControls #ayvpImpFile{display:none}
  690. ${document.documentElement.attributes["dark"] ? configGuiStylesDark : ""}
  691. </style>
  692. <form id="ayvpPopup">
  693. <div id="ayvpCaption">YouTube Video Progress Configuration</div>
  694. <table id="ayvpContent">
  695. <tr>
  696. <td>Progressbar Auto Hide: <span title="Auto hide when YouTube video controls is visible">[?]</span></td>
  697. <td>
  698. <label class="radio"><input id="ayvpPbae" type="radio" name="ahide" value="0" /> Disabled</label>
  699. <label class="radio"><input id="ayvpPbad" type="radio" name="ahide" value="1" /> Enabled</label>
  700. </td>
  701. </tr>
  702. <tr>
  703. <td>Show Progressbar Below Video: <span title="Applies only when not in fullscreen mode">[?]</span></td>
  704. <td>
  705. <label class="radio"><input id="ayvpPbpi" type="radio" name="ppos" value="0" /> No</label>
  706. <label class="radio"><input id="ayvpPbpo" type="radio" name="ppos" value="1" /> Yes</label>
  707. </td>
  708. </tr>
  709. <tr>
  710. <td>Progressbar Style:</td>
  711. <td>
  712. <label class="radio"><input id="ayvpPbsl" type="radio" name="style" value="0" /> Line</label>
  713. <label class="radio"><input id="ayvpPbsd" type="radio" name="style" value="1" /> Dot</label>
  714. </td>
  715. </tr>
  716. <tr>
  717. <td>Progressbar Height:</td>
  718. <td>
  719. <input id="ayvpPbh" type="number" min="0" max="999" maxlength="3" value="${cfg.pbHeight}" required /> pixels
  720. <div class="preview">
  721. Preview:
  722. <div class="barContainer" title="Preview for line style progress only">
  723. .
  724. <div class="barBase" style="height:${cfg.pbHeight}px;background:${cfg.pbColor}">
  725. <div class="barBuf" style="background:${cfg.pbBufferColor}">
  726. <div class="barElap" style="background:${cfg.pbElapsedColor}"></div>
  727. </div>
  728. </div>
  729. </div>
  730. </div>
  731. </td>
  732. </tr>
  733. ${
  734. (() => {
  735. return [
  736. ["Background", "b", parseColor(cfg.pbColor), "Base"],
  737. ["Elapsed", "e", parseColor(cfg.pbElapsedColor), "Elap"],
  738. ["Buffer", "u", parseColor(cfg.pbBufferColor), "Buf"]
  739. ].map(t => {
  740. return `<tr>
  741. <td>Progressbar ${t[0]} Color:</td>
  742. <td bar="${t[3]}">#<input class="hrgb" maxlength="6" pattern="(?:[0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})" value="${t[2][0]}" required />
  743. <input class="picker" type="color" value="#${t[2][0]}" prevvalue="#${t[2][0]}" />
  744. Opacity: <input class="opacity" type="number" min="0.0" max="1.0" step="0.1" value="${t[2][1]}" required /> (0.0 - 1.0)</td>
  745. </tr>`
  746. }).join("");
  747. })()
  748. }
  749. <tr>
  750. <td>Progressbar Dot-style Size:</td>
  751. <td>
  752. <input id="ayvpPbds" type="number" min="0" max="999" maxlength="3" value="${cfg.pbDotStyleSize}" required /> pixels
  753. </td>
  754. </tr>
  755. <tr>
  756. <td>Progress Text: <span title="If disabled, chapter box will also be disabled">[?]</span></td>
  757. <td>
  758. <label class="radio"><input id="ayvpPtds" type="radio" name="ptext" value="0" /> Disabled</label>
  759. <label class="radio"><input id="ayvpPten" type="radio" name="ptext" value="1" /> Enabled</label>
  760. </td>
  761. </tr>
  762. <tr>
  763. <td>Progress Text In Document Title: <span title="Include progress text in page title (browser tab title)">[?]</span></td>
  764. <td>
  765. <label class="radio"><input id="ayvpPttd" type="radio" name="dtitle" value="0" /> Disabled</label>
  766. <label class="radio"><input id="ayvpPtte" type="radio" name="dtitle" value="1" /> Enabled</label>
  767. </td>
  768. </tr>
  769. <tr>
  770. <td>Preferred Subtitle Language:</td>
  771. <td>
  772. <select id="ayvpPslc"></select>
  773. </td>
  774. </tr>
  775. </table>
  776. <div id="ayvpControls">
  777. <button id="ayvpExp">Export</button>
  778. <button id="ayvpImp">Import</button><input id="ayvpImpFile" type="file" />
  779. <button id="ayvpOk" type="submit">OK</button>
  780. <button id="ayvpCancel">Cancel</button>
  781. </div>
  782. </form>`);
  783. elePop.querySelectorAll("input[name=ahide]")[cfg.pbAutohide ? 1 : 0].checked = true;
  784. elePop.querySelectorAll("input[name=ppos]")[cfg.pbPosBelow ? 1 : 0].checked = true;
  785. elePop.querySelectorAll("input[name=style]")[cfg.pbDotStyle ? 1 : 0].checked = true;
  786. elePop.querySelector(".barContainer").firstChild.data = "Sample content ".repeat(5);
  787. elePop.querySelectorAll("input[name=ptext]")[cfg.ptEnabled ? 1 : 0].checked = true;
  788. elePop.querySelectorAll("input[name=dtitle]")[cfg.ptDocTitle ? 1 : 0].checked = true;
  789. a = elePop.querySelector("#ayvpPslc");
  790. langList.forEach((l, e) => {
  791. (e = document.createElement("OPTION")).value = l[1];
  792. e.textContent = l[0];
  793. a.appendChild(e);
  794. });
  795. a.value = cfg.subLangCode;
  796. function applyConfig(a) {
  797. if (window.vidprogress) {
  798. if (window.vidprogress2) {
  799. vidprogress2.style.height = (cfg.pbDotStyle ? cfg.pbDotStyleSize : cfg.pbHeight) + "px";
  800. vidprogress2.style.background = cfg.pbColor;
  801. vidprogress2b.style.background = cfg.pbElapsedColor;
  802. vidprogress2c.style.background = cfg.pbBufferColor;
  803. if (cfg.pbDotStyle) {
  804. vidprogress2c.style.marginLeft = vidprogress2b.style.marginLeft = Math.floor(cfg.pbDotStyleSize / 2) + "px;";
  805. vidprogress2c.style.width = vidprogress2b.style.width = cfg.pbDotStyleSize + "px";
  806. } else {
  807. vidprogress2c.style.left = vidprogress2b.style.left = "";
  808. vidprogress2c.style.marginLeft = vidprogress2b.style.marginLeft = "";
  809. vidprogress2c.style.width = vidprogress2b.style.width = "";
  810. }
  811. }
  812. if (window.subs && subs.style.fontWeight) selectCaption();
  813. vidprogress.style.setProperty("display", cfg.ptEnabled ? "inline-block" : "none", "important");
  814. if (window.vidchap) vidchap.style.display = !cfg.ptEnabled ? "none" : "";
  815. }
  816. }
  817. function saveConfig(ev, a) {
  818. if (ev) ev.preventDefault();
  819. if (JSON.parse(GM_getValue("cfg", JSON.stringify(cfg))).ver > cfg.ver) {
  820. alert(`Can not save configuration changes.
  821. Current script instance is older than the installed script.
  822. Please reload the page to use the newer script then try again.`);
  823. elePop.remove();
  824. return;
  825. }
  826. cfg.pbAutohide = elePop.querySelector('input[name=ahide]:checked').value === "1";
  827. cfg.pbPosBelow = elePop.querySelector('input[name=ppos]:checked').value === "1";
  828. cfg.pbDotStyle = elePop.querySelector('input[name=style]:checked').value === "1";
  829. cfg.pbHeight = parseInt(ayvpPbh.value);
  830. [["pbColor", "Base"], ["pbElapsedColor", "Elap"], ["pbBufferColor", "Buf"]].forEach(v => {
  831. cfg[v[0]] = toRgba(elePop.querySelector(`td[bar=${v[1]}] .picker`).value, elePop.querySelector(`td[bar=${v[1]}] .opacity`).value);
  832. });
  833. cfg.pbDotStyleSize = parseInt(ayvpPbds.value);
  834. cfg.ptEnabled = elePop.querySelector('input[name=ptext]:checked').value === "1";
  835. if (!(cfg.ptDocTitle = elePop.querySelector('input[name=dtitle]:checked').value === "1")) document.title = orgTitle;
  836. cfg.subLangCode = ayvpPslc.value;
  837. GM_setValue("cfg", JSON.stringify(cfg));
  838. applyConfig();
  839. if (ev) elePop.remove();
  840. }
  841. elePop.lastElementChild.addEventListener("submit", saveConfig);
  842. elePop.addEventListener("keydown", ev => ev.stopPropagation(), true);
  843. elePop.addEventListener("input", (ev, e, b) => {
  844. if ((e = ev.target).id) { //height. line style
  845. if (e.id === "ayvpPbh") elePop.querySelector(".barBase").style.height = e.value + "px";
  846. } else { //colors
  847. b = elePop.querySelector(".bar" + e.parentNode.getAttribute("bar"));
  848. if (e.type === "color") { //picker
  849. b.style.background = e.value + ("0" + Math.floor(e.nextElementSibling.value * 255).toString(16)).substr(-2);
  850. } else if (e.type === "number") { //opacity
  851. b.style.background = e.previousElementSibling.value + ("0" + Math.floor(e.value * 255).toString(16)).substr(-2);
  852. } else { //hrgb
  853. e.nextElementSibling.value = "#" + e.value;
  854. b.style.background = e.nextElementSibling.value + ("0" + Math.floor(e.nextElementSibling.nextElementSibling.value * 255).toString(16)).substr(-2);
  855. }
  856. }
  857. });
  858. elePop.addEventListener("change", (ev, e, r) => {
  859. if ((e = ev.target).className === "picker") {
  860. if (e.value.substr(1).toUpperCase() !== e.getAttribute("prevvalue")) {
  861. e.setAttribute("prevvalue", e.previousElementSibling.value = e.value.substr(1).toUpperCase());
  862. elePop.querySelector(".bar" + e.parentNode.getAttribute("bar")).style.background =
  863. e.value + ("0" + Math.floor(e.nextElementSibling.value * 255).toString(16)).substr(-2);
  864. }
  865. } else if (e.id === "ayvpImpFile") {
  866. r = new FileReader();
  867. r.onload = (o, t, z) => {
  868. try {
  869. o = JSON.parse(r.result);
  870. if (!o.id === "ayvp") throw 0;
  871. if (o.ver > cfg.ver) {
  872. alert(`Can not import configuration changes.
  873. Current script instance is older than the installed script.
  874. Please reload the page to use the newer script then try again.`);
  875. return;
  876. }
  877. cfg = o;
  878. GM_setValue(cfg, r.result);
  879. applyConfig();
  880. elePop.remove();
  881. } catch(z) {
  882. alert(z.message ? "Invalid configuration file." : z);
  883. }
  884. };
  885. r.readAsText(e.files[0]);
  886. }
  887. });
  888. elePop.addEventListener("click", (ev, a) => {
  889. switch (ev.target.id) {
  890. case "ayvpExp":
  891. if (JSON.parse(GM_getValue("cfg", JSON.stringify(cfg))).ver > cfg.ver) {
  892. alert(`Can not export configuration changes.
  893. Current script instance is older than the installed script.
  894. Please reload the page to use the newer script then try again.`);
  895. elePop.remove();
  896. break;
  897. }
  898. saveConfig();
  899. JSON.stringify(cfg);
  900. document.body.appendChild(a = document.createElement("A")).href = URL.createObjectURL(new Blob([JSON.stringify(cfg)], {type: "application/json"}));
  901. a.download = `YouTubeVideoProgressbar.json`;
  902. a.click();
  903. a.remove();
  904. URL.revokeObjectURL(a.href);
  905. break;
  906. case "ayvpImp":
  907. ayvpImpFile.click();
  908. break;
  909. case "ayvpConfig":
  910. case "ayvpCancel":
  911. elePop.remove();
  912. break;
  913. default:
  914. return;
  915. }
  916. ev.preventDefault();
  917. });
  918. document.documentElement.append(elePop);
  919. });
  920.  
  921. addEventListener("yt-page-data-updated", processInfo);
  922. addEventListener("yt-player-released", processPlayer);
  923. addEventListener("load", init);
  924. addEventListener("spfprocess", () => {
  925. setTimeout(init, contentLoadProcessDelay);
  926. });
  927.  
  928. })(unsafeWindow);

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址