@oxymoron.13/tizentube-s95b 0.2.1 → 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.
@@ -1,33 +1,25 @@
1
1
  /* S95B Performance Edition – GPL-3.0-only */
2
2
  export const STRINGS = Object.freeze({
3
3
  de: Object.freeze({
4
- title: 'S95B · Performance', subtitle: 'TizenTube Performance Edition 0.2.1',
5
- language: 'Sprache', codec: 'Videocodec', quality: 'Bevorzugte Qualität', automatic: 'Automatisch',
6
- light: 'Ressourcenschonender Modus', diag: 'Diagnoseanzeige',
7
- reset: 'Änderungen zurücksetzen', close: 'Schließen', on: 'Ein', off: 'Aus',
8
- hint: 'Gelbe Taste: Einstellungen · Pfeile: Navigieren · OK: Ändern · Zurück: Schließen',
9
- restart: 'Codec-Änderungen gelten ab dem nächsten Video. Eine feste Qualität gilt sofort; „Automatisch“ wirkt eventuell erst nach einem Neustart von YouTube.',
10
- safe: 'Kein Eingriff in Firmware, Bildrate oder Decoder.',
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.',
11
10
  statVideo: 'Video', statFrames: 'Verlorene Bilder', statCodec: 'Codec-Wunsch',
12
- statUnavailable: 'Vom Web-Player nicht verfügbar', statWait: 'Warte auf Video',
13
- 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.',
14
- lightHint: 'Schaltet Vorschauen und DeArrow ab; vorherige Werte werden gesichert.',
15
- restoreHint: 'Stellt nur die von diesem Menü geänderten Werte wieder her.',
11
+ statUnavailable: 'nicht verfügbar', statWait: 'wartet auf Video',
16
12
  languageDE: 'Deutsch', languageEN: 'English',
17
13
  }),
18
14
  en: Object.freeze({
19
- title: 'S95B · Performance', subtitle: 'TizenTube Performance Edition 0.2.1',
20
- language: 'Language', codec: 'Video codec', quality: 'Preferred quality', automatic: 'Automatic',
21
- light: 'Low-overhead mode', diag: 'Diagnostic overlay',
22
- reset: 'Restore changed settings', close: 'Close', on: 'On', off: 'Off',
23
- hint: 'Yellow button: settings · Arrows: navigate · OK: change · Back: close',
24
- restart: 'Codec changes apply from the next video. A fixed quality applies immediately; “Automatic” may only take effect after restarting YouTube.',
25
- safe: 'No firmware, display-rate or decoder changes.',
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.',
26
21
  statVideo: 'Video', statFrames: 'Dropped frames', statCodec: 'Codec preference',
27
- statUnavailable: 'Not available from web player', statWait: 'Waiting for video',
28
- 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.',
29
- lightHint: 'Disables previews and DeArrow; original values are backed up.',
30
- restoreHint: 'Restores only values changed from this menu.',
22
+ statUnavailable: 'not available', statWait: 'waiting for video',
31
23
  languageDE: 'Deutsch', languageEN: 'English',
32
24
  })
33
25
  });
@@ -1,20 +1,18 @@
1
1
  /* S95B Performance Edition – GPL-3.0-only
2
- * No global player hooks, firmware calls, network requests, or external scripts.
3
- * UI runs only when the TizenTube userscript loads, keyboard events only when
4
- * explicitly invoked. Uses existing TizenTube configRead/configWrite APIs.
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
5
  */
6
- import { configRead, configWrite } from '../config.js';
6
+ import { configRead, configWrite, configChangeEmitter } from '../config.js';
7
+ import { optionShow } from '../ui/settings.js';
7
8
  import { translate } from './s95bI18n.js';
8
9
 
9
10
  const STATE_KEY = 's95b-perf-edition-v1';
10
11
  const EDITED = ['enablePreviews', 'enableDeArrow', 'enableDeArrowThumbnails'];
11
- const defaultState = { lang: 'de', light: false, diagnostics: false, previous: null, codecPrevious: null, qualityPrevious: null };
12
+ const CODECS = ['any', 'vp9', 'av01', 'avc1'];
13
+ const QUALITIES = ['auto', '2160p', '1440p', '1080p', '720p'];
14
+ const defaultState = { lang: 'de', light: false, previous: null };
12
15
  let state = { ...defaultState };
13
- let overlay = null;
14
- let diagnostic = null;
15
- let diagTimer = null;
16
- let menuButtons = [];
17
- let selected = 0;
18
16
 
19
17
  function readState() {
20
18
  try {
@@ -22,43 +20,27 @@ function readState() {
22
20
  if (data && typeof data === 'object') {
23
21
  state = { ...defaultState,
24
22
  lang: data.lang === 'en' ? 'en' : 'de', light: data.light === true,
25
- diagnostics: data.diagnostics === true,
26
- previous: data.previous && typeof data.previous === 'object' ? data.previous : null,
27
- codecPrevious: typeof data.codecPrevious === 'string' ? data.codecPrevious : null,
28
- qualityPrevious: typeof data.qualityPrevious === 'string' ? data.qualityPrevious : null };
23
+ previous: data.previous && typeof data.previous === 'object' ? data.previous : null };
29
24
  }
30
25
  } catch (_) { state = { ...defaultState }; }
31
26
  }
32
27
  function save() {
33
28
  try { localStorage.setItem(STATE_KEY, JSON.stringify(state)); } catch (_) { /* no persistent storage */ }
34
29
  }
35
- function currentCodec() {
36
- const value = configRead('preferredVideoCodec');
37
- return value === 'vp9' || value === 'av01' || value === 'avc1' ? value : 'any';
38
- }
30
+ function t(key) { return translate(state.lang, key); }
31
+
39
32
  function codecLabel(value) {
40
- return value === 'any' ? translate(state.lang, 'automatic') : value === 'vp9' ? 'VP9' : value === 'av01' ? 'AV1' : 'H.264';
41
- }
42
- const CODECS = ['any', 'vp9', 'av01', 'avc1'];
43
- function changeCodec() {
44
- if (state.codecPrevious === null) state.codecPrevious = String(configRead('preferredVideoCodec') || 'any');
45
- configWrite('preferredVideoCodec', CODECS[(CODECS.indexOf(currentCodec()) + 1) % CODECS.length]);
46
- save();
47
- }
48
- // Uses TizenTube's own preferredVideoQuality setting and handler (same values as its settings menu).
49
- const QUALITIES = ['auto', '2160p', '1440p', '1080p', '720p'];
50
- function currentQuality() {
51
- const value = String(configRead('preferredVideoQuality') || 'auto');
52
- return QUALITIES.indexOf(value) >= 0 ? value : 'auto';
33
+ return value === 'any' ? t('automatic') : value === 'vp9' ? 'VP9' : value === 'av01' ? 'AV1' : 'H.264';
53
34
  }
54
35
  function qualityLabel(value) {
55
- return value === 'auto' ? translate(state.lang, 'automatic') : value === '2160p' ? '2160p (4K)' : value;
36
+ return value === 'auto' ? t('automatic') : value === '2160p' ? '2160p (4K)' : value;
56
37
  }
57
- function changeQuality() {
58
- if (state.qualityPrevious === null) state.qualityPrevious = String(configRead('preferredVideoQuality') || 'auto');
59
- configWrite('preferredVideoQuality', QUALITIES[(QUALITIES.indexOf(currentQuality()) + 1) % QUALITIES.length]);
60
- save();
38
+ function currentCodec() {
39
+ const value = configRead('preferredVideoCodec');
40
+ return CODECS.indexOf(value) >= 0 ? value : 'any';
61
41
  }
42
+
43
+ // Low-overhead mode: switches off a few TizenTube features and remembers the previous values.
62
44
  function turnLight(enable) {
63
45
  if (enable === state.light) return;
64
46
  if (enable) {
@@ -78,169 +60,83 @@ function turnLight(enable) {
78
60
  }
79
61
  save();
80
62
  }
81
- function reset() {
82
- turnLight(false);
83
- if (state.codecPrevious !== null) {
84
- configWrite('preferredVideoCodec', state.codecPrevious);
85
- state.codecPrevious = null;
86
- }
87
- if (state.qualityPrevious !== null) {
88
- configWrite('preferredVideoQuality', state.qualityPrevious);
89
- state.qualityPrevious = null;
90
- }
91
- state.diagnostics = false;
92
- stopDiagnostics();
93
- save();
94
- }
95
- function create(tag, attrs, value) {
96
- const n = document.createElement(tag);
97
- if (attrs) Object.keys(attrs).forEach(key => {
98
- if (key === 'className') n.className = attrs[key];
99
- else n.setAttribute(key, attrs[key]);
100
- });
101
- if (typeof value === 'string') n.textContent = value;
102
- return n;
103
- }
104
- function drawMenu() {
105
- if (!overlay) return;
106
- overlay.textContent = '';
107
- const t = key => translate(state.lang, key);
108
- const card = create('section', { className: 's95b-card', role: 'dialog', 'aria-modal': 'true', 'aria-label': t('title') });
109
- const eyebrow = create('div', { className: 's95b-eyebrow' }, t('subtitle'));
110
- const title = create('h2', null, t('title'));
111
- card.append(eyebrow, title);
112
- const fields = [
113
- [t('language'), state.lang === 'de' ? t('languageDE') : t('languageEN'), () => { state.lang = state.lang === 'de' ? 'en' : 'de'; save(); }],
114
- [t('codec'), codecLabel(currentCodec()), changeCodec],
115
- [t('quality'), qualityLabel(currentQuality()), changeQuality],
116
- [t('light'), state.light ? t('on') : t('off'), () => turnLight(!state.light)],
117
- [t('diag'), state.diagnostics ? t('on') : t('off'), () => { state.diagnostics = !state.diagnostics; save(); if (state.diagnostics) startDiagnostics(); else stopDiagnostics(); }],
118
- [t('reset'), '', reset],
119
- [t('close'), '', hideMenu]
120
- ];
121
- menuButtons = fields.map((field, index) => {
122
- const button = create('button', { type: 'button', className: 's95b-option', 'data-index': String(index) });
123
- button.append(create('span', null, field[0]), create('strong', null, field[1]));
124
- button.addEventListener('click', () => { selected = index; field[2](); if (overlay) drawMenu(); });
125
- card.appendChild(button);
126
- return button;
127
- });
128
- card.append(create('p', { className: 's95b-explanation' }, t('codecHint')));
129
- card.append(create('p', { className: 's95b-explanation' }, t('lightHint')));
130
- card.append(create('p', { className: 's95b-note' }, t('restart')));
131
- card.append(create('p', { className: 's95b-note' }, t('restoreHint')));
132
- card.append(create('p', { className: 's95b-footer' }, t('safe')));
133
- card.append(create('p', { className: 's95b-footer' }, t('hint')));
134
- overlay.appendChild(card);
135
- selected = Math.max(0, Math.min(selected, menuButtons.length - 1));
136
- highlight();
137
- }
138
- function highlight() {
139
- menuButtons.forEach((b, i) => {
140
- b.classList.toggle('s95b-active', i === selected);
141
- b.setAttribute('tabindex', i === selected ? '0' : '-1');
142
- });
143
- if (menuButtons[selected]) menuButtons[selected].focus();
144
- }
145
- function openMenu() {
146
- if (!document.body) return;
147
- if (overlay) { hideMenu(); return; }
148
- overlay = create('div', { id: 's95b-overlay' });
149
- document.body.appendChild(overlay);
150
- selected = 0;
151
- drawMenu();
152
- }
153
- function hideMenu() {
154
- if (overlay) overlay.remove();
155
- overlay = null;
156
- menuButtons = [];
157
- }
158
- function fmtVideo(v) {
159
- const t = key => translate(state.lang, key);
160
- if (!v) return t('statWait');
161
- const width = v.videoWidth || 0;
162
- const height = v.videoHeight || 0;
163
- return width > 0 && height > 0 ? `${width} × ${height}` : t('statUnavailable');
164
- }
165
- function updateDiagnostics() {
166
- if (!diagnostic) return;
167
- const t = key => translate(state.lang, key);
63
+
64
+ // One snapshot of what the web player reports, taken each time the menu opens.
65
+ function statsLine() {
168
66
  const video = document.querySelector('video');
169
- const codec = codecLabel(currentCodec());
67
+ let size = t('statWait');
170
68
  let dropped = t('statUnavailable');
171
- if (video && typeof video.getVideoPlaybackQuality === 'function') {
172
- try {
173
- const q = video.getVideoPlaybackQuality();
174
- if (q && Number.isFinite(q.droppedVideoFrames)) dropped = String(q.droppedVideoFrames);
175
- } catch (_) { /* diagnostics should never interrupt playback */ }
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
+ }
176
79
  }
177
- diagnostic.textContent = [t('statVideo') + ': ' + fmtVideo(video), t('statFrames') + ': ' + dropped,
178
- t('statCodec') + ': ' + codec].join(' | ');
80
+ return t('statVideo') + ': ' + size + ' | ' + t('statFrames') + ': ' + dropped +
81
+ ' | ' + t('statCodec') + ': ' + codecLabel(currentCodec());
179
82
  }
180
- function startDiagnostics() {
181
- if (!document.body || diagnostic) return;
182
- diagnostic = create('div', { id: 's95b-diagnostics', role: 'status' });
183
- document.body.appendChild(diagnostic);
184
- updateDiagnostics();
185
- // Interval is deliberately disabled unless explicitly enabled by the user.
186
- diagTimer = setInterval(updateDiagnostics, 2000);
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
+ ];
187
100
  }
188
- function stopDiagnostics() {
189
- if (diagTimer !== null) clearInterval(diagTimer);
190
- diagTimer = null;
191
- if (diagnostic) diagnostic.remove();
192
- diagnostic = null;
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); }
193
111
  }
112
+
194
113
  function onKeys(e) {
195
114
  const code = e.keyCode || e.which;
196
- const isKeydown = e.type === undefined || e.type === 'keydown';
197
- // Samsung ColorF2Yellow: 405 per Tizen docs; 170 is the alternative code TizenTube
198
- // documents for yellow (it handles 172/191 for green/blue the same way). F2 = keyboard.
199
- // Red (403), green (404) and blue (406) are already used by TizenTube itself.
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.
200
117
  if (code === 405 || code === 170 || e.key === 'ColorF2Yellow' || e.key === 'F2') {
201
118
  e.preventDefault(); e.stopImmediatePropagation();
202
- if (isKeydown) openMenu();
203
- return;
119
+ if (e.type === undefined || e.type === 'keydown') openMenu();
204
120
  }
205
- if (!overlay) return; // Do not interfere with normal YouTube key navigation.
206
- const back = code === 10009 || code === 8 || code === 27 || e.key === 'Escape' || e.key === 'Backspace';
207
- const up = code === 38 || e.key === 'ArrowUp';
208
- const down = code === 40 || e.key === 'ArrowDown';
209
- const enter = code === 13 || e.key === 'Enter';
210
- if (!(back || up || down || enter)) return;
211
- // keyup/keypress of handled keys are swallowed too, so the YouTube UI below never sees half a key press.
212
- e.preventDefault(); e.stopImmediatePropagation();
213
- if (!isKeydown) return;
214
- if (back) hideMenu();
215
- else if (up || down) { selected = (selected + (up ? -1 : 1) + menuButtons.length) % menuButtons.length; highlight(); }
216
- else if (menuButtons[selected]) menuButtons[selected].click();
217
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
+
218
130
  function init() {
219
- if (!document.body || document.getElementById('s95b-style')) return;
220
- const css = `
221
- #s95b-overlay {position:fixed;top:0;left:0;right:0;bottom:0;z-index:2147483646;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.78);font-family:Arial,Helvetica,sans-serif;color:#f7fafc;}
222
- #s95b-overlay * {box-sizing:border-box;}
223
- #s95b-overlay .s95b-card {width:88vw;max-width:930px;max-height:90vh;overflow:auto;background:#111a27;border:2px solid #34506c;border-radius:18px;padding:28px 34px;box-shadow:0 20px 70px rgba(0,0,0,.55);}
224
- #s95b-overlay .s95b-eyebrow {font-size:19px;color:#a8cce0;}
225
- #s95b-overlay h2 {font-size:38px;line-height:1.2;margin:6px 0 18px;}
226
- #s95b-overlay button {width:100%;display:flex;align-items:center;justify-content:space-between;font-size:24px;line-height:1.2;color:#fff;text-align:left;padding:14px 18px;margin:6px 0;border:2px solid #26374b;border-radius:10px;background:#192638;cursor:pointer;}
227
- #s95b-overlay button strong {color:#bcecff;font-size:22px;margin-left:24px;}
228
- #s95b-overlay button.s95b-active,#s95b-overlay button:focus {background:#176687;border-color:#bcecff;outline:none;}
229
- #s95b-overlay p {font-size:17px;line-height:1.45;}
230
- #s95b-overlay .s95b-explanation {color:#bcd9df;margin:17px 0 4px;}
231
- #s95b-overlay .s95b-note {color:#d7dce5;margin:7px 0;}
232
- #s95b-overlay .s95b-footer {color:#91a0b3;margin:10px 0 0;font-size:15px;}
233
- #s95b-diagnostics {pointer-events:none;position:fixed;bottom:2vh;left:2vw;z-index:2147483645;padding:10px 16px;color:white;background:rgba(0,0,0,.7);font:15px Arial,Helvetica,sans-serif;border-radius:8px;}
234
- `;
235
- const style = create('style', { id: 's95b-style' });
236
- style.textContent = css;
237
- document.head.appendChild(style);
238
- // Register our listener in the capture phase; only known menu keys are consumed.
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.
239
136
  window.addEventListener('keydown', onKeys, true);
240
137
  window.addEventListener('keyup', onKeys, true);
241
138
  window.addEventListener('keypress', onKeys, true);
242
- if (state.diagnostics) startDiagnostics();
243
139
  }
140
+
244
141
  readState();
245
- if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init, { once: true });
246
- else init();
142
+ init();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@oxymoron.13/tizentube-s95b",
3
3
  "appName": "TizenTube S95B Performance (Fork)",
4
- "version": "0.2.1",
4
+ "version": "0.2.3",
5
5
  "description": "Unofficial fork of TizenTube (@foxreis/tizentube) for TizenBrew: reversible performance profile, German/English settings, smaller bundle.",
6
6
  "packageType": "mods",
7
7
  "websiteURL": "https://youtube.com/tv?additionalDataUrl=http%3A%2F%2Flocalhost%3A8085%2Fdial%2Fapps%2FYouTube",