Yays! (Yet Another Youtube Script)

A lightweight and non-intrusive userscript that control video playback and set the preferred player size and playback quality on YouTube.

当前为 2014-05-02 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Yays! (Yet Another Youtube Script)
  3. // @namespace youtube
  4. // @description A lightweight and non-intrusive userscript that control video playback and set the preferred player size and playback quality on YouTube.
  5. // @version 1.11
  6. // @author Eugene Nouvellieu <eugenox_gmail_com>
  7. // @license MIT License
  8. // @include http*://*.youtube.com/*
  9. // @include http*://youtube.com/*
  10. // @run-at document-start
  11. // @noframes
  12. // @grant GM_deleteValue
  13. // @grant GM_getValue
  14. // @grant GM_setValue
  15. // @grant GM_xmlhttpRequest
  16. // @homepageURL https://eugenox.appspot.com/script/yays
  17. // @icon https://eugenox.appspot.com/blob/yays/yays.icon.png
  18. // ==/UserScript==
  19.  
  20. // Copyright (c) 2012-2013 Eugene Nouvellieu <eugenox_gmail_com>
  21. //
  22. // Permission is hereby granted, free of charge, to any person obtaining a copy of
  23. // this software and associated documentation files (the "Software"), to deal in
  24. // the Software without restriction, including without limitation the rights to
  25. // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
  26. // the Software, and to permit persons to whom the Software is furnished to do so,
  27. // subject to the following conditions:
  28. //
  29. // The above copyright notice and this permission notice shall be included in all
  30. // copies or substantial portions of the Software.
  31. //
  32. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  33. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
  34. // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
  35. // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
  36. // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
  37. // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  38.  
  39. function YAYS(unsafeWindow) {
  40.  
  41. 'use strict';
  42.  
  43. /*
  44. * Meta.
  45. */
  46.  
  47. var Meta = {
  48. title: 'Yays! (Yet Another Youtube Script)',
  49. version: '1.11',
  50. releasedate: 'Feb 16, 2014',
  51. site: 'https://eugenox.appspot.com/script/yays',
  52. ns: 'yays'
  53. };
  54.  
  55. /*
  56. * Script context.
  57. */
  58.  
  59. var Context = unsafeWindow[Meta.ns] = {
  60. ns: Meta.ns
  61. };
  62.  
  63. /*
  64. * Utility functions.
  65. */
  66.  
  67. function each(iterable, callback, scope) {
  68. if (iterable.length) {
  69. for (var i = 0, len = iterable.length; i < len; ++i) {
  70. callback.call(scope, i, iterable[i]);
  71. }
  72. }
  73. else {
  74. for (var key in iterable) {
  75. if (iterable.hasOwnProperty(key)) {
  76. callback.call(scope, key, iterable[key]);
  77. }
  78. }
  79. }
  80. }
  81.  
  82. function map() {
  83. var
  84. args = Array.prototype.constructor.apply([], arguments),
  85. callback = args.shift() || bind(Array.prototype.constructor, []),
  86. results = [];
  87.  
  88. if (args.length > 1) {
  89. var i, getter = function(arg) { return arg[i]; }, len = Math.max.apply(Math, map(function(arg) { return arg.length; }, args));
  90.  
  91. for (i = 0; i < len; ++i) {
  92. results.push(callback.apply(null, map(getter, args)));
  93. }
  94. }
  95. else {
  96. var i, arg = args[0], len = arg.length;
  97.  
  98. for (i = 0; i < len; ++i) {
  99. results.push(callback(arg[i]));
  100. }
  101. }
  102.  
  103. return results;
  104. }
  105.  
  106. function unique(values) {
  107. values.sort();
  108.  
  109. for (var i = 0, j; i < values.length; ) {
  110. j = i;
  111.  
  112. while (values[j] === values[j - 1]) {
  113. j++;
  114. }
  115.  
  116. if (j - i) {
  117. values.splice(i, j - i);
  118. }
  119. else {
  120. ++i;
  121. }
  122. }
  123.  
  124. return values;
  125. }
  126.  
  127. function combine(keys, values) {
  128. var object = {};
  129.  
  130. map(function(key, value) { object[key] = value; }, keys, values);
  131.  
  132. return object;
  133. }
  134.  
  135. function merge(target, source, override) {
  136. override = override === undefined || override;
  137.  
  138. for (var key in source) {
  139. if (override || ! target.hasOwnProperty(key)) {
  140. target[key] = source[key];
  141. }
  142. }
  143.  
  144. return target;
  145. }
  146.  
  147. function extend(base, proto) {
  148. function T() {}
  149. T.prototype = base.prototype;
  150.  
  151. return merge(new T(), proto);
  152. }
  153.  
  154. function noop() {
  155. return;
  156. }
  157.  
  158. function bind(func, scope, args) {
  159. return function() {
  160. return func.apply(scope, args === undefined ? arguments : args);
  161. };
  162. }
  163.  
  164. function intercept(original, extension) {
  165. original = original || noop;
  166.  
  167. return function() {
  168. extension.apply(this, arguments);
  169.  
  170. return original.apply(this, arguments);
  171. };
  172. }
  173.  
  174. function asyncCall(func, scope, args) {
  175. setTimeout(bind(func, scope, args), 0);
  176. }
  177.  
  178. function asyncProxy(func) {
  179. return function() {
  180. asyncCall(func, this, arguments);
  181. };
  182. }
  183.  
  184. function buildURL(path, parameters) {
  185. var query = [];
  186. each(parameters, function(key, value) { query.push(key.concat('=', encodeURIComponent(value))); });
  187.  
  188. return path.concat('?', query.join('&'));
  189. }
  190.  
  191. function parseJSON(data) {
  192. if (typeof JSON != 'undefined') {
  193. return JSON.parse(data);
  194. }
  195.  
  196. return eval('(' + data + ')');
  197. }
  198.  
  199. /*
  200. * Console singleton.
  201. */
  202.  
  203. var Console = {
  204. debug: function() {
  205. unsafeWindow.console.debug.apply(unsafeWindow.console, Array.prototype.concat.apply(['[' + Meta.ns + ']'], arguments));
  206. }
  207. };
  208.  
  209. /*
  210. * DOM Helper singleton.
  211. */
  212.  
  213. var DH = {
  214. ELEMENT_NODE: 1,
  215.  
  216. build: function(def) {
  217. switch (Object.prototype.toString.call(def)) {
  218. case '[object Object]':
  219. var node = this.createElement(def.tag || 'div');
  220.  
  221. if ('style' in def) {
  222. this.style(node, def.style);
  223. }
  224.  
  225. if ('attributes' in def) {
  226. this.attributes(node, def.attributes);
  227. }
  228.  
  229. if ('listeners' in def) {
  230. this.listeners(node, def.listeners);
  231. }
  232.  
  233. if ('children' in def) {
  234. this.append(node, def.children);
  235. }
  236.  
  237. return node;
  238.  
  239. case '[object String]':
  240. return this.createTextNode(def);
  241.  
  242. default:
  243. return def;
  244. }
  245. },
  246.  
  247. id: bind(unsafeWindow.document.getElementById, unsafeWindow.document),
  248. tagName: bind(unsafeWindow.document.getElementsByTagName, unsafeWindow.document),
  249. createElement: bind(unsafeWindow.document.createElement, unsafeWindow.document),
  250. createTextNode: bind(unsafeWindow.document.createTextNode, unsafeWindow.document),
  251.  
  252. style: function(node, style) {
  253. each(style, node.style.setProperty, node.style);
  254. },
  255.  
  256. append: function(node, children) {
  257. each([].concat(children), function(i, child) { node.appendChild(this.build(child)); }, this);
  258. node.normalize();
  259. },
  260.  
  261. insertAfter: function(node, children) {
  262. var parent = node.parentNode, sibling = node.nextSibling;
  263. if (sibling) {
  264. each([].concat(children), function(i, child) { parent.insertBefore(this.build(child), sibling); }, this);
  265. parent.normalize();
  266. }
  267. else {
  268. this.append(parent, children);
  269. }
  270. },
  271.  
  272. prepend: function(node, children) {
  273. if (node.hasChildNodes()) {
  274. each([].concat(children), function(i, child) { node.insertBefore(this.build(child), node.firstChild); }, this);
  275. }
  276. else {
  277. this.append(node, children);
  278. }
  279. },
  280.  
  281. remove: function(node) {
  282. node.parentNode.removeChild(node);
  283. },
  284.  
  285. attributes: function(node, attributes) {
  286. each(attributes, node.setAttribute, node);
  287. },
  288.  
  289. hasClass: function(node, cls) {
  290. return node.hasAttribute('class') && new RegExp('\\b' + cls + '\\b').test(node.getAttribute('class'));
  291. },
  292.  
  293. addClass: function(node, clss) {
  294. node.setAttribute('class', node.hasAttribute('class') ? unique(node.getAttribute('class').concat(' ', clss).trim().split(/ +/)).join(' ') : clss);
  295. },
  296.  
  297. delClass: function(node, clss) {
  298. if (node.hasAttribute('class')) {
  299. node.setAttribute('class', node.getAttribute('class').replace(new RegExp('\\s*\\b(?:' + clss.replace(/ +/g, '|') + ')\\b\\s*', 'g'), ' ').trim());
  300. }
  301. },
  302.  
  303. listeners: function(node, listeners) {
  304. each(listeners, function(type, listener) { this.on(node, type, listener); }, this);
  305. },
  306.  
  307. on: function(node, type, listener) {
  308. node.addEventListener(type, listener, false);
  309. },
  310.  
  311. un: function(node, type, listener) {
  312. node.removeEventListener(type, listener, false);
  313. },
  314.  
  315. unwrap: function(element) {
  316. try {
  317. return XPCNativeWrapper.unwrap(element);
  318. }
  319. catch (e) {
  320. return element;
  321. }
  322. },
  323.  
  324. walk: function(node, path) {
  325. var steps = path.split('/'), step = null;
  326.  
  327. while (node && (step = steps.shift())) {
  328. if (step == '..') {
  329. node = node.parentNode;
  330. continue;
  331. }
  332.  
  333. var selector = /^(\w*)(?:\[(\d+)\])?$/.exec(step), name = selector[1], index = Number(selector[2]) || 0;
  334.  
  335. for (var i = 0, j = 0, nodes = node.childNodes; node = nodes.item(i); ++i)
  336. if (node.nodeType == this.ELEMENT_NODE && (! name || node.tagName.toLowerCase() == name) && j++ == index) {
  337. break;
  338. }
  339. }
  340.  
  341. return node;
  342. },
  343.  
  344. ready: function(listener) {
  345. var document = unsafeWindow.document;
  346.  
  347. if (document.readyState == 'loading') {
  348. DH.on(document, 'readystatechange', function onStateChange() {
  349. DH.un(document, 'readystatechange', onStateChange);
  350.  
  351. listener();
  352. });
  353. }
  354. else {
  355. listener();
  356. }
  357. }
  358. };
  359.  
  360. /*
  361. * i18n
  362. */
  363.  
  364. var _ = (function() {
  365. var vocabulary = ["Playback", "START", "PAUSE", "STOP", "AUTO PAUSE", "AUTO STOP", "Set default playback state", "Quality", "AUTO", "ORIGINAL", "Set default video quality", "Size", "AUTO", "WIDE", "FIT", "Set default player size", "Player settings", "Help"];
  366. var dictionary = {};
  367.  
  368. function translation(language) {
  369. switch (language) {
  370. // Hungarian - eugenox
  371. case 'hu':
  372. return ["Lej\u00e1tsz\u00e1s", "ELIND\u00cdTVA", "SZ\u00dcNETELTETVE", "MEG\u00c1LL\u00cdTVA", "AUTOMATIKUS SZ\u00dcNETELTET\u00c9S", "AUTOMATIKUS MEG\u00c1LL\u00cdT\u00c1S", "Lej\u00e1tsz\u00e1s alap\u00e9rtelmezett \u00e1llapota", "Min\u0151s\u00e9g", "AUTO", "EREDETI", "Vide\u00f3k alap\u00e9rtelmezett felbont\u00e1sa", "M\u00e9ret", "AUTO", "SZ\u00c9LES", "ILLESZTETT", "Lej\u00e1tsz\u00f3 alap\u00e9rtelmezett m\u00e9rete", "Lej\u00e1tsz\u00f3 be\u00e1ll\u00edt\u00e1sai", "S\u00fag\u00f3"];
  373. // Dutch - Mike-RaWare
  374. case 'nl':
  375. return [null, null, null, null, null, null, null, "Kwaliteit", "AUTOMATISCH", null, "Stel standaard videokwaliteit in", null, null, null, null, null, null, null];
  376. // Spanish - yonane, Dinepada, jdarlan
  377. case 'es':
  378. return ["Reproducci\u00f3n autom\u00e1tica", "Iniciar", "Pausar", "Detener", "Auto pausa", "Auto detener", "Fijar reproducci\u00f3n autom\u00e1tica", "Calidad", "Auto", "Original", "Calidad por defecto", "Tama\u00f1o", "Autom\u00e1tico", "Ancho", "Ajustar", "Tama\u00f1o del reproductor predeterminado", "Configuraci\u00f3n", "Ayuda"];
  379. // German - xemino, ich01
  380. case 'de':
  381. return ["Wiedergabe", "Start", "Pause", "Stop", "Auto-Pause", "Auto-Stop", "Standardm\u00e4\u00dfigen Wiedergabezustand setzen", "Qualit\u00e4t", "Auto", "Original", "Standard Video Qualit\u00e4t setzen", "Gr\u00f6\u00dfe", "Auto", "Breit", "Passend", "Standard Player Gr\u00f6\u00dfe setzen", "Einstellungen", "Hilfe"];
  382. // Portuguese - Pitukinha
  383. case 'pt':
  384. return [null, null, null, null, null, null, null, "Qualidade", "AUTOM\u00c1TICO", null, "Defini\u00e7\u00e3o padr\u00e3o de v\u00eddeo", null, null, null, null, null, "Configura\u00e7\u00e3o do usu\u00e1rio", null];
  385. // Greek - TastyTeo
  386. case 'el':
  387. return ["\u0391\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae", null, "\u03a0\u0391\u03a5\u03a3\u0397", "\u03a3\u03a4\u0391\u039c\u0391\u03a4\u0397\u039c\u0391", "\u0391\u03a5\u03a4\u039f\u039c\u0391\u03a4\u0397 \u03a0\u0391\u03a5\u03a3\u0397", "\u0391\u03a5\u03a4\u039f\u039c\u0391\u03a4\u039f \u03a3\u03a4\u0391\u039c\u0391\u03a4\u0397\u039c\u0391", "\u039f\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2", "\u03a0\u03bf\u03b9\u03cc\u03c4\u03b7\u03c4\u03b1", "\u0391\u03a5\u03a4\u039f\u039c\u0391\u03a4\u039f", "\u03a0\u03a1\u039f\u0395\u03a0\u0399\u039b\u039f\u0393\u0397", "\u039f\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03c0\u03bf\u03b9\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b2\u03af\u03bd\u03c4\u03b5\u03bf", "\u039c\u03ad\u03b3\u03b5\u03b8\u03bf\u03c2", "\u0391\u03a5\u03a4\u039f\u039c\u0391\u03a4\u039f", "\u03a0\u039b\u0391\u03a4\u03a5", "\u03a0\u03a1\u039f\u03a3\u0391\u03a1\u039c\u039f\u0393\u0397", "\u039f\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03b1\u03bd\u03ac\u03bb\u03c5\u03c3\u03b7\u03c2 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ad\u03b1", "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ad\u03b1", "\u0392\u03bf\u03ae\u03b8\u03b5\u03b9\u03b1"];
  388. // French - eXa
  389. case 'fr':
  390. return [null, null, null, null, null, null, null, "Qualit\u00e9", "AUTO", "ORIGINAL", "Qualit\u00e9 par d\u00e9faut", "Taille", "AUTO", "LARGE", "ADAPT\u00c9", "Taille par d\u00e9faut du lecteur", "Options du lecteur", "Aide"];
  391. // Slovenian - Paranoia.Com
  392. case 'sl':
  393. return [null, null, null, null, null, null, null, "Kakovost", "Samodejno", null, "Nastavi privzeto kakovost videa", null, null, null, null, null, "Nastavitve predvajalnika", "Pomo\u010d"];
  394. // Russian - an1k3y
  395. case 'ru':
  396. return [null, null, null, null, null, null, null, "\u041a\u0430\u0447\u0435\u0441\u0442\u0432\u043e", "\u0410\u0412\u0422\u041e", null, "\u041a\u0430\u0447\u0435\u0441\u0442\u0432\u043e \u0432\u0438\u0434\u0435\u043e \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e", "\u0420\u0410\u0417\u041c\u0415\u0420", null, "\u0420\u0410\u0417\u0412\u0415\u0420\u041d\u0423\u0422\u042c", "\u0420\u0410\u0421\u0422\u042f\u041d\u0423\u0422\u042c", "\u0420\u0430\u0437\u043c\u0435\u0440 \u0432\u0438\u0434\u0435\u043e \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e", "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u043b\u0435\u0435\u0440\u0430", "\u041f\u043e\u043c\u043e\u0449\u044c"];
  397. // Hebrew - baryoni
  398. case 'iw':
  399. return [null, null, null, null, null, null, null, "\u05d0\u05d9\u05db\u05d5\u05ea", "\u05d0\u05d5\u05d8\u05d5\u05de\u05d8\u05d9\u05ea", null, "\u05d4\u05d2\u05d3\u05e8 \u05d0\u05ea \u05d0\u05d9\u05db\u05d5\u05ea \u05d1\u05e8\u05d9\u05e8\u05ea \u05d4\u05de\u05d7\u05d3\u05dc \u05e9\u05dc \u05d4\u05d5\u05d9\u05d3\u05d0\u05d5", "\u05d2\u05d5\u05d3\u05dc", null, "\u05e8\u05d7\u05d1", "\u05de\u05dc\u05d0", "\u05d4\u05d2\u05d3\u05e8 \u05d0\u05ea \u05d2\u05d5\u05d3\u05dc \u05d1\u05e8\u05d9\u05e8\u05ea \u05d4\u05de\u05d7\u05d3\u05dc \u05e9\u05dc \u05d4\u05e0\u05d2\u05df", "\u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05e0\u05d2\u05df", "\u05e2\u05d6\u05e8\u05d4"];
  400. // Chinese - blankhang
  401. case 'zh':
  402. return ["\u64ad\u653e\u6a21\u5f0f", "\u64ad\u653e", "\u6682\u505c", "\u505c\u6b62", "\u81ea\u52a8\u6682\u505c", "\u81ea\u52a8\u505c\u6b62", "\u8bbe\u7f6e\u9ed8\u8ba4\u64ad\u653e\u6a21\u5f0f", "\u89c6\u9891\u8d28\u91cf", "\u81ea\u52a8", "\u539f\u753b", "\u8bbe\u7f6e\u9ed8\u8ba4\u89c6\u9891\u8d28\u91cf", "\u64ad\u653e\u5668\u5927\u5c0f", "\u81ea\u52a8", "\u5bbd\u5c4f", "\u81ea\u9002\u5e94", "\u8bbe\u7f6e\u64ad\u653e\u5668\u9ed8\u8ba4\u5927\u5c0f", "\u64ad\u653e\u5668\u8bbe\u7f6e", "\u5e2e\u52a9"];
  403. // Polish - mkvs
  404. case 'pl':
  405. return ["Odtwarzanie", "URUCHOM", "WSTRZYMAJ", "ZATRZYMAJ", "AUTOMATYCZNIE WSTRZYMAJ", "AUTOMATYCZNIE ZATRZYMAJ", "Ustaw domy\u015blny stan odtwarzania", "Jako\u015b\u0107", "AUTOMATYCZNA", "ORYGINALNA", "Ustaw domy\u015bln\u0105 jako\u015b\u0107 film\u00f3w", "Rozmiar", "AUTOMATYCZNY", "SZEROKI", "DOPASOWANY", "Ustaw domy\u015blny rozmiar odtwarzacza", "Ustawienia odtwarzacza", "Pomoc"];
  406. // Swedish - eson
  407. case 'sv':
  408. return ["Uppspelning", "START", "PAUS", "STOPP", "AUTOPAUS", "AUTOSTOPP", "Ange uppspelningsl\u00e4ge", "Kvalitet", "AUTO", "ORIGINAL", "Ange standardkvalitet", "Storlek", "AUTO", "BRED", "ANPASSAD", "Ange standardstorlek", "Inst\u00e4llningar", "Hj\u00e4lp"];
  409. // Ukrainian - mukolah
  410. case 'uk':
  411. return ["\u0421\u0442\u0430\u043d \u043f\u0440\u043e\u0433\u0440\u0430\u0432\u0430\u0447\u0430", "\u0412\u0406\u0414\u0422\u0412\u041e\u0420\u0418\u0422\u0418", "\u041f\u0420\u0418\u0417\u0423\u041f\u0418\u041d\u0418\u0422\u0418", "\u0417\u0423\u041f\u0418\u041d\u0418\u0422\u0418", "\u0410\u0412\u0422\u041e\u041c\u0410\u0422\u0418\u0427\u041d\u0415 \u041f\u0420\u0418\u0417\u0423\u041f\u0418\u041d\u0415\u041d\u041d\u042f", "\u0410\u0412\u0422\u041e\u041c\u0410\u0422\u0418\u0427\u041d\u0415 \u0417\u0423\u041f\u0418\u041d\u0415\u041d\u041d\u042f", "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u0438\u0439 \u0441\u0442\u0430\u043d \u0432\u0456\u0434\u0442\u0432\u043e\u0440\u0435\u043d\u043d\u044f", "\u042f\u043a\u0456\u0441\u0442\u044c", "\u0410\u0412\u0422\u041e\u041c\u0410\u0422\u0418\u0427\u041d\u041e", "\u041e\u0420\u0418\u0413\u0406\u041d\u0410\u041b\u042c\u041d\u0410", "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u0443 \u044f\u043a\u0456\u0441\u0442\u044c \u0432\u0456\u0434\u0442\u0432\u043e\u0440\u0435\u043d\u043d\u044f", "\u0420\u043e\u0437\u043c\u0456\u0440", "\u0410\u0412\u0422\u041e\u041c\u0410\u0422\u0418\u0427\u041d\u0418\u0419", "\u0428\u0418\u0420\u041e\u041a\u0418\u0419", "\u041f\u041e \u0428\u0418\u0420\u0418\u041d\u0406 \u0415\u041a\u0420\u0410\u041d\u0423", "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u0438\u0439 \u0440\u043e\u0437\u043c\u0456\u0440 \u043f\u0440\u043e\u0433\u0440\u0430\u0432\u0430\u0447\u0430", "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u043e\u0433\u0440\u0430\u0432\u0430\u0447\u0430", "\u0414\u043e\u043f\u043e\u043c\u043e\u0433\u0430"];
  412. }
  413.  
  414. return [];
  415. }
  416.  
  417. function TranslationMixin(text) {
  418. this.toString = this.valueOf = function() { return dictionary[text] || text; };
  419. }
  420.  
  421. DH.ready(function() {
  422. dictionary = combine(vocabulary, translation((document.documentElement.lang || 'en').substr(0, 2)));
  423. });
  424.  
  425. return function(text) {
  426. return merge(new String(text), new TranslationMixin(text));
  427. };
  428. })();
  429.  
  430. /*
  431. * Configuration handler singleton.
  432. */
  433.  
  434. var Config = (function(namespace) {
  435. // Greasemonkey compatible
  436. if (typeof GM_getValue == 'function') {
  437. return {
  438. get: GM_getValue,
  439. set: GM_setValue,
  440. del: GM_deleteValue
  441. };
  442. }
  443.  
  444. // HTML5
  445. return {
  446. get: function(key) {
  447. return unsafeWindow.localStorage.getItem(namespace + '.' + key);
  448. },
  449.  
  450. set: function(key, value) {
  451. unsafeWindow.localStorage.setItem(namespace + '.' + key, value);
  452. },
  453.  
  454. del: function(key) {
  455. unsafeWindow.localStorage.removeItem(namespace + '.' + key);
  456. }
  457. };
  458. })(Meta.ns);
  459.  
  460. /**
  461. * @class JSONRequest
  462. * Create XHR or JSONP requests.
  463. */
  464. var JSONRequest = (function() {
  465. var Request = null;
  466.  
  467. // XHR
  468. if (typeof GM_xmlhttpRequest == 'function') {
  469. Request = function(url, parameters, callback) {
  470. this._callback = callback;
  471.  
  472. GM_xmlhttpRequest({
  473. method: 'GET',
  474. url: buildURL(url, parameters),
  475. onload: bind(this._onLoad, this)
  476. });
  477. };
  478.  
  479. Request.prototype = {
  480. _onLoad: function(response) {
  481. this._callback(parseJSON(response.responseText));
  482. }
  483. };
  484. }
  485. // JSONP
  486. else {
  487. Context.jsonp = [];
  488.  
  489. Request = function(url, parameters, callback) {
  490. this._callback = callback;
  491. this._id = Context.jsonp.push(bind(this._onLoad, this)) - 1;
  492.  
  493. parameters.callback = Context.ns.concat('.jsonp[', this._id, ']');
  494.  
  495. this._scriptNode = document.body.appendChild(DH.build({
  496. tag: 'script',
  497. attributes: {
  498. 'type': 'text/javascript',
  499. 'src': buildURL(url, parameters)
  500. }
  501. }));
  502. };
  503.  
  504. Request.prototype = {
  505. _callback: null,
  506. _id: null,
  507. _scriptNode: null,
  508.  
  509. _onLoad: function(response) {
  510. this._callback(response);
  511.  
  512. document.body.removeChild(this._scriptNode);
  513. delete Context.jsonp[this._id];
  514. }
  515. };
  516. }
  517.  
  518. return Request;
  519. })();
  520.  
  521. /*
  522. * Update checker.
  523. */
  524.  
  525. DH.ready(function() {
  526. if (new Date().valueOf() - Number(Config.get('update_checked_at')) < 86400000) { // 1 day
  527. return;
  528. }
  529.  
  530. var popup = null;
  531.  
  532. new JSONRequest(Meta.site + '/changelog', {version: Meta.version}, function(changelog) {
  533. Config.set('update_checked_at', new Date().valueOf().toFixed());
  534.  
  535. if (changelog && changelog.length) {
  536. popup = renderPopup(changelog);
  537. }
  538. });
  539.  
  540. function renderPopup(changelog) {
  541. return document.body.appendChild(DH.build({
  542. style: {
  543. 'position': 'fixed',
  544. 'bottom': '0',
  545. 'width': '100%',
  546. 'z-index': '1000',
  547. 'background-color': '#f1f1f1',
  548. 'border-top': '1px solid #cccccc'
  549. },
  550. children: {
  551. style: {
  552. 'margin': '15px'
  553. },
  554. children: [{
  555. tag: 'strong',
  556. children: ['There is an update available for ', Meta.title, '.']
  557. }, {
  558. tag: 'p',
  559. style: {
  560. 'margin': '10px 0'
  561. },
  562. children: [
  563. 'You are using version ', {
  564. tag: 'strong',
  565. children: Meta.version
  566. }, ', released on ', {
  567. tag: 'em',
  568. children: Meta.releasedate
  569. }, '. Please consider updating to the latest version.'
  570. ]
  571. }, {
  572. style: {
  573. 'margin': '10px 0',
  574. 'max-height': '150px',
  575. 'overflow-y': 'auto'
  576. },
  577. children: {
  578. tag: 'a',
  579. children: 'Show changes',
  580. listeners: {
  581. click: function(e) {
  582. e.preventDefault();
  583.  
  584. DH.insertAfter(e.target, map(function(entry) {
  585. return {
  586. style: {
  587. 'margin-bottom': '5px'
  588. },
  589. children: [{
  590. tag: 'strong',
  591. children: entry.version
  592. }, ' ', {
  593. tag: 'em',
  594. children: ['(', entry.date, ')']
  595. }, {
  596. style: {
  597. 'padding': '0 0 2px 10px',
  598. 'white-space': 'pre'
  599. },
  600. children: [].concat(entry.note).join('\n')
  601. }]
  602. };
  603. }, [].concat(changelog)));
  604.  
  605. DH.remove(e.target);
  606. }
  607. }
  608. }
  609. }, {
  610. children: map(function(text, handler) {
  611. return DH.build({
  612. tag: 'button',
  613. attributes: {
  614. 'type': 'button',
  615. 'class': 'yt-uix-button yt-uix-button-default'
  616. },
  617. style: {
  618. 'margin-right': '10px',
  619. 'padding': '5px 15px'
  620. },
  621. children: text,
  622. listeners: {
  623. 'click': handler
  624. }
  625. });
  626. }, ['Update', 'Dismiss'], [openDownloadSite, removePopup])
  627. }]
  628. }
  629. }));
  630. }
  631.  
  632. function removePopup() {
  633. document.body.removeChild(popup);
  634. }
  635.  
  636. function openDownloadSite() {
  637. removePopup();
  638. unsafeWindow.open(buildURL(Meta.site + '/download', {version: Meta.version}));
  639. }
  640. });
  641.  
  642. /*
  643. * Migrations.
  644. */
  645.  
  646. (function(currentVersion) {
  647. var previousVersion = Config.get('version') || '1.0';
  648.  
  649. if (previousVersion == currentVersion) {
  650. return;
  651. }
  652.  
  653. previousVersion = map(Number, previousVersion.split('.'));
  654.  
  655. each([
  656. {
  657. // Added "144p" to the quality levels.
  658. version: '1.7', apply: function() {
  659. var videoQuality = Number(Config.get('video_quality'));
  660. if (videoQuality > 0 && videoQuality < 7) {
  661. Config.set('video_quality', ++videoQuality);
  662. }
  663. }
  664. },
  665. {
  666. // Autoplay reworked.
  667. version: '1.8', apply: function() {
  668. switch (Number(Config.get('auto_play'))) {
  669. case 1: // OFF > PAUSE
  670. Config.set('video_playback', 1);
  671. break;
  672.  
  673. case 2: // AUTO > AUTO PAUSE
  674. Config.set('video_playback', 3);
  675. break;
  676. }
  677.  
  678. Config.del('auto_play');
  679. }
  680. },
  681. {
  682. // Added "1440p" to the quality levels.
  683. version: '1.10', apply: function() {
  684. var videoQuality = Number(Config.get('video_quality'));
  685. if (videoQuality > 6) {
  686. Config.set('video_quality', ++videoQuality);
  687. }
  688. }
  689. }
  690. ], function(i, migration) {
  691. var migrationVersion = map(Number, migration.version.split('.'));
  692.  
  693. for (var i = 0, parts = Math.max(previousVersion.length, migrationVersion.length); i < parts; ++i) {
  694. if ((previousVersion[i] || 0) < (migrationVersion[i] || 0)) {
  695. Console.debug('Applying migration', migration.version);
  696.  
  697. migration.apply();
  698.  
  699. break;
  700. }
  701. }
  702. });
  703.  
  704. Config.set('version', currentVersion);
  705. })(Meta.version);
  706.  
  707. /**
  708. * @class Player
  709. */
  710. function Player(element) {
  711. this._element = element;
  712.  
  713. this._exportApiInterface();
  714.  
  715. Console.debug('Player ready');
  716.  
  717. this._muted = Number(this.isMuted());
  718.  
  719. this._addStateChangeListener();
  720. }
  721.  
  722. merge(Player, {
  723. UNSTARTED: -1,
  724. ENDED: 0,
  725. PLAYING: 1,
  726. PAUSED: 2,
  727. BUFFERING: 3,
  728. CUED: 5,
  729.  
  730. instance: null,
  731.  
  732. create: function(element) {
  733. switch (element.tagName) {
  734. case 'EMBED':
  735. return new FlashPlayer(element);
  736. case 'DIV':
  737. return new HTML5Player(element);
  738. }
  739.  
  740. throw 'Unknown player type';
  741. },
  742.  
  743. initialize: function(element) {
  744. if (! element) {
  745. throw 'Invalid player element';
  746. }
  747.  
  748. if (this.instance) {
  749. if (this.instance._element === element) {
  750. throw 'Player already initialized';
  751. }
  752.  
  753. this.instance.invalidate();
  754. }
  755.  
  756. return this.instance = this.create(element);
  757. }
  758. });
  759.  
  760. Player.prototype = {
  761. _element: null,
  762. _muted: 0,
  763.  
  764. _exportApiInterface: function() {
  765. each(this._element.getApiInterface(), function(i, method) {
  766. if (! (method in this)) {
  767. this[method] = bind(this._element[method], this._element);
  768. }
  769. }, this);
  770. },
  771.  
  772. _unexportApiInterface: function() {
  773. each(this._element.getApiInterface(), function(i, method) {
  774. if (this.hasOwnProperty(method)) {
  775. delete this[method];
  776. }
  777. }, this);
  778. },
  779.  
  780. _onStateChange: function(state) {
  781. Console.debug('State changed to', ['unstarted', 'ended', 'playing', 'paused', 'buffering', undefined, 'cued'][state + 1]);
  782.  
  783. this.onStateChange(state);
  784. },
  785.  
  786. _addStateChangeListener: function() {
  787. Context.onPlayerStateChange = asyncProxy(bind(this._onStateChange, this));
  788. this.addEventListener('onStateChange', Context.ns + '.onPlayerStateChange');
  789. },
  790.  
  791. _removeStateChangeListener: function() {
  792. Context.onPlayerStateChange = noop;
  793. this.removeEventListener('onStateChange', Context.ns + '.onPlayerStateChange');
  794. },
  795.  
  796. invalidate: function() {
  797. this._removeStateChangeListener();
  798. this._unexportApiInterface();
  799.  
  800. delete this.onStateChange;
  801. delete this._element;
  802.  
  803. Console.debug('Player invalidated');
  804.  
  805. this.invalidate = noop;
  806. },
  807.  
  808. onStateChange: noop,
  809.  
  810. isPlayerState: function() {
  811. return Array.prototype.indexOf.call(arguments, this.getPlayerState()) > -1;
  812. },
  813.  
  814. isVideoLoaded: function() {
  815. return Boolean(this.getVideoId());
  816. },
  817.  
  818. getVideoId: function() {
  819. try {
  820. return this.getVideoData().video_id;
  821. }
  822. catch (e) {
  823. return (this.getVideoUrl().match(/\bv=([\w-]+)/) || [, undefined])[1];
  824. }
  825. },
  826.  
  827. restartPlayback: function() {
  828. var
  829. code = (location.hash + location.search).match(/\bt=(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s?)?/) || new Array(4),
  830. seconds = (Number(code[1]) || 0) * 3600 + (Number(code[2]) || 0) * 60 + (Number(code[3]) || 0);
  831.  
  832. this.seekTo(seconds, true);
  833. },
  834.  
  835. resetState: function() {
  836. this.seekTo(this.getCurrentTime(), true);
  837. },
  838.  
  839. mute: function() {
  840. if (! this._muted++) {
  841. this._element.mute();
  842.  
  843. Console.debug('Player muted');
  844. }
  845. },
  846.  
  847. unMute: function() {
  848. if (! --this._muted) {
  849. this._element.unMute();
  850.  
  851. Console.debug('Player unmuted');
  852. }
  853. },
  854.  
  855. playVideo: function() {
  856. this._element.playVideo();
  857.  
  858. Console.debug('Playback started');
  859. },
  860.  
  861. pauseVideo: function() {
  862. this._element.pauseVideo();
  863.  
  864. Console.debug('Playback paused');
  865. },
  866.  
  867. stopVideo: function() {
  868. this._element.stopVideo();
  869.  
  870. Console.debug('Playback stopped');
  871. },
  872.  
  873. setPlaybackQuality: function(quality) {
  874. this._element.setPlaybackQuality(quality);
  875.  
  876. Console.debug('Quality changed to', quality);
  877. }
  878. };
  879.  
  880. /**
  881. * @class FlashPlayer
  882. */
  883. function FlashPlayer(element) {
  884. Player.call(this, element);
  885. }
  886.  
  887. FlashPlayer.prototype = extend(Player, {
  888. _exportApiInterface: function() {
  889. try {
  890. Player.prototype._exportApiInterface.call(this);
  891. }
  892. catch (e) {
  893. throw 'Player has not loaded yet';
  894. }
  895. },
  896.  
  897. _unexportApiInterface: function() {
  898. try {
  899. Player.prototype._unexportApiInterface.call(this);
  900. }
  901. catch (e) {
  902. Console.debug('Player has unloaded');
  903. }
  904. },
  905.  
  906. _removeStateChangeListener: function() {
  907. try {
  908. Player.prototype._removeStateChangeListener.call(this);
  909. }
  910. catch (e) {
  911. Console.debug('Player has unloaded');
  912. }
  913. }
  914. });
  915.  
  916. /**
  917. * @class HTML5Player
  918. */
  919. function HTML5Player(element) {
  920. Player.call(this, element);
  921.  
  922. this._state = element.getPlayerState();
  923. }
  924.  
  925. HTML5Player.prototype = extend(Player, {
  926. _state: null,
  927.  
  928. _onStateChange: function(state) {
  929. this._state = state;
  930.  
  931. Player.prototype._onStateChange.call(this, state);
  932. },
  933.  
  934. getPlayerState: function() {
  935. return this._state;
  936. },
  937.  
  938. restartPlayback: function() {
  939. Player.prototype.restartPlayback.call(this);
  940.  
  941. this.restartPlayback = noop;
  942. },
  943.  
  944. playVideo: function() {
  945. Player.prototype.playVideo.call(this);
  946.  
  947. this._state = Player.PLAYING;
  948. },
  949.  
  950. pauseVideo: function() {
  951. Player.prototype.pauseVideo.call(this);
  952.  
  953. this._state = Player.PAUSED;
  954. },
  955.  
  956. stopVideo: function() {
  957. Player.prototype.stopVideo.call(this);
  958.  
  959. this._state = Player.CUED;
  960. },
  961.  
  962. setPlaybackQuality: function(quality) {
  963. Player.prototype.setPlaybackQuality.call(this, quality);
  964.  
  965. asyncCall(function() {
  966. if (this.isPlayerState(Player.PLAYING, Player.BUFFERING)) {
  967. this.playVideo();
  968. }
  969. }, this);
  970. }
  971. });
  972.  
  973. /**
  974. * @class Button
  975. */
  976. function Button(label, tooltip) {
  977. this._node = DH.build(this._def(tooltip, label, this._indicator = DH.build('-')));
  978. }
  979.  
  980. Button.prototype = {
  981. _indicator: null,
  982. _node: null,
  983.  
  984. _def: function(tooltip, label, indicator) {
  985. return {
  986. tag: 'button',
  987. attributes: {
  988. 'type': 'button',
  989. 'class': 'yt-uix-button yt-uix-button-default yt-uix-tooltip',
  990. 'title': tooltip
  991. },
  992. listeners: {
  993. 'click': bind(this._onClick, this)
  994. },
  995. style: {
  996. 'margin': '0 0.5%'
  997. },
  998. children: [{
  999. tag: 'span',
  1000. attributes: {
  1001. 'class': 'yt-uix-button-content'
  1002. },
  1003. children: label
  1004. }, {
  1005. tag: 'span',
  1006. style: {
  1007. 'font-size': '14px',
  1008. 'margin-left': '5px'
  1009. },
  1010. attributes: {
  1011. 'class': 'yt-uix-button-content'
  1012. },
  1013. children: indicator
  1014. }]
  1015. };
  1016. },
  1017.  
  1018. _onClick: function() {
  1019. this.handler();
  1020. this.refresh();
  1021. },
  1022.  
  1023. refresh: function() {
  1024. this._indicator.data = this.display();
  1025. },
  1026.  
  1027. render: function() {
  1028. this.refresh();
  1029. return this._node;
  1030. },
  1031.  
  1032. handler: noop,
  1033. display: noop
  1034. };
  1035.  
  1036. /**
  1037. * @class PlayerOption
  1038. */
  1039. function PlayerOption(player, key) {
  1040. this._player = player;
  1041. this._key = key;
  1042. }
  1043.  
  1044. PlayerOption.prototype = {
  1045. _player: null,
  1046. _key: null,
  1047.  
  1048. get: function() {
  1049. return Number(Config.get(this._key) || '0');
  1050. },
  1051.  
  1052. set: function(value) {
  1053. Config.set(this._key, Number(value));
  1054. },
  1055.  
  1056. apply: noop,
  1057. cease: noop
  1058. };
  1059.  
  1060. /**
  1061. * @class PlayerOption.Button
  1062. */
  1063. PlayerOption.Button = function(option) {
  1064. Button.call(this, this.label, this.tooltip);
  1065.  
  1066. this._option = option;
  1067. };
  1068.  
  1069. PlayerOption.Button.extend = function(attributes) {
  1070. var superclass = this;
  1071.  
  1072. function Button(option) {
  1073. superclass.call(this, option);
  1074. }
  1075.  
  1076. Button.prototype = extend(superclass, attributes);
  1077.  
  1078. return Button;
  1079. };
  1080.  
  1081. PlayerOption.Button.prototype = extend(Button, {
  1082. _option: null,
  1083.  
  1084. label: null,
  1085. tooltip: null,
  1086. states: null,
  1087.  
  1088. handler: function() {
  1089. this._option.set((this._option.get() + 1) % this.states.length);
  1090. },
  1091.  
  1092. display: function() {
  1093. return this.states[this._option.get()];
  1094. }
  1095. });
  1096.  
  1097. /**
  1098. * @class SilentPlayerOption
  1099. */
  1100. function SilentPlayerOption(player, key) {
  1101. PlayerOption.call(this, player, key);
  1102. }
  1103.  
  1104. SilentPlayerOption.prototype = extend(PlayerOption, {
  1105. _muted: false,
  1106.  
  1107. mute: function(state) {
  1108. if (this._muted != state) {
  1109. if (state) {
  1110. this._player.mute();
  1111. }
  1112. else {
  1113. this._player.unMute();
  1114. }
  1115.  
  1116. this._muted = state;
  1117. }
  1118. },
  1119.  
  1120. cease: function() {
  1121. this.mute(false);
  1122. }
  1123. });
  1124.  
  1125. /**
  1126. * @class VideoPlayback
  1127. */
  1128. function VideoPlayback(player) {
  1129. SilentPlayerOption.call(this, player, 'video_playback');
  1130.  
  1131. if (player.isVideoLoaded()) {
  1132. switch (this.get()) {
  1133. case 0: // PLAY
  1134. this._applied = true;
  1135. break;
  1136.  
  1137. case 3: // AUTO PAUSE
  1138. case 4: // AUTO STOP
  1139. // Video is visible or opened in the same window.
  1140. if (this._isVisible() || unsafeWindow.history.length > 1) {
  1141. this._applied = true;
  1142. }
  1143. // Video is opened in a background tab.
  1144. else {
  1145. this._handler = bind(this._handler, this);
  1146.  
  1147. DH.on(unsafeWindow, 'focus', this._handler);
  1148. DH.on(unsafeWindow, 'blur', this._handler);
  1149. }
  1150. break;
  1151. }
  1152. }
  1153. else {
  1154. this._applied = true;
  1155. }
  1156. }
  1157.  
  1158. VideoPlayback.prototype = extend(SilentPlayerOption, {
  1159. _applied: false,
  1160. _timer: null,
  1161.  
  1162. _handler: function(e) {
  1163. switch (e.type) {
  1164. case 'focus':
  1165. if (this._timer === null) {
  1166. this._timer = setTimeout(bind(function() {
  1167. if (this._applied) {
  1168. this._player.resetState();
  1169. this._player.playVideo();
  1170.  
  1171. Console.debug('Playback autostarted');
  1172. }
  1173. else {
  1174. this._applied = true;
  1175.  
  1176. Console.debug('Playback not affected');
  1177.  
  1178. this.mute(false);
  1179. }
  1180.  
  1181. DH.un(unsafeWindow, 'focus', this._handler);
  1182. DH.un(unsafeWindow, 'blur', this._handler);
  1183.  
  1184. this._timer = null;
  1185. }, this), 500);
  1186. }
  1187. break;
  1188.  
  1189. case 'blur':
  1190. if (this._timer !== null) {
  1191. clearTimeout(this._timer);
  1192.  
  1193. this._timer = null;
  1194. }
  1195. break;
  1196. }
  1197. },
  1198.  
  1199. // @see http://www.w3.org/TR/page-visibility/
  1200. _isVisible: function() {
  1201. var doc = unsafeWindow.document;
  1202. return doc.hidden === false || doc.mozHidden === false || doc.webkitHidden === false;
  1203. },
  1204.  
  1205. apply: function() {
  1206. if (! this._applied) {
  1207. this.mute(true);
  1208.  
  1209. if (this._player.isPlayerState(Player.PLAYING)) {
  1210. this._applied = true;
  1211.  
  1212. this._player.restartPlayback();
  1213.  
  1214. if (this.get() % 2) { // (AUTO) PAUSE
  1215. this._player.pauseVideo();
  1216. }
  1217. else { // (AUTO) STOP
  1218. this._player.stopVideo();
  1219. }
  1220.  
  1221. this.mute(false);
  1222. }
  1223. }
  1224. }
  1225. });
  1226.  
  1227. /**
  1228. * @class VideoPlayback.Button
  1229. */
  1230. VideoPlayback.Button = PlayerOption.Button.extend({
  1231. label: _('Playback'),
  1232. tooltip: _('Set default playback state'),
  1233. states: [_('START'), _('PAUSE'), _('STOP'), _('AUTO PAUSE'), _('AUTO STOP')]
  1234. });
  1235.  
  1236. /**
  1237. * @class VideoQuality
  1238. */
  1239. function VideoQuality(player) {
  1240. SilentPlayerOption.call(this, player, 'video_quality');
  1241.  
  1242. this._applied = ! (player.isVideoLoaded() && this.get());
  1243. }
  1244.  
  1245. VideoQuality.prototype = extend(SilentPlayerOption, {
  1246. _applied: false,
  1247.  
  1248. apply: function() {
  1249. if (! this._applied) {
  1250. this.mute(true);
  1251.  
  1252. if (this._player.isPlayerState(Player.PLAYING, Player.PAUSED, Player.BUFFERING)) {
  1253. this._applied = true;
  1254.  
  1255. var quality = ['tiny', 'small', 'medium', 'large', 'hd720', 'hd1080', 'hd1440', 'highres'][this.get() - 1];
  1256.  
  1257. if (quality != this._player.getPlaybackQuality()) {
  1258. this._player.restartPlayback();
  1259. this._player.setPlaybackQuality(quality);
  1260. }
  1261.  
  1262. asyncCall(this.apply, this);
  1263. }
  1264. } else if (this._player.isPlayerState(Player.PLAYING, Player.CUED)) {
  1265. this.mute(false);
  1266. }
  1267. }
  1268. });
  1269.  
  1270. /**
  1271. * @class VideoQuality.Button
  1272. */
  1273. VideoQuality.Button = PlayerOption.Button.extend({
  1274. label: _('Quality'),
  1275. tooltip: _('Set default video quality'),
  1276. states: [_('AUTO'), '144p', '240p', '360p', '480p', '720p', '1080p', '1440p', _('ORIGINAL')]
  1277. });
  1278.  
  1279. /**
  1280. * @class PlayerSize
  1281. */
  1282. function PlayerSize(player) {
  1283. PlayerOption.call(this, player, 'player_size');
  1284. }
  1285.  
  1286. PlayerSize.prototype = extend(PlayerOption, {
  1287. apply: function() {
  1288. var mode = this.get();
  1289.  
  1290. switch (mode) {
  1291. case 2: // FIT
  1292. DH.id('yays-player-size') || DH.append(document.body, {
  1293. tag: 'style',
  1294. attributes: {
  1295. 'id': 'yays-player-size',
  1296. 'type': 'text/css'
  1297. },
  1298. children: [
  1299. '.watch-medium,',
  1300. '.watch-medium .player-width {',
  1301. 'width: 960px !important;',
  1302. '}',
  1303. '.watch-medium .player-height {',
  1304. 'height: 569.578px !important;',
  1305. '}',
  1306. '.watch-medium .watch7-playlist-bar-left {',
  1307. 'width: 660px;',
  1308. '}',
  1309. '.watch-medium #watch7-playlist-tray-container {',
  1310. 'left: 660px;',
  1311. '}',
  1312. '.watch-medium .html5-video-content,',
  1313. '.watch-medium .html5-main-video {',
  1314. 'transform: matrix(1.12412, 0, 0, 1.12412, 53, 29.7892) !important;',
  1315. '-o-transform: matrix(1.12412, 0, 0, 1.12412, 53, 29.7892) !important;',
  1316. '-moz-transform: matrix(1.12412, 0, 0, 1.12412, 53, 29.7892) !important;',
  1317. '-webkit-transform: matrix(1.12412, 0, 0, 1.12412, 53, 29.7892) !important;',
  1318. '}',
  1319. '.watch-medium .html5-main-video {',
  1320. 'z-index: -1;',
  1321. '}'
  1322. ]
  1323. });
  1324.  
  1325. // no break;
  1326.  
  1327. case 1: // WIDE
  1328. var container = DH.id('watch7-container'), player = DH.id('player');
  1329.  
  1330. DH.addClass(container, 'watch-wide');
  1331. DH.delClass(player, 'watch-small');
  1332. DH.addClass(player, 'watch-medium watch-playlist-collapsed');
  1333.  
  1334. break;
  1335.  
  1336. default:
  1337. return;
  1338. }
  1339.  
  1340. Console.debug('Size set to', ['wide', 'fit'][mode - 1]);
  1341. }
  1342. });
  1343.  
  1344. /**
  1345. * @class PlayerSize.Button
  1346. */
  1347. PlayerSize.Button = PlayerOption.Button.extend({
  1348. label: _('Size'),
  1349. tooltip: _('Set default player size'),
  1350. states: [_('AUTO'), _('WIDE'), _('FIT')]
  1351. });
  1352.  
  1353. /**
  1354. * @class UI
  1355. * Abstract UI class.
  1356. */
  1357. function UI(content) {
  1358. this.content = content;
  1359. this.button = DH.build(this._def.button(bind(this.toggle, this)));
  1360. this.panel = DH.build(this._def.panel(content));
  1361. }
  1362.  
  1363. merge(UI, {
  1364. instance: null,
  1365.  
  1366. initialize: function(type, content) {
  1367. if (this.instance) {
  1368. this.instance.destroy();
  1369. }
  1370.  
  1371. return this.instance = new type(content);
  1372. }
  1373. });
  1374.  
  1375. UI.prototype = {
  1376. _def: {
  1377. icon: {
  1378. tag: 'img',
  1379. attributes: {
  1380. 'src': 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAA4ElEQVQoz32RMU4CQRhG38xqQ0e7CbHCnnxHEM/AEUiIthZegFAYErIhegTuwAWIGYiWWGKypY0bkgUZCxZ2JIuvmnkz8//fzECA2ppqqnbozJ8NOZfA2tVKZwE0lFcGbADwoExeo6KCujxTzb1LLBBxDgsRpK/xmtuK5Uf3BEZvNKgXakEHmNAq5t+sjHxw5tp9gJosT27xHxe8By0m2rc4kPFpAPTAoDJkHyJQj2Fl9Zv4K51Z4OdsgB1YcC8kQO4MOQSjsUvKb9pn2crLa1ua4zOnAMRzrlhxly4PBn4BWEpBljV5iJUAAAAASUVORK5CYII='
  1381. }
  1382. },
  1383.  
  1384. button: function(click) {
  1385. return {
  1386. listeners: {
  1387. 'click': click
  1388. }
  1389. };
  1390. },
  1391.  
  1392. panel: function(content) {
  1393. return [{
  1394. style: {
  1395. 'margin-bottom': '10px'
  1396. },
  1397. children: [{
  1398. tag: 'strong',
  1399. children: _('Player settings')
  1400. }, {
  1401. tag: 'a',
  1402. attributes: {
  1403. 'href': Meta.site,
  1404. 'target': '_blank'
  1405. },
  1406. style: {
  1407. 'margin-left': '4px',
  1408. 'vertical-align': 'super',
  1409. 'font-size': '10px'
  1410. },
  1411. children: _('Help')
  1412. }]
  1413. }, {
  1414. style: {
  1415. 'text-align': 'center'
  1416. },
  1417. children: content.render()
  1418. }];
  1419. }
  1420. },
  1421.  
  1422. content: null,
  1423. button: null,
  1424. panel: null,
  1425.  
  1426. destroy: function() {
  1427. DH.remove(this.button);
  1428. DH.remove(this.panel);
  1429. },
  1430.  
  1431. toggle: function() {
  1432. this.content.refresh();
  1433. }
  1434. };
  1435.  
  1436. /**
  1437. * @class UI.Content
  1438. */
  1439. UI.Content = function(buttons) {
  1440. this._buttons = buttons;
  1441. };
  1442.  
  1443. UI.Content.prototype = {
  1444. _buttons: null,
  1445.  
  1446. render: function() {
  1447. return map(function(button) { return button.render(); }, this._buttons);
  1448. },
  1449.  
  1450. refresh: function() {
  1451. each(this._buttons, function(i, button) { button.refresh(); });
  1452. }
  1453. };
  1454.  
  1455. /**
  1456. * @class WatchUI
  1457. */
  1458. function WatchUI(buttons) {
  1459. UI.call(this, new UI.Content(buttons));
  1460.  
  1461. DH.append(DH.id('watch7-secondary-actions'), this.button);
  1462. DH.append(DH.id('watch7-action-panels'), this.panel);
  1463. }
  1464.  
  1465. WatchUI.prototype = extend(UI, {
  1466. _def: {
  1467. button: function(click) {
  1468. return {
  1469. tag: 'span',
  1470. children: {
  1471. tag: 'button',
  1472. attributes: {
  1473. 'type': 'button',
  1474. 'role': 'button',
  1475. 'class': 'action-panel-trigger yt-uix-button yt-uix-button-text yt-uix-button-empty yt-uix-tooltip',
  1476. 'data-button-toggle': 'true',
  1477. 'data-trigger-for': 'action-panel-yays',
  1478. 'data-tooltip-text': _('Player settings')
  1479. },
  1480. listeners: {
  1481. 'click': click
  1482. },
  1483. children: {
  1484. tag: 'span',
  1485. attributes: {
  1486. 'class': 'yt-uix-button-icon-wrapper'
  1487. },
  1488. children: [UI.prototype._def.icon, {
  1489. tag: 'span',
  1490. attributes: {
  1491. 'class': 'yt-uix-button-valign'
  1492. }
  1493. }]
  1494. }
  1495. }
  1496. };
  1497. },
  1498.  
  1499. panel: function(content) {
  1500. return {
  1501. attributes: {
  1502. 'id': 'action-panel-yays',
  1503. 'class': 'action-panel-content hid',
  1504. 'data-panel-loaded': 'true'
  1505. },
  1506. style: {
  1507. 'display': 'none',
  1508. 'color': '#333'
  1509. },
  1510. children: UI.prototype._def.panel(content)
  1511. };
  1512. }
  1513. }
  1514. });
  1515.  
  1516. /**
  1517. * @class ChannelUI
  1518. */
  1519. function ChannelUI(buttons) {
  1520. UI.call(this, new UI.Content(buttons));
  1521.  
  1522. DH.append(DH.id('channel-navigation-menu'), {
  1523. tag: 'li',
  1524. children: [this.button, this.panel]
  1525. });
  1526. }
  1527.  
  1528. ChannelUI.prototype = extend(UI, {
  1529. _def: {
  1530. button: function(click) {
  1531. return {
  1532. tag: 'button',
  1533. attributes: {
  1534. 'type': 'button',
  1535. 'role': 'button',
  1536. 'class': 'epic-nav-item-empty yt-uix-button-epic-nav-item yt-uix-button yt-uix-button-empty yt-uix-tooltip flip',
  1537. 'data-button-menu-id': 'yays-panel-dropdown',
  1538. 'data-tooltip-text': _('Player settings')
  1539. },
  1540. style: {
  1541. 'position': 'absolute',
  1542. 'right': '20px',
  1543. 'width': '30px'
  1544. },
  1545. listeners: {
  1546. 'click': click
  1547. },
  1548. children: {
  1549. tag: 'span',
  1550. attributes: {
  1551. 'class': 'yt-uix-button-icon-wrapper'
  1552. },
  1553. style: {
  1554. 'opacity': '0.75'
  1555. },
  1556. children: [UI.prototype._def.icon, {
  1557. tag: 'span',
  1558. attributes: {
  1559. 'class': 'yt-uix-button-valign'
  1560. }
  1561. }]
  1562. }
  1563. };
  1564. },
  1565.  
  1566. panel: function(content) {
  1567. return {
  1568. attributes: {
  1569. 'id': 'yays-panel-dropdown',
  1570. 'class': 'epic-nav-item-dropdown hid'
  1571. },
  1572. style: {
  1573. 'padding': '5px 10px 10px',
  1574. 'width': '450px'
  1575. },
  1576. children: UI.prototype._def.panel(content)
  1577. };
  1578. }
  1579. }
  1580. });
  1581.  
  1582. /**
  1583. * @class FeatherUI
  1584. */
  1585. function FeatherUI(buttons) {
  1586. UI.call(this, new FeatherUI.Content(buttons));
  1587.  
  1588. var toolbar = DH.walk(DH.id('movie_player'), '../../div[2]');
  1589.  
  1590. DH.append(toolbar, this.button);
  1591. DH.insertAfter(toolbar, this.panel);
  1592. }
  1593.  
  1594. FeatherUI.prototype = extend(UI, {
  1595. _def: {
  1596. button: function(click) {
  1597. return {
  1598. tag: 'button',
  1599. attributes: {
  1600. 'class': 'b'
  1601. },
  1602. style: {
  1603. 'margin-left': '10px'
  1604. },
  1605. listeners: {
  1606. 'click': click
  1607. },
  1608. children: merge({
  1609. style: {
  1610. 'vertical-align': 'sub',
  1611. 'opacity': '0.82'
  1612. }
  1613. }, UI.prototype._def.icon)
  1614. };
  1615. },
  1616.  
  1617. panel: function(content) {
  1618. return {
  1619. attributes: {
  1620. 'class': 'hid'
  1621. },
  1622. style: {
  1623. 'padding': '10px',
  1624. 'margin-top': '0.5em'
  1625. },
  1626. children: UI.prototype._def.panel(content)
  1627. }
  1628. }
  1629. },
  1630.  
  1631. toggle: function() {
  1632. UI.prototype.toggle.call(this);
  1633.  
  1634. (DH.hasClass(this.panel, 'hid') ? DH.delClass : DH.addClass).call(DH, this.panel, 'hid');
  1635. }
  1636. });
  1637.  
  1638. /**
  1639. * @class FeatherUI.Content
  1640. */
  1641. FeatherUI.Content = function(buttons) {
  1642. UI.Content.call(this, buttons);
  1643. };
  1644.  
  1645. FeatherUI.Content.prototype = extend(UI.Content, {
  1646. render: function() {
  1647. var nodes = UI.Content.prototype.render.call(this);
  1648.  
  1649. return map(function(node) {
  1650. DH.attributes(node, {
  1651. 'class': 'b'
  1652. });
  1653.  
  1654. DH.style(DH.walk(node, 'span[0]'), {
  1655. 'font-size': '11px'
  1656. });
  1657.  
  1658. DH.style(DH.walk(node, 'span[1]'), {
  1659. 'font-weight': 'bold'
  1660. });
  1661.  
  1662. return node;
  1663. }, nodes);
  1664. }
  1665. });
  1666.  
  1667. /*
  1668. * Ready callbacks.
  1669. */
  1670.  
  1671. function onReady(player) {
  1672. var videoPlayback = new VideoPlayback(player), videoQuality = new VideoQuality(player), previousVideo = player.getVideoId();
  1673.  
  1674. player.onStateChange = function(state) {
  1675. try {
  1676. var currentVideo = player.getVideoId();
  1677.  
  1678. if (currentVideo == previousVideo) {
  1679. videoQuality.apply();
  1680. videoPlayback.apply();
  1681. }
  1682. else {
  1683. videoQuality.cease();
  1684. videoPlayback.cease();
  1685.  
  1686. throw null;
  1687. }
  1688. }
  1689. catch (e) {
  1690. player.invalidate();
  1691.  
  1692. asyncCall(onPlayerReady);
  1693. }
  1694. };
  1695.  
  1696. videoQuality.apply();
  1697. videoPlayback.apply();
  1698.  
  1699. DH.ready(function() {
  1700. var page = DH.id('page');
  1701.  
  1702. if (page) {
  1703. if (DH.hasClass(page, 'watch')) {
  1704. var playerSize = new PlayerSize(player);
  1705.  
  1706. playerSize.apply();
  1707.  
  1708. UI.initialize(WatchUI, [
  1709. new VideoQuality.Button(videoQuality),
  1710. new PlayerSize.Button(playerSize),
  1711. new VideoPlayback.Button(videoPlayback)
  1712. ]);
  1713. }
  1714. else if (DH.hasClass(page, 'channel')) {
  1715. UI.initialize(ChannelUI, [
  1716. new VideoQuality.Button(videoQuality),
  1717. new VideoPlayback.Button(videoPlayback)
  1718. ]);
  1719. }
  1720. }
  1721. else {
  1722. UI.initialize(FeatherUI, [
  1723. new VideoQuality.Button(videoQuality),
  1724. new VideoPlayback.Button(videoPlayback)
  1725. ]);
  1726. }
  1727. });
  1728. }
  1729.  
  1730. function findPlayerNode() {
  1731. function findApiNode(nodes) {
  1732. for (var i = 0, node; node = nodes.item(i); ++i) {
  1733. do {
  1734. if (node.hasAttribute('id') && node.getAttribute('id').indexOf('player') > -1) {
  1735. return node;
  1736. }
  1737. }
  1738. while ((node = node.parentNode) && node.nodeType == DH.ELEMENT_NODE);
  1739. }
  1740.  
  1741. return null;
  1742. }
  1743.  
  1744. return findApiNode(DH.tagName('video')) || findApiNode(DH.tagName('embed'));
  1745. }
  1746.  
  1747. function onPlayerReady() {
  1748. try {
  1749. var player = Player.initialize(DH.unwrap(findPlayerNode()));
  1750.  
  1751. onReady(player);
  1752. }
  1753. catch (e) {
  1754. Console.debug(e);
  1755. }
  1756. }
  1757.  
  1758. unsafeWindow.onYouTubePlayerReady = intercept(unsafeWindow.onYouTubePlayerReady, asyncProxy(onPlayerReady));
  1759.  
  1760. onPlayerReady();
  1761.  
  1762. } // YAYS
  1763.  
  1764. if (window.top === window.self) {
  1765. if (this.unsafeWindow) { // Greasemonkey.
  1766. YAYS(unsafeWindow);
  1767. }
  1768. else {
  1769. var node = document.createElement('script');
  1770. node.setAttribute('type', 'text/javascript');
  1771. node.text = '(' + YAYS.toString() + ')(window);';
  1772.  
  1773. document.documentElement.appendChild(node);
  1774. document.documentElement.removeChild(node);
  1775. }
  1776. }