Greasy Fork 支持简体中文。

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. 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.

目前為 2024-05-12 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name Saima customizer
  3. // @name:ru Saima: кастомизация
  4. // @namespace http://tampermonkey.net/
  5. // @version 1.0.1
  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. 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.
  7. // @description:ru Настройка расширения Saima ( https://saima.ai/ ): быстрый контроль скорости WPM (слов в минуту); правильное размещение кнопки Saima одновременно с кнопкой SponsorBlock. Для быстрого изменения скорости наведите курсор на кнопку Saima в нижней части видеоплеера (как на изображении) и крутите колёсико мыши.
  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. })();