NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name OpenEdu.Ru Tweaks
// @namespace almaceleste
// @version 0.5.0
// @description some tweaks for openedu.ru
// @description:ru некоторые твики для openedu.ru
// @author (ɔ) almaceleste (https://almaceleste.github.io)
// @license AGPL-3.0-or-later; http://www.gnu.org/licenses/agpl
// @icon https://cdn.openedu.ru/EMQUAJW/default/default/images/favicon.bd3d272022e9.ico
// @icon64 https://cdn.openedu.ru/EMQUAJW/default/default/images/favicon.bd3d272022e9.ico
// @homepageURL https://greasyfork.org/en/users/174037-almaceleste
// @homepageURL https://openuserjs.org/users/almaceleste
// @homepageURL https://github.com/almaceleste/userscripts
// @supportURL https://github.com/almaceleste/userscripts/issues
// @updateURL https://github.com/almaceleste/userscripts/raw/master/src/OpenEdu.Ru_Tweaks.user.js
// @downloadURL https://github.com/almaceleste/userscripts/raw/master/src/OpenEdu.Ru_Tweaks.user.js
// @downloadURL https://openuserjs.org/install/almaceleste/OpenEdu.Ru_Tweaks.user.js
// @require https://code.jquery.com/jquery-3.3.1.js
// @require https://raw.githubusercontent.com/uzairfarooq/arrive/master/minified/arrive.min.js
// @require https://openuserjs.org/src/libs/sizzle/GM_config.js
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant GM_openInTab
// @grant GM_getResourceText
// @resource css https://github.com/almaceleste/userscripts/raw/master/css/default.css
// @match http*://courses.openedu.ru/courses/course*
// ==/UserScript==
// ==OpenUserJS==
// @author almaceleste
// ==/OpenUserJS==
// script global variables
// arrive.js options
const existing = {
existing: true
};
// set id that will used for an id of the settings window (frame)
const configId = 'openeduRuTweaksCfg';
// set url to a usercript icon in the settings window
const iconUrl = GM_info.script.icon64;
// set patterns for replacing specific selectors in a default, centralized css (for settings window)
const pattern = {};
pattern[`#${configId}`] = /#configId/g;
pattern[`${iconUrl}`] = /iconUrl/g;
// get remote default css
let css = GM_getResourceText('css');
// iterate patterns and replace found substrings with them
Object.keys(pattern).forEach((key) => {
css = css.replace(pattern[key], key);
});
const windowcss = css;
// main parameters of the settings window (frame). specific to each script due to the different amount of the parameters in each script
const iframecss = `
height: 250px;
width: 435px;
border: 1px solid;
border-radius: 3px;
position: fixed;
z-index: 9999;
`;
// register settings menu in a userscript manager (Tampermonkey, Greasemonkey or other)
GM_registerMenuCommand(`${GM_info.script.name} Settings`, () => {
GM_config.open();
GM_config.frame.style = iframecss;
});
// definition of the settings parameters for the script
GM_config.init({
id: `${configId}`,
title: `${GM_info.script.name} ${GM_info.script.version}`,
fields: {
videoquality: {
section: ['', 'Course tweaks'],
label: 'hd video quality',
labelPos: 'right',
title: 'always use HD video quality (if available)',
type: 'checkbox',
default: true,
},
videokeybinding: {
label: 'video keybinding',
labelPos: 'right',
title: `always use keyboard shortcuts to control video:
<Space> - play/pause
<Left>/<Right> - backward/forward (5 sec)
<Down>/<Up> - fast backward/forward (10 sec)`,
type: 'checkbox',
default: true,
},
videokeybindings: {
title: `
<Space> - play/pause
<Left>/<Right> - backward/forward (5 sec)
<Down>/<Up> - fast backward/forward (10 sec)
<1...4> - video speed control (0.75x, 1.0x, 1.25x, 1.50x)`,
type: 'multicheckbox',
options: {
pause: true,
rewind: true,
fastrewind: true,
speedcontrol: true,
},
default: {
pause: true,
rewind: true,
fastrewind: true,
speedcontrol: true,
},
},
managesidesubtitles: {
title: 'whether or not to control side subtitles',
label: 'manage side subtitles',
labelPos: 'right',
type: 'checkbox',
default: true,
},
sidesubtitles: {
title: 'check this option to enable subtitles, uncheck to disable',
type: 'multicheckbox',
options: {
'side subtitles': false
},
default: {
'side subtitles': false
},
},
support: {
section: ['', 'Support'],
label: 'almaceleste.github.io',
title: 'more info on almaceleste.github.io',
type: 'button',
click: () => {
GM_openInTab('https://almaceleste.github.io', {
active: true,
insert: true,
setParent: true
});
}
},
},
types: {
multicheckbox: {
default: {},
toNode: function() {
let field = this.settings,
values = this.value,
options = field.options,
id = this.id,
configId = this.configId,
labelPos = field.labelPos,
create = this.create;
console.log('toNode:', field, values, options);
function addLabel(pos, labelEl, parentNode, beforeEl) {
if (!beforeEl) beforeEl = parentNode.firstChild;
switch (pos) {
case 'right': case 'below':
if (pos == 'below')
parentNode.appendChild(create('br', {}));
parentNode.appendChild(labelEl);
break;
default:
if (pos == 'above')
parentNode.insertBefore(create('br', {}), beforeEl);
parentNode.insertBefore(labelEl, beforeEl);
}
}
let retNode = create('div', {
className: 'config_var multicheckbox',
id: `${configId}_${id}_var`,
title: field.title || ''
}),
firstProp;
// Retrieve the first prop
for (let i in field) { firstProp = i; break; }
let label = field.label ? create('label', {
className: 'field_label',
id: `${configId}_${id}_field_label`,
for: `${configId}_field_${id}`,
}, field.label) : null;
let wrap = create('ul', {
id: `${configId}_field_${id}`
});
this.node = wrap;
// for (const key in values) {
for (const key in options) {
// console.log('toNode:', key);
const inputId = `${configId}_${id}_${key}_checkbox`;
const li = wrap.appendChild(create('li', {
}));
li.appendChild(create('input', {
checked: values.hasOwnProperty(key) ? values[key] : options[key],
id: inputId,
type: 'checkbox',
value: key,
}));
li.appendChild(create('label', {
className: 'option_label',
for: inputId,
}, key));
}
retNode.appendChild(wrap);
if (label) {
// If the label is passed first, insert it before the field
// else insert it after
if (!labelPos)
labelPos = firstProp == "label" ? "left" : "right";
addLabel(labelPos, label, retNode);
}
return retNode;
},
toValue: function() {
let node = this.node,
id = node.id,
rval = {};
// console.log('toValue:', node, this);
if (!node) return rval;
let nodelist = node.querySelectorAll(`#${id} input`);
// console.log('nodelist:', document.querySelectorAll(`#${id} input:checked`), nodelist);
nodelist.forEach((input) => {
// console.log('toValue:', input);
const value = input.checked;
const key = input.value;
rval[key] = value;
});
// console.log('toValue:', rval);
return rval;
},
reset: function() {
let node = this.node,
values = this.default;
// console.log('reset:', node, values, Object.values(values));
const inputs = node.getElementsByTagName('input');
for (const index in inputs) {
const input = inputs[index];
input.checked = values[input.value];
}
}
}
},
css: windowcss,
events: {
save: function() {
GM_config.close();
}
},
});
// functions that are used in the main function
// userscipt main function that started when page are loaded
(function() {
'use strict';
// set variables
const video = '.tc-wrapper > .video-wrapper';
const player = `${video} > .video-player`;
const source = `${player} video > source`;
const controls = `${video} > .video-controls`;
const progress = `${controls} > .slider > .progress-handle`;
const pause = `${controls} .vcr > .control.video_control`;
const secondarycontrols = `${controls} .secondary-controls`;
const qualitybtn = `${secondarycontrols} > button.control.quality-control`;
const videospeeds = `${secondarycontrols} > .speeds > .video-speeds`;
const videospeed1 = `${videospeeds} > li:nth-last-child(1) > button.control`;
const videospeed2 = `${videospeeds} > li:nth-last-child(2) > button.control`;
const videospeed3 = `${videospeeds} > li:nth-last-child(3) > button.control`;
const videospeed4 = `${videospeeds} > li:nth-last-child(4) > button.control`;
const sidesubtitlesbtn = `${secondarycontrols} > .grouped-controls > .toggle-transcript`;
$(document).arrive(player, existing, () => {
if (GM_config.get('videoquality')) {
const src = $(source).attr('src');
if (src.search(/sd\.mp4/)) $(qualitybtn).trigger('click');
}
if (GM_config.get('videokeybinding')) {
// const keybindings = GM_config.get('videokeybindings');
$(progress).attr('id', 'progress-handle');
const el = document.getElementById('progress-handle');
const ev = document.createEvent("Events");
ev.initEvent("keydown", false, true);
$(window).on({
keydown: (e) => {
const keybindings = GM_config.get('videokeybindings');
// console.log('on:', e.type, e.keyCode, e);
if (e.target.localName != 'input') {
if (e.target.id != 'progress-handle') {
let n = 0;
switch (e.keyCode) {
case 37: // left
if (keybindings.rewind) {
// e.preventDefault();
$(progress).focus();
ev.which = 37;
ev.keyCode = 37;
while (n < 5) {
el.dispatchEvent(ev);
n++;
}
}
break;
case 39: // right
if (keybindings.rewind) {
// e.preventDefault();
$(progress).focus();
ev.which = 39;
ev.keyCode = 39;
while (n < 5) {
el.dispatchEvent(ev);
n++;
}
}
break;
case 38: // up
if (keybindings.fastrewind) {
// e.preventDefault();
$(progress).focus();
ev.which = 39;
ev.keyCode = 39;
while (n < 10) {
el.dispatchEvent(ev);
n++;
}
}
break;
case 40: // down
if (keybindings.fastrewind) {
// e.preventDefault();
$(progress).focus();
ev.which = 37;
ev.keyCode = 37;
while (n < 10) {
el.dispatchEvent(ev);
n++;
}
}
break;
default:
break;
}
}
}
},
keypress: (e) => {
const keybindings = GM_config.get('videokeybindings');
// console.log('on:', e.type, e.keyCode, e);
if (e.target.localName != 'input') {
switch (e.keyCode) {
case 32: // space
if (keybindings.pause) {
e.preventDefault();
$(pause).trigger('click');
}
break;
case 49: // 1
if (keybindings.speedcontrol) {
e.preventDefault();
$(videospeed1).trigger('click');
}
break;
case 50: // 2
if (keybindings.speedcontrol) {
e.preventDefault();
$(videospeed2).trigger('click');
}
break;
case 51: // 3
if (keybindings.speedcontrol) {
e.preventDefault();
$(videospeed3).trigger('click');
}
break;
case 52: // 4
if (keybindings.speedcontrol) {
e.preventDefault();
$(videospeed4).trigger('click');
}
break;
default:
break;
}
}
},
keyup: (e) => {
if (e.target.id == 'progress-handle') $(progress).blur();
}
});
window.addEventListener('keydown', (e) => {
const keybindings = GM_config.get('videokeybindings');
if (e.target.localName != 'input') {
switch (e.keyCode) {
case 37: // left
case 39: // right
if (keybindings.rewind)
e.preventDefault();
break;
case 38: // up
case 40: // down
if (keybindings.fastrewind)
e.preventDefault();
break;
default:
break;
}
}
}, {
capture: true,
// passive: false
});
}
if (GM_config.get('managesidesubtitles')) {
const sidesubtitles = GM_config.get('sidesubtitles')['side subtitles'];
$(document).arrive(sidesubtitlesbtn, existing, () => {
if (sidesubtitles != $(sidesubtitlesbtn).hasClass('is-active')) {
$(sidesubtitlesbtn).click();
}
});
}
})
})();