// ==UserScript==
// @name Saima customizer
// @name:ru Saima: кастомизация
// @namespace http://tampermonkey.net/
// @version 1.0.4
// @description Saima extension customization ( https://saima.ai/ ): fast speed control WPM (words per minute); correct placement of the Saima button at the same time as the SponsorBlock button. To quickly change the speed, move the cursor over the Saima button at the bottom of the video player (as in the image) and roll the mouse wheel.
// @description:ru Настройка расширения Saima ( https://saima.ai/ ): быстрый контроль скорости WPM (слов в минуту); правильное размещение кнопки Saima одновременно с кнопкой SponsorBlock. Для быстрого изменения скорости наведите курсор на кнопку Saima в нижней части видеоплеера (как на изображении) и крутите колёсико мыши.
// @author Igor Lebedev
// @license GNU Generic Public License v3
// @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @match http://*.youtube.com/*
// @match https://*.youtube.com/*
// @require https://openuserjs.org/src/libs/sizzle/GM_config.js
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @run-at document-idle
// ==/UserScript==
//debugger;
/* global GM_config */
(() => {
'use strict';
GM_config.init({
id: 'sc_config',
title: GM_info.script.name + ' Settings',
fields: {
DEBUG_MODE: {
label: 'Debug mode',
type: 'checkbox',
default: false,
title: 'Log debug messages to the console'
},
CHECK_FREQUENCY_LAUNCH_SAIMA: {
label: 'Check frequency (ms) launch of the Saima extension',
type: 'number',
min: 1000,
default: 5000,
title: 'The number of milliseconds to wait between checking the launch of the Saima extension'
},
CHECK_FREQUENCY_LAUNCH_SPONSOR_BLOCK: {
label: 'Check frequency (ms) launch of the SponsorBlock extension',
type: 'number',
min: 1000,
default: 5000,
title: 'The number of milliseconds to wait between checking the launch of the Saima extension'
},
WPM_SPEED: {
label: 'WPM speed with one scroll of the wheel',
type: 'number',
min: 1,
max: 400,
default: 30,
title: 'Changing the WPM (words per minute) speed with one scroll of the wheel'
},
SPEED_INDICATOR_TRANSPARENT: {
label: 'Transparency of the speed indicator',
type: 'number',
min: 0.1,
max: 1,
default: 0.3,
title: 'Transparency of the speed indicator on the button'
},
},
events: {
init: onInit
}
})
GM_registerMenuCommand('Settings', () => {
GM_config.open()
})
class Debugger {
constructor (name, enabled) {
this.debug = {}
if (!window.console) {
return () => { }
}
Object.getOwnPropertyNames(window.console).forEach(key => {
if (typeof window.console[key] === 'function') {
if (enabled) {
this.debug[key] = window.console[key].bind(window.console, name + ': ')
} else {
this.debug[key] = () => { }
}
}
})
return this.debug
}
}
var DEBUG
const SELECTORS = {
PLAYER: '#movie_player',
StartSegmentButton: '#startSegmentButton',
SaimaButtonContainer: '#saima-button-container',
SaimaButtonContainer_button: '#saima-button-container > div > button',
SaimaButtonContainer_button_canvas: '#saima-button-container > div > button > canvas',
SaimaTextField: '.__saima__text-field',
}
const WPM_Min = 107 // Минимальная скорость (слов в минуту)
const WPM_Max = 498 // Максимальная скорость (слов в минуту)
const WPM_IntervalLenght = WPM_Max - WPM_Min // величина рабочего диапазаона скоростей WPM
let CanvasMaskTransparent = 0.3 // прозрачность маски
let WheelWPM = 30 // Изменение скорости WPM за одну прокрутку колёсика
function onInit() {
DEBUG = new Debugger(GM_info.script.name, GM_config.get('DEBUG_MODE'))
let CanvasMaskTransparent = GM_config.get('SPEED_INDICATOR_TRANSPARENT')
WheelWPM = GM_config.get('WPM_SPEED')
Move_after_SponsorBlock()
Quick_WPM_speed_control()
}
// Перестановка на плеере кнопки Saima правее кнокпи SponsorBlock - во избежание смещений кнопки Saima при наведении на кнопку SponsorBlock
function Move_after_SponsorBlock(){
let jsInitChecktimer = null
function isDownloadElements(evt) {
function wait () {
//ожидание загрузки страницы до необходимого значения
if (watchThresholdReached()) {
try {
const StartSegmentButton = document.querySelector(SELECTORS.StartSegmentButton)
const SaimaButtonContainer = document.querySelector(SELECTORS.SaimaButtonContainer)
if (StartSegmentButton && SaimaButtonContainer) {
// Перестановка
StartSegmentButton.after(SaimaButtonContainer)
clearInterval(jsInitChecktimer)
jsInitChecktimer = null
}
} catch (e) {
DEBUG.info(`Failed to like video: ${e}. `)
}
}
}
jsInitChecktimer = setInterval(wait, GM_config.get('CHECK_FREQUENCY_LAUNCH_SPONSOR_BLOCK'))
}
isDownloadElements()
}
// Быстрое изменение скорости WPM
function Quick_WPM_speed_control() {
let jsInitChecktimer2 = null
function isDownloadStartSegmentButton(evt) {
function wait () {
//ожидание загрузки страницы до необходимого значения
if (watchThresholdReached()) {
try {
const SaimaButtonContainer = document.querySelector(SELECTORS.SaimaButtonContainer)
const SaimaTextField = document.querySelector(SELECTORS.SaimaTextField)
if (SaimaButtonContainer && SaimaTextField) {
const SaimaButtonContainer_button = document.querySelector(SELECTORS.SaimaButtonContainer_button)
if (SaimaButtonContainer_button) {
let SaimaButtonContainer_spanIco_SpeedProp = 1 - (SaimaTextField._value - WPM_Min) / WPM_IntervalLenght
let canvas = document.createElement('canvas')
let ctx = canvas.getContext('2d')
// Предполагая, что размеры контейнера уже установлены
canvas.width = SaimaButtonContainer_button.offsetWidth
canvas.height = SaimaButtonContainer_button.offsetHeight
canvas.style.position = 'absolute'
// Добавляем эффект маски
ctx.fillStyle = `rgba(0, 0, 0, ${CanvasMaskTransparent})`
// ctx.fillRect(0, 0, canvas.width, canvas.height / 2)
ctx.fillRect(0, 0, canvas.width, canvas.height * SaimaButtonContainer_spanIco_SpeedProp)
// Добавляем canvas в контейнер
SaimaButtonContainer_button.appendChild(canvas)
}
SaimaButtonContainer.addEventListener('wheel', function(event) {
// Предотвращаем стандартное поведение прокрутки страницы
event.preventDefault()
let RepeatedPresses = 1 // Количество нажатий на одну прокрутку колеса мыши
let TimeOutMS = 1 // Интервал между нажатиями
// A function to simulate a button click
function simulateClickWrap(sign) {
function simulateClick() {
let valueNew = SaimaTextField.valueAsNumber + sign * WheelWPM
switch(sign) {
case 1:
if (valueNew > WPM_Max) valueNew = WPM_Max
break
case -1:
if (valueNew < WPM_Min) valueNew = WPM_Min
break
}
SaimaTextField.value = valueNew
SaimaTextField._value = valueNew
// Создаем новое событие 'input'
let eventInput = new Event('input', {
bubbles: true, // Событие будет всплывать вверх по DOM-дереву
cancelable: false // Событие 'input' обычно не предполагает отмену его действий
});
// Искусственно вызываем событие 'input' на элементе input
SaimaTextField.dispatchEvent(eventInput)
}
if ((event.deltaY < 0 && SaimaTextField.valueAsNumber < WPM_Max) || (event.deltaY > 0 && SaimaTextField.valueAsNumber > WPM_Min)) {
for (let i = 0; i < RepeatedPresses; i++) {
setTimeout((function(index) {
return function() {
simulateClick()
// console.log('Button clicked:', index);
}
})(i), i * TimeOutMS) // Задержка TimeOutMS между кликами
}
}
const SaimaButtonContainer_button_canvas = document.querySelector(SELECTORS.SaimaButtonContainer_button_canvas)
if (SaimaButtonContainer_button_canvas) {
let SaimaButtonContainer_spanIco_SpeedProp = 1 - (SaimaTextField._value - WPM_Min) / WPM_IntervalLenght
function updateMaskHeight(newHeightFraction) {
let ctx = SaimaButtonContainer_button_canvas.getContext('2d')
// Очистка холста
ctx.clearRect(0, 0, SaimaButtonContainer_button_canvas.width, SaimaButtonContainer_button_canvas.height)
// Перерисовка маски с новой высотой
ctx.fillStyle = `rgba(0, 0, 0, ${CanvasMaskTransparent})`
ctx.fillRect(0, 0, SaimaButtonContainer_button_canvas.width, SaimaButtonContainer_button_canvas.height * newHeightFraction)
}
// Обновление высоты маски от высоты холста
updateMaskHeight(SaimaButtonContainer_spanIco_SpeedProp)
}
}
// event.deltaY содержит значение, которое указывает направление прокрутки:
// положительное значение для прокрутки вниз, отрицательное - вверх.
if (event.deltaY < 0) {
// console.log('Прокрутка вверх')
// if (SaimaTextField.valueAsNumber < WPM_Max) simulateClickWrap(1)
simulateClickWrap(1)
} else {
// console.log('Прокрутка вниз')
// if (SaimaTextField.valueAsNumber > WPM_Min) simulateClickWrap(-1)
simulateClickWrap(-1)
}
})
clearInterval(jsInitChecktimer2)
jsInitChecktimer2 = null
}
} catch (e) {
DEBUG.info(`Failed to like video: ${e}. `)
}
}
}
jsInitChecktimer2 = setInterval(wait, GM_config.get('CHECK_FREQUENCY_LAUNCH_SAIMA'))
}
isDownloadStartSegmentButton()
}
function watchThresholdReached () {
const player = document.querySelector(SELECTORS.PLAYER)
if (player) {
return true
}
return false
}
// onInit()
window.addEventListener("yt-navigate-start", e => { onInit() }) // переиницализация при обновлении страницы без перезагрузки скрипта
})();