Saima customizer

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.

当前为 2024-05-12 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Saima customizer
  3. // @name:ru Saima: кастомизация
  4. // @namespace http://tampermonkey.net/
  5. // @version 1.0.0
  6. // @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.
  7. // @description:ru Настройка расширения Saima ( https://saima.ai/ ): быстрый контроль скорости WPM (слов в минуту); правильное размещение кнопки Saima одновременно с кнопкой SponsorBlock.
  8. // @author Igor Lebedev
  9. // @license MIT
  10. // @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
  11. // @match http://*.youtube.com/*
  12. // @match https://*.youtube.com/*
  13. // @require https://openuserjs.org/src/libs/sizzle/GM_config.js
  14. // @grant GM_getValue
  15. // @grant GM_setValue
  16. // @grant GM_registerMenuCommand
  17. // @run-at document-idle
  18.  
  19.  
  20. // ==/UserScript==
  21. //debugger;
  22.  
  23. /* global GM_config */
  24.  
  25. (() => {
  26. 'use strict';
  27.  
  28. GM_config.init({
  29. id: 'sc_config',
  30. title: GM_info.script.name + ' Settings',
  31. fields: {
  32. DEBUG_MODE: {
  33. label: 'Debug mode',
  34. type: 'checkbox',
  35. default: false,
  36. title: 'Log debug messages to the console'
  37. },
  38. CHECK_FREQUENCY_LAUNCH_SAIMA: {
  39. label: 'Check frequency (ms) launch of the Saima extension',
  40. type: 'number',
  41. min: 1000,
  42. default: 5000,
  43. title: 'The number of milliseconds to wait between checking the launch of the Saima extension'
  44. },
  45. CHECK_FREQUENCY_LAUNCH_SPONSOR_BLOCK: {
  46. label: 'Check frequency (ms) launch of the SponsorBlock extension',
  47. type: 'number',
  48. min: 1000,
  49. default: 5000,
  50. title: 'The number of milliseconds to wait between checking the launch of the Saima extension'
  51. },
  52. WPM_SPEED: {
  53. label: 'WPM speed with one scroll of the wheel',
  54. type: 'number',
  55. min: 1,
  56. max: 400,
  57. default: 30,
  58. title: 'Changing the WPM (words per minute) speed with one scroll of the wheel'
  59. },
  60. SPEED_INDICATOR_TRANSPARENT: {
  61. label: 'Transparency of the speed indicator',
  62. type: 'number',
  63. min: 0.1,
  64. max: 1,
  65. default: 0.3,
  66. title: 'Transparency of the speed indicator on the button'
  67. },
  68. },
  69. events: {
  70. init: onInit
  71. }
  72. })
  73.  
  74. GM_registerMenuCommand('Settings', () => {
  75. GM_config.open()
  76. })
  77.  
  78. class Debugger {
  79. constructor (name, enabled) {
  80. this.debug = {}
  81. if (!window.console) {
  82. return () => { }
  83. }
  84. Object.getOwnPropertyNames(window.console).forEach(key => {
  85. if (typeof window.console[key] === 'function') {
  86. if (enabled) {
  87. this.debug[key] = window.console[key].bind(window.console, name + ': ')
  88. } else {
  89. this.debug[key] = () => { }
  90. }
  91. }
  92. })
  93. return this.debug
  94. }
  95. }
  96.  
  97. var DEBUG
  98.  
  99. const SELECTORS = {
  100. PLAYER: '#movie_player',
  101. StartSegmentButton: '#startSegmentButton',
  102. SaimaButtonContainer: '#saima-button-container',
  103. SaimaButtonContainer_button: '#saima-button-container > div > button',
  104. SaimaButtonContainer_button_canvas: '#saima-button-container > div > button > canvas',
  105. SaimaTextField: '.__saima__text-field',
  106. }
  107. const WPM_Min = 107 // Минимальная скорость (слов в минуту)
  108. const WPM_Max = 498 // Максимальная скорость (слов в минуту)
  109. const WPM_IntervalLenght = WPM_Max - WPM_Min // величина рабочего диапазаона скоростей WPM
  110. let CanvasMaskTransparent = 0.3 // прозрачность маски
  111. let WheelWPM = 30 // Изменение скорости WPM за одну прокрутку колёсика
  112.  
  113.  
  114. function onInit() {
  115. DEBUG = new Debugger(GM_info.script.name, GM_config.get('DEBUG_MODE'))
  116.  
  117. let CanvasMaskTransparent = GM_config.get('SPEED_INDICATOR_TRANSPARENT')
  118. WheelWPM = GM_config.get('WPM_SPEED')
  119.  
  120. Move_after_SponsorBlock()
  121. Quick_WPM_speed_control()
  122. }
  123.  
  124. // Перестановка на плеере кнопки Saima правее кнокпи SponsorBlock - во избежание смещений кнопки Saima при наведении на кнопку SponsorBlock
  125. function Move_after_SponsorBlock(){
  126. let jsInitChecktimer = null
  127. function isDownloadElements(evt) {
  128. function wait () {
  129. //ожидание загрузки страницы до необходимого значения
  130. if (watchThresholdReached()) {
  131. try {
  132. const StartSegmentButton = document.querySelector(SELECTORS.StartSegmentButton)
  133. const SaimaButtonContainer = document.querySelector(SELECTORS.SaimaButtonContainer)
  134. if (StartSegmentButton && SaimaButtonContainer) {
  135. // Перестановка
  136. StartSegmentButton.after(SaimaButtonContainer)
  137. clearInterval(jsInitChecktimer)
  138. jsInitChecktimer = null
  139. }
  140. } catch (e) {
  141. DEBUG.info(`Failed to like video: ${e}. `)
  142. }
  143. }
  144. }
  145. jsInitChecktimer = setInterval(wait, GM_config.get('CHECK_FREQUENCY_LAUNCH_SPONSOR_BLOCK'))
  146. }
  147. isDownloadElements()
  148. }
  149.  
  150. // Быстрое изменение скорости WPM
  151. function Quick_WPM_speed_control() {
  152.  
  153. let jsInitChecktimer2 = null
  154. function isDownloadStartSegmentButton(evt) {
  155. function wait () {
  156. //ожидание загрузки страницы до необходимого значения
  157. if (watchThresholdReached()) {
  158. try {
  159. const SaimaButtonContainer = document.querySelector(SELECTORS.SaimaButtonContainer)
  160. const SaimaTextField = document.querySelector(SELECTORS.SaimaTextField)
  161.  
  162. if (SaimaButtonContainer && SaimaTextField) {
  163. const SaimaButtonContainer_button = document.querySelector(SELECTORS.SaimaButtonContainer_button)
  164. if (SaimaButtonContainer_button) {
  165. let SaimaButtonContainer_spanIco_SpeedProp = 1 - (SaimaTextField._value - WPM_Min) / WPM_IntervalLenght
  166.  
  167. let canvas = document.createElement('canvas')
  168. let ctx = canvas.getContext('2d')
  169.  
  170. // Предполагая, что размеры контейнера уже установлены
  171. canvas.width = SaimaButtonContainer_button.offsetWidth
  172. canvas.height = SaimaButtonContainer_button.offsetHeight
  173. canvas.style.position = 'absolute'
  174.  
  175. // Добавляем эффект маски
  176. ctx.fillStyle = `rgba(0, 0, 0, ${CanvasMaskTransparent})`
  177. // ctx.fillRect(0, 0, canvas.width, canvas.height / 2)
  178. ctx.fillRect(0, 0, canvas.width, canvas.height * SaimaButtonContainer_spanIco_SpeedProp)
  179.  
  180. // Добавляем canvas в контейнер
  181. SaimaButtonContainer_button.appendChild(canvas)
  182. }
  183.  
  184.  
  185. SaimaButtonContainer.addEventListener('wheel', function(event) {
  186.  
  187. // Предотвращаем стандартное поведение прокрутки страницы
  188. event.preventDefault()
  189. let RepeatedPresses = 1 // Количество нажатий на одну прокрутку колеса мыши
  190. let TimeOutMS = 1 // Интервал между нажатиями
  191.  
  192. // A function to simulate a button click
  193. function simulateClickWrap(sign) {
  194. function simulateClick() {
  195.  
  196. let valueNew = SaimaTextField.valueAsNumber + sign * WheelWPM
  197. switch(sign) {
  198. case 1:
  199. if (valueNew > WPM_Max) valueNew = WPM_Max
  200. break
  201. case -1:
  202. if (valueNew < WPM_Min) valueNew = WPM_Min
  203. break
  204. }
  205.  
  206. SaimaTextField.value = valueNew
  207. SaimaTextField._value = valueNew
  208.  
  209. // Создаем новое событие 'input'
  210. let eventInput = new Event('input', {
  211. bubbles: true, // Событие будет всплывать вверх по DOM-дереву
  212. cancelable: false // Событие 'input' обычно не предполагает отмену его действий
  213. });
  214.  
  215. // Искусственно вызываем событие 'input' на элементе input
  216. SaimaTextField.dispatchEvent(eventInput)
  217.  
  218. }
  219. if ((event.deltaY < 0 && SaimaTextField.valueAsNumber < WPM_Max) || (event.deltaY > 0 && SaimaTextField.valueAsNumber > WPM_Min)) {
  220. for (let i = 0; i < RepeatedPresses; i++) {
  221. setTimeout((function(index) {
  222. return function() {
  223. simulateClick()
  224. // console.log('Button clicked:', index);
  225. }
  226. })(i), i * TimeOutMS) // Задержка TimeOutMS между кликами
  227. }
  228. }
  229.  
  230. const SaimaButtonContainer_button_canvas = document.querySelector(SELECTORS.SaimaButtonContainer_button_canvas)
  231. if (SaimaButtonContainer_button_canvas) {
  232. let SaimaButtonContainer_spanIco_SpeedProp = 1 - (SaimaTextField._value - WPM_Min) / WPM_IntervalLenght
  233.  
  234. function updateMaskHeight(newHeightFraction) {
  235. let ctx = SaimaButtonContainer_button_canvas.getContext('2d')
  236. // Очистка холста
  237. ctx.clearRect(0, 0, SaimaButtonContainer_button_canvas.width, SaimaButtonContainer_button_canvas.height)
  238.  
  239. // Перерисовка маски с новой высотой
  240. ctx.fillStyle = `rgba(0, 0, 0, ${CanvasMaskTransparent})`
  241. ctx.fillRect(0, 0, SaimaButtonContainer_button_canvas.width, SaimaButtonContainer_button_canvas.height * newHeightFraction)
  242. }
  243.  
  244. // Обновление высоты маски от высоты холста
  245. updateMaskHeight(SaimaButtonContainer_spanIco_SpeedProp)
  246. }
  247.  
  248. }
  249.  
  250. // event.deltaY содержит значение, которое указывает направление прокрутки:
  251. // положительное значение для прокрутки вниз, отрицательное - вверх.
  252. if (event.deltaY < 0) {
  253. // console.log('Прокрутка вверх')
  254. // if (SaimaTextField.valueAsNumber < WPM_Max) simulateClickWrap(1)
  255. simulateClickWrap(1)
  256. } else {
  257. // console.log('Прокрутка вниз')
  258. // if (SaimaTextField.valueAsNumber > WPM_Min) simulateClickWrap(-1)
  259. simulateClickWrap(-1)
  260. }
  261. })
  262.  
  263. clearInterval(jsInitChecktimer2)
  264. jsInitChecktimer2 = null
  265. }
  266.  
  267. } catch (e) {
  268. DEBUG.info(`Failed to like video: ${e}. `)
  269. }
  270. }
  271.  
  272. }
  273. jsInitChecktimer2 = setInterval(wait, GM_config.get('CHECK_FREQUENCY_LAUNCH_SAIMA'))
  274. }
  275. isDownloadStartSegmentButton()
  276. }
  277.  
  278. function watchThresholdReached () {
  279. const player = document.querySelector(SELECTORS.PLAYER)
  280. if (player) {
  281. return true
  282. }
  283. return false
  284. }
  285.  
  286.  
  287.  
  288. // onInit()
  289. window.addEventListener("yt-navigate-start", e => { onInit() }) // переиницализация при обновлении страницы без перезагрузки скрипта
  290.  
  291. })();