USER
Упрости и оптимизируй код, покажи полный изменный код без твоих комментариев. Не меняй названия функций и переменных
```js
/**
* Initializes the VideoHandler class by setting up data promises, fetching data, initializing UI elements, and setting up event listeners.
*/
async init() {
if (this.initialized) return;
const dataPromises = {
autoTranslate: votStorage.get("autoTranslate", 0),
dontTranslateLanguage: votStorage.get("dontTranslateLanguage", lang),
dontTranslateYourLang: votStorage.get("dontTranslateYourLang", 1),
autoSetVolumeYandexStyle: votStorage.get("autoSetVolumeYandexStyle", 1),
autoVolume: votStorage.get("autoVolume", defaultAutoVolume),
buttonPos: votStorage.get("buttonPos", "default"),
showVideoSlider: votStorage.get("showVideoSlider", 1),
syncVolume: votStorage.get("syncVolume", 0),
downloadWithName: votStorage.get("downloadWithName", 1),
subtitlesMaxLength: votStorage.get("subtitlesMaxLength", 300),
highlightWords: votStorage.get("highlightWords", 0),
subtitlesFontSize: votStorage.get("subtitlesFontSize", 20),
subtitlesOpacity: votStorage.get("subtitlesOpacity", 20),
subtitlesDownloadFormat: votStorage.get("subtitlesDownloadFormat", "srt"),
responseLanguage: votStorage.get("responseLanguage", lang),
defaultVolume: votStorage.get("defaultVolume", 100),
audioProxy: votStorage.get("audioProxy", 0),
onlyBypassMediaCSP: votStorage.get(
"onlyBypassMediaCSP",
Number(!!this.audioContext),
),
newAudioPlayer: votStorage.get(
"newAudioPlayer",
Number(!!this.audioContext),
),
showPiPButton: votStorage.get("showPiPButton", 0),
translateAPIErrors: votStorage.get("translateAPIErrors", 1),
translationService: votStorage.get(
"translationService",
defaultTranslationService,
),
detectService: votStorage.get("detectService", defaultDetectService),
hotkeyButton: votStorage.get("hotkeyButton", null),
m3u8ProxyHost: votStorage.get("m3u8ProxyHost", m3u8ProxyHost),
translateProxyEnabled: votStorage.get("translateProxyEnabled", 0),
proxyWorkerHost: votStorage.get("proxyWorkerHost", proxyWorkerHost),
audioBooster: votStorage.get("audioBooster", 0),
useNewModel: votStorage.get("useNewModel", 1),
localeHash: votStorage.get("locale-hash", ""),
localeUpdatedAt: votStorage.get("locale-updated-at", 0),
};
this.data = Object.fromEntries(
await Promise.all(
Object.entries(dataPromises).map(async ([key, promise]) => [
key,
await promise,
]),
),
);
console.log("[VOT] data from db: ", this.data);
// convert old m3u8-proxy-worker to new media-proxy
if (this.data.m3u8ProxyHost === "m3u8-proxy.toil.cc") {
this.data.m3u8ProxyHost = m3u8ProxyHost;
await votStorage.set("m3u8ProxyHost", m3u8ProxyHost);
console.log(
`[VOT] Old m3u8 proxy host converted to new ${this.data.m3u8ProxyHost} media-proxy`,
);
}
// convert old vot-worker domain to actual
// TODO: remove converter in one of the next versions after release 1.7.0
if (this.data.proxyWorkerHost === "vot.toil.cc") {
this.data.proxyWorkerHost = proxyWorkerHost;
await votStorage.set("proxyWorkerHost", proxyWorkerHost);
console.log(
`[VOT] Old proxy worker host converted to new ${this.data.proxyWorkerHost}`,
);
}
if (
!this.data.translateProxyEnabled &&
GM_info?.scriptHandler &&
proxyOnlyExtensions.includes(GM_info.scriptHandler)
) {
this.data.translateProxyEnabled = 1;
await votStorage.set("translateProxyEnabled", 1);
debug.log("translateProxyEnabled", this.data.translateProxyEnabled);
}
debug.log("Extension compatibility passed...");
this.votOpts = {
headers: this.data.translateProxyEnabled
? {}
: {
"sec-ch-ua": null,
"sec-ch-ua-mobile": null,
"sec-ch-ua-platform": null,
// "sec-ch-ua-model": null,
// "sec-ch-ua-platform-version": null,
// "sec-ch-ua-wow64": null,
// "sec-ch-ua-arch": null,
// "sec-ch-ua-bitness": null,
// "sec-ch-ua-full-version": null,
// "sec-ch-ua-full-version-list": null,
},
fetchFn: GM_fetch,
hostVOT: votBackendUrl,
host: this.data.translateProxyEnabled
? this.data.proxyWorkerHost
: workerHost,
};
this.votClient = new (
this.data.translateProxyEnabled ? VOTWorkerClient : VOTClient
)(this.votOpts);
this.subtitlesWidget = new SubtitlesWidget(
this.video,
this.container,
this.site,
);
this.subtitlesWidget.setMaxLength(this.data.subtitlesMaxLength);
this.subtitlesWidget.setHighlightWords(this.data.highlightWords);
this.subtitlesWidget.setFontSize(this.data.subtitlesFontSize);
this.subtitlesWidget.setOpacity(this.data.subtitlesOpacity);
this.initUI();
this.initUIEvents();
this.videoData = await this.getVideoData();
this.createPlayer();
this.setSelectMenuValues(
this.videoData.detectedLanguage,
this.data.responseLanguage ?? "ru",
);
this.translateToLang = this.data.responseLanguage ?? "ru";
this.initExtraEvents();
await Promise.all([this.updateSubtitles(), this.autoTranslate()]);
this.initialized = true;
}
initUI() {
// VOT Button
{
this.votButton = ui.createVOTButton(
localizationProvider.get("translateVideo"),
);
this.votButton.container.style.opacity = 0;
// use an additional check because sometimes this.video.clientWidth = 0
if (
this.data?.buttonPos &&
this.data?.buttonPos !== "default" &&
this.container.clientWidth > 550
) {
this.votButton.container.dataset.direction = "column";
this.votButton.container.dataset.position = this.data?.buttonPos;
} else {
this.votButton.container.dataset.direction = "row";
this.votButton.container.dataset.position = "default";
}
this.container.appendChild(this.votButton.container);
this.votButton.pipButton.hidden =
!isPiPAvailable() || !this.data?.showPiPButton;
this.votButton.separator2.hidden =
!isPiPAvailable() || !this.data?.showPiPButton;
this.votButton.container.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
});
}
// VOT Menu
{
this.votMenu = ui.createVOTMenu(localizationProvider.get("VOTSettings"));
this.votMenu.container.dataset.position =
this.container.clientWidth && this.container.clientWidth > 550
? this.data?.buttonPos
: "default";
this.container.appendChild(this.votMenu.container);
this.votDownloadButton = ui.createIconButton(
svg`<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="100%"
viewBox="0 0 24 24"
class="vot-loader"
id="vot-loader-download"
>
<path
class="vot-loader-main"
d="M12 15.575C11.8667 15.575 11.7417 15.5542 11.625 15.5125C11.5083 15.4708 11.4 15.4 11.3 15.3L7.7 11.7C7.5 11.5 7.40417 11.2667 7.4125 11C7.42083 10.7333 7.51667 10.5 7.7 10.3C7.9 10.1 8.1375 9.99583 8.4125 9.9875C8.6875 9.97917 8.925 10.075 9.125 10.275L11 12.15V5C11 4.71667 11.0958 4.47917 11.2875 4.2875C11.4792 4.09583 11.7167 4 12 4C12.2833 4 12.5208 4.09583 12.7125 4.2875C12.9042 4.47917 13 4.71667 13 5V12.15L14.875 10.275C15.075 10.075 15.3125 9.97917 15.5875 9.9875C15.8625 9.99583 16.1 10.1 16.3 10.3C16.4833 10.5 16.5792 10.7333 16.5875 11C16.5958 11.2667 16.5 11.5 16.3 11.7L12.7 15.3C12.6 15.4 12.4917 15.4708 12.375 15.5125C12.2583 15.5542 12.1333 15.575 12 15.575ZM6 20C5.45 20 4.97917 19.8042 4.5875 19.4125C4.19583 19.0208 4 18.55 4 18V16C4 15.7167 4.09583 15.4792 4.2875 15.2875C4.47917 15.0958 4.71667 15 5 15C5.28333 15 5.52083 15.0958 5.7125 15.2875C5.90417 15.4792 6 15.7167 6 16V18H18V16C18 15.7167 18.0958 15.4792 18.2875 15.2875C18.4792 15.0958 18.7167 15 19 15C19.2833 15 19.5208 15.0958 19.7125 15.2875C19.9042 15.4792 20 15.7167 20 16V18C20 18.55 19.8042 19.0208 19.4125 19.4125C19.0208 19.8042 18.55 20 18 20H6Z"
/>
<path class="vot-loader-helper" d="" />
</svg>`,
);
this.votDownloadButton.hidden = true;
this.votMenu.headerContainer.appendChild(this.votDownloadButton);
this.votDownloadSubtitlesButton = ui.createIconButton(
svg`<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="100%"
viewBox="0 0 24 24"
>
<path
d="M4 20q-.825 0-1.413-.588T2 18V6q0-.825.588-1.413T4 4h16q.825 0 1.413.588T22 6v12q0 .825-.588 1.413T20 20H4Zm2-4h8v-2H6v2Zm10 0h2v-2h-2v2ZM6 12h2v-2H6v2Zm4 0h8v-2h-8v2Z"
/>
</svg>`,
);
this.votDownloadSubtitlesButton.hidden = true;
this.votMenu.headerContainer.appendChild(this.votDownloadSubtitlesButton);
this.votSettingsButton = ui.createIconButton(
svg`<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="100%"
viewBox="0 -960 960 960"
>
<path
d="M555-80H405q-15 0-26-10t-13-25l-12-93q-13-5-24.5-12T307-235l-87 36q-14 5-28 1t-22-17L96-344q-8-13-5-28t15-24l75-57q-1-7-1-13.5v-27q0-6.5 1-13.5l-75-57q-12-9-15-24t5-28l74-129q7-14 21.5-17.5T220-761l87 36q11-8 23-15t24-12l12-93q2-15 13-25t26-10h150q15 0 26 10t13 25l12 93q13 5 24.5 12t22.5 15l87-36q14-5 28-1t22 17l74 129q8 13 5 28t-15 24l-75 57q1 7 1 13.5v27q0 6.5-2 13.5l75 57q12 9 15 24t-5 28l-74 128q-8 13-22.5 17.5T738-199l-85-36q-11 8-23 15t-24 12l-12 93q-2 15-13 25t-26 10Zm-73-260q58 0 99-41t41-99q0-58-41-99t-99-41q-59 0-99.5 41T342-480q0 58 40.5 99t99.5 41Zm0-80q-25 0-42.5-17.5T422-480q0-25 17.5-42.5T482-540q25 0 42.5 17.5T542-480q0 25-17.5 42.5T482-420Zm-2-60Zm-40 320h79l14-106q31-8 57.5-23.5T639-327l99 41 39-68-86-65q5-14 7-29.5t2-31.5q0-16-2-31.5t-7-29.5l86-65-39-68-99 42q-22-23-48.5-38.5T533-694l-13-106h-79l-14 106q-31 8-57.5 23.5T321-633l-99-41-39 68 86 64q-5 15-7 30t-2 32q0 16 2 31t7 30l-86 65 39 68 99-42q22 23 48.5 38.5T427-266l13 106Z"
/>
</svg>`,
);
this.votMenu.headerContainer.appendChild(this.votSettingsButton);
this.votTranslationLanguageSelect = ui.createVOTLanguageSelect({
fromTitle:
localizationProvider.get("langs")[this.video.detectedLanguage],
fromDialogTitle: localizationProvider.get("videoLanguage"),
fromItems: genOptionsByOBJ(
availableLangs,
this.videoData.detectedLanguage,
),
fromOnSelectCB: async (e) => {
debug.log(
"[fromOnSelectCB] select from language",
e.target.dataset.votValue,
);
this.videoData = await this.getVideoData();
this.setSelectMenuValues(
e.target.dataset.votValue,
this.videoData.responseLanguage,
);
},
toTitle: localizationProvider.get("langs")[this.video.responseLanguage],
toDialogTitle: localizationProvider.get("translationLanguage"),
toItems: genOptionsByOBJ(availableTTS, this.videoData.responseLanguage),
toOnSelectCB: async (e) => {
const newLang = e.target.dataset.votValue;
debug.log("[toOnSelectCB] select to language", newLang);
this.data.responseLanguage = this.translateToLang = newLang;
await votStorage.set("responseLanguage", this.data.responseLanguage);
debug.log(
"Response Language value changed. New value: ",
this.data.responseLanguage,
);
this.videoData = await this.getVideoData();
this.setSelectMenuValues(
this.videoData.detectedLanguage,
this.data.responseLanguage,
);
},
});
this.votMenu.bodyContainer.appendChild(
this.votTranslationLanguageSelect.container,
);
this.votSubtitlesSelect = ui.createVOTSelect(
localizationProvider.get("VOTSubtitlesDisabled"),
localizationProvider.get("VOTSubtitles"),
[
{
label: localizationProvider.get("VOTSubtitlesDisabled"),
value: "disabled",
selected: true,
disabled: false,
},
],
{
onSelectCb: async (e) => {
await this.changeSubtitlesLang(e.target.dataset.votValue);
},
labelElement: ui.createVOTSelectLabel(
localizationProvider.get("VOTSubtitles"),
),
},
);
this.votMenu.bodyContainer.appendChild(this.votSubtitlesSelect.container);
this.votVideoVolumeSlider = ui.createSlider(
html`${localizationProvider.get("VOTVolume")}:
<strong>${this.getVideoVolume() * 100}%</strong>`,
this.getVideoVolume() * 100,
);
this.votVideoVolumeSlider.container.hidden =
this.data.showVideoSlider !== 1 ||
this.votButton.container.dataset.status !== "success";
this.votMenu.bodyContainer.appendChild(
this.votVideoVolumeSlider.container,
);
this.votVideoTranslationVolumeSlider = ui.createSlider(
html`${localizationProvider.get("VOTVolumeTranslation")}:
<strong>${this.data?.defaultVolume ?? 100}%</strong>`,
this.data?.defaultVolume ?? 100,
0,
this.data.audioBooster ? maxAudioVolume : 100,
);
this.votVideoTranslationVolumeSlider.container.hidden =
this.votButton.container.dataset.status !== "success";
this.votMenu.bodyContainer.appendChild(
this.votVideoTranslationVolumeSlider.container,
);
this.votMenu.container.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
});
}
// VOT Settings
{
this.votSettingsDialog = ui.createDialog(
localizationProvider.get("VOTSettings"),
);
document.documentElement.appendChild(this.votSettingsDialog.container);
this.votTranslationHeader = ui.createHeader(
localizationProvider.get("translationSettings"),
);
this.votSettingsDialog.bodyContainer.appendChild(
this.votTranslationHeader,
);
this.votAutoTranslateCheckbox = ui.createCheckbox(
localizationProvider.get("VOTAutoTranslate"),
this.data?.autoTranslate ?? false,
);
this.votSettingsDialog.bodyContainer.appendChild(
this.votAutoTranslateCheckbox.container,
);
this.votDontTranslateYourLangSelect = ui.createVOTSelect(
localizationProvider.get("langs")[this.data.dontTranslateLanguage],
localizationProvider.get("VOTDontTranslateYourLang"),
genOptionsByOBJ(availableLangs, this.data.dontTranslateLanguage),
{
onSelectCb: async (e) => {
this.data.dontTranslateLanguage = e.target.dataset.votValue;
await votStorage.set(
"dontTranslateLanguage",
this.data.dontTranslateLanguage,
);
},
labelElement: ui.createCheckbox(
localizationProvider.get("VOTDontTranslateYourLang"),
this.data?.dontTranslateYourLang ?? true,
).container,
},
);
this.votSettingsDialog.bodyContainer.appendChild(
this.votDontTranslateYourLangSelect.container,
);
this.changehotkeyButton = ui.createOutlinedButton(
createHotkeyText(this.data.hotkeyButton),
);
this.votSettingsDialog.bodyContainer.appendChild(this.changehotkeyButton);
this.votAutoSetVolumeCheckbox = ui.createCheckbox(
`${localizationProvider.get("VOTAutoSetVolume")}`,
this.data?.autoSetVolumeYandexStyle ?? true,
);
this.votSettingsDialog.bodyContainer.appendChild(
this.votAutoSetVolumeCheckbox.container,
);
this.votAutoSetVolumeSlider = ui.createSlider(
html`<strong
>${Math.round(
(this.data?.autoVolume ?? defaultAutoVolume) * 100,
)}%</strong
>`,
Math.round((this.data?.autoVolume ?? defaultAutoVolume) * 100),
0,
100,
);
this.votSettingsDialog.bodyContainer.appendChild(
this.votAutoSetVolumeSlider.container,
);
this.votShowVideoSliderCheckbox = ui.createCheckbox(
localizationProvider.get("VOTShowVideoSlider"),
this.data?.showVideoSlider ?? false,
);
this.votSettingsDialog.bodyContainer.appendChild(
this.votShowVideoSliderCheckbox.container,
);
this.votAudioBoosterCheckbox = ui.createCheckbox(
localizationProvider.get("VOTAudioBooster"),
this.data?.audioBooster ?? false,
);
if (!this.audioContext) {
this.votAudioBoosterCheckbox.input.disabled = true;
this.votAudioBoosterCheckbox.container.title =
localizationProvider.get("VOTNeedWebAudioAPI");
}
this.votSettingsDialog.bodyContainer.appendChild(
this.votAudioBoosterCheckbox.container,
);
this.votSyncVolumeCheckbox = ui.createCheckbox(
localizationProvider.get("VOTSyncVolume"),
this.data?.syncVolume ?? false,
);
this.votSettingsDialog.bodyContainer.appendChild(
this.votSyncVolumeCheckbox.container,
);
this.votDownloadWithNameCheckbox = ui.createCheckbox(
localizationProvider.get("VOTDownloadWithName"),
this.data?.downloadWithName ?? false,
);
this.votSettingsDialog.bodyContainer.appendChild(
this.votDownloadWithNameCheckbox.container,
);
this.votUseNewModelCheckbox = ui.createCheckbox(
localizationProvider.get("VOTUseNewModel"),
this.data?.useNewModel ?? false,
);
this.votSettingsDialog.bodyContainer.appendChild(
this.votUseNewModelCheckbox.container,
);
this.votTranslationServiceSelect = ui.createVOTSelect(
this.data.translationService.toUpperCase(),
localizationProvider.get("VOTTranslationService"),
genOptionsByOBJ(translateServices, this.data.translationService),
{
onSelectCb: async (e) => {
this.data.translationService = e.target.dataset.votValue;
await votStorage.set(
"translationService",
this.data.translationService,
);
},
labelElement: ui.createCheckbox(
localizationProvider.get("VOTTranslateAPIErrors"),
this.data.translateAPIErrors ?? true,
).container,
},
);
this.votTranslationServiceSelect.container.hidden =
localizationProvider.lang === "ru";
this.votSettingsDialog.bodyContainer.appendChild(
this.votTranslationServiceSelect.container,
);
this.votDetectServiceSelect = ui.createVOTSelect(
this.data.detectService.toUpperCase(),
localizationProvider.get("VOTDetectService"),
genOptionsByOBJ(detectServices, this.data.detectService),
{
onSelectCb: async (e) => {
this.data.detectService = e.target.dataset.votValue;
await votStorage.set("detectService", this.data.detectService);
},
labelElement: ui.createVOTSelectLabel(
localizationProvider.get("VOTDetectService"),
),
},
);
this.votSettingsDialog.bodyContainer.appendChild(
this.votDetectServiceSelect.container,
);
// SUBTITLES
this.votSubtitlesHeader = ui.createHeader(
localizationProvider.get("subtitlesSettings"),
);
this.votSettingsDialog.bodyContainer.appendChild(this.votSubtitlesHeader);
this.votSubtitlesDetails = ui.createDetails(
localizationProvider.get("VOTSubtitlesDesign"),
);
this.votSettingsDialog.bodyContainer.appendChild(
this.votSubtitlesDetails.container,
);
// PROXY
this.votProxyHeader = ui.createHeader(
localizationProvider.get("proxySettings"),
);
this.votSettingsDialog.bodyContainer.appendChild(this.votProxyHeader);
this.votM3u8ProxyHostTextfield = ui.createTextfield(
localizationProvider.get("VOTM3u8ProxyHost"),
this.data?.m3u8ProxyHost,
m3u8ProxyHost,
);
this.votSettingsDialog.bodyContainer.appendChild(
this.votM3u8ProxyHostTextfield.container,
);
this.votProxyWorkerHostTextfield = ui.createTextfield(
localizationProvider.get("VOTProxyWorkerHost"),
this.data?.proxyWorkerHost,
proxyWorkerHost,
);
this.votSettingsDialog.bodyContainer.appendChild(
this.votProxyWorkerHostTextfield.container,
);
this.votAudioProxyCheckbox = ui.createCheckbox(
localizationProvider.get("VOTAudioProxy"),
this.data?.audioProxy ?? false,
);
this.votSettingsDialog.bodyContainer.appendChild(
this.votAudioProxyCheckbox.container,
);
this.votNewAudioPlayerCheckbox = ui.createCheckbox(
localizationProvider.get("VOTNewAudioPlayer"),
this.data?.newAudioPlayer ?? false,
);
if (!this.audioContext) {
this.votNewAudioPlayerCheckbox.input.disabled = true;
this.votNewAudioPlayerCheckbox.container.title =
localizationProvider.get("VOTNeedWebAudioAPI");
}
this.votSettingsDialog.bodyContainer.appendChild(
this.votNewAudioPlayerCheckbox.container,
);
this.votOnlyBypassMediaCSPCheckbox = ui.createCheckbox(
localizationProvider.get("VOTOnlyBypassMediaCSP") +
(this.site.needBypassCSP
? ` (${localizationProvider.get("VOTMediaCSPEnabledOnSite")})`
: ""),
this.data?.onlyBypassMediaCSP ?? false,
);
this.votOnlyBypassMediaCSPCheckbox.container.classList.add(
"vot-checkbox-sub",
);
if (!this.audioContext) {
this.votOnlyBypassMediaCSPCheckbox.container.title =
localizationProvider.get("VOTNeedWebAudioAPI");
}
this.votOnlyBypassMediaCSPCheckbox.input.disabled =
!this.data.newAudioPlayer && this.audioContext;
if (!this.data.newAudioPlayer) {
this.votOnlyBypassMediaCSPCheckbox.container.hidden = true;
}
this.votSettingsDialog.bodyContainer.appendChild(
this.votOnlyBypassMediaCSPCheckbox.container,
);
// ABOUT
this.votAboutHeader = ui.createHeader(localizationProvider.get("about"));
this.votSettingsDialog.bodyContainer.appendChild(this.votAboutHeader);
this.votLanguageSelect = ui.createVOTSelect(
localizationProvider.get("langs")[
votStorage.syncGet("locale-lang-override", "auto")
],
localizationProvider.get("VOTMenuLanguage"),
genOptionsByOBJ(
availableLocales,
votStorage.syncGet("locale-lang-override", "auto"),
),
{
onSelectCb: async (e) => {
await votStorage.set(
"locale-lang-override",
e.target.dataset.votValue,
);
},
labelElement: ui.createVOTSelectLabel(
localizationProvider.get("VOTMenuLanguage"),
),
},
);
this.votSettingsDialog.bodyContainer.appendChild(
this.votLanguageSelect.container,
);
this.votShowPiPButtonCheckbox = ui.createCheckbox(
localizationProvider.get("VOTShowPiPButton"),
this.data?.showPiPButton ?? false,
);
this.votShowPiPButtonCheckbox.container.hidden = !isPiPAvailable();
this.votSettingsDialog.bodyContainer.appendChild(
this.votShowPiPButtonCheckbox.container,
);
this.votVersionInfo = ui.createInformation(
`${localizationProvider.get("VOTVersion")}:`,
GM_info.script.version,
);
this.votSettingsDialog.bodyContainer.appendChild(
this.votVersionInfo.container,
);
this.votAuthorsInfo = ui.createInformation(
`${localizationProvider.get("VOTAuthors")}:`,
GM_info.script.author,
);
this.votSettingsDialog.bodyContainer.appendChild(
this.votAuthorsInfo.container,
);
this.votLoaderInfo = ui.createInformation(
`${localizationProvider.get("VOTLoader")}:`,
`${GM_info.scriptHandler} v${GM_info.version}`,
);
this.votSettingsDialog.bodyContainer.appendChild(
this.votLoaderInfo.container,
);
this.votBrowserInfo = ui.createInformation(
`${localizationProvider.get("VOTBrowser")}:`,
`${browserInfo.browser.name} ${browserInfo.browser.version} (${browserInfo.os.name} ${browserInfo.os.version})`,
);
this.votSettingsDialog.bodyContainer.appendChild(
this.votBrowserInfo.container,
);
this.votLocaleInfo = ui.createInformation(
`${localizationProvider.get("VOTLocaleHash")}:`,
html`${this.data.localeHash}<br />(${localizationProvider.get(
"VOTUpdatedAt",
)}
${new Date(this.data.localeUpdatedAt * 1000).toLocaleString()})`,
);
this.votSettingsDialog.bodyContainer.appendChild(
this.votLocaleInfo.container,
);
this.votUpdateLocaleFilesButton = ui.createOutlinedButton(
localizationProvider.get("VOTUpdateLocaleFiles"),
);
this.votSettingsDialog.bodyContainer.appendChild(
this.votUpdateLocaleFilesButton,
);
this.votResetSettingsButton = ui.createButton(
localizationProvider.get("resetSettings"),
);
this.votSettingsDialog.bodyContainer.appendChild(
this.votResetSettingsButton,
);
}
}
async handleTranslationBtnClick() {
debug.log(
"[click translationBtn]",
this.audioPlayer,
this.audioPlayer.player,
);
if (this.audioPlayer.player.src) {
debug.log(
"[click translationBtn] audio.src is not empty",
this.audioPlayer.player.src,
);
this.stopTranslate();
return;
}
if (this.hls.url) {
debug.log("[click translationBtn] hls is not empty", this.hls.url);
this.stopTranslate();
return;
}
try {
debug.log("[click translationBtn] trying execute translation");
if (!this.videoData.videoId) {
throw new VOTLocalizedError("VOTNoVideoIDFound");
}
// при скролле ленты клипов в вк сохраняется старый айди видео для перевода,
// но для субтитров используется новый, поэтому перед запуском перевода необходимо получить актуальный айди
if (this.site.host === "vk" && this.site.additionalData === "clips") {
this.videoData = await this.getVideoData();
}
await this.translateExecutor(this.videoData.videoId);
} catch (err) {
console.error("[VOT]", err);
if (err?.name === "VOTLocalizedError") {
this.transformBtn("error", err.localizedMessage);
} else {
this.transformBtn("error", err?.message);
}
}
}
initUIEvents() {
// VOT Button
{
this.votButton.translateButton.addEventListener("click", async () => {
await this.handleTranslationBtnClick();
});
this.votButton.pipButton.addEventListener("click", () => {
(async () => {
if (this.video !== document.pictureInPictureElement) {
await this.video.requestPictureInPicture();
} else {
await document.exitPictureInPicture();
}
})();
});
this.votButton.menuButton.addEventListener("click", () => {
this.votMenu.container.hidden = !this.votMenu.container.hidden;
});
this.votButton.container.addEventListener("mousedown", () => {
this.dragging = true;
});
this.container.addEventListener("mouseup", () => {
this.dragging = false;
});
this.container.addEventListener("mousemove", async (e) => {
if (this.dragging) {
e.preventDefault();
const percentX = (e.clientX / this.container.clientWidth) * 100;
// const percentY = (e.clientY / this.video.clientHeight) * 100;
const isBigContainer = this.container.clientWidth > 550;
const isLeft = percentX <= 44;
const isRightSide = percentX >= 66 ? "right" : "default";
const side = isLeft ? "left" : isRightSide;
this.data.buttonPos = isBigContainer ? side : "default";
this.votButton.container.dataset.direction =
this.data.buttonPos === "default" ? "row" : "column";
this.votButton.container.dataset.position = this.data.buttonPos;
this.votMenu.container.dataset.position = this.data.buttonPos;
if (isBigContainer) {
await votStorage.set("buttonPos", this.data.buttonPos);
}
}
});
}
// VOT Menu
{
this.votDownloadButton.addEventListener("click", async () => {
if (!this.downloadTranslationUrl) {
return;
}
if (!this.data.downloadWithName) {
return window.open(this.downloadTranslationUrl, "_blank").focus();
}
const votLoader = document.querySelector("#vot-loader-download");
const primaryColor = getComputedStyle(
this.votMenu.container,
).getPropertyValue("--vot-primary-rgb");
const updateAnimation = ui.animateLoader(votLoader, primaryColor);
const res = await GM_fetch(this.downloadTranslationUrl);
const reader = res.body.getReader();
const contentLength = +res.headers.get("Content-Length");
let receivedLength = 0;
const chunks = [];
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
chunks.push(value);
receivedLength += value.length;
updateAnimation(Math.round((receivedLength / contentLength) * 100));
}
ui.afterAnimateLoader(votLoader, primaryColor);
const blob = new Blob(chunks);
const filename = clearFileName(
this.videoData.title ?? this.videoData.videoId,
);
const arrayBuffer = await blob.arrayBuffer();
const writer = new ID3Writer(arrayBuffer);
writer.setFrame("TIT2", filename);
writer.addTag();
downloadBlob(writer.getBlob(), `${filename}.mp3`);
});
this.votDownloadSubtitlesButton.addEventListener("click", async () => {
const format = this.data.subtitlesDownloadFormat;
const subsContent = convertSubs(this.yandexSubtitles, format);
const blob = new Blob(
[format === "json" ? JSON.stringify(subsContent) : subsContent],
{
type: "text/plain",
},
);
const filename = this.data.downloadWithName
? clearFileName(this.videoData.title ?? this.videoData.videoId)
: `subtitles_${this.videoData.videoId}`;
downloadBlob(blob, `${filename}.${format}`);
});
this.votSettingsButton.addEventListener("click", () => {
this.votSettingsDialog.container.hidden =
!this.votSettingsDialog.container.hidden;
if (document.fullscreenElement || document.webkitFullscreenElement) {
document.webkitExitFullscreen && document.webkitExitFullscreen();
document.exitFullscreen && document.exitFullscreen();
}
});
this.votVideoVolumeSlider.input.addEventListener("input", (e) => {
const value = Number(e.target.value);
this.votVideoVolumeSlider.label.querySelector("strong").textContent =
`${value}%`;
this.setVideoVolume(value / 100);
if (this.data.syncVolume) {
this.syncVolumeWrapper("video", value);
}
});
this.votVideoTranslationVolumeSlider.input.addEventListener(
"input",
(e) => {
(async () => {
this.data.defaultVolume = Number(e.target.value);
await votStorage.set("defaultVolume", this.data.defaultVolume);
this.votVideoTranslationVolumeSlider.label.querySelector(
"strong",
).textContent = `${this.data.defaultVolume}%`;
this.audioPlayer.player.volume = this.data.defaultVolume / 100;
if (!this.data.syncVolume) {
return;
}
this.syncVolumeWrapper("translation", this.data.defaultVolume);
if (
["youtube", "googledrive"].includes(this.site.host) &&
this.site.additionalData !== "mobile"
) {
// fix update youtube volume slider
this.setVideoVolume(this.tempOriginalVolume / 100);
}
})();
},
);
}
// VOT Settings
{
this.votAutoTranslateCheckbox.input.addEventListener("change", (e) => {
(async () => {
this.data.autoTranslate = Number(e.target.checked);
await Promise.all([
votStorage.set("autoTranslate", this.data.autoTranslate),
this.autoTranslate(),
]);
debug.log(
"autoTranslate value changed. New value: ",
this.data.autoTranslate,
);
})();
});
this.votDontTranslateYourLangSelect.labelElement.addEventListener(
"change",
(e) => {
(async () => {
this.data.dontTranslateYourLang = Number(e.target.checked);
await votStorage.set(
"dontTranslateYourLang",
this.data.dontTranslateYourLang,
);
debug.log(
"dontTranslateYourLang value changed. New value: ",
this.data.dontTranslateYourLang,
);
})();
},
);
const updateHotkey = async (newKey) => {
await votStorage.set("hotkeyButton", newKey);
this.data.hotkeyButton = newKey;
this.changehotkeyButton.textContent = createHotkeyText(newKey);
};
const keydownHandler = (e) => {
const newKey = e.code === "Escape" ? null : e.code;
updateHotkey(newKey);
document.removeEventListener("keydown", keydownHandler);
};
this.changehotkeyButton.addEventListener("click", () => {
this.changehotkeyButton.textContent =
localizationProvider.get("VOTPressNewHotkey");
document.addEventListener("keydown", keydownHandler);
});
this.votAutoSetVolumeCheckbox.input.addEventListener("change", (e) => {
(async () => {
this.data.autoSetVolumeYandexStyle = Number(e.target.checked);
await votStorage.set(
"autoSetVolumeYandexStyle",
this.data.autoSetVolumeYandexStyle,
);
debug.log(
"autoSetVolumeYandexStyle value changed. New value: ",
this.data.autoSetVolumeYandexStyle,
);
})();
});
this.votAutoSetVolumeSlider.input.addEventListener("input", (e) => {
(async () => {
const presetAutoVolume = Number(e.target.value);
this.data.autoVolume = (presetAutoVolume / 100).toFixed(2);
await votStorage.set("autoVolume", this.data.autoVolume);
this.votAutoSetVolumeSlider.label.querySelector(
"strong",
).textContent = `${presetAutoVolume}%`;
})();
});
this.votShowVideoSliderCheckbox.input.addEventListener("change", (e) => {
(async () => {
this.data.showVideoSlider = Number(e.target.checked);
await votStorage.set("showVideoSlider", this.data.showVideoSlider);
debug.log(
"showVideoSlider value changed. New value: ",
this.data.showVideoSlider,
);
this.votVideoVolumeSlider.container.hidden =
this.data.showVideoSlider !== 1 ||
this.votButton.container.dataset.status !== "success";
})();
});
this.votAudioBoosterCheckbox.input.addEventListener("change", (e) => {
(async () => {
this.data.audioBooster = Number(e.target.checked);
await votStorage.set("audioBooster", this.data.audioBooster);
debug.log(
"audioBooster value changed. New value: ",
this.data.audioBooster,
);
const currentAudioVolume =
this.votVideoTranslationVolumeSlider.input.value;
this.votVideoTranslationVolumeSlider.input.max = this.data
.audioBooster
? maxAudioVolume
: 100;
if (!this.data.audioBooster) {
this.votVideoTranslationVolumeSlider.input.value =
currentAudioVolume > 100 ? 100 : currentAudioVolume;
this.votVideoTranslationVolumeSlider.input.dispatchEvent(
new Event("input"),
);
}
})();
});
this.votSyncVolumeCheckbox.input.addEventListener("change", (e) => {
(async () => {
this.data.syncVolume = Number(e.target.checked);
await votStorage.set("syncVolume", this.data.syncVolume);
debug.log(
"syncVolume value changed. New value: ",
this.data.syncVolume,
);
})();
});
this.votDownloadWithNameCheckbox.input.addEventListener("change", (e) => {
(async () => {
this.data.downloadWithName = Number(e.target.checked);
await votStorage.set("downloadWithName", this.data.downloadWithName);
debug.log(
"downloadWithName value changed. New value: ",
this.data.downloadWithName,
);
})();
});
this.votUseNewModelCheckbox.input.addEventListener("change", (e) => {
(async () => {
this.data.useNewModel = Number(e.target.checked);
await votStorage.set("useNewModel", this.data.useNewModel);
debug.log(
"useNewModel value changed. New value: ",
this.data.useNewModel,
);
this.stopTranslate();
})();
});
this.votTranslationServiceSelect.labelElement.addEventListener(
"change",
(e) => {
(async () => {
this.data.translateAPIErrors = Number(e.target.checked);
await votStorage.set(
"translateAPIErrors",
this.data.translateAPIErrors,
);
debug.log(
"translateAPIErrors value changed. New value: ",
this.data.translateAPIErrors,
);
})();
},
);
// SUBTITLES
this.votSubtitlesDetails.container.addEventListener("click", () => {
this.votSubtitlesDialog = ui.createDialog(
localizationProvider.get("VOTSubtitlesDesign"),
);
this.votSubtitlesDialog.container.classList.add("vot-dialog-temp");
this.votSubtitlesDialog.container.hidden = false;
// remove the modal so that they do not accumulate
this.votSubtitlesDialog.backdrop.onclick =
this.votSubtitlesDialog.closeButton.onclick = () => {
this.votSubtitlesDialog.container.remove();
};
// subtitles elements
this.votSubtitlesHighlightWordsCheckbox = ui.createCheckbox(
localizationProvider.get("VOTHighlightWords"),
this.data?.highlightWords ?? false,
);
this.votSubtitlesDialog.bodyContainer.appendChild(
this.votSubtitlesHighlightWordsCheckbox.container,
);
this.votSubtitlesDownloadFormatSelect = ui.createVOTSelect(
this.data.subtitlesDownloadFormat.toUpperCase(),
localizationProvider.get("VOTSubtitlesDownloadFormat"),
genOptionsByOBJ(subtitlesFormats, this.data.subtitlesDownloadFormat),
{
onSelectCb: async (e) => {
this.data.subtitlesDownloadFormat = e.target.dataset.votValue;
await votStorage.set(
"subtitlesDownloadFormat",
this.data.subtitlesDownloadFormat,
);
},
labelElement: ui.createVOTSelectLabel(
localizationProvider.get("VOTSubtitlesDownloadFormat"),
),
},
);
this.votSubtitlesDialog.bodyContainer.appendChild(
this.votSubtitlesDownloadFormatSelect.container,
);
this.votSubtitlesMaxLengthSlider = ui.createSlider(
html`${localizationProvider.get("VOTSubtitlesMaxLength")}:
<strong>${this.data?.subtitlesMaxLength ?? 300}</strong>`,
this.data?.subtitlesMaxLength ?? 300,
50,
300,
);
this.votSubtitlesDialog.bodyContainer.appendChild(
this.votSubtitlesMaxLengthSlider.container,
);
this.votSubtitlesFontSizeSlider = ui.createSlider(
html`${localizationProvider.get("VOTSubtitlesFontSize")}:
<strong>${this.data?.subtitlesFontSize ?? 20}</strong>`,
this.data?.subtitlesFontSize ?? 20,
8,
50,
);
this.votSubtitlesDialog.bodyContainer.appendChild(
this.votSubtitlesFontSizeSlider.container,
);
this.votSubtitlesOpacitySlider = ui.createSlider(
html`${localizationProvider.get("VOTSubtitlesOpacity")}:
<strong>${this.data?.subtitlesOpacity ?? 20}</strong>`,
this.data?.subtitlesOpacity ?? 20,
0,
100,
);
this.votSubtitlesDialog.bodyContainer.appendChild(
this.votSubtitlesOpacitySlider.container,
);
// subtitles events
this.votSubtitlesHighlightWordsCheckbox.input.addEventListener(
"change",
(e) => {
(async () => {
this.data.highlightWords = Number(e.target.checked);
await votStorage.set("highlightWords", this.data.highlightWords);
debug.log(
"highlightWords value changed. New value: ",
this.data.highlightWords,
);
this.subtitlesWidget.setHighlightWords(this.data.highlightWords);
})();
},
);
this.votSubtitlesMaxLengthSlider.input.addEventListener(
"input",
(e) => {
(async () => {
this.data.subtitlesMaxLength = Number(e.target.value);
await votStorage.set(
"subtitlesMaxLength",
this.data.subtitlesMaxLength,
);
this.votSubtitlesMaxLengthSlider.label.querySelector(
"strong",
).textContent = `${this.data.subtitlesMaxLength}`;
this.subtitlesWidget.setMaxLength(this.data.subtitlesMaxLength);
})();
},
);
this.votSubtitlesFontSizeSlider.input.addEventListener("input", (e) => {
(async () => {
this.data.subtitlesFontSize = Number(e.target.value);
await votStorage.set(
"subtitlesFontSize",
this.data.subtitlesFontSize,
);
this.votSubtitlesFontSizeSlider.label.querySelector(
"strong",
).textContent = `${this.data.subtitlesFontSize}`;
this.subtitlesWidget.setFontSize(this.data.subtitlesFontSize);
})();
});
this.votSubtitlesOpacitySlider.input.addEventListener("input", (e) => {
(async () => {
this.data.subtitlesOpacity = Number(e.target.value);
await votStorage.set(
"subtitlesOpacity",
this.data.subtitlesOpacity,
);
this.votSubtitlesOpacitySlider.label.querySelector(
"strong",
).textContent = `${this.data.subtitlesOpacity}`;
this.subtitlesWidget.setOpacity(this.data.subtitlesOpacity);
})();
});
document.documentElement.appendChild(this.votSubtitlesDialog.container);
});
// OTHER
this.votShowPiPButtonCheckbox.input.addEventListener("change", (e) => {
(async () => {
this.data.showPiPButton = Number(e.target.checked);
await votStorage.set("showPiPButton", this.data.showPiPButton);
debug.log(
"showPiPButton value changed. New value: ",
this.data.showPiPButton,
);
this.votButton.pipButton.hidden =
!isPiPAvailable() || !this.data.showPiPButton;
this.votButton.separator2.hidden =
!isPiPAvailable() || !this.data.showPiPButton;
})();
});
// PROXY
this.votM3u8ProxyHostTextfield.input.addEventListener("change", (e) => {
(async () => {
this.data.m3u8ProxyHost = e.target.value || m3u8ProxyHost;
await votStorage.set("m3u8ProxyHost", this.data.m3u8ProxyHost);
debug.log(
"m3u8ProxyHost value changed. New value: ",
this.data.m3u8ProxyHost,
);
})();
});
this.votProxyWorkerHostTextfield.input.addEventListener("change", (e) => {
(async () => {
this.data.proxyWorkerHost = e.target.value || proxyWorkerHost;
await votStorage.set("proxyWorkerHost", this.data.proxyWorkerHost);
debug.log(
"proxyWorkerHost value changed. New value: ",
this.data.proxyWorkerHost,
);
if (this.data.translateProxyEnabled) {
this.votClient.host = this.data.proxyWorkerHost;
}
})();
});
this.votAudioProxyCheckbox.input.addEventListener("change", (e) => {
(async () => {
this.data.audioProxy = Number(e.target.checked);
await votStorage.set("audioProxy", this.data.audioProxy);
debug.log(
"audioProxy value changed. New value: ",
this.data.audioProxy,
);
})();
});
this.votOnlyBypassMediaCSPCheckbox.input.addEventListener(
"change",
(e) => {
(async () => {
this.data.onlyBypassMediaCSP = Number(e.target.checked);
await votStorage.set(
"onlyBypassMediaCSP",
this.data.onlyBypassMediaCSP,
);
debug.log(
"onlyBypassMediaCSP value changed. New value: ",
this.data.onlyBypassMediaCSP,
);
this.stopTranslate();
this.createPlayer();
})();
},
);
this.votNewAudioPlayerCheckbox.input.addEventListener("change", (e) => {
(async () => {
const checked = e.target.checked;
this.data.newAudioPlayer = Number(checked);
await votStorage.set("newAudioPlayer", this.data.newAudioPlayer);
debug.log(
"newAudioPlayer value changed. New value: ",
this.data.newAudioPlayer,
);
this.stopTranslate();
this.createPlayer();
this.votOnlyBypassMediaCSPCheckbox.input.disabled =
this.votOnlyBypassMediaCSPCheckbox.container.hidden = !checked;
})();
});
this.votUpdateLocaleFilesButton.addEventListener("click", () => {
(async () => {
await votStorage.set("locale-hash", "");
await localizationProvider.update(true);
window.location.reload();
})();
});
this.votResetSettingsButton.addEventListener("click", () => {
(async () => {
localizationProvider.reset();
const valuesForClear = await votStorage.list();
for (let i = 0; i < valuesForClear.length; i++) {
const v = valuesForClear[i];
if (!localizationProvider.gmValues.includes(v)) {
votStorage.syncDelete(v);
}
}
window.location.reload();
})();
});
}
}
releaseExtraEvents() {
this.resizeObserver?.disconnect();
if (
["youtube", "googledrive"].includes(this.site.host) &&
this.site.additionalData !== "mobile"
) {
this.syncVolumeObserver?.disconnect();
}
if (this.extraEvents) {
for (let i = 0; i < this.extraEvents.length; i++) {
const e = this.extraEvents[i];
e.element.removeEventListener(e.event, e.handler);
}
}
}
initExtraEvents() {
this.extraEvents = [];
const addExtraEventListener = (element, event, handler) => {
this.extraEvents.push({
element,
event,
handler,
});
element.addEventListener(event, handler);
};
const addExtraEventListeners = (element, events, handler) => {
for (const event of events) {
addExtraEventListener(element, event, handler);
}
};
this.resizeObserver = new ResizeObserver((entries) => {
for (let i = 0; i < entries.length; i++) {
const e = entries[i];
this.votMenu.container.setAttribute(
"style",
`--vot-container-height: ${e.contentRect.height}px`,
);
}
const isBigWidth = this.container.clientWidth > 550;
this.votButton.container.dataset.position =
this.votMenu.container.dataset.position = isBigWidth
? this.data?.buttonPos
: "default";
this.votButton.container.dataset.direction =
this.data?.buttonPos && this.data?.buttonPos !== "default" && isBigWidth
? "column"
: "row";
});
this.resizeObserver.observe(this.video);
this.votMenu.container.setAttribute(
"style",
`--vot-container-height: ${this.video.getBoundingClientRect().height}px`,
);
// Sync menu volume slider with youtube original video (youtube only)
if (
["youtube", "googledrive"].includes(this.site.host) &&
this.site.additionalData !== "mobile"
) {
this.syncVolumeObserver = new MutationObserver((mutations) => {
if (!this.audioPlayer.player.src || !this.data.syncVolume) {
return;
}
for (let i = 0; i < mutations.length; i++) {
const mutation = mutations[i];
if (
mutation.type === "attributes" &&
mutation.attributeName === "aria-valuenow"
) {
if (this.firstSyncVolume) {
// disable sync if it's sync when the translation is enabled
this.firstSyncVolume = false;
return;
}
// youtube sets setMuted and returns the old value if the slider is moved to 0
// also fixes the operation if the video is muted via the hotkey
const videoVolume = this.isMuted()
? 0
: this.getVideoVolume() * 100;
const finalVolume = Math.round(videoVolume);
this.data.defaultVolume = finalVolume;
this.audioPlayer.player.volume = this.data.defaultVolume / 100;
this.syncVolumeWrapper("video", finalVolume);
}
}
});
const ytpVolumePanel = document.querySelector(".ytp-volume-panel");
if (ytpVolumePanel) {
this.syncVolumeObserver.observe(ytpVolumePanel, {
attributes: true,
childList: false,
subtree: true,
attributeOldValue: true,
});
}
}
document.addEventListener("click", (event) => {
const e = event.target;
const button = this.votButton.container;
const menu = this.votMenu.container;
const container = this.container;
const settings = this.votSettingsDialog.container;
const tempDialog = document.querySelector(".vot-dialog-temp");
const isButton = button.contains(e);
const isMenu = menu.contains(e);
const isVideo = container.contains(e);
const isSettings = settings.contains(e);
const isTempDialog = tempDialog?.contains(e) ?? false;
debug.log(
`[document click] ${isButton} ${isMenu} ${isVideo} ${isSettings} ${isTempDialog}`,
);
if (!(!isButton && !isMenu && !isSettings && !isTempDialog)) return;
if (!isVideo) this.logout(0);
this.votMenu.container.hidden = true;
});
document.addEventListener("keydown", async (event) => {
const code = event.code;
// Проверка, если активный элемент - это вводимый элемент
const activeElement = document.activeElement;
const isInputElement =
["input", "textarea"].includes(activeElement.tagName.toLowerCase()) ||
activeElement.isContentEditable;
if (!isInputElement && code === this.data.hotkeyButton) {
await this.handleTranslationBtnClick();
}
});
let eventContainer = this.site.eventSelector
? document.querySelector(this.site.eventSelector)
: this.container;
if (eventContainer)
addExtraEventListeners(
eventContainer,
["mousemove", "mouseout"],
this.resetTimer,
);
addExtraEventListener(
this.votButton.container,
"mousemove",
this.changeOpacityOnEvent,
);
addExtraEventListener(
this.votMenu.container,
"mousemove",
this.changeOpacityOnEvent,
);
addExtraEventListeners(
document,
["touchstart", "touchmove", "touchend"],
this.changeOpacityOnEvent,
);
// fix youtube hold to fast
addExtraEventListener(this.votButton.container, "mousedown", (e) => {
e.stopImmediatePropagation();
});
addExtraEventListener(this.votMenu.container, "mousedown", (e) => {
e.stopImmediatePropagation();
});
// fix draggable menu in youtube (#394, #417)
if (this.site.host === "youtube") {
this.container.draggable = false;
}
if (this.site.host === "googledrive") {
this.container.style.height = "100%";
}
addExtraEventListener(this.video, "canplay", async () => {
// Временное решение
if (this.site.host === "rutube" && this.video.src) {
return;
}
await this.setCanPlay();
});
addExtraEventListener(this.video, "emptied", async () => {
if (
this.video.src &&
(await getVideoID(this.site, this.video)) === this.videoData.videoId
)
return;
debug.log("lipsync mode is emptied");
this.videoData = "";
this.stopTranslation();
});
if (!["rutube", "ok"].includes(this.site.host)) {
addExtraEventListener(this.video, "volumechange", () => {
this.syncVideoVolumeSlider();
});
}
if (this.site.host === "youtube" && !this.site.additionalData) {
addExtraEventListener(document, "yt-page-data-updated", async () => {
debug.log("yt-page-data-updated");
// fix #802
if (!window.location.pathname.includes("/shorts/")) {
return;
}
await this.setCanPlay();
});
}
}
```ASSISTANT
```js
async init() {
if (this.initialized) return;
const keysWithDefaults = {
autoTranslate: 0,
dontTranslateLanguage: lang,
dontTranslateYourLang: 1,
autoSetVolumeYandexStyle: 1,
autoVolume: defaultAutoVolume,
buttonPos: "default",
showVideoSlider: 1,
syncVolume: 0,
downloadWithName: 1,
subtitlesMaxLength: 300,
highlightWords: 0,
subtitlesFontSize: 20,
subtitlesOpacity: 20,
subtitlesDownloadFormat: "srt",
responseLanguage: lang,
defaultVolume: 100,
audioProxy: 0,
onlyBypassMediaCSP: Number(!!this.audioContext),
newAudioPlayer: Number(!!this.audioContext),
showPiPButton: 0,
translateAPIErrors: 1,
translationService: defaultTranslationService,
detectService: defaultDetectService,
hotkeyButton: null,
m3u8ProxyHost: m3u8ProxyHost,
translateProxyEnabled: 0,
proxyWorkerHost: proxyWorkerHost,
audioBooster: 0,
useNewModel: 1,
localeHash: "",
localeUpdatedAt: 0,
};
const entries = Object.entries(keysWithDefaults).map(([key, defaultValue]) => [
key,
votStorage.get(key, defaultValue),
]);
this.data = Object.fromEntries(await Promise.all(entries.map(([k, p]) => p.then(v => [k, v]))));
console.log("[VOT] data from db: ", this.data);
if (this.data.m3u8ProxyHost === "m3u8-proxy.toil.cc") {
this.data.m3u8ProxyHost = m3u8ProxyHost;
await votStorage.set("m3u8ProxyHost", m3u8ProxyHost);
console.log(`[VOT] Old m3u8 proxy host converted to new ${this.data.m3u8ProxyHost} media-proxy`);
}
if (this.data.proxyWorkerHost === "vot.toil.cc") {
this.data.proxyWorkerHost = proxyWorkerHost;
await votStorage.set("proxyWorkerHost", proxyWorkerHost);
console.log(`[VOT] Old proxy worker host converted to new ${this.data.proxyWorkerHost}`);
}
if (!this.data.translateProxyEnabled && GM_info?.scriptHandler && proxyOnlyExtensions.includes(GM_info.scriptHandler)) {
this.data.translateProxyEnabled = 1;
await votStorage.set("translateProxyEnabled", 1);
debug.log("translateProxyEnabled", this.data.translateProxyEnabled);
}
debug.log("Extension compatibility passed...");
this.votOpts = {
headers: this.data.translateProxyEnabled ? {} : {
"sec-ch-ua": null,
"sec-ch-ua-mobile": null,
"sec-ch-ua-platform": null,
},
fetchFn: GM_fetch,
hostVOT: votBackendUrl,
host: this.data.translateProxyEnabled ? this.data.proxyWorkerHost : workerHost,
};
this.votClient = new (this.data.translateProxyEnabled ? VOTWorkerClient : VOTClient)(this.votOpts);
this.subtitlesWidget = new SubtitlesWidget(this.video, this.container, this.site);
this.subtitlesWidget.setMaxLength(this.data.subtitlesMaxLength);
this.subtitlesWidget.setHighlightWords(this.data.highlightWords);
this.subtitlesWidget.setFontSize(this.data.subtitlesFontSize);
this.subtitlesWidget.setOpacity(this.data.subtitlesOpacity);
this.initUI();
this.initUIEvents();
this.videoData = await this.getVideoData();
this.createPlayer();
this.setSelectMenuValues(this.videoData.detectedLanguage, this.data.responseLanguage ?? "ru");
this.translateToLang = this.data.responseLanguage ?? "ru";
this.initExtraEvents();
await Promise.all([this.updateSubtitles(), this.autoTranslate()]);
this.initialized = true;
}
initUI() {
this.votButton = ui.createVOTButton(localizationProvider.get("translateVideo"));
this.votButton.container.style.opacity = 0;
const isCustomPos = this.data.buttonPos && this.data.buttonPos !== "default" && this.container.clientWidth > 550;
this.votButton.container.dataset.direction = isCustomPos ? "column" : "row";
this.votButton.container.dataset.position = isCustomPos ? this.data.buttonPos : "default";
this.container.appendChild(this.votButton.container);
this.votButton.pipButton.hidden = !isPiPAvailable() || !this.data.showPiPButton;
this.votButton.separator2.hidden = !isPiPAvailable() || !this.data.showPiPButton;
this.votButton.container.addEventListener("click", e => {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
});
this.votMenu = ui.createVOTMenu(localizationProvider.get("VOTSettings"));
this.votMenu.container.dataset.position = this.container.clientWidth > 550 ? this.data.buttonPos : "default";
this.container.appendChild(this.votMenu.container);
const createHiddenButton = (svgContent, hidden = true) => {
const button = ui.createIconButton(svgContent);
button.hidden = hidden;
return button;
};
this.votDownloadButton = createHiddenButton(`
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="100%" viewBox="0 0 24 24" class="vot-loader" id="vot-loader-download">
<path class="vot-loader-main" d="M12 15.575C11.8667 15.575 11.7417 15.5542 11.625 15.5125C11.5083 15.4708 11.4 15.4 11.3 15.3L7.7 11.7C7.5 11.5 7.40417 11.2667 7.4125 11C7.42083 10.7333 7.51667 10.5 7.7 10.3C7.9 10.1 8.1375 9.99583 8.4125 9.9875C8.6875 9.97917 8.925 10.075 9.125 10.275L11 12.15V5C11 4.71667 11.0958 4.47917 11.2875 4.2875C11.4792 4.09583 11.7167 4 12 4C12.2833 4 12.5208 4.09583 12.7125 4.2875C12.9042 4.47917 13 4.71667 13 5V12.15L14.875 10.275C15.075 10.075 15.3125 9.97917 15.5875 9.9875C15.8625 9.99583 16.1 10.1 16.3 10.3C16.4833 10.5 16.5792 10.7333 16.5875 11C16.5958 11.2667 16.5 11.5 16.3 11.7L12.7 15.3C12.6 15.4 12.4917 15.4708 12.375 15.5125C12.2583 15.5542 12.1333 15.575 12 15.575ZM6 20C5.45 20 4.97917 19.8042 4.5875 19.4125C4.19583 19.0208 4 18.55 4 18V16C4 15.7167 4.09583 15.4792 4.2875 15.2875C4.47917 15.0958 4.71667 15 5 15C5.28333 15 5.52083 15.0958 5.7125 15.2875C5.90417 15.4792 6 15.7167 6 16V18H18V16C18 15.7167 18.0958 15.4792 18.2875 15.2875C18.4792 15.0958 18.7167 15 19 15C19.2833 15 19.5208 15.0958 19.7125 15.2875C19.9042 15.4792 20 15.7167 20 16V18C20 18.55 19.8042 19.0208 19.4125 19.4125C19.0208 19.8042 18.55 20 18 20H6Z"/>
<path class="vot-loader-helper" d="" />
</svg>`, true);
this.votMenu.headerContainer.appendChild(this.votDownloadButton);
this.votDownloadSubtitlesButton = createHiddenButton(`
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="100%" viewBox="0 0 24 24">
<path d="M4 20q-.825 0-1.413-.588T2 18V6q0-.825.588-1.413T4 4h16q.825 0 1.413.588T22 6v12q0 .825-.588 1.413T20 20H4Zm2-4h8v-2H6v2Zm10 0h2v-2h-2v2ZM6 12h2v-2H6v2Zm4 0h8v-2h-8v2Z"/>
</svg>`, true);
this.votMenu.headerContainer.appendChild(this.votDownloadSubtitlesButton);
this.votSettingsButton = ui.createIconButton(`
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="100%" viewBox="0 -960 960 960">
<path d="M555-80H405q-15 0-26-10t-13-25l-12-93q-13-5-24.5-12T307-235l-87 36q-14 5-28 1t-22-17L96-344q-8-13-5-28t15-24l75-57q-1-7-1-13.5v-27q0-6.5 1-13.5l-75-57q-12-9-15-24t5-28l74-129q7-14 21.5-17.5T220-761l87 36q11-8 23-15t24-12l12-93q2-15 13-25t26-10h150q15 0 26 10t13 25l12 93q13 5 24.5 12t22.5 15l87-36q14-5 28-1t22 17l74 129q8 13 5 28t-15 24l-75 57q1 7 1 13.5v27q0 6.5-2 13.5l75 57q12 9 15 24t-5 28l-74 128q-8 13-22.5 17.5T738-199l-85-36q-11 8-23 15t-24 12l-12 93q-2 15-13 25t-26 10Zm-73-260q58 0 99-41t41-99q0-58-41-99t-99-41q-59 0-99.5 41T342-480q0 58 40.5 99t99.5 41Zm0-80q-25 0-42.5-17.5T422-480q0-25 17.5-42.5T482-540q25 0 42.5 17.5T542-480q0 25-17.5 42.5T482-420Zm-2-60Zm-40 320h79l14-106q31-8 57.5-23.5T639-327l99 41 39-68-86-65q5-14 7-29.5t2-31.5q0-16-2-31.5t-7-29.5l86-65-39-68-99 42q-22-23-48.5-38.5T533-694l-13-106h-79l-14 106q-31 8-57.5 23.5T321-633l-99-41-39 68 86 64q-5 15-7 30t-2 32q0 16 2 31t7 30l-86 65 39 68 99-42q22 23 48.5 38.5T427-266l13 106Z"/>
</svg>`);
this.votMenu.headerContainer.appendChild(this.votSettingsButton);
this.votTranslationLanguageSelect = ui.createVOTLanguageSelect({
fromTitle: localizationProvider.get("langs")[this.video.detectedLanguage],
fromDialogTitle: localizationProvider.get("videoLanguage"),
fromItems: genOptionsByOBJ(availableLangs, this.videoData.detectedLanguage),
fromOnSelectCB: async e => {
debug.log("[fromOnSelectCB] select from language", e.target.dataset.votValue);
this.videoData = await this.getVideoData();
this.setSelectMenuValues(e.target.dataset.votValue, this.videoData.responseLanguage || "ru");
},
toTitle: localizationProvider.get("langs")[this.video.responseLanguage],
toDialogTitle: localizationProvider.get("translationLanguage"),
toItems: genOptionsByOBJ(availableTTS, this.videoData.responseLanguage),
toOnSelectCB: async e => {
const newLang = e.target.dataset.votValue;
debug.log("[toOnSelectCB] select to language", newLang);
this.data.responseLanguage = this.translateToLang = newLang;
await votStorage.set("responseLanguage", this.data.responseLanguage);
debug.log("Response Language value changed. New value: ", this.data.responseLanguage);
this.videoData = await this.getVideoData();
this.setSelectMenuValues(this.videoData.detectedLanguage, this.data.responseLanguage);
},
});
this.votMenu.bodyContainer.appendChild(this.votTranslationLanguageSelect.container);
this.votSubtitlesSelect = ui.createVOTSelect(
localizationProvider.get("VOTSubtitlesDisabled"),
localizationProvider.get("VOTSubtitles"),
[{ label: localizationProvider.get("VOTSubtitlesDisabled"), value: "disabled", selected: true, disabled: false }],
{
onSelectCb: async e => { await this.changeSubtitlesLang(e.target.dataset.votValue); },
labelElement: ui.createVOTSelectLabel(localizationProvider.get("VOTSubtitles")),
}
);
this.votMenu.bodyContainer.appendChild(this.votSubtitlesSelect.container);
this.votVideoVolumeSlider = ui.createSlider(
html`${localizationProvider.get("VOTVolume")}: <strong>${this.getVideoVolume() * 100}%</strong>`,
this.getVideoVolume() * 100
);
this.votVideoVolumeSlider.container.hidden = this.data.showVideoSlider !== 1 || this.votButton.container.dataset.status !== "success";
this.votMenu.bodyContainer.appendChild(this.votVideoVolumeSlider.container);
this.votVideoTranslationVolumeSlider = ui.createSlider(
html`${localizationProvider.get("VOTVolumeTranslation")}: <strong>${this.data.defaultVolume || 100}%</strong>`,
this.data.defaultVolume || 100,
0,
this.data.audioBooster ? maxAudioVolume : 100
);
this.votVideoTranslationVolumeSlider.container.hidden = this.votButton.container.dataset.status !== "success";
this.votMenu.bodyContainer.appendChild(this.votVideoTranslationVolumeSlider.container);
this.votMenu.container.addEventListener("click", e => {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
});
}
async handleTranslationBtnClick() {
debug.log("[click translationBtn]", this.audioPlayer, this.audioPlayer.player);
if (this.audioPlayer.player.src || this.hls.url) {
debug.log("[click translationBtn] source exists", this.audioPlayer.player.src || this.hls.url);
this.stopTranslate();
return;
}
try {
debug.log("[click translationBtn] trying execute translation");
if (!this.videoData.videoId) throw new VOTLocalizedError("VOTNoVideoIDFound");
if (this.site.host === "vk" && this.site.additionalData === "clips") {
this.videoData = await this.getVideoData();
}
await this.translateExecutor(this.videoData.videoId);
} catch (err) {
console.error("[VOT]", err);
this.transformBtn("error", err?.name === "VOTLocalizedError" ? err.localizedMessage : err?.message);
}
}
initUIEvents() {
this.votButton.translateButton.addEventListener("click", this.handleTranslationBtnClick.bind(this));
this.votButton.pipButton.addEventListener("click", async () => {
if (this.video !== document.pictureInPictureElement) {
await this.video.requestPictureInPicture();
} else {
await document.exitPictureInPicture();
}
});
this.votButton.menuButton.addEventListener("click", () => {
this.votMenu.container.hidden = !this.votMenu.container.hidden;
});
this.votButton.container.addEventListener("mousedown", () => { this.dragging = true; });
this.container.addEventListener("mouseup", () => { this.dragging = false; });
this.container.addEventListener("mousemove", async e => {
if (this.dragging) {
e.preventDefault();
const percentX = (e.clientX / this.container.clientWidth) * 100;
const isBig = this.container.clientWidth > 550;
const side = percentX <= 44 ? "left" : (percentX >= 66 ? "right" : "default");
this.data.buttonPos = isBig ? (percentX <= 44 ? "left" : "right") : "default";
this.votButton.container.dataset.direction = this.data.buttonPos !== "default" && isBig ? "column" : "row";
this.votButton.container.dataset.position = this.data.buttonPos;
this.votMenu.container.dataset.position = this.data.buttonPos;
if (isBig) await votStorage.set("buttonPos", this.data.buttonPos);
}
});
this.votDownloadButton.addEventListener("click", async () => {
if (!this.downloadTranslationUrl) return;
if (!this.data.downloadWithName) return window.open(this.downloadTranslationUrl, "_blank").focus();
const votLoader = document.querySelector("#vot-loader-download");
const primaryColor = getComputedStyle(this.votMenu.container).getPropertyValue("--vot-primary-rgb");
const updateAnimation = ui.animateLoader(votLoader, primaryColor);
const res = await GM_fetch(this.downloadTranslationUrl);
const reader = res.body.getReader();
const contentLength = +res.headers.get("Content-Length");
let receivedLength = 0;
const chunks = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
receivedLength += value.length;
updateAnimation(Math.round((receivedLength / contentLength) * 100));
}
ui.afterAnimateLoader(votLoader, primaryColor);
const blob = new Blob(chunks);
const filename = clearFileName(this.videoData.title || this.videoData.videoId);
const arrayBuffer = await blob.arrayBuffer();
const writer = new ID3Writer(arrayBuffer);
writer.setFrame("TIT2", filename);
writer.addTag();
downloadBlob(writer.getBlob(), `${filename}.mp3`);
});
this.votDownloadSubtitlesButton.addEventListener("click", async () => {
const format = this.data.subtitlesDownloadFormat;
const subsContent = convertSubs(this.yandexSubtitles, format);
const blob = new Blob([format === "json" ? JSON.stringify(subsContent) : subsContent], { type: "text/plain" });
const filename = this.data.downloadWithName ? clearFileName(this.videoData.title || this.videoData.videoId) : `subtitles_${this.videoData.videoId}`;
downloadBlob(blob, `${filename}.${format}`);
});
this.votSettingsButton.addEventListener("click", () => {
this.votSettingsDialog.container.hidden = !this.votSettingsDialog.container.hidden;
if (document.fullscreenElement || document.webkitFullscreenElement) {
document.webkitExitFullscreen?.();
document.exitFullscreen?.();
}
});
this.votVideoVolumeSlider.input.addEventListener("input", e => {
const value = Number(e.target.value);
this.votVideoVolumeSlider.label.querySelector("strong").textContent = `${value}%`;
this.setVideoVolume(value / 100);
if (this.data.syncVolume) this.syncVolumeWrapper("video", value);
});
this.votVideoTranslationVolumeSlider.input.addEventListener("input", async e => {
const value = Number(e.target.value);
this.data.defaultVolume = value;
await votStorage.set("defaultVolume", value);
this.votVideoTranslationVolumeSlider.label.querySelector("strong").textContent = `${value}%`;
this.audioPlayer.player.volume = value / 100;
if (this.data.syncVolume) {
this.syncVolumeWrapper("translation", value);
if (["youtube", "googledrive"].includes(this.site.host) && this.site.additionalData !== "mobile") {
this.setVideoVolume(this.tempOriginalVolume / 100);
}
}
});
this.votAutoTranslateCheckbox.input.addEventListener("change", async e => {
this.data.autoTranslate = Number(e.target.checked);
await votStorage.set("autoTranslate", this.data.autoTranslate);
await this.autoTranslate();
debug.log("autoTranslate value changed. New value: ", this.data.autoTranslate);
});
this.votDontTranslateYourLangSelect.labelElement.addEventListener("change", async e => {
this.data.dontTranslateYourLang = Number(e.target.checked);
await votStorage.set("dontTranslateYourLang", this.data.dontTranslateYourLang);
debug.log("dontTranslateYourLang value changed. New value: ", this.data.dontTranslateYourLang);
});
const updateHotkey = async newKey => {
await votStorage.set("hotkeyButton", newKey);
this.data.hotkeyButton = newKey;
this.changehotkeyButton.textContent = createHotkeyText(newKey);
};
const keydownHandler = e => {
const newKey = e.code === "Escape" ? null : e.code;
updateHotkey(newKey);
document.removeEventListener("keydown", keydownHandler);
};
this.changehotkeyButton.addEventListener("click", () => {
this.changehotkeyButton.textContent = localizationProvider.get("VOTPressNewHotkey");
document.addEventListener("keydown", keydownHandler);
});
this.votAutoSetVolumeCheckbox.input.addEventListener("change", async e => {
this.data.autoSetVolumeYandexStyle = Number(e.target.checked);
await votStorage.set("autoSetVolumeYandexStyle", this.data.autoSetVolumeYandexStyle);
debug.log("autoSetVolumeYandexStyle value changed. New value: ", this.data.autoSetVolumeYandexStyle);
});
this.votAutoSetVolumeSlider.input.addEventListener("input", async e => {
const presetAutoVolume = Number(e.target.value);
this.data.autoVolume = (presetAutoVolume / 100).toFixed(2);
await votStorage.set("autoVolume", this.data.autoVolume);
this.votAutoSetVolumeSlider.label.querySelector("strong").textContent = `${presetAutoVolume}%`;
});
this.votShowVideoSliderCheckbox.input.addEventListener("change", async e => {
this.data.showVideoSlider = Number(e.target.checked);
await votStorage.set("showVideoSlider", this.data.showVideoSlider);
debug.log("showVideoSlider value changed. New value: ", this.data.showVideoSlider);
this.votVideoVolumeSlider.container.hidden = this.data.showVideoSlider !== 1 || this.votButton.container.dataset.status !== "success";
});
this.votAudioBoosterCheckbox.input.addEventListener("change", async e => {
this.data.audioBooster = Number(e.target.checked);
await votStorage.set("audioBooster", this.data.audioBooster);
debug.log("audioBooster value changed. New value: ", this.data.audioBooster);
const currentAudioVolume = this.votVideoTranslationVolumeSlider.input.value;
this.votVideoTranslationVolumeSlider.input.max = this.data.audioBooster ? maxAudioVolume : 100;
if (!this.data.audioBooster) {
this.votVideoTranslationVolumeSlider.input.value = Math.min(currentAudioVolume, 100);
this.votVideoTranslationVolumeSlider.input.dispatchEvent(new Event("input"));
}
});
this.votSyncVolumeCheckbox.input.addEventListener("change", async e => {
this.data.syncVolume = Number(e.target.checked);
await votStorage.set("syncVolume", this.data.syncVolume);
debug.log("syncVolume value changed. New value: ", this.data.syncVolume);
});
this.votDownloadWithNameCheckbox.input.addEventListener("change", async e => {
this.data.downloadWithName = Number(e.target.checked);
await votStorage.set("downloadWithName", this.data.downloadWithName);
debug.log("downloadWithName value changed. New value: ", this.data.downloadWithName);
});
this.votUseNewModelCheckbox.input.addEventListener("change", async e => {
this.data.useNewModel = Number(e.target.checked);
await votStorage.set("useNewModel", this.data.useNewModel);
debug.log("useNewModel value changed. New value: ", this.data.useNewModel);
this.stopTranslate();
});
this.votTranslationServiceSelect.labelElement.addEventListener("change", async e => {
this.data.translateAPIErrors = Number(e.target.checked);
await votStorage.set("translateAPIErrors", this.data.translateAPIErrors);
debug.log("translateAPIErrors value changed. New value: ", this.data.translateAPIErrors);
});
this.votSubtitlesDetails.container.addEventListener("click", () => {
this.votSubtitlesDialog = ui.createDialog(localizationProvider.get("VOTSubtitlesDesign"));
this.votSubtitlesDialog.container.classList.add("vot-dialog-temp");
this.votSubtitlesDialog.container.hidden = false;
this.votSubtitlesDialog.backdrop.onclick = this.votSubtitlesDialog.closeButton.onclick = () => {
this.votSubtitlesDialog.container.remove();
};
this.votSubtitlesHighlightWordsCheckbox = ui.createCheckbox(localizationProvider.get("VOTHighlightWords"), this.data.highlightWords);
this.votSubtitlesDialog.bodyContainer.appendChild(this.votSubtitlesHighlightWordsCheckbox.container);
this.votSubtitlesDownloadFormatSelect = ui.createVOTSelect(
this.data.subtitlesDownloadFormat.toUpperCase(),
localizationProvider.get("VOTSubtitlesDownloadFormat"),
genOptionsByOBJ(subtitlesFormats, this.data.subtitlesDownloadFormat),
{
onSelectCb: async e => {
this.data.subtitlesDownloadFormat = e.target.dataset.votValue;
await votStorage.set("subtitlesDownloadFormat", this.data.subtitlesDownloadFormat);
},
labelElement: ui.createVOTSelectLabel(localizationProvider.get("VOTSubtitlesDownloadFormat")),
}
);
this.votSubtitlesDialog.bodyContainer.appendChild(this.votSubtitlesDownloadFormatSelect.container);
this.votSubtitlesMaxLengthSlider = ui.createSlider(
html`${localizationProvider.get("VOTSubtitlesMaxLength")}: <strong>${this.data.subtitlesMaxLength}</strong>`,
this.data.subtitlesMaxLength,
50,
300
);
this.votSubtitlesDialog.bodyContainer.appendChild(this.votSubtitlesMaxLengthSlider.container);
this.votSubtitlesFontSizeSlider = ui.createSlider(
html`${localizationProvider.get("VOTSubtitlesFontSize")}: <strong>${this.data.subtitlesFontSize}</strong>`,
this.data.subtitlesFontSize,
8,
50
);
this.votSubtitlesDialog.bodyContainer.appendChild(this.votSubtitlesFontSizeSlider.container);
this.votSubtitlesOpacitySlider = ui.createSlider(
html`${localizationProvider.get("VOTSubtitlesOpacity")}: <strong>${this.data.subtitlesOpacity}</strong>`,
this.data.subtitlesOpacity,
0,
100
);
this.votSubtitlesDialog.bodyContainer.appendChild(this.votSubtitlesOpacitySlider.container);
this.votSubtitlesHighlightWordsCheckbox.input.addEventListener("change", async e => {
this.data.highlightWords = Number(e.target.checked);
await votStorage.set("highlightWords", this.data.highlightWords);
debug.log("highlightWords value changed. New value: ", this.data.highlightWords);
this.subtitlesWidget.setHighlightWords(this.data.highlightWords);
});
this.votSubtitlesMaxLengthSlider.input.addEventListener("input", async e => {
this.data.subtitlesMaxLength = Number(e.target.value);
await votStorage.set("subtitlesMaxLength", this.data.subtitlesMaxLength);
this.votSubtitlesMaxLengthSlider.label.querySelector("strong").textContent = `${this.data.subtitlesMaxLength}`;
this.subtitlesWidget.setMaxLength(this.data.subtitlesMaxLength);
});
this.votSubtitlesFontSizeSlider.input.addEventListener("input", async e => {
this.data.subtitlesFontSize = Number(e.target.value);
await votStorage.set("subtitlesFontSize", this.data.subtitlesFontSize);
this.votSubtitlesFontSizeSlider.label.querySelector("strong").textContent = `${this.data.subtitlesFontSize}`;
this.subtitlesWidget.setFontSize(this.data.subtitlesFontSize);
});
this.votSubtitlesOpacitySlider.input.addEventListener("input", async e => {
this.data.subtitlesOpacity = Number(e.target.value);
await votStorage.set("subtitlesOpacity", this.data.subtitlesOpacity);
this.votSubtitlesOpacitySlider.label.querySelector("strong").textContent = `${this.data.subtitlesOpacity}`;
this.subtitlesWidget.setOpacity(this.data.subtitlesOpacity);
});
document.documentElement.appendChild(this.votSubtitlesDialog.container);
});
this.votShowPiPButtonCheckbox.input.addEventListener("change", async e => {
this.data.showPiPButton = Number(e.target.checked);
await votStorage.set("showPiPButton", this.data.showPiPButton);
debug.log("showPiPButton value changed. New value: ", this.data.showPiPButton);
this.votButton.pipButton.hidden = !isPiPAvailable() || !this.data.showPiPButton;
this.votButton.separator2.hidden = !isPiPAvailable() || !this.data.showPiPButton;
});
this.votM3u8ProxyHostTextfield.input.addEventListener("change", async e => {
this.data.m3u8ProxyHost = e.target.value || m3u8ProxyHost;
await votStorage.set("m3u8ProxyHost", this.data.m3u8ProxyHost);
debug.log("m3u8ProxyHost value changed. New value: ", this.data.m3u8ProxyHost);
});
this.votProxyWorkerHostTextfield.input.addEventListener("change", async e => {
this.data.proxyWorkerHost = e.target.value || proxyWorkerHost;
await votStorage.set("proxyWorkerHost", this.data.proxyWorkerHost);
debug.log("proxyWorkerHost value changed. New value: ", this.data.proxyWorkerHost);
if (this.data.translateProxyEnabled) {
this.votClient.host = this.data.proxyWorkerHost;
}
});
this.votAudioProxyCheckbox.input.addEventListener("change", async e => {
this.data.audioProxy = Number(e.target.checked);
await votStorage.set("audioProxy", this.data.audioProxy);
debug.log("audioProxy value changed. New value: ", this.data.audioProxy);
});
this.votOnlyBypassMediaCSPCheckbox.input.addEventListener("change", async e => {
this.data.onlyBypassMediaCSP = Number(e.target.checked);
await votStorage.set("onlyBypassMediaCSP", this.data.onlyBypassMediaCSP);
debug.log("onlyBypassMediaCSP value changed. New value: ", this.data.onlyBypassMediaCSP);
this.stopTranslate();
this.createPlayer();
});
this.votNewAudioPlayerCheckbox.input.addEventListener("change", async e => {
const checked = e.target.checked;
this.data.newAudioPlayer = Number(checked);
await votStorage.set("newAudioPlayer", this.data.newAudioPlayer);
debug.log("newAudioPlayer value changed. New value: ", this.data.newAudioPlayer);
this.stopTranslate();
this.createPlayer();
this.votOnlyBypassMediaCSPCheckbox.input.disabled = this.votOnlyBypassMediaCSPCheckbox.container.hidden = !checked;
});
this.votUpdateLocaleFilesButton.addEventListener("click", async () => {
await votStorage.set("locale-hash", "");
await localizationProvider.update(true);
window.location.reload();
});
this.votResetSettingsButton.addEventListener("click", async () => {
localizationProvider.reset();
const keys = await votStorage.list();
for (const key of keys) {
if (!localizationProvider.gmValues.includes(key)) {
votStorage.syncDelete(key);
}
}
window.location.reload();
});
}
releaseExtraEvents() {
this.resizeObserver?.disconnect();
if (["youtube", "googledrive"].includes(this.site.host) && this.site.additionalData !== "mobile") {
this.syncVolumeObserver?.disconnect();
}
this.extraEvents?.forEach(({ element, event, handler }) => {
element.removeEventListener(event, handler);
});
}
initExtraEvents() {
this.extraEvents = [];
const addExtraEventListener = (element, event, handler) => {
this.extraEvents.push({ element, event, handler });
element.addEventListener(event, handler);
};
const addExtraEventListeners = (element, events, handler) => {
events.forEach(event => addExtraEventListener(element, event, handler));
};
this.resizeObserver = new ResizeObserver(entries => {
entries.forEach(e => {
this.votMenu.container.style.setProperty("--vot-container-height", `${e.contentRect.height}px`);
});
const isBigWidth = this.container.clientWidth > 550;
const pos = isBigWidth ? this.data.buttonPos : "default";
this.votButton.container.dataset.position = pos;
this.votMenu.container.dataset.position = pos;
this.votButton.container.dataset.direction = (pos !== "default" && isBigWidth) ? "column" : "row";
});
this.resizeObserver.observe(this.video);
this.votMenu.container.style.setProperty("--vot-container-height", `${this.video.getBoundingClientRect().height}px`);
if (["youtube", "googledrive"].includes(this.site.host) && this.site.additionalData !== "mobile") {
this.syncVolumeObserver = new MutationObserver(mutations => {
if (!this.audioPlayer.player.src || !this.data.syncVolume) return;
mutations.forEach(mutation => {
if (mutation.type === "attributes" && mutation.attributeName === "aria-valuenow") {
if (this.firstSyncVolume) {
this.firstSyncVolume = false;
return;
}
const videoVolume = this.isMuted() ? 0 : this.getVideoVolume() * 100;
const finalVolume = Math.round(videoVolume);
this.data.defaultVolume = finalVolume;
this.audioPlayer.player.volume = finalVolume / 100;
this.syncVolumeWrapper("video", finalVolume);
}
});
});
const ytpVolumePanel = document.querySelector(".ytp-volume-panel");
if (ytpVolumePanel) {
this.syncVolumeObserver.observe(ytpVolumePanel, { attributes: true, subtree: true });
}
}
document.addEventListener("click", event => {
const e = event.target;
const { container: button } = this.votButton;
const { container: menu } = this.votMenu;
const { container: settings } = this.votSettingsDialog;
const tempDialog = document.querySelector(".vot-dialog-temp");
const isButton = button.contains(e);
const isMenu = menu.contains(e);
const isVideo = this.container.contains(e);
const isSettings = settings.contains(e);
const isTempDialog = tempDialog?.contains(e) ?? false;
debug.log(`[document click] ${isButton} ${isMenu} ${isVideo} ${isSettings} ${isTempDialog}`);
if (!(!isButton && !isMenu && !isSettings && !isTempDialog) && !isVideo) this.logout(0);
if (!isButton && !isMenu && !isSettings && !isTempDialog) this.votMenu.container.hidden = true;
});
document.addEventListener("keydown", async event => {
const { code } = event;
const active = document.activeElement;
const isInput = ["input", "textarea"].includes(active.tagName.toLowerCase()) || active.isContentEditable;
if (!isInput && code === this.data.hotkeyButton) {
await this.handleTranslationBtnClick();
}
});
const eventContainer = this.site.eventSelector ? document.querySelector(this.site.eventSelector) : this.container;
if (eventContainer) addExtraEventListeners(eventContainer, ["mousemove", "mouseout"], this.resetTimer.bind(this));
["mousemove", "mouseout"].forEach(event => {
addExtraEventListener(this.votButton.container, event, this.changeOpacityOnEvent.bind(this));
addExtraEventListener(this.votMenu.container, event, this.changeOpacityOnEvent.bind(this));
});
addExtraEventListeners(document, ["touchstart", "touchmove", "touchend"], this.changeOpacityOnEvent.bind(this));
addExtraEventListener(this.votButton.container, "mousedown", e => e.stopImmediatePropagation());
addExtraEventListener(this.votMenu.container, "mousedown", e => e.stopImmediatePropagation());
if (this.site.host === "youtube") {
this.container.draggable = false;
}
if (this.site.host === "googledrive") {
this.container.style.height = "100%";
}
addExtraEventListener(this.video, "canplay", async () => {
if (this.site.host === "rutube" && this.video.src) return;
await this.setCanPlay();
});
addExtraEventListener(this.video, "emptied", async () => {
if (this.video.src && (await getVideoID(this.site, this.video)) === this.videoData.videoId) return;
debug.log("lipsync mode is emptied");
this.videoData = "";
this.stopTranslation();
});
if (!["rutube", "ok"].includes(this.site.host)) {
addExtraEventListener(this.video, "volumechange", () => this.syncVideoVolumeSlider());
}
if (this.site.host === "youtube" && !this.site.additionalData) {
addExtraEventListener(document, "yt-page-data-updated", async () => {
debug.log("yt-page-data-updated");
if (!window.location.pathname.includes("/shorts/")) return;
await this.setCanPlay();
});
}
}
```