@oxymoron.13/tizentube-s95b 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +49 -42
  2. package/S95B-CHANGES.txt +10 -4
  3. package/UPSTREAM-README.md +42 -0
  4. package/dist/userScript.js +2 -2
  5. package/mods/config.js +117 -0
  6. package/mods/domrect-polyfill.js +104 -0
  7. package/mods/features/adblock.js +584 -0
  8. package/mods/features/autoFrameRate.js +42 -0
  9. package/mods/features/enableFeatures.js +19 -0
  10. package/mods/features/moreSubtitles.js +378 -0
  11. package/mods/features/pictureInPicture.js +198 -0
  12. package/mods/features/preferredVideoQuality.js +101 -0
  13. package/mods/features/s95bCodec.js +45 -0
  14. package/mods/features/s95bI18n.js +28 -0
  15. package/mods/features/s95bMenu.js +142 -0
  16. package/mods/features/sponsorblock.js +437 -0
  17. package/mods/features/standaloneUserscript.js +107 -0
  18. package/mods/features/updater.js +119 -0
  19. package/mods/features/userAgentSpoofing.js +73 -0
  20. package/mods/features/videoQueuing.js +64 -0
  21. package/mods/package-lock.json +2321 -0
  22. package/mods/package.json +33 -0
  23. package/mods/resolveCommand.js +376 -0
  24. package/mods/rollup.config.js +41 -0
  25. package/mods/spatial-navigation-polyfill.js +1756 -0
  26. package/mods/tiny-sha256.js +99 -0
  27. package/mods/translations/i18nResources.js +11 -0
  28. package/mods/translations/index.js +18 -0
  29. package/mods/translations/language-names.js +981 -0
  30. package/mods/translations/resources/ar.json +308 -0
  31. package/mods/translations/resources/az.json +308 -0
  32. package/mods/translations/resources/bs.json +308 -0
  33. package/mods/translations/resources/cs.json +308 -0
  34. package/mods/translations/resources/de.json +308 -0
  35. package/mods/translations/resources/dk.json +308 -0
  36. package/mods/translations/resources/el.json +308 -0
  37. package/mods/translations/resources/en.json +308 -0
  38. package/mods/translations/resources/es-419.json +308 -0
  39. package/mods/translations/resources/es.json +308 -0
  40. package/mods/translations/resources/fi-FI.json +308 -0
  41. package/mods/translations/resources/fr.json +311 -0
  42. package/mods/translations/resources/hr.json +308 -0
  43. package/mods/translations/resources/hu.json +308 -0
  44. package/mods/translations/resources/id.json +308 -0
  45. package/mods/translations/resources/it.json +308 -0
  46. package/mods/translations/resources/ja.json +308 -0
  47. package/mods/translations/resources/lt.json +308 -0
  48. package/mods/translations/resources/lv.json +308 -0
  49. package/mods/translations/resources/pl.json +308 -0
  50. package/mods/translations/resources/pt-BR.json +308 -0
  51. package/mods/translations/resources/pt-PT.json +308 -0
  52. package/mods/translations/resources/ro.json +310 -0
  53. package/mods/translations/resources/ru.json +308 -0
  54. package/mods/translations/resources/sr-Cyrl.json +308 -0
  55. package/mods/translations/resources/sr-Latn.json +308 -0
  56. package/mods/translations/resources/tlh.json +308 -0
  57. package/mods/translations/resources/tr.json +308 -0
  58. package/mods/translations/resources/uk.json +308 -0
  59. package/mods/translations/resources/vi.json +309 -0
  60. package/mods/translations/resources/zh-CN.json +308 -0
  61. package/mods/translations/resources/zh-TW.json +308 -0
  62. package/mods/translations/update-translations.js +55 -0
  63. package/mods/ui/chapters.js +137 -0
  64. package/mods/ui/clock.js +71 -0
  65. package/mods/ui/customCommandExecution.js +49 -0
  66. package/mods/ui/customGuideAction.js +119 -0
  67. package/mods/ui/customUI.js +192 -0
  68. package/mods/ui/customYTSettings.js +29 -0
  69. package/mods/ui/disableWhosWatching.js +48 -0
  70. package/mods/ui/settings.js +1024 -0
  71. package/mods/ui/sidebarModification.js +179 -0
  72. package/mods/ui/speedUI.js +119 -0
  73. package/mods/ui/theme.js +26 -0
  74. package/mods/ui/ui.css +157 -0
  75. package/mods/ui/ui.js +261 -0
  76. package/mods/ui/ytUI.js +510 -0
  77. package/mods/userScript.js +28 -0
  78. package/mods/utils/ASTParser.js +67 -0
  79. package/mods/utils/innerTubeCalls.js +138 -0
  80. package/package.json +11 -5
  81. package/service/package-lock.json +4193 -0
  82. package/service/package.json +29 -0
  83. package/service/rollup.config.js +47 -0
  84. package/service/service.js +118 -0
@@ -0,0 +1,45 @@
1
+ /* S95B Performance Edition – GPL-3.0-only.
2
+ * Pure, conservative format selector. Does not touch Samsung/Tizen APIs.
3
+ */
4
+ export function selectCodecFormats(response, codec) {
5
+ if (!response || !response.streamingData || !Array.isArray(response.streamingData.adaptiveFormats)) {
6
+ return { changed: false, reason: 'no-formats' };
7
+ }
8
+ if (codec !== 'vp9' && codec !== 'av01' && codec !== 'avc1') {
9
+ return { changed: false, reason: 'automatic' };
10
+ }
11
+
12
+ const formats = response.streamingData.adaptiveFormats;
13
+ const video = formats.filter(item => item && typeof item.mimeType === 'string' && /^video\//i.test(item.mimeType));
14
+ const isMatch = item => {
15
+ const mime = item.mimeType.toLowerCase();
16
+ if (codec === 'vp9') return /(?:vp9|vp09)/.test(mime);
17
+ if (codec === 'av01') return /av01/.test(mime);
18
+ return /avc1/.test(mime);
19
+ };
20
+ const selected = video.filter(isMatch);
21
+ if (!selected.length) return { changed: false, reason: 'codec-unavailable' };
22
+ if (selected.length === video.length) return { changed: false, reason: 'already-selected' };
23
+
24
+ // Conservative quality parity: preserve original formats if filtering could
25
+ // remove the best available height/fps combination. Metadata absent => no filter.
26
+ const valid = item => Number.isFinite(item.height) && item.height > 0 &&
27
+ Number.isFinite(item.fps) && item.fps > 0;
28
+ if (!video.every(valid) || !selected.every(valid)) {
29
+ return { changed: false, reason: 'quality-unknown' };
30
+ }
31
+ const maxHeight = Math.max(...video.map(item => item.height));
32
+ const maxFpsAtHeight = Math.max(...video.filter(item => item.height === maxHeight).map(item => item.fps));
33
+ const bestMatch = selected.some(item => item.height >= maxHeight && item.fps >= maxFpsAtHeight);
34
+ if (!bestMatch) return { changed: false, reason: 'quality-fallback' };
35
+
36
+ // Keep audio and unrecognized entries untouched. No media decoding or resampling.
37
+ const next = formats.filter(item => !item || typeof item.mimeType !== 'string' ||
38
+ !/^video\//i.test(item.mimeType) || isMatch(item));
39
+ if (!next.some(item => item && /^audio\//i.test(item.mimeType || ''))) {
40
+ // Do not make a pre-existing audio problem worse by filtering streams.
41
+ return { changed: false, reason: 'missing-audio' };
42
+ }
43
+ response.streamingData.adaptiveFormats = next;
44
+ return { changed: true, reason: 'selected' };
45
+ }
@@ -0,0 +1,28 @@
1
+ /* S95B Performance Edition – GPL-3.0-only */
2
+ export const STRINGS = Object.freeze({
3
+ de: Object.freeze({
4
+ title: 'S95B · Performance', language: 'Sprache', codec: 'Videocodec', quality: 'Bevorzugte Qualität',
5
+ automatic: 'Automatisch', light: 'Ressourcenschonender Modus',
6
+ lightHint: 'Schaltet Vorschauen und DeArrow ab; vorherige Werte werden beim Ausschalten wiederhergestellt.',
7
+ codecHint: 'H.264 liefert bei YouTube in der Regel höchstens 1080p; 4K gibt es nur als VP9 oder AV1. Ein Codec wird nur erzwungen, wenn dadurch keine Qualität verloren geht. Gilt ab dem nächsten Video.',
8
+ qualityHint: 'Eine feste Qualität gilt sofort. „Automatisch“ wirkt eventuell erst nach einem Neustart von YouTube.',
9
+ languageHint: 'Gilt für dieses Menü, beim nächsten Öffnen.',
10
+ statVideo: 'Video', statFrames: 'Verlorene Bilder', statCodec: 'Codec-Wunsch',
11
+ statUnavailable: 'nicht verfügbar', statWait: 'wartet auf Video',
12
+ languageDE: 'Deutsch', languageEN: 'English',
13
+ }),
14
+ en: Object.freeze({
15
+ title: 'S95B · Performance', language: 'Language', codec: 'Video codec', quality: 'Preferred quality',
16
+ automatic: 'Automatic', light: 'Low-overhead mode',
17
+ lightHint: 'Disables previews and DeArrow; the previous values are restored when switched off.',
18
+ codecHint: 'On YouTube, H.264 usually tops out at 1080p; 4K is only available as VP9 or AV1. A codec is only forced if no quality is lost. Applies from the next video.',
19
+ qualityHint: 'A fixed quality applies immediately. “Automatic” may only take effect after restarting YouTube.',
20
+ languageHint: 'Applies to this menu, the next time it is opened.',
21
+ statVideo: 'Video', statFrames: 'Dropped frames', statCodec: 'Codec preference',
22
+ statUnavailable: 'not available', statWait: 'waiting for video',
23
+ languageDE: 'Deutsch', languageEN: 'English',
24
+ })
25
+ });
26
+ export function translate(lang, key) {
27
+ return (STRINGS[lang] || STRINGS.de)[key] || STRINGS.en[key] || key;
28
+ }
@@ -0,0 +1,142 @@
1
+ /* S95B Performance Edition – GPL-3.0-only
2
+ * The yellow remote key opens a menu that YouTube itself draws (TizenTube's own optionShow),
3
+ * the same mechanism as the green (settings) and blue (speed) menus. No DOM overlay is added to the page.
4
+ * Uses only existing TizenTube configRead/configWrite APIs. No firmware calls, network requests or timers.
5
+ */
6
+ import { configRead, configWrite, configChangeEmitter } from '../config.js';
7
+ import { optionShow } from '../ui/settings.js';
8
+ import { translate } from './s95bI18n.js';
9
+
10
+ const STATE_KEY = 's95b-perf-edition-v1';
11
+ const EDITED = ['enablePreviews', 'enableDeArrow', 'enableDeArrowThumbnails'];
12
+ const CODECS = ['any', 'vp9', 'av01', 'avc1'];
13
+ const QUALITIES = ['auto', '2160p', '1440p', '1080p', '720p'];
14
+ const defaultState = { lang: 'de', light: false, previous: null };
15
+ let state = { ...defaultState };
16
+
17
+ function readState() {
18
+ try {
19
+ const data = JSON.parse(localStorage.getItem(STATE_KEY) || 'null');
20
+ if (data && typeof data === 'object') {
21
+ state = { ...defaultState,
22
+ lang: data.lang === 'en' ? 'en' : 'de', light: data.light === true,
23
+ previous: data.previous && typeof data.previous === 'object' ? data.previous : null };
24
+ }
25
+ } catch (_) { state = { ...defaultState }; }
26
+ }
27
+ function save() {
28
+ try { localStorage.setItem(STATE_KEY, JSON.stringify(state)); } catch (_) { /* no persistent storage */ }
29
+ }
30
+ function t(key) { return translate(state.lang, key); }
31
+
32
+ function codecLabel(value) {
33
+ return value === 'any' ? t('automatic') : value === 'vp9' ? 'VP9' : value === 'av01' ? 'AV1' : 'H.264';
34
+ }
35
+ function qualityLabel(value) {
36
+ return value === 'auto' ? t('automatic') : value === '2160p' ? '2160p (4K)' : value;
37
+ }
38
+ function currentCodec() {
39
+ const value = configRead('preferredVideoCodec');
40
+ return CODECS.indexOf(value) >= 0 ? value : 'any';
41
+ }
42
+
43
+ // Low-overhead mode: switches off a few TizenTube features and remembers the previous values.
44
+ function turnLight(enable) {
45
+ if (enable === state.light) return;
46
+ if (enable) {
47
+ const values = {};
48
+ EDITED.forEach(key => { values[key] = configRead(key); });
49
+ state.previous = values;
50
+ EDITED.forEach(key => configWrite(key, false));
51
+ state.light = true;
52
+ } else {
53
+ if (state.previous) EDITED.forEach(key => {
54
+ if (Object.prototype.hasOwnProperty.call(state.previous, key)) {
55
+ configWrite(key, state.previous[key]);
56
+ }
57
+ });
58
+ state.previous = null;
59
+ state.light = false;
60
+ }
61
+ save();
62
+ }
63
+
64
+ // One snapshot of what the web player reports, taken each time the menu opens.
65
+ function statsLine() {
66
+ const video = document.querySelector('video');
67
+ let size = t('statWait');
68
+ let dropped = t('statUnavailable');
69
+ if (video) {
70
+ const width = video.videoWidth || 0;
71
+ const height = video.videoHeight || 0;
72
+ size = width > 0 && height > 0 ? width + ' × ' + height : t('statUnavailable');
73
+ if (typeof video.getVideoPlaybackQuality === 'function') {
74
+ try {
75
+ const q = video.getVideoPlaybackQuality();
76
+ if (q && Number.isFinite(q.droppedVideoFrames)) dropped = String(q.droppedVideoFrames);
77
+ } catch (_) { /* diagnostics must never interrupt playback */ }
78
+ }
79
+ }
80
+ return t('statVideo') + ': ' + size + ' | ' + t('statFrames') + ': ' + dropped +
81
+ ' | ' + t('statCodec') + ': ' + codecLabel(currentCodec());
82
+ }
83
+
84
+ function buildOptions() {
85
+ return [
86
+ { name: t('language'), value: null, menuId: 's95b-language',
87
+ menuHeader: { title: t('language'), subtitle: t('languageHint') },
88
+ options: [
89
+ { name: t('languageDE'), key: 's95bLanguage', value: 'de' },
90
+ { name: t('languageEN'), key: 's95bLanguage', value: 'en' }
91
+ ] },
92
+ { name: t('codec'), icon: 'VIDEO_QUALITY', value: null, menuId: 's95b-codec',
93
+ menuHeader: { title: t('codec'), subtitle: t('codecHint') },
94
+ options: CODECS.map(codec => ({ name: codecLabel(codec), key: 'preferredVideoCodec', value: codec })) },
95
+ { name: t('quality'), icon: 'VIDEO_QUALITY', value: null, menuId: 's95b-quality',
96
+ menuHeader: { title: t('quality'), subtitle: t('qualityHint') },
97
+ options: QUALITIES.map(quality => ({ name: qualityLabel(quality), key: 'preferredVideoQuality', value: quality })) },
98
+ { name: t('light'), subtitle: t('lightHint'), value: 's95bLowOverhead' }
99
+ ];
100
+ }
101
+
102
+ function openMenu() {
103
+ try {
104
+ optionShow({
105
+ menuId: 's95b-menu',
106
+ menuHeader: { title: t('title'), subtitle: statsLine() },
107
+ selectedIndex: 0,
108
+ options: buildOptions()
109
+ }, false);
110
+ } catch (e) { console.warn('[S95B] menu could not be opened:', e); }
111
+ }
112
+
113
+ function onKeys(e) {
114
+ const code = e.keyCode || e.which;
115
+ // Samsung ColorF2Yellow: 405 per Tizen docs, 170 as the alternative code TizenTube mentions, F2 on a keyboard.
116
+ // Red (403), green (404) and blue (406) are used by TizenTube itself. Nothing else is touched.
117
+ if (code === 405 || code === 170 || e.key === 'ColorF2Yellow' || e.key === 'F2') {
118
+ e.preventDefault(); e.stopImmediatePropagation();
119
+ if (e.type === undefined || e.type === 'keydown') openMenu();
120
+ }
121
+ }
122
+
123
+ function onConfigChange(event) {
124
+ const detail = event && event.detail;
125
+ if (!detail) return;
126
+ if (detail.key === 's95bLowOverhead') turnLight(detail.value === true);
127
+ else if (detail.key === 's95bLanguage') { state.lang = detail.value === 'en' ? 'en' : 'de'; save(); }
128
+ }
129
+
130
+ function init() {
131
+ // Menu entries read their current value from the TizenTube config, so mirror our stored state into it first.
132
+ if (configRead('s95bLowOverhead') !== state.light) configWrite('s95bLowOverhead', state.light);
133
+ if (configRead('s95bLanguage') !== state.lang) configWrite('s95bLanguage', state.lang);
134
+ configChangeEmitter.addEventListener('configChange', onConfigChange);
135
+ // Capture phase on window: only the yellow key is consumed.
136
+ window.addEventListener('keydown', onKeys, true);
137
+ window.addEventListener('keyup', onKeys, true);
138
+ window.addEventListener('keypress', onKeys, true);
139
+ }
140
+
141
+ readState();
142
+ init();
@@ -0,0 +1,437 @@
1
+ import sha256 from '../tiny-sha256.js';
2
+ import { configRead } from '../config.js';
3
+ import { showToast } from '../ui/ytUI.js';
4
+ import { t } from 'i18next';
5
+
6
+ // Copied from https://github.com/ajayyy/SponsorBlock/blob/da1a535de784540ee10166a75a3eb8537073838c/src/config.ts#L113-L134
7
+ const barTypes = {
8
+ sponsor: {
9
+ color: '#00d400',
10
+ opacity: '0.7',
11
+ name: t('sponsorblock.segments.sponsor') || 'sponsored segment'
12
+ },
13
+ intro: {
14
+ color: '#00ffff',
15
+ opacity: '0.7',
16
+ name: t('sponsorblock.segments.intro') || 'intro'
17
+ },
18
+ outro: {
19
+ color: '#0202ed',
20
+ opacity: '0.7',
21
+ name: t('sponsorblock.segments.outro') || 'outro'
22
+ },
23
+ interaction: {
24
+ color: '#cc00ff',
25
+ opacity: '0.7',
26
+ name: t('sponsorblock.segments.interaction') || 'interaction reminder'
27
+ },
28
+ selfpromo: {
29
+ color: '#ffff00',
30
+ opacity: '0.7',
31
+ name: t('sponsorblock.segments.selfpromo') || 'self-promotion'
32
+ },
33
+ preview: {
34
+ color: '#008fd6',
35
+ opacity: '0.7',
36
+ name: t('sponsorblock.segments.preview') || 'recap or preview'
37
+ },
38
+ filler: {
39
+ color: "#7300FF",
40
+ opacity: "0.9",
41
+ name: t('sponsorblock.segments.filler') || 'tangents'
42
+ },
43
+ music_offtopic: {
44
+ color: '#ff9900',
45
+ opacity: '0.7',
46
+ name: t('sponsorblock.segments.music_offtopic') || 'non-music part'
47
+ },
48
+ poi_highlight: {
49
+ color: '#9b044c',
50
+ opacity: '0.7',
51
+ name: t('sponsorblock.segments.poi_highlight') || 'highlight'
52
+ }
53
+ };
54
+
55
+ const sponsorblockAPI = 'https://sponsor.ajay.app/api';
56
+
57
+ class SponsorBlockHandler {
58
+ video = null;
59
+ active = true;
60
+
61
+ attachVideoTimeout = null;
62
+ nextSkipTimeout = null;
63
+ sliderInterval = null;
64
+
65
+ observer = null;
66
+ scheduleSkipHandler = null;
67
+ durationChangeHandler = null;
68
+ segments = null;
69
+ skippableCategories = [];
70
+ manualSkippableCategories = [];
71
+ skippedCategories = new Map();
72
+
73
+ constructor(videoID) {
74
+ this.videoID = videoID;
75
+ }
76
+
77
+ async init() {
78
+ const videoHash = sha256(this.videoID).substring(0, 4);
79
+ const categories = [
80
+ 'sponsor',
81
+ 'intro',
82
+ 'outro',
83
+ 'interaction',
84
+ 'selfpromo',
85
+ 'preview',
86
+ 'filler',
87
+ 'music_offtopic',
88
+ 'poi_highlight'
89
+ ];
90
+ const resp = await fetch(
91
+ `${sponsorblockAPI}/skipSegments/${videoHash}?categories=${encodeURIComponent(
92
+ JSON.stringify(categories)
93
+ )}`
94
+ );
95
+ const results = await resp.json();
96
+
97
+ const result = results.find((v) => v.videoID === this.videoID);
98
+ console.info(this.videoID, 'Got it:', result);
99
+
100
+ if (!result || !result.segments || !result.segments.length) {
101
+ console.info(this.videoID, 'No segments found.');
102
+ return;
103
+ }
104
+
105
+ this.segments = result.segments;
106
+ this.manualSkippableCategories = configRead('sponsorBlockManualSkips');
107
+ this.skippableCategories = this.getSkippableCategories();
108
+
109
+ this.scheduleSkipHandler = () => {
110
+ const slider = document.querySelector('div[idomkey="slider"]');
111
+ const sliderRect = slider?.getBoundingClientRect();
112
+ const isOldUI = !document.querySelector('div[idomkey="Metadata-Section"]');
113
+ if (isOldUI && sliderRect) {
114
+ this.segmentsoverlay.style.setProperty('top', `${sliderRect.top}px`, 'important');
115
+ }
116
+ this.scheduleSkip();
117
+ }
118
+ this.durationChangeHandler = () => this.buildOverlay();
119
+
120
+ this.attachVideo();
121
+ this.buildOverlay();
122
+ }
123
+
124
+ getSkippableCategories() {
125
+ const skippableCategories = [];
126
+ if (configRead('enableSponsorBlockSponsor')) {
127
+ skippableCategories.push('sponsor');
128
+ }
129
+ if (configRead('enableSponsorBlockIntro')) {
130
+ skippableCategories.push('intro');
131
+ }
132
+ if (configRead('enableSponsorBlockOutro')) {
133
+ skippableCategories.push('outro');
134
+ }
135
+ if (configRead('enableSponsorBlockInteraction')) {
136
+ skippableCategories.push('interaction');
137
+ }
138
+ if (configRead('enableSponsorBlockSelfPromo')) {
139
+ skippableCategories.push('selfpromo');
140
+ }
141
+ if (configRead('enableSponsorBlockPreview')) {
142
+ skippableCategories.push('preview');
143
+ }
144
+ if (configRead('enableSponsorBlockFiller')) {
145
+ skippableCategories.push('filler');
146
+ }
147
+ if (configRead('enableSponsorBlockMusicOfftopic')) {
148
+ skippableCategories.push('music_offtopic');
149
+ }
150
+ return skippableCategories;
151
+ }
152
+
153
+ attachVideo() {
154
+ clearTimeout(this.attachVideoTimeout);
155
+ this.attachVideoTimeout = null;
156
+
157
+ this.video = document.querySelector('video');
158
+ if (!this.video) {
159
+ console.info(this.videoID, 'No video yet...');
160
+ this.attachVideoTimeout = setTimeout(() => this.attachVideo(), 100);
161
+ return;
162
+ }
163
+
164
+ console.info(this.videoID, 'Video found, binding...');
165
+
166
+ this.video.addEventListener('play', this.scheduleSkipHandler);
167
+ this.video.addEventListener('pause', this.scheduleSkipHandler);
168
+ this.video.addEventListener('timeupdate', this.scheduleSkipHandler);
169
+ this.video.addEventListener('durationchange', this.durationChangeHandler);
170
+ }
171
+
172
+ buildOverlay() {
173
+ if (this.segmentsoverlay) {
174
+ console.info('Overlay already built');
175
+ return;
176
+ }
177
+
178
+ if (!this.video || !this.video.duration) {
179
+ console.info('No video duration yet');
180
+ return;
181
+ }
182
+
183
+ const videoDuration = this.video.duration;
184
+ const slider = document.querySelector('div[idomkey="slider"]');
185
+ if (!slider) return setTimeout(() => this.buildOverlay(), 100);
186
+
187
+ this.segmentsoverlay = document.createElement('div');
188
+
189
+ this.segmentsoverlay.classList.add('ytLrProgressBarSlider', 'ytLrProgressBarSliderRectangularProgressBar');
190
+ this.segmentsoverlay.style.setProperty('z-index', '10', 'important');
191
+ this.segmentsoverlay.style.setProperty('background-color', 'rgba(0, 0, 0, 0)', 'important');
192
+ this.segmentsoverlay.style.setProperty('width', '72rem', 'important');
193
+ this.segmentsoverlay.style.setProperty('left', '4rem', 'important');
194
+ const sliderRect = slider.getBoundingClientRect();
195
+ if (!slider.classList.contains('ytLrProgressBarSlider')) {
196
+ for (let i = 0; i < slider.classList.length; i++) {
197
+ this.segmentsoverlay.classList.add(slider.classList[i]);
198
+ }
199
+ this.segmentsoverlay.style.setProperty('height', `${sliderRect.height}px`, 'important');
200
+ this.segmentsoverlay.style.setProperty('bottom', `${sliderRect.bottom - sliderRect.top}px`, 'important');
201
+ }
202
+ this.segments.forEach((segment) => {
203
+ const [start, end] = segment.segment;
204
+ const barType = barTypes[segment.category] || {
205
+ color: 'blue',
206
+ opacity: 0.7
207
+ };
208
+
209
+ const leftPercent = videoDuration ? (100.0 * start) / videoDuration : 0;
210
+ const widthPercent = videoDuration ? (100.0 * (end - start)) / videoDuration : 0;
211
+
212
+ const elm = document.createElement('div');
213
+ elm.style.setProperty('background-color', barType.color, 'important');
214
+ elm.style.setProperty('opacity', barType.opacity, 'important');
215
+ elm.style.setProperty('height', '100%', 'important');
216
+ elm.style.setProperty('width', `${segment.category === 'poi_highlight' ? 1 : widthPercent}%`, 'important');
217
+ elm.style.setProperty('left', `${leftPercent}%`, 'important');
218
+ elm.style.setProperty('position', 'absolute', 'important');
219
+ console.info('Generated element', elm, 'from', segment);
220
+ this.segmentsoverlay.appendChild(elm);
221
+ });
222
+
223
+ this.observer = new MutationObserver((mutations) => {
224
+ mutations.forEach((m) => {
225
+ if (m.removedNodes) {
226
+ for (const node of m.removedNodes) {
227
+ if (node === this.segmentsoverlay) {
228
+ console.info('bringing back segments overlay');
229
+ this.slider.appendChild(this.segmentsoverlay);
230
+ }
231
+ }
232
+ }
233
+
234
+ if (document.querySelector('ytlr-progress-bar').getAttribute('hybridnavfocusable') === 'false') {
235
+ this.segmentsoverlay.style.setProperty('display', 'none', 'important');
236
+ } else {
237
+ this.segmentsoverlay.style.setProperty('display', 'block', 'important');
238
+ }
239
+ });
240
+ });
241
+
242
+ this.sliderInterval = setInterval(() => {
243
+ this.slider = document.querySelector('ytlr-redux-connect-ytlr-progress-bar');
244
+ if (this.slider) {
245
+ clearInterval(this.sliderInterval);
246
+ this.sliderInterval = null;
247
+ this.observer.observe(this.slider, {
248
+ childList: true,
249
+ subtree: true
250
+ });
251
+ this.slider.appendChild(this.segmentsoverlay);
252
+ }
253
+ }, 500);
254
+ }
255
+
256
+ scheduleSkip() {
257
+ clearTimeout(this.nextSkipTimeout);
258
+ this.nextSkipTimeout = null;
259
+
260
+ if (!this.active) {
261
+ console.info(this.videoID, 'No longer active, ignoring...');
262
+ return;
263
+ }
264
+
265
+ if (this.video.paused) {
266
+ console.info(this.videoID, 'Currently paused, ignoring...');
267
+ return;
268
+ }
269
+
270
+ // Sometimes timeupdate event (that calls scheduleSkip) gets fired right before
271
+ // already scheduled skip routine below. Let's just look back a little bit
272
+ // and, in worst case, perform a skip at negative interval (immediately)...
273
+ const nextSegments = this.segments.filter(
274
+ (seg) =>
275
+ seg.segment[0] > this.video.currentTime - 0.3 &&
276
+ seg.segment[1] > this.video.currentTime - 0.3
277
+ );
278
+ nextSegments.sort((s1, s2) => s1.segment[0] - s2.segment[0]);
279
+
280
+ if (!nextSegments.length) {
281
+ console.info(this.videoID, 'No more segments');
282
+ return;
283
+ }
284
+
285
+ const [segment] = nextSegments;
286
+ const [start, end] = segment.segment;
287
+ console.info(
288
+ this.videoID,
289
+ 'Scheduling skip of',
290
+ segment,
291
+ 'in',
292
+ start - this.video.currentTime
293
+ );
294
+
295
+ this.nextSkipTimeout = setTimeout(() => {
296
+ if (this.video.paused) {
297
+ console.info(this.videoID, 'Currently paused, ignoring...');
298
+ return;
299
+ }
300
+ if (!this.skippableCategories.includes(segment.category)) {
301
+ console.info(
302
+ this.videoID,
303
+ 'Segment',
304
+ segment.category,
305
+ 'is not skippable, ignoring...'
306
+ );
307
+ return;
308
+ }
309
+
310
+ const skipName = barTypes[segment.category]?.name || segment.category;
311
+ console.info(this.videoID, 'Skipping', segment);
312
+ if (!this.manualSkippableCategories.includes(segment.category)) {
313
+ const wasSkippedBefore = this.skippedCategories.get(segment.UUID)
314
+ if (wasSkippedBefore) {
315
+ wasSkippedBefore.count++;
316
+ wasSkippedBefore.lastSkipped = Date.now();
317
+ this.skippedCategories.set(segment.UUID, wasSkippedBefore);
318
+
319
+ if (wasSkippedBefore.lastSkipped - wasSkippedBefore.firstSkipped < 1000) {
320
+ if (!wasSkippedBefore.hasShownToast) {
321
+ if (configRead('enableSponsorBlockToasts')) {
322
+ showToast('SponsorBlock', t('sponsorblock.toasts.notSkipping', { segment: skipName, count: wasSkippedBefore.count }));
323
+ }
324
+ wasSkippedBefore.hasShownToast = true;
325
+ this.skippedCategories.set(segment.UUID, wasSkippedBefore);
326
+ }
327
+ return;
328
+ }
329
+ } else {
330
+ this.skippedCategories.set(segment.UUID, {
331
+ count: 1,
332
+ firstSkipped: Date.now(),
333
+ lastSkipped: Date.now(),
334
+ hasShownToast: false
335
+ });
336
+ }
337
+ if (configRead('enableSponsorBlockToasts')) {
338
+ showToast('SponsorBlock', t('sponsorblock.toasts.skipping', { segment: skipName }));
339
+ }
340
+ if (this.video.duration - end < 1) {
341
+ this.video.currentTime = end - 1;
342
+ } else this.video.currentTime = end;
343
+ this.scheduleSkip();
344
+ }
345
+ }, (start - this.video.currentTime) * 1000);
346
+ }
347
+
348
+ destroy() {
349
+ console.info(this.videoID, 'Destroying');
350
+
351
+ this.active = false;
352
+
353
+ if (this.nextSkipTimeout) {
354
+ clearTimeout(this.nextSkipTimeout);
355
+ this.nextSkipTimeout = null;
356
+ }
357
+
358
+ if (this.attachVideoTimeout) {
359
+ clearTimeout(this.attachVideoTimeout);
360
+ this.attachVideoTimeout = null;
361
+ }
362
+
363
+ if (this.sliderInterval) {
364
+ clearInterval(this.sliderInterval);
365
+ this.sliderInterval = null;
366
+ }
367
+
368
+ if (this.observer) {
369
+ this.observer.disconnect();
370
+ this.observer = null;
371
+ }
372
+
373
+ if (this.segmentsoverlay) {
374
+ this.segmentsoverlay.remove();
375
+ this.segmentsoverlay = null;
376
+ }
377
+
378
+ if (this.video) {
379
+ this.video.removeEventListener('play', this.scheduleSkipHandler);
380
+ this.video.removeEventListener('pause', this.scheduleSkipHandler);
381
+ this.video.removeEventListener('timeupdate', this.scheduleSkipHandler);
382
+ this.video.removeEventListener(
383
+ 'durationchange',
384
+ this.durationChangeHandler
385
+ );
386
+ }
387
+
388
+ this.skippedCategories.clear();
389
+ }
390
+ }
391
+
392
+ // When this global variable was declared using let and two consecutive hashchange
393
+ // events were fired (due to bubbling? not sure...) the second call handled below
394
+ // would not see the value change from first call, and that would cause multiple
395
+ // SponsorBlockHandler initializations... This has been noticed on Chromium 38.
396
+ // This either reveals some bug in chromium/webpack/babel scope handling, or
397
+ // shows my lack of understanding of javascript. (or both)
398
+ window.sponsorblock = null;
399
+
400
+ window.addEventListener(
401
+ 'hashchange',
402
+ () => {
403
+ const newURL = new URL(location.hash.substring(1), location.href);
404
+ // A hack, but it works, so...
405
+ const videoID = newURL.search.replace('?v=', '').split('&')[0];
406
+ const needsReload =
407
+ videoID &&
408
+ (!window.sponsorblock || window.sponsorblock.videoID != videoID);
409
+
410
+ console.info(
411
+ 'hashchange',
412
+ videoID,
413
+ window.sponsorblock,
414
+ window.sponsorblock ? window.sponsorblock.videoID : null,
415
+ needsReload
416
+ );
417
+
418
+ if (needsReload) {
419
+ if (window.sponsorblock) {
420
+ try {
421
+ window.sponsorblock.destroy();
422
+ } catch (err) {
423
+ console.warn('window.sponsorblock.destroy() failed!', err);
424
+ }
425
+ window.sponsorblock = null;
426
+ }
427
+
428
+ if (configRead('enableSponsorBlock')) {
429
+ window.sponsorblock = new SponsorBlockHandler(videoID);
430
+ window.sponsorblock.init();
431
+ } else {
432
+ console.info('SponsorBlock disabled, not loading');
433
+ }
434
+ }
435
+ },
436
+ false
437
+ );