diff --git a/chrome/player/modules/SubtitleTranslator.mjs b/chrome/player/modules/SubtitleTranslator.mjs
new file mode 100755
index 00000000..67b83f09
--- /dev/null
+++ b/chrome/player/modules/SubtitleTranslator.mjs
@@ -0,0 +1,143 @@
+/**
+ * SubtitleTranslator.mjs
+ * "Interactive" Version: Opens the specific API URL to force the CAPTCHA check.
+ */
+export class SubtitleTranslator {
+ constructor() {
+ this.apis = [
+ {
+ name: "gtx",
+ url: "https://translate.googleapis.com/translate_a/single",
+ params: { client: "gtx", dt: "t" },
+ batchSize: 8,
+ delay: 3000
+ },
+ {
+ name: "clients5",
+ url: "https://clients5.google.com/translate_a/t",
+ params: { client: "dict-chrome-ex" },
+ batchSize: 12,
+ delay: 3000
+ }
+ ];
+ this.currentApiIndex = 0;
+ }
+
+ async translateStream(cues, targetLang, onProgress) {
+ if (!cues || cues.length === 0) throw new Error("No cues found.");
+
+ let api = this.apis[this.currentApiIndex];
+
+ // FIX: Use double newline. This forces Google to treat cues as separate paragraphs.
+ // Single newlines cause Japanese/Chinese lines to merge during translation.
+ let delimiter = "\n\n";
+
+ for (let i = 0; i < cues.length; i += api.batchSize) {
+ api = this.apis[this.currentApiIndex];
+
+ const batch = cues.slice(i, i + api.batchSize);
+ const texts = batch.map(c => c.text.replace(/\r?\n|\r/g, " ").trim());
+
+ try {
+ const translatedTexts = await this.fetchWithRetry(texts, targetLang, delimiter, api);
+
+ const segmentCues = [];
+ batch.forEach((cue, index) => {
+ // Safety check: ensure we have a translation for this index
+ if (translatedTexts[index]) {
+ segmentCues.push({
+ startTime: cue.startTime,
+ endTime: cue.endTime,
+ text: translatedTexts[index]
+ });
+ }
+ });
+
+ if (onProgress) onProgress(segmentCues, null);
+ await this.delay(api.delay);
+
+ } catch (e) {
+ console.warn(`[FastStream] API ${api.name} error:`, e);
+
+ // CHECK FOR BAN
+ if (e.message.includes("429") || e.message.includes("Rate Limit")) {
+ if (onProgress) onProgress([], "⚠️ Google Ban Detected. Opening Unblocker...");
+
+ // CONSTRUCT THE EXACT URL THAT IS BLOCKED
+ const testUrl = `https://translate.googleapis.com/translate_a/single?client=gtx&dt=t&sl=auto&tl=${targetLang}&q=Hello%20World`;
+
+ // 1. Open the specific API URL
+ window.open(testUrl, "_blank");
+
+ // 2. Pause and ask user
+ const resolved = confirm(
+ "Google has temporarily blocked the translation signal.\n\n" +
+ "1. A new tab has opened with a weird code page.\n" +
+ "2. IF you see a CAPTCHA ('I am not a robot'), solve it.\n" +
+ "3. IF you just see text code (JSON), close the tab.\n\n" +
+ "Click OK when you are done to resume."
+ );
+
+ if (resolved) {
+ if (onProgress) onProgress([], "Resuming...");
+ // Wait 5 seconds for the unblock to register
+ await this.delay(5000);
+ i -= api.batchSize; // Retry batch
+ continue;
+ } else {
+ throw new Error("Translation stopped by user.");
+ }
+ }
+
+ // Normal rotation for other errors
+ this.currentApiIndex = (this.currentApiIndex + 1) % this.apis.length;
+ api = this.apis[this.currentApiIndex];
+ i -= api.batchSize;
+ await this.delay(2000);
+ }
+ }
+ }
+
+ async fetchWithRetry(texts, targetLang, delimiter, api) {
+ const combinedText = texts.join(delimiter);
+ const params = new URLSearchParams({
+ ...api.params,
+ sl: "auto",
+ tl: targetLang,
+ q: combinedText
+ });
+
+ const response = await fetch(`${api.url}?${params.toString()}`);
+
+ // If 429 or redirected to the "Sorry" page
+ if (response.status === 429 || response.url.includes("google.com/sorry")) {
+ throw new Error("429 Rate Limit");
+ }
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}`);
+ }
+
+ const data = await response.json();
+ let fullText = "";
+
+ if (api.name === "clients5") {
+ if (Array.isArray(data)) {
+ if (typeof data[0] === 'string') fullText = data[0];
+ else data.forEach(item => { if (item && item[0]) fullText += item[0]; });
+ }
+ } else {
+ if (data && data[0]) {
+ data[0].forEach(segment => {
+ if (segment[0]) fullText += segment[0];
+ });
+ }
+ }
+
+ // Split by the double newline to get original segments back
+ return fullText.split(delimiter).map(s => s.trim());
+ }
+
+ delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+}
\ No newline at end of file
diff --git a/chrome/player/options/OptionsStore.mjs b/chrome/player/options/OptionsStore.mjs
index 7ddf6a37..d96a9ed1 100644
--- a/chrome/player/options/OptionsStore.mjs
+++ b/chrome/player/options/OptionsStore.mjs
@@ -15,11 +15,18 @@ export class OptionsStore {
/**
* Initialize the store by loading options and wiring external updates.
*/
- static async init() {
- if (this.#initialized) return this.#options;
- this.#initialized = true;
- this.#options = await Utils.getOptionsFromStorage();
+ static async init() {
+ // 1. If we have options, return them immediately
+ if (this.#initialized && this.#options) return this.#options;
+
+ // 2. Load the options first
+ const options = await Utils.getOptionsFromStorage();
+ this.#options = options;
this.#wireExternalUpdates();
+
+ // 3. ONLY NOW mark as initialized
+ this.#initialized = true;
+
return this.#options;
}
diff --git a/chrome/player/options/defaults/DefaultLanguages.mjs b/chrome/player/options/defaults/DefaultLanguages.mjs
new file mode 100644
index 00000000..c110c460
--- /dev/null
+++ b/chrome/player/options/defaults/DefaultLanguages.mjs
@@ -0,0 +1,23 @@
+export const DefaultLanguages = {
+ "af": "Afrikaans", "sq": "Albanian", "am": "Amharic", "ar": "Arabic", "hy": "Armenian",
+ "az": "Azerbaijani", "eu": "Basque", "be": "Belarusian", "bn": "Bengali", "bs": "Bosnian",
+ "bg": "Bulgarian", "ca": "Catalan", "ceb": "Cebuano", "ny": "Chichewa", "zh-CN": "Chinese (Simplified)",
+ "zh-TW": "Chinese (Traditional)", "co": "Corsican", "hr": "Croatian", "cs": "Czech", "da": "Danish",
+ "nl": "Dutch", "en": "English", "eo": "Esperanto", "et": "Estonian", "tl": "Filipino",
+ "fi": "Finnish", "fr": "French", "fy": "Frisian", "gl": "Galician", "ka": "Georgian",
+ "de": "German", "el": "Greek", "gu": "Gujarati", "ht": "Haitian Creole", "ha": "Hausa",
+ "haw": "Hawaiian", "iw": "Hebrew", "hi": "Hindi", "hmn": "Hmong", "hu": "Hungarian",
+ "is": "Icelandic", "ig": "Igbo", "id": "Indonesian", "ga": "Irish", "it": "Italian",
+ "ja": "Japanese", "jw": "Javanese", "kn": "Kannada", "kk": "Kazakh", "km": "Khmer",
+ "ko": "Korean", "ku": "Kurdish (Kurmanji)", "ky": "Kyrgyz", "lo": "Lao", "la": "Latin",
+ "lv": "Latvian", "lt": "Lithuanian", "lb": "Luxembourgish", "mk": "Macedonian", "mg": "Malagasy",
+ "ms": "Malay", "ml": "Malayalam", "mt": "Maltese", "mi": "Maori", "mr": "Marathi",
+ "mn": "Mongolian", "my": "Myanmar (Burmese)", "ne": "Nepali", "no": "Norwegian", "ps": "Pashto",
+ "fa": "Persian", "pl": "Polish", "pt": "Portuguese", "pa": "Punjabi", "ro": "Romanian",
+ "ru": "Russian", "sm": "Samoan", "gd": "Scots Gaelic", "sr": "Serbian", "st": "Sesotho",
+ "sn": "Shona", "sd": "Sindhi", "si": "Sinhala", "sk": "Slovak", "sl": "Slovenian",
+ "so": "Somali", "es": "Spanish", "su": "Sundanese", "sw": "Swahili", "sv": "Swedish",
+ "tg": "Tajik", "ta": "Tamil", "te": "Telugu", "th": "Thai", "tr": "Turkish",
+ "uk": "Ukrainian", "ur": "Urdu", "uz": "Uzbek", "vi": "Vietnamese", "cy": "Welsh",
+ "xh": "Xhosa", "yi": "Yiddish", "yo": "Yoruba", "zu": "Zulu"
+};
\ No newline at end of file
diff --git a/chrome/player/options/defaults/DefaultOptions.mjs b/chrome/player/options/defaults/DefaultOptions.mjs
index d2f993a6..416f4f6b 100644
--- a/chrome/player/options/defaults/DefaultOptions.mjs
+++ b/chrome/player/options/defaults/DefaultOptions.mjs
@@ -47,4 +47,6 @@ export const DefaultOptions = {
videoDelay: 0,
maximumDownloaders: 6,
youtubePlayerID: '',
+ autoTranslate: false, // Default to off for Store compliance
+ defaultTranslateLanguage: 'en', // Default language code
};
diff --git a/chrome/player/options/index.html b/chrome/player/options/index.html
index 6624b66e..9f764c93 100644
--- a/chrome/player/options/index.html
+++ b/chrome/player/options/index.html
@@ -203,7 +203,23 @@
+
+
+
+
+
+
Enable Auto-Translate
+
+
+
+
+
+
Default Translation Language
+
+
+
+
diff --git a/chrome/player/options/options.mjs b/chrome/player/options/options.mjs
index a480213e..677d90f5 100644
--- a/chrome/player/options/options.mjs
+++ b/chrome/player/options/options.mjs
@@ -17,6 +17,7 @@ import {DaltonizerTypes} from './defaults/DaltonizerTypes.mjs';
import {DefaultToolSettings} from './defaults/ToolSettings.mjs';
import {DefaultQualities} from './defaults/DefaultQualities.mjs';
import {ColorThemes} from './defaults/ColorThemes.mjs';
+import {DefaultLanguages} from './defaults/DefaultLanguages.mjs';
let Options = {};
const analyzeVideos = document.getElementById('analyzevideos');
@@ -53,6 +54,8 @@ const optionsSearchBar = document.getElementById('searchbar');
const optionsResetButton = document.getElementById('resetsearch');
// const ytclient = document.getElementById('ytclient');
const maxdownloaders = document.getElementById('maxdownloaders');
+const autoTranslate = document.getElementById('autotranslate');
+const defaultTranslateLang = document.getElementById('defaulttranslatelang');
autoEnableURLSInput.setAttribute('autocapitalize', 'off');
autoEnableURLSInput.setAttribute('autocomplete', 'off');
autoEnableURLSInput.setAttribute('autocorrect', 'off');
@@ -159,6 +162,14 @@ async function loadOptions(newOptions) {
document.getElementById('dev').style.display = '';
}
initsearch();
+
+ autoTranslate.checked = !!Options.autoTranslate;
+// Re-run builder to ensure correct selection is shown
+buildLanguageMenu();
+
+// Disable if feature is off
+const langSelect = defaultTranslateLang.querySelector('select');
+if (langSelect) langSelect.disabled = !Options.autoTranslate;
}
function createSelectMenu(container, options, selected, localPrefix, callback) {
@@ -240,6 +251,34 @@ createSelectMenu(qualityMenu, Object.values(DefaultQualities), Options.defaultQu
// optionChanged();
// });
+// Custom builder for Language Dropdown
+function buildLanguageMenu() {
+ defaultTranslateLang.replaceChildren(); // Clear existing
+ const select = document.createElement('select');
+
+ // Sort languages alphabetically by Name, not Code
+ const sortedLangs = Object.entries(DefaultLanguages).sort((a, b) => a[1].localeCompare(b[1]));
+
+ for (const [code, name] of sortedLangs) {
+ const option = document.createElement('option');
+ option.value = code;
+ option.textContent = name;
+ if (code === Options.defaultTranslateLanguage) {
+ option.selected = true;
+ }
+ select.appendChild(option);
+ }
+
+ select.addEventListener('change', (e) => {
+ Options.defaultTranslateLanguage = e.target.value;
+ optionChanged();
+ });
+
+ defaultTranslateLang.appendChild(select);
+}
+// Run it immediately
+buildLanguageMenu();
+
document.querySelectorAll('.option').forEach((option) => {
option.addEventListener('click', (e) => {
if (e.target.tagName !== 'INPUT') {
@@ -675,3 +714,11 @@ if (EnvUtils.isExtension()) {
// SPLICER:NO_UPDATE_CHECKER:REMOVE_END
}
+autoTranslate.addEventListener('change', () => {
+ Options.autoTranslate = autoTranslate.checked;
+
+ const select = defaultTranslateLang.querySelector('select');
+ if (select) select.disabled = !Options.autoTranslate;
+
+ optionChanged();
+});
\ No newline at end of file
diff --git a/chrome/player/ui/subtitles/SubtitlesManager.mjs b/chrome/player/ui/subtitles/SubtitlesManager.mjs
index 9fece71f..47d3795a 100644
--- a/chrome/player/ui/subtitles/SubtitlesManager.mjs
+++ b/chrome/player/ui/subtitles/SubtitlesManager.mjs
@@ -7,317 +7,372 @@ import {RequestUtils} from '../../utils/RequestUtils.mjs';
import {SubtitleUtils} from '../../utils/SubtitleUtils.mjs';
import {Utils} from '../../utils/Utils.mjs';
import {WebUtils} from '../../utils/WebUtils.mjs';
+import {OptionsStore} from '../../options/OptionsStore.mjs';
+import {DefaultLanguages} from '../../options/defaults/DefaultLanguages.mjs';
import {DOMElements} from '../DOMElements.mjs';
import {OpenSubtitlesSearch, OpenSubtitlesSearchEvents} from './OpenSubtitlesSearch.mjs';
import {SubtitlesSettingsManager, SubtitlesSettingsManagerEvents} from './SubtitlesSettingsManager.mjs';
import {SubtitleSyncer} from './SubtitleSyncer.mjs';
+// NOTE: Removed top-level SubtitleTranslator import to prevent player crash if file missing
+
export class SubtitlesManager extends EventEmitter {
constructor(client) {
super();
this.client = client;
-
this.tracks = [];
this.activeTracks = [];
this.isTestSubtitleActive = false;
-
this.subtitleTrackListElements = [];
this.subtitleTrackDisplayElements = [];
-
+
+ // Initialize the store so we listen for changes from the Options Page!
+ OptionsStore.init();
+
this.settingsManager = new SubtitlesSettingsManager();
this.settingsManager.on(SubtitlesSettingsManagerEvents.SETTINGS_CHANGED, this.onSettingsChanged.bind(this));
this.settingsManager.loadSettings();
-
this.openSubtitlesSearch = new OpenSubtitlesSearch(client.version);
this.openSubtitlesSearch.on(OpenSubtitlesSearchEvents.TRACK_DOWNLOADED, this.onSubtitleTrackDownloaded.bind(this));
-
this.subtitleSyncer = new SubtitleSyncer(client);
-
+
this.setupUI();
}
- loadTrackAndActivateBest(subtitleTrack, autoset = false) {
- const returnedTrack = this.addTrack(subtitleTrack);
- if (returnedTrack !== subtitleTrack) {
- return returnedTrack;
- }
+ loadTrackAndActivateBest(t,a=false){const r=this.addTrack(t);if(r!==t)return r;const d=this.settingsManager.getSettings().defaultLanguage;if(a&&this.activeTracks.length<=1&&this.client.options.autoEnableBestSubtitles){if(Localize.getLanguageMatchLevel(t.language,d)>0){const e=this.activeTracks.find(k=>k.language&&t.language&&k.language.substring(0,d.length)===d);if(!e)this.activateTrack(t);else if(e.label&&e.label.toLowerCase().includes('auto')&&t.label&&!t.label.toLowerCase().includes('auto')){this.deactivateTrack(e);this.activateTrack(t)}}}return r}
+ addTrack(t){const e=this.tracks.find(k=>k.equals(t));if(e)return e;this.tracks.push(t);this.updateTrackList();this.client.interfaceController.showControlBar();this.client.interfaceController.queueControlsHide(1e3);return t}
+ activateTrack(t){if(this.tracks.indexOf(t)===-1)return;if(this.activeTracks.indexOf(t)===-1){this.activeTracks.push(t);this.updateTrackList()}}
+ deactivateTrack(t){const i=this.activeTracks.indexOf(t);if(i!==-1){this.activeTracks.splice(i,1);this.updateTrackList()}}
+ toggleSubtitles(){if(this.activeTracks.length===0){if(this.lastActiveTracks){this.lastActiveTracks.forEach(t=>this.activateTrack(t));this.lastActiveTracks=null}else this.activateTrack(this.tracks[0])}else{this.lastActiveTracks=this.activeTracks.slice();this.activeTracks.length=0;this.updateTrackList()}}
+ clearTracks(){this.tracks.length=0;this.activeTracks.length=0;this.updateTrackList();this.subtitleSyncer.stop()}
+ removeTrack(t){let i=this.tracks.indexOf(t);if(i!==-1)this.tracks.splice(i,1);i=this.activeTracks.indexOf(t);if(i!==-1)this.activeTracks.splice(i,1);this.updateTrackList();this.subtitleSyncer.toggleTrack(t,true)}
+ onSettingsChanged(s){this.openSubtitlesSearch.setLanguageInputValue(s.defaultLanguage);this.refreshSubtitleStyles();this.renderSubtitles()}
+ onSubtitleTrackDownloaded(t){this.activateTrack(this.addTrack(t))}
+ onCaptionsButtonInteract(e){if(e.shiftKey){this.openSubtitlesSearch.toggleUI();e.stopPropagation();return}if(!this.isOpen())this.openUI();else this.closeUI();e.stopPropagation()}
+ closeUI(){if(DOMElements.subtitlesMenu.style.display==='none')return false;DOMElements.subtitlesMenu.style.display='none';WebUtils.setLabels(DOMElements.subtitles,Localize.getMessage('player_subtitlesmenu_open_label'));return true}
+ openUI(){this.emit('open',{target:DOMElements.subtitles});DOMElements.subtitlesMenu.style.display='';WebUtils.setLabels(DOMElements.subtitles,Localize.getMessage('player_subtitlesmenu_close_label'))}
+ isOpen(){return DOMElements.subtitlesMenu.style.display!=='none'}
- const defLang = this.settingsManager.getSettings().defaultLanguage;
- if (autoset && this.activeTracks.length <= 1 && this.client.options.autoEnableBestSubtitles) {
- if (Localize.getLanguageMatchLevel(subtitleTrack.language, defLang) > 0) {
- // check if active tracks exist with same language
- const existingActive = this.activeTracks.find((t) => {
- return t.language && subtitleTrack.language && t.language.substring(0, defLang.length) === defLang;
+ setupUI() {
+ try {
+ DOMElements.subtitles.addEventListener('click', this.onCaptionsButtonInteract.bind(this));
+ DOMElements.subtitles.tabIndex = 0;
+ DOMElements.subtitles.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter') this.onCaptionsButtonInteract(e);
});
- if (!existingActive) {
- this.activateTrack(subtitleTrack);
- } else {
- // Check if existing has "auto" in label, if new one doesn't, prefer new one
- if (existingActive.label && existingActive.label.toLowerCase().includes('auto') &&
- subtitleTrack.label && !subtitleTrack.label.toLowerCase().includes('auto')) {
- this.deactivateTrack(existingActive);
- this.activateTrack(subtitleTrack);
- }
- }
- }
- }
-
- return returnedTrack;
- }
-
- addTrack(track) {
- const existing = this.tracks.find((t) => t.equals(track));
- if (existing) {
- return existing;
- }
-
- this.tracks.push(track);
-
- this.updateTrackList();
- this.client.interfaceController.showControlBar();
- this.client.interfaceController.queueControlsHide(1000);
-
- return track;
- }
-
- activateTrack(track) {
- if (this.tracks.indexOf(track) === -1) {
- console.error('Cannot activate track that is not loaded', track);
- return;
- }
-
- if (this.activeTracks.indexOf(track) === -1) {
- this.activeTracks.push(track);
- this.updateTrackList();
- }
- }
- deactivateTrack(track) {
- const ind = this.activeTracks.indexOf(track);
- if (ind !== -1) {
- this.activeTracks.splice(ind, 1);
- this.updateTrackList();
- }
- }
-
- toggleSubtitles() {
- if (this.activeTracks.length === 0) {
- if (this.lastActiveTracks) {
- this.lastActiveTracks.forEach((track) => {
- this.activateTrack(track);
+ DOMElements.subtitlesOptionsTestButton.addEventListener('click', (e) => {
+ this.isTestSubtitleActive = !this.isTestSubtitleActive;
+ if (this.isTestSubtitleActive) {
+ DOMElements.subtitlesOptionsTestButton.textContent = Localize.getMessage('player_subtitlesmenu_testbtn_stop');
+ DOMElements.playerContainer.style.backgroundImage = 'linear-gradient(to right, black, white)';
+ } else {
+ DOMElements.subtitlesOptionsTestButton.textContent = Localize.getMessage('player_subtitlesmenu_testbtn');
+ DOMElements.playerContainer.style.backgroundImage = '';
+ }
+ this.renderSubtitles();
});
- this.lastActiveTracks = null;
- } else {
- this.activateTrack(this.tracks[0]);
- }
- } else {
- this.lastActiveTracks = this.activeTracks.slice();
- this.activeTracks.length = 0;
- this.updateTrackList();
- }
- }
-
- clearTracks() {
- this.tracks.length = 0;
- this.activeTracks.length = 0;
- this.updateTrackList();
- this.subtitleSyncer.stop();
- }
-
- removeTrack(track) {
- let ind = this.tracks.indexOf(track);
- if (ind !== -1) this.tracks.splice(ind, 1);
- ind = this.activeTracks.indexOf(track);
- if (ind !== -1) this.activeTracks.splice(ind, 1);
- this.updateTrackList();
- this.subtitleSyncer.toggleTrack(track, true);
- }
-
- onSettingsChanged(settings) {
- this.openSubtitlesSearch.setLanguageInputValue(settings.defaultLanguage);
- this.refreshSubtitleStyles();
- this.renderSubtitles();
- }
-
- onSubtitleTrackDownloaded(track) {
- this.activateTrack(this.addTrack(track));
- }
-
- onCaptionsButtonInteract(e) {
- if (e.shiftKey) {
- this.openSubtitlesSearch.toggleUI();
- e.stopPropagation();
- return;
- }
-
- if (!this.isOpen()) {
- this.openUI();
- } else {
- this.closeUI();
- }
- e.stopPropagation();
- }
-
- closeUI() {
- if (DOMElements.subtitlesMenu.style.display === 'none') {
- return false;
- }
- DOMElements.subtitlesMenu.style.display = 'none';
- WebUtils.setLabels(DOMElements.subtitles, Localize.getMessage('player_subtitlesmenu_open_label'));
- return true;
- }
-
- openUI() {
- this.emit('open', {
- target: DOMElements.subtitles,
- });
- DOMElements.subtitlesMenu.style.display = '';
-
- WebUtils.setLabels(DOMElements.subtitles, Localize.getMessage('player_subtitlesmenu_close_label'));
- }
-
- isOpen() {
- return DOMElements.subtitlesMenu.style.display !== 'none';
- }
-
- setupUI() {
- DOMElements.subtitles.addEventListener('click', this.onCaptionsButtonInteract.bind(this));
- DOMElements.subtitles.tabIndex = 0;
-
- DOMElements.subtitles.addEventListener('keydown', (e) => {
- if (e.key === 'Enter') {
- this.onCaptionsButtonInteract(e);
- }
- });
-
- DOMElements.subtitlesOptionsTestButton.addEventListener('click', (e) => {
- this.isTestSubtitleActive = !this.isTestSubtitleActive;
- if (this.isTestSubtitleActive) {
- DOMElements.subtitlesOptionsTestButton.textContent = Localize.getMessage('player_subtitlesmenu_testbtn_stop');
- DOMElements.playerContainer.style.backgroundImage = 'linear-gradient(to right, black, white)';
- } else {
- DOMElements.subtitlesOptionsTestButton.textContent = Localize.getMessage('player_subtitlesmenu_testbtn');
- DOMElements.playerContainer.style.backgroundImage = '';
- }
-
- this.renderSubtitles();
- });
- WebUtils.setupTabIndex(DOMElements.subtitlesOptionsTestButton);
-
- const filechooser = document.createElement('input');
- filechooser.type = 'file';
- filechooser.style.display = 'none';
- filechooser.accept = '.vtt, .srt';
- filechooser.ariaHidden = true;
- filechooser.ariaLabel = 'Upload subtitle file';
-
- filechooser.addEventListener('change', () => {
- const files = filechooser.files;
- if (!files || !files[0]) return;
- const file = files[0];
- const name = file.name;
- // var ext = name.substring(name.length - 4);
-
- const reader = new FileReader();
- reader.onload = () => {
- const dt = reader.result;
- const track = new SubtitleTrack(name, null);
- track.loadText(dt);
-
- this.addTrack(track);
- };
- reader.readAsText(file);
- });
- DOMElements.playerContainer.appendChild(filechooser);
-
- const filebutton = document.createElement('div');
- filebutton.classList.add('subtitle-menu-option');
- WebUtils.setupTabIndex(filebutton);
- filebutton.textContent = Localize.getMessage('player_subtitlesmenu_uploadbtn');
-
- filebutton.addEventListener('click', (e) => {
- filechooser.click();
- });
- DOMElements.subtitlesView.appendChild(filebutton);
-
- const urlbutton = document.createElement('div');
- urlbutton.classList.add('subtitle-menu-option');
- urlbutton.textContent = Localize.getMessage('player_subtitlesmenu_urlbtn');
- WebUtils.setupTabIndex(urlbutton);
- urlbutton.addEventListener('click', async (e) => {
- const url = await AlertPolyfill.prompt(Localize.getMessage('player_subtitlesmenu_urlprompt'), '', undefined, 'url');
-
- if (url) {
- AlertPolyfill.toast('info', Localize.getMessage('player_subtitles_addtrack_downloading'));
- RequestUtils.requestSimple(url, (err, req, body) => {
- if (!err && body) {
- try {
- const track = new SubtitleTrack('URL Track', null);
- track.loadText(body);
-
- this.addTrack(track);
-
- AlertPolyfill.toast('success', Localize.getMessage('player_subtitles_addtrack_success'));
- } catch (e) {
- AlertPolyfill.toast('error', Localize.getMessage('player_subtitles_addtrack_error'), e?.message);
+ WebUtils.setupTabIndex(DOMElements.subtitlesOptionsTestButton);
+
+ const filechooser = document.createElement('input');
+ filechooser.type = 'file';
+ filechooser.style.display = 'none';
+ filechooser.accept = '.vtt, .srt';
+ filechooser.ariaHidden = true;
+ filechooser.ariaLabel = 'Upload subtitle file';
+ filechooser.addEventListener('change', () => {
+ const files = filechooser.files;
+ if (!files || !files[0]) return;
+ const file = files[0];
+ const name = file.name;
+ const reader = new FileReader();
+ reader.onload = () => {
+ const dt = reader.result;
+ const track = new SubtitleTrack(name, null);
+ track.loadText(dt);
+ this.addTrack(track);
+ };
+ reader.readAsText(file);
+ });
+ DOMElements.playerContainer.appendChild(filechooser);
+
+ const filebutton = document.createElement('div');
+ filebutton.classList.add('subtitle-menu-option');
+ WebUtils.setupTabIndex(filebutton);
+ filebutton.textContent = Localize.getMessage('player_subtitlesmenu_uploadbtn');
+ filebutton.addEventListener('click', () => filechooser.click());
+ DOMElements.subtitlesView.appendChild(filebutton);
+
+ const urlbutton = document.createElement('div');
+ urlbutton.classList.add('subtitle-menu-option');
+ urlbutton.textContent = Localize.getMessage('player_subtitlesmenu_urlbtn');
+ WebUtils.setupTabIndex(urlbutton);
+ urlbutton.addEventListener('click', async () => {
+ const url = await AlertPolyfill.prompt(Localize.getMessage('player_subtitlesmenu_urlprompt'), '', undefined, 'url');
+ if (url) {
+ AlertPolyfill.toast('info', Localize.getMessage('player_subtitles_addtrack_downloading'));
+ RequestUtils.requestSimple(url, (err, req, body) => {
+ if (!err && body) {
+ try {
+ const track = new SubtitleTrack('URL Track', null);
+ track.loadText(body);
+ this.addTrack(track);
+ AlertPolyfill.toast('success', Localize.getMessage('player_subtitles_addtrack_success'));
+ } catch (e) {
+ AlertPolyfill.toast('error', Localize.getMessage('player_subtitles_addtrack_error'), e?.message);
+ }
+ } else {
+ AlertPolyfill.toast('error', Localize.getMessage('player_subtitles_addtrack_error'), err?.message);
+ }
+ });
}
- } else {
- AlertPolyfill.toast('error', Localize.getMessage('player_subtitles_addtrack_error'), err?.message);
- }
});
- }
- });
-
- DOMElements.subtitlesView.appendChild(urlbutton);
-
- const internetbutton = document.createElement('div');
- internetbutton.textContent = Localize.getMessage('player_subtitlesmenu_searchbtn');
- internetbutton.classList.add('subtitle-menu-option');
- internetbutton.classList.add('disable-when-mini');
- WebUtils.setupTabIndex(internetbutton);
- internetbutton.addEventListener('click', (e) => {
- this.openSubtitlesSearch.toggleUI();
- });
- DOMElements.subtitlesView.appendChild(internetbutton);
-
- const clearbutton = document.createElement('div');
- clearbutton.textContent = Localize.getMessage('player_subtitlesmenu_clearbtn');
- WebUtils.setupTabIndex(clearbutton);
- clearbutton.classList.add('subtitle-menu-option');
-
- clearbutton.addEventListener('click', (e) => {
- this.clearTracks();
- });
- DOMElements.subtitlesView.appendChild(clearbutton);
-
- const optionsbutton = document.createElement('div');
- optionsbutton.classList.add('subtitle-menu-option');
- optionsbutton.textContent = Localize.getMessage('player_subtitlesmenu_settingsbtn');
- WebUtils.setupTabIndex(optionsbutton);
-
- optionsbutton.addEventListener('click', (e) => {
- this.settingsManager.openUI();
- });
-
- WebUtils.setupTabIndex(DOMElements.subtitlesOptionsBackButton);
-
- DOMElements.subtitlesView.appendChild(optionsbutton);
-
- DOMElements.subtitlesMenu.addEventListener('click', (e) => {
- e.stopPropagation();
- });
+ DOMElements.subtitlesView.appendChild(urlbutton);
+
+ const internetbutton = document.createElement('div');
+ internetbutton.textContent = Localize.getMessage('player_subtitlesmenu_searchbtn');
+ internetbutton.classList.add('subtitle-menu-option');
+ internetbutton.classList.add('disable-when-mini');
+ WebUtils.setupTabIndex(internetbutton);
+ internetbutton.addEventListener('click', () => this.openSubtitlesSearch.toggleUI());
+ DOMElements.subtitlesView.appendChild(internetbutton);
+
+ // --- TRANSLATOR UI WITH COMPACT LAYOUT (No Scroll) ---
+ try {
+ const translateContainer = document.createElement('div');
+ translateContainer.className = 'subtitle-menu-option';
+
+ // STYLE: Basic container setup
+ translateContainer.style.justifyContent = 'space-between';
+ translateContainer.style.alignItems = 'center';
+ translateContainer.style.padding = '8px 8px';
+ translateContainer.style.cursor = 'default';
+ translateContainer.style.borderBottom = '1px solid rgba(255,255,255,0.1)';
+
+ // LABEL
+ const translateLabel = document.createElement('span');
+ translateLabel.textContent = "Trans:";
+ translateLabel.style.marginRight = '5px';
+ translateLabel.style.fontSize = '13px';
+ translateLabel.style.color = '#ccc';
+ translateContainer.appendChild(translateLabel);
+
+ // DROPDOWN
+ const langSelect = document.createElement('select');
+ langSelect.style.flex = '1';
+ langSelect.style.width = '0';
+ langSelect.style.marginRight = '5px';
+ langSelect.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
+ langSelect.style.color = 'white';
+ langSelect.style.border = '1px solid #555';
+ langSelect.style.borderRadius = '3px';
+ langSelect.style.padding = '2px 2px';
+ langSelect.style.fontSize = '12px';
+ langSelect.style.outline = 'none';
+
+ // Populate Dropdown
+ Object.keys(DefaultLanguages).forEach(code => {
+ const option = document.createElement('option');
+ option.value = code;
+ option.textContent = DefaultLanguages[code];
+ option.style.backgroundColor = '#222';
+ option.style.color = 'white';
+ langSelect.appendChild(option);
+ });
+
+ // LOGIC: Handle Visibility & Language Selection
+ const updateUIFromOptions = (options) => {
+ // 1. Show/Hide based on setting
+ translateContainer.style.display = options.autoTranslate ? 'flex' : 'none';
+
+ // 2. Select the saved language (if valid)
+ if (options.defaultTranslateLanguage && DefaultLanguages[options.defaultTranslateLanguage]) {
+ langSelect.value = options.defaultTranslateLanguage;
+ } else {
+ langSelect.value = 'en'; // Default fallback
+ }
+ };
+
+ // 1. Initial Render (Might be defaults)
+ updateUIFromOptions(OptionsStore.get());
+
+ // 2. FORCE UPDATE after storage is ready (Fixes "Off by default" bug)
+ OptionsStore.init().then(() => {
+ const loadedOptions = OptionsStore.get();
+ updateUIFromOptions(loadedOptions);
+
+ // CRITICAL FIX: If enabled in saved settings, make sure we really show it
+ if (loadedOptions.autoTranslate) {
+ translateContainer.style.display = 'flex';
+ }
+ });
+
+ // C. Listen for live changes (e.g. from Options Page)
+ OptionsStore.subscribe((options) => {
+ updateUIFromOptions(options);
+ });
+
+ // D. Save changes when user picks a language (Fixes "Not Saved")
+ langSelect.addEventListener('change', (e) => {
+ const newLang = e.target.value;
+ OptionsStore.set({ defaultTranslateLanguage: newLang });
+ });
+
+ langSelect.addEventListener('click', (e) => e.stopPropagation());
+ langSelect.addEventListener('mousedown', (e) => e.stopPropagation());
+ translateContainer.appendChild(langSelect);
+
+ // GO BUTTON
+ const goBtn = document.createElement('button');
+ goBtn.textContent = "Go";
+ goBtn.className = "fs-control-button";
+ goBtn.style.cursor = 'pointer';
+ goBtn.style.color = 'white';
+ goBtn.style.backgroundColor = 'transparent';
+ goBtn.style.border = '1px solid #777';
+ goBtn.style.borderRadius = '3px';
+ goBtn.style.padding = '2px 8px';
+ goBtn.style.fontSize = '12px';
+ goBtn.style.fontWeight = 'bold';
+
+ goBtn.onmouseover = () => { goBtn.style.borderColor = '#fff'; goBtn.style.backgroundColor = 'rgba(255,255,255,0.1)'; };
+ goBtn.onmouseout = () => { goBtn.style.borderColor = '#777'; goBtn.style.backgroundColor = 'transparent'; };
+
+ goBtn.addEventListener('click', async (e) => {
+ e.stopPropagation();
+ if (this.activeTracks.length === 0) {
+ AlertPolyfill.toast('error', 'No active subtitles to translate.');
+ return;
+ }
+
+ const targetLang = langSelect.value;
+ const targetName = DefaultLanguages[targetLang];
+ AlertPolyfill.toast('info', `Translation Started: ${targetName}`);
+
+ const sourceCues = this.activeTracks[0].cues;
+ const totalCues = sourceCues.length;
+ let processedCount = 0;
+
+ // --- CREATE STATUS BADGE WITH AUTO-HIDE ---
+ const statusEl = document.createElement('div');
+ statusEl.id = 'fs-translation-status';
+ statusEl.style.cssText = `
+ position: absolute;
+ top: 20px;
+ right: 20px;
+ z-index: 2147483647;
+ background-color: rgba(20, 20, 20, 0.9);
+ color: white;
+ padding: 10px 15px;
+ border-radius: 6px;
+ font-family: sans-serif;
+ font-size: 14px;
+ border: 1px solid rgba(255,255,255,0.2);
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ pointer-events: none;
+ box-shadow: 0 4px 6px rgba(0,0,0,0.3);
+ transition: opacity 0.5s ease-in-out;
+ opacity: 1;
+ `;
+ statusEl.innerHTML = `Translating... 0%`;
+
+ if (DOMElements.playerContainer) {
+ DOMElements.playerContainer.appendChild(statusEl);
+ }
+
+ // --- AUTO-HIDE LOGIC ---
+ let hideTimeout;
+ const resetHideTimer = () => {
+ statusEl.style.opacity = '1';
+ clearTimeout(hideTimeout);
+ hideTimeout = setTimeout(() => {
+ statusEl.style.opacity = '0';
+ }, 2500);
+ };
+
+ DOMElements.playerContainer.addEventListener('mousemove', resetHideTimer);
+ DOMElements.playerContainer.addEventListener('mouseenter', resetHideTimer);
+ resetHideTimer();
+
+ const removeStatus = () => {
+ DOMElements.playerContainer.removeEventListener('mousemove', resetHideTimer);
+ DOMElements.playerContainer.removeEventListener('mouseenter', resetHideTimer);
+ clearTimeout(hideTimeout);
+ statusEl.remove();
+ };
+
+ if (!this.translator) {
+ try {
+ const { SubtitleTranslator } = await import('../../modules/SubtitleTranslator.mjs');
+ this.translator = new SubtitleTranslator();
+ } catch (err) {
+ console.error(err);
+ AlertPolyfill.toast('error', 'Missing "SubtitleTranslator.mjs" in modules folder!');
+ statusEl.remove();
+ return;
+ }
+ }
+
+ const newTrack = new SubtitleTrack(`Auto (${targetLang})`, targetLang);
+ newTrack.loadText("WEBVTT\n\n");
+ newTrack.cues = [];
+
+ const tracksToDisable = [...this.activeTracks];
+ tracksToDisable.forEach(track => {
+ this.deactivateTrack(track);
+ });
+
+ this.addTrack(newTrack);
+ this.activateTrack(newTrack);
+ this.closeUI();
+
+ try {
+ await this.translator.translateStream(sourceCues, targetLang, (newSegmentCues, statusMsg) => {
+ if (statusMsg) {
+ statusEl.innerHTML = `${statusMsg}`;
+ return;
+ }
+
+ newSegmentCues.forEach(c => {
+ if (!c.dom) {
+ c.dom = WebVTT.convertCueToDOMTree(window, c.text);
+ }
+ newTrack.cues.push(c);
+ });
+
+ newTrack.cues.sort((a, b) => a.startTime - b.startTime);
+ this.renderSubtitles();
+
+ processedCount += newSegmentCues.length;
+ const percent = Math.min(100, Math.floor((processedCount / totalCues) * 100));
+ statusEl.innerHTML = `Translating... ${percent}%`;
+ });
+
+ statusEl.innerHTML = `✔ Complete`;
+ setTimeout(() => removeStatus(), 3000);
+
+ } catch (err) {
+ console.error(err);
+ statusEl.innerHTML = `Error: ${err.message}`;
+ setTimeout(() => removeStatus(), 5000);
+ }
+ });
+ translateContainer.appendChild(goBtn);
+
+ DOMElements.subtitlesView.appendChild(translateContainer);
+ } catch (uiErr) {
+ console.error("Failed to add Translate UI", uiErr);
+ }
- window.addEventListener('resize', () => {
- this.checkTrackBounds();
- });
+ const clearBtn = document.createElement('div'); clearBtn.className='subtitle-menu-option'; clearBtn.textContent=Localize.getMessage('player_subtitlesmenu_clearbtn');
+ clearBtn.onclick = () => this.clearTracks(); DOMElements.subtitlesView.appendChild(clearBtn);
- DOMElements.subtitlesMenu.addEventListener('mousedown', (e) => {
- e.stopPropagation();
- });
+ const optBtn = document.createElement('div'); optBtn.className='subtitle-menu-option'; optBtn.textContent=Localize.getMessage('player_subtitlesmenu_settingsbtn');
+ optBtn.onclick = () => this.settingsManager.openUI(); DOMElements.subtitlesView.appendChild(optBtn);
- DOMElements.subtitlesMenu.addEventListener('mouseup', (e) => {
- e.stopPropagation();
- });
+ } catch (err) { console.error("UI Setup Error", err); }
}
createTrackEntryElements(i) {
@@ -345,7 +400,6 @@ export class SubtitlesManager extends EventEmitter {
resyncTool.title = Localize.getMessage('player_subtitlesmenu_resynctool_label');
resyncTool.className = 'fluid_button fluid_button_wand subtitle-resync-tool subtitle-tool';
trackElement.appendChild(resyncTool);
- // svg use
const svgIconHourglass = WebUtils.createSVGIcon('assets/fluidplayer/static/icons.svg#hourglass');
resyncTool.appendChild(svgIconHourglass);
@@ -354,46 +408,6 @@ export class SubtitlesManager extends EventEmitter {
e.stopPropagation();
}, true);
- const downloadTrack = document.createElement('div');
- downloadTrack.title = Localize.getMessage('player_subtitlesmenu_savetool_label');
- downloadTrack.className = 'fluid_button fluid_button_download subtitle-download-tool subtitle-tool';
-
- // svg use
- const svgIconDownload = WebUtils.createSVGIcon('assets/fluidplayer/static/icons.svg#download');
- downloadTrack.appendChild(svgIconDownload);
-
- trackElement.appendChild(downloadTrack);
-
- downloadTrack.addEventListener('click', async (e) => {
- e.stopPropagation();
- const suggestedName = trackElement.textContent.replaceAll(' ', '_');
- const dlname = chrome?.extension?.inIncognitoContext ? suggestedName : await AlertPolyfill.prompt(Localize.getMessage('player_filename_prompt'), suggestedName);
-
- if (!dlname) {
- return;
- }
-
- const srt = SubtitleUtils.cuesToSrt(this.tracks[i].cues);
- const blob = new Blob([srt], {
- type: 'text/plain',
- });
- const url = window.URL.createObjectURL(blob);
- await Utils.downloadURL(url, dlname + '.srt');
- window.URL.revokeObjectURL(url);
- }, true);
-
- const removeTrack = document.createElement('div');
- removeTrack.classList.add('subtitle-remove-tool');
- removeTrack.classList.add('subtitle-tool');
- removeTrack.title = Localize.getMessage('player_subtitlesmenu_removetool_label');
- trackElement.appendChild(removeTrack);
-
- removeTrack.addEventListener('click', (e) => {
- this.removeTrack(this.tracks[i]);
- e.stopPropagation();
- }, true);
-
-
const shiftLTrack = document.createElement('div');
shiftLTrack.classList.add('subtitle-shiftl-tool');
shiftLTrack.classList.add('subtitle-tool');
@@ -420,33 +434,45 @@ export class SubtitlesManager extends EventEmitter {
e.stopPropagation();
}, true);
+ const downloadTrack = document.createElement('div');
+ downloadTrack.title = Localize.getMessage('player_subtitlesmenu_savetool_label');
+ downloadTrack.className = 'fluid_button fluid_button_download subtitle-download-tool subtitle-tool';
+ const svgIconDownload = WebUtils.createSVGIcon('assets/fluidplayer/static/icons.svg#download');
+ downloadTrack.appendChild(svgIconDownload);
+ trackElement.appendChild(downloadTrack);
- trackElement.addEventListener('mouseenter', () => {
- trackElement.focus();
- });
+ downloadTrack.addEventListener('click', async (e) => {
+ e.stopPropagation();
+ const suggestedName = trackElement.textContent.replaceAll(' ', '_');
+ const dlname = chrome?.extension?.inIncognitoContext ? suggestedName : await AlertPolyfill.prompt(Localize.getMessage('player_filename_prompt'), suggestedName);
+ if (!dlname) return;
+ const srt = SubtitleUtils.cuesToSrt(this.tracks[i].cues);
+ const blob = new Blob([srt], { type: 'text/plain' });
+ const url = window.URL.createObjectURL(blob);
+ await Utils.downloadURL(url, dlname + '.srt');
+ window.URL.revokeObjectURL(url);
+ }, true);
- trackElement.addEventListener('mouseleave', () => {
- trackElement.blur();
- });
+ const removeTrack = document.createElement('div');
+ removeTrack.classList.add('subtitle-remove-tool');
+ removeTrack.classList.add('subtitle-tool');
+ removeTrack.title = Localize.getMessage('player_subtitlesmenu_removetool_label');
+ trackElement.appendChild(removeTrack);
+ removeTrack.addEventListener('click', (e) => {
+ this.removeTrack(this.tracks[i]);
+ e.stopPropagation();
+ }, true);
+
+ trackElement.addEventListener('mouseenter', () => trackElement.focus());
+ trackElement.addEventListener('mouseleave', () => trackElement.blur());
trackElement.addEventListener('keydown', (e) => {
- if (e.code === 'Delete' || e.code === 'Backspace') {
- e.stopPropagation();
- removeTrack.click();
- } else if (e.code === 'BracketRight') {
- e.stopPropagation();
- shiftRTrack.click();
- } else if (e.code === 'BracketLeft') {
- e.stopPropagation();
- shiftLTrack.click();
- } else if (e.code === 'KeyD') {
- e.stopPropagation();
- downloadTrack.click();
- } else if (e.code === 'KeyR') {
- e.stopPropagation();
- resyncTool.click();
- }
+ if (e.code === 'Delete' || e.code === 'Backspace') { e.stopPropagation(); removeTrack.click(); }
+ else if (e.code === 'BracketRight') { e.stopPropagation(); shiftRTrack.click(); }
+ else if (e.code === 'BracketLeft') { e.stopPropagation(); shiftLTrack.click(); }
+ else if (e.code === 'KeyD') { e.stopPropagation(); downloadTrack.click(); }
+ else if (e.code === 'KeyR') { e.stopPropagation(); resyncTool.click(); }
});
return {
@@ -456,24 +482,15 @@ export class SubtitlesManager extends EventEmitter {
const activeIndex = this.activeTracks.indexOf(track);
const nameCandidate = (track.language ? ('(' + track.language + ') ') : '') + (track.label || `Track ${i + 1}`);
let name = nameCandidate;
- // limit to 30 chars
- if (name.length > 30) {
- name = name.substring(0, 30) + '...';
- }
+ if (name.length > 30) name = name.substring(0, 30) + '...';
if (activeIndex !== -1) {
trackElement.classList.add('subtitle-track-active');
-
- if (this.activeTracks.length > 1) {
- trackName.textContent = (activeIndex + 1) + ': ' + name;
- } else {
- trackName.textContent = name;
- }
+ trackName.textContent = (this.activeTracks.length > 1) ? (activeIndex + 1) + ': ' + name : name;
} else {
trackElement.classList.remove('subtitle-track-active');
trackName.textContent = name;
}
-
trackName.title = nameCandidate;
},
};
@@ -482,26 +499,19 @@ export class SubtitlesManager extends EventEmitter {
updateTrackList() {
const cachedElements = this.subtitleTrackListElements;
const tracks = this.tracks;
-
- // Remove extra elements
for (let i = cachedElements.length - 1; i >= tracks.length; i--) {
const el = cachedElements[i];
el.trackElement.remove();
cachedElements.splice(i, 1);
}
-
- // Add new elements
for (let i = cachedElements.length; i < tracks.length; i++) {
const elements = this.createTrackEntryElements(i);
cachedElements.push(elements);
DOMElements.subtitlesList.appendChild(elements.trackElement);
}
-
- // Update elements
for (let i = 0; i < tracks.length; i++) {
cachedElements[i].update();
}
-
this.renderSubtitles();
}
@@ -523,20 +533,15 @@ export class SubtitlesManager extends EventEmitter {
const wrapper = document.createElement('div');
wrapper.className = 'subtitle-track-wrapper';
wrapper.appendChild(trackContainer);
-
wrapper.style.marginBottom = '5px';
-
let yStart = 0;
-
const mouseup = (e) => {
DOMElements.playerContainer.removeEventListener('mousemove', mousemove);
DOMElements.playerContainer.removeEventListener('mouseup', mouseup);
e.stopPropagation();
};
-
const mousemove = (e) => {
- // drag by adjusting margin-bottom
const oldDiff = yStart - e.clientY;
let diff = oldDiff;
let current = wrapper;
@@ -547,49 +552,36 @@ export class SubtitlesManager extends EventEmitter {
diff -= (newMarginBottom - marginBottom);
current = current.nextElementSibling;
} while (current && diff < 0);
-
const adjustedDiff = oldDiff - diff;
-
const previousSibling = wrapper.previousElementSibling;
if (previousSibling) {
const previousMarginBottom = parseInt(previousSibling.style.marginBottom) || 0;
const newPreviousMarginBottom = Math.max(previousMarginBottom - adjustedDiff, 5);
previousSibling.style.marginBottom = newPreviousMarginBottom + 'px';
}
-
yStart = e.clientY;
this.checkTrackBounds();
e.stopPropagation();
};
-
wrapper.addEventListener('mousedown', (e) => {
yStart = e.clientY;
DOMElements.playerContainer.addEventListener('mousemove', mousemove);
DOMElements.playerContainer.addEventListener('mouseup', mouseup);
e.stopPropagation();
});
-
-
- return {
- trackContainer,
- wrapper,
- };
+ return { trackContainer, wrapper };
}
- // Make sure subtitles are not outside of the video
checkTrackBounds() {
const trackElements = this.subtitleTrackDisplayElements;
const playerHeight = DOMElements.playerContainer.offsetHeight - parseInt(window.getComputedStyle(DOMElements.subtitlesContainer).bottom);
-
let totalTrackHeight = 0;
for (let i = 0; i < trackElements.length; i++) {
const trackWrapper = trackElements[i].parentElement;
const marginBottom = parseInt(trackWrapper.style.marginBottom);
totalTrackHeight += trackWrapper.offsetHeight + marginBottom;
}
-
let shrinkAmount = Math.max(totalTrackHeight - playerHeight, 0);
- // shrink margin-bottom from topmost track downwards
for (let i = 0; i < trackElements.length && shrinkAmount > 0; i++) {
const trackWrapper = trackElements[i].parentElement;
const marginBottom = parseInt(trackWrapper.style.marginBottom) || 0;
@@ -615,89 +607,57 @@ export class SubtitlesManager extends EventEmitter {
const cachedElements = this.subtitleTrackDisplayElements;
const tracks = this.activeTracks;
let trackLen = tracks.length;
+ if (this.isTestSubtitleActive) trackLen++;
- if (this.isTestSubtitleActive) {
- trackLen++;
- }
-
- // Remove extra elements
for (let i = cachedElements.length - 1; i >= trackLen; i--) {
const el = cachedElements[i];
el.parentElement.remove();
cachedElements.splice(i, 1);
}
-
- // Add new elements
for (let i = cachedElements.length; i < trackLen; i++) {
const {trackContainer, wrapper} = this.createSubtitleDisplayElements(i);
-
cachedElements.push(trackContainer);
DOMElements.subtitlesContainer.appendChild(wrapper);
}
- // Update elements
const currentTime = this.client.state.currentTime;
let subtitlesVisible = 0;
for (let i = 0; i < tracks.length; i++) {
const trackContainer = cachedElements[i];
- // trackContainer.replaceChildren();
const cues = tracks[i].cues;
-
let cueIndex = Utils.binarySearch(cues, this.client.state.currentTime, (time, cue) => {
- if (cue.startTime > time) {
- return -1;
- } else if (cue.startTime < time) {
- return 1;
- }
- return 0;
+ if (cue.startTime > time) return -1; else if (cue.startTime < time) return 1; return 0;
});
-
const toAdd = [];
- if (cueIndex < -1) {
- cueIndex = -cueIndex - 2;
- }
-
- while (cueIndex > 0 && cues[cueIndex - 1].endTime >= currentTime && cues[cueIndex - 1].startTime <= currentTime) {
- cueIndex--;
- }
-
+ if (cueIndex < -1) cueIndex = -cueIndex - 2;
+ while (cueIndex > 0 && cues[cueIndex - 1].endTime >= currentTime && cues[cueIndex - 1].startTime <= currentTime) cueIndex--;
while (cueIndex >= 0 &&cueIndex < cues.length && cues[cueIndex].endTime >= currentTime && cues[cueIndex].startTime <= currentTime) {
const cue = cues[cueIndex];
- if (!cue.dom) {
- cue.dom = WebVTT.convertCueToDOMTree(window, cue.text);
- }
+ if (!cue.dom) cue.dom = WebVTT.convertCueToDOMTree(window, cue.text);
toAdd.push(cue.dom);
cueIndex++;
}
-
if (!toAdd.length) {
trackContainer.style.opacity = 0;
-
- // Remove all children except one
const fillerCue = trackContainer.children[0] || document.createElement('div');
WebUtils.replaceChildrenPerformant(trackContainer, [fillerCue]);
if (!fillerCue.textContent) fillerCue.textContent = '|';
} else {
trackContainer.style.opacity = '';
-
WebUtils.replaceChildrenPerformant(trackContainer, toAdd);
subtitlesVisible++;
}
}
-
if (this.isTestSubtitleActive) {
const trackContainer = cachedElements[trackLen - 1];
-
trackContainer.style.opacity = '';
-
if (!this.testCue) {
const cue = document.createElement('div');
cue.textContent = Localize.getMessage('player_testsubtitle');
this.testCue = cue;
}
-
WebUtils.replaceChildrenPerformant(trackContainer, [this.testCue]);
subtitlesVisible++;
}
@@ -709,11 +669,10 @@ export class SubtitlesManager extends EventEmitter {
} else {
DOMElements.subtitlesContainer.style.display = 'none';
}
-
this.checkTrackBounds();
}
mediaInfoSet() {
this.openSubtitlesSearch.setMediaInfo(this.client.mediaInfo);
}
-}
+}
\ No newline at end of file