diff --git a/better-xcloud.user.js b/better-xcloud.user.js index a7fdf68..53025f6 100644 --- a/better-xcloud.user.js +++ b/better-xcloud.user.js @@ -735,12 +735,19 @@ class StreamBadges { class StreamStats { + static get PING() { return 'ping'; } + static get FPS() { return 'fps'; } + static get BITRATE() { return 'btr'; } + static get DECODE_TIME() { return 'dt'; } + static get PACKETS_LOST() { return 'pl'; } + static get FRAMES_LOST() { return 'fl'; } + static #interval; static #updateInterval = 1000; static #$container; static #$fps; - static #$rtt; + static #$ping; static #$dt; static #$pl; static #$fl; @@ -873,23 +880,25 @@ class StreamStats { } else if (stat.type === 'candidate-pair' && stat.state === 'succeeded') { // Round Trip Time const roundTripTime = typeof stat.currentRoundTripTime !== 'undefined' ? stat.currentRoundTripTime * 1000 : '???'; - StreamStats.#$rtt.textContent = `${roundTripTime}ms`; + StreamStats.#$ping.textContent = roundTripTime; if (PREF_STATS_CONDITIONAL_FORMATTING) { grade = (roundTripTime > 100) ? 'bad' : (roundTripTime > 75) ? 'ok' : (roundTripTime > 40) ? 'good' : ''; } - StreamStats.#$rtt.setAttribute('data-grade', grade); + StreamStats.#$ping.setAttribute('data-grade', grade); } }); }); } static #refreshStyles() { + const PREF_ITEMS = PREFS.get(Preferences.STATS_ITEMS); const PREF_POSITION = PREFS.get(Preferences.STATS_POSITION); const PREF_TRANSPARENT = PREFS.get(Preferences.STATS_TRANSPARENT); const PREF_OPACITY = PREFS.get(Preferences.STATS_OPACITY); const PREF_TEXT_SIZE = PREFS.get(Preferences.STATS_TEXT_SIZE); + StreamStats.#$container.setAttribute('data-stats', '[' + PREF_ITEMS.join('][') + ']'); StreamStats.#$container.setAttribute('data-position', PREF_POSITION); StreamStats.#$container.setAttribute('data-transparent', PREF_TRANSPARENT); StreamStats.#$container.style.opacity = PREF_OPACITY + '%'; @@ -915,19 +924,22 @@ class StreamStats { } const CE = createElement; - StreamStats.#$container = CE('div', {'class': 'better-xcloud-stats-bar better-xcloud-gone'}, - CE('label', {}, 'FPS'), - StreamStats.#$fps = CE('span', {}, 0), - CE('label', {}, 'RTT'), - StreamStats.#$rtt = CE('span', {}, '0ms'), - CE('label', {}, 'DT'), - StreamStats.#$dt = CE('span', {}, '0ms'), - CE('label', {}, 'BR'), - StreamStats.#$br = CE('span', {}, '0 Mbps'), - CE('label', {}, 'PL'), - StreamStats.#$pl = CE('span', {}, '0 (0.00%)'), - CE('label', {}, 'FL'), - StreamStats.#$fl = CE('span', {}, '0 (0.00%)')); + const STATS = { + [StreamStats.PING]: (StreamStats.#$ping = CE('span', {}, '0')), + [StreamStats.FPS]: (StreamStats.#$fps = CE('span', {}, '0')), + [StreamStats.BITRATE]: (StreamStats.#$br = CE('span', {}, '0 Mbps')), + [StreamStats.DECODE_TIME]: (StreamStats.#$dt = CE('span', {}, '0ms')), + [StreamStats.PACKETS_LOST]: (StreamStats.#$pl = CE('span', {}, '0 (0.00%)')), + [StreamStats.FRAMES_LOST]: (StreamStats.#$fl = CE('span', {}, '0 (0.00%)')), + }; + + const $barFragment = document.createDocumentFragment(); + for (let statKey in STATS) { + const $div = CE('div', {'class': `better-xcloud-stat-${statKey}`}, CE('label', {}, statKey.toUpperCase()), STATS[statKey]); + $barFragment.appendChild($div); + } + + StreamStats.#$container = CE('div', {'class': 'better-xcloud-stats-bar better-xcloud-gone'}, $barFragment); let clickTimeout; StreamStats.#$container.addEventListener('mousedown', e => { @@ -949,48 +961,59 @@ class StreamStats { const refreshFunc = e => { StreamStats.#refreshStyles() }; - const $position = PREFS.toElement(Preferences.STATS_POSITION, refreshFunc); let $close; - const $showStartup = PREFS.toElement(Preferences.STATS_SHOW_WHEN_PLAYING); - const $quickGlance = PREFS.toElement(Preferences.STATS_QUICK_GLANCE, e => { - e.target.checked ? StreamStats.quickGlanceSetup() : StreamStats.quickGlanceStop(); - }); - const $transparent = PREFS.toElement(Preferences.STATS_TRANSPARENT, refreshFunc); - const $formatting = PREFS.toElement(Preferences.STATS_CONDITIONAL_FORMATTING, refreshFunc); - const $opacity = PREFS.toElement(Preferences.STATS_OPACITY, refreshFunc); - const $textSize = PREFS.toElement(Preferences.STATS_TEXT_SIZE, refreshFunc); + + + const STATS_UI = { + [Preferences.STATS_SHOW_WHEN_PLAYING]: { + 'label': 'Show stats when starting the game', + }, + [Preferences.STATS_QUICK_GLANCE]: { + 'label': 'Enable "Quick Glance" mode', + 'onChange': e => { + e.target.checked ? StreamStats.quickGlanceSetup() : StreamStats.quickGlanceStop(); + }, + }, + [Preferences.STATS_ITEMS]: { + 'label': 'Stats', + 'onChange': refreshFunc, + }, + [Preferences.STATS_POSITION]: { + 'label': 'Position', + 'onChange': refreshFunc, + }, + [Preferences.STATS_TEXT_SIZE]: { + 'label': 'Text size', + 'onChange': refreshFunc, + }, + [Preferences.STATS_OPACITY]: { + 'label': 'Opacity (50-100%)', + 'onChange': refreshFunc, + }, + [Preferences.STATS_TRANSPARENT]: { + 'label': 'Transparent background', + 'onChange': refreshFunc, + }, + [Preferences.STATS_CONDITIONAL_FORMATTING]: { + 'label': 'Conditional formatting text color', + 'onChange': refreshFunc, + }, + }; + + const $fragment = document.createDocumentFragment(); + for (let settingKey in STATS_UI) { + const setting = STATS_UI[settingKey]; + + $fragment.appendChild(CE('div', {}, + CE('label', {'for': `xcloud_setting_${settingKey}`}, setting.label), + PREFS.toElement(settingKey, setting.onChange) + )); + } StreamStats.#$settings = CE('div', {'class': 'better-xcloud-stats-settings'}, CE('b', {}, 'Stream Stats Settings'), - CE('div', {}, - CE('label', {'for': `xcloud_setting_${Preferences.STATS_SHOW_WHEN_PLAYING}`}, 'Show stats when starting the game'), - $showStartup - ), - CE('div', {}, - CE('label', {'for': `xcloud_setting_${Preferences.STATS_QUICK_GLANCE}`}, 'Enable "Quick Glance" mode'), - $quickGlance - ), - CE('div', {}, - CE('label', {}, 'Position'), - $position - ), - CE('div', {}, - CE('label', {}, 'Text size'), - $textSize - ), - CE('div', {}, - CE('label', {'for': `xcloud_setting_${Preferences.STATS_OPACITY}`}, 'Opacity (50-100%)'), - $opacity - ), - CE('div', {}, - CE('label', {'for': `xcloud_setting_${Preferences.STATS_TRANSPARENT}`}, 'Transparent background'), - $transparent - ), - CE('div', {}, - CE('label', {'for': `xcloud_setting_${Preferences.STATS_CONDITIONAL_FORMATTING}`}, 'Conditional formatting text color'), - $formatting - ), + $fragment, $close = CE('button', {}, 'Close')); $close.addEventListener('click', e => StreamStats.hideSettingsUi()); @@ -1133,6 +1156,7 @@ class Preferences { static get AUDIO_MIC_ON_PLAYING() { return 'audio_mic_on_playing'; } + static get STATS_ITEMS() { return 'stats_items'; }; static get STATS_SHOW_WHEN_PLAYING() { return 'stats_show_when_playing'; } static get STATS_QUICK_GLANCE() { return 'stats_quick_glance'; } static get STATS_POSITION() { return 'stats_position'; } @@ -1310,6 +1334,18 @@ class Preferences { [Preferences.AUDIO_MIC_ON_PLAYING]: { 'default': false, }, + + [Preferences.STATS_ITEMS]: { + 'default': [StreamStats.PING, StreamStats.FPS, StreamStats.BITRATE, StreamStats.DECODE_TIME, StreamStats.PACKETS_LOST, StreamStats.FRAMES_LOST], + 'multiple_options': { + [StreamStats.PING]: 'Ping', + [StreamStats.FPS]: 'FPS', + [StreamStats.BITRATE]: 'Bitrate', + [StreamStats.DECODE_TIME]: 'Decode time', + [StreamStats.PACKETS_LOST]: 'Packets lost', + [StreamStats.FRAMES_LOST]: 'Frames lost', + }, + }, [Preferences.STATS_SHOW_WHEN_PLAYING]: { 'default': false, }, @@ -1410,6 +1446,17 @@ class Preferences { if ('options' in config && !(value in config.options)) { value = config.default; + } else if ('multiple_options' in config) { + if (value.length) { + const validOptions = Object.keys(config.multiple_options); + value.forEach((item, idx) => { + (validOptions.indexOf(item) === -1) && value.splice(idx, 1); + }); + } + + if (!value.length) { + value = config.default; + } } } @@ -1428,7 +1475,7 @@ class Preferences { let $control; if ('options' in setting) { - $control = CE('select', {id: 'xcloud_setting_' + key}); + $control = CE('select', {'id': 'xcloud_setting_' + key}); for (let value in setting.options) { const label = setting.options[value]; @@ -1439,6 +1486,39 @@ class Preferences { $control.value = currentValue; $control.addEventListener('change', e => { PREFS.set(key, e.target.value); + onChange && onChange(e); + }); + } else if ('multiple_options' in setting) { + $control = CE('select', {'id': 'xcloud_setting_' + key, 'multiple': true}); + for (let value in setting.multiple_options) { + const label = setting.multiple_options[value]; + + const $option = CE('option', {value: value}, label); + $option.selected = currentValue.indexOf(value) > -1; + + $control.appendChild($option); + } + + $control.addEventListener('mousedown', function(e) { + e.preventDefault(); + const $this = this; + const orgScrollTop = $this.scrollTop; + e.target.selected = !e.target.selected; + + const $parent = e.target.parentElement; + $parent.focus(); + $parent.dispatchEvent(new Event('change')); + + setTimeout(() => ($this.scrollTop = orgScrollTop), 0); + }); + + $control.addEventListener('mousemove', e => e.preventDefault()); + + // $control.value = currentValue; + $control.addEventListener('change', e => { + const values = Array.from(e.target.selectedOptions).map(e => e.value); + PREFS.set(key, values); + onChange && onChange(e); }); } else if (typeof setting.default === 'number') { @@ -1746,6 +1826,32 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] { text-wrap: nowrap; } +.better-xcloud-stats-bar > div { + display: none; + margin-right: 8px; + border-right: 2px solid #fff; + padding-right: 8px; +} + +.better-xcloud-stats-bar[data-stats*="[fps]"] > .better-xcloud-stat-fps, +.better-xcloud-stats-bar[data-stats*="[ping]"] > .better-xcloud-stat-ping, +.better-xcloud-stats-bar[data-stats*="[btr]"] > .better-xcloud-stat-btr, +.better-xcloud-stats-bar[data-stats*="[dt]"] > .better-xcloud-stat-dt, +.better-xcloud-stats-bar[data-stats*="[pl]"] > .better-xcloud-stat-pl, +.better-xcloud-stats-bar[data-stats*="[fl]"] > .better-xcloud-stat-fl { + display: inline-block; +} + +.better-xcloud-stats-bar[data-stats$="[fps]"] > .better-xcloud-stat-fps, +.better-xcloud-stats-bar[data-stats$="[ping]"] > .better-xcloud-stat-ping, +.better-xcloud-stats-bar[data-stats$="[btr]"] > .better-xcloud-stat-btr, +.better-xcloud-stats-bar[data-stats$="[dt]"] > .better-xcloud-stat-dt, +.better-xcloud-stats-bar[data-stats$="[pl]"] > .better-xcloud-stat-pl, +.better-xcloud-stats-bar[data-stats$="[fl]"] > .better-xcloud-stat-fl { + margin-right: 0; + border-right: none; +} + .better-xcloud-stats-bar[data-display=glancing]::before { content: '👀 '; vertical-align: middle; @@ -1753,15 +1859,18 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] { .better-xcloud-stats-bar[data-position=top-left] { left: 0; + border-radius: 0 0 4px 0; } .better-xcloud-stats-bar[data-position=top-right] { right: 0; + border-radius: 0 0 0 4px; } .better-xcloud-stats-bar[data-position=top-center] { transform: translate(-50%, 0); left: 50%; + border-radius: 0 0 4px 4px; } .better-xcloud-stats-bar[data-transparent=true] { @@ -1781,9 +1890,6 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] { min-width: 60px; display: inline-block; text-align: right; - padding-right: 8px; - margin-right: 8px; - border-right: 2px solid #fff; vertical-align: middle; } @@ -1803,11 +1909,6 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] { min-width: 30px; } -.better-xcloud-stats-bar span:last-of-type { - border: 0; - margin-right: 0; -} - .better-xcloud-stats-settings { display: none; position: fixed;