diff --git a/src/modules/controller-shortcut.ts b/src/modules/controller-shortcut.ts index b7ac3cb..9fc3519 100644 --- a/src/modules/controller-shortcut.ts +++ b/src/modules/controller-shortcut.ts @@ -7,6 +7,8 @@ import { MkbHandler } from "./mkb/mkb-handler"; import { StreamStats } from "./stream/stream-stats"; import { MicrophoneShortcut } from "./shortcuts/shortcut-microphone"; import { StreamUiShortcut } from "./shortcuts/shortcut-stream-ui"; +import { PrefKey, getPref } from "@utils/preferences"; +import { SoundShortcut } from "./shortcuts/shortcut-sound"; enum ShortcutAction { STREAM_SCREENSHOT_CAPTURE = 'stream-screenshot-capture', @@ -92,6 +94,14 @@ export class ControllerShortcut { case ShortcutAction.STREAM_MENU_TOGGLE: StreamUiShortcut.showHideStreamMenu(); break; + + case ShortcutAction.STREAM_VOLUME_INC: + SoundShortcut.increaseGainNodeVolume(10); + break; + + case ShortcutAction.STREAM_VOLUME_DEC: + SoundShortcut.decreaseGainNodeVolume(10); + break; } } @@ -232,8 +242,8 @@ export class ControllerShortcut { [ShortcutAction.STREAM_MICROPHONE_TOGGLE]: [t('stream'), t('microphone'), t('toggle')], [ShortcutAction.STREAM_MENU_TOGGLE]: [t('stream'), t('menu'), t('show')], // [ShortcutAction.STREAM_SOUND_TOGGLE]: [t('stream'), t('sound'), t('toggle')], - // [ShortcutAction.STREAM_VOLUME_INC]: getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && [t('stream'), t('volume'), t('increase')], - // [ShortcutAction.STREAM_VOLUME_DEC]: getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && [t('stream'), t('volume'), t('decrease')], + [ShortcutAction.STREAM_VOLUME_INC]: getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && [t('stream'), t('volume'), t('increase')], + [ShortcutAction.STREAM_VOLUME_DEC]: getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && [t('stream'), t('volume'), t('decrease')], } }; diff --git a/src/modules/shortcuts/shortcut-sound.ts b/src/modules/shortcuts/shortcut-sound.ts new file mode 100644 index 0000000..9d27d91 --- /dev/null +++ b/src/modules/shortcuts/shortcut-sound.ts @@ -0,0 +1,53 @@ +import { t } from "@utils/translation"; +import { STATES } from "@utils/global"; +import { PrefKey, getPref, setPref } from "@utils/preferences"; +import { Toast } from "@utils/toast"; +import { BxEvent } from "@/utils/bx-event"; +import { ceilToNearest, floorToNearest } from "@/utils/utils"; + +export class SoundShortcut { + static increaseGainNodeVolume(amount: number) { + SoundShortcut.#adjustGainNodeVolume(amount); + } + + static decreaseGainNodeVolume(amount: number) { + SoundShortcut.#adjustGainNodeVolume(-1 * Math.abs(amount)); + } + + static #adjustGainNodeVolume(amount: number): number { + if (!getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL)) { + return 0; + } + + const currentValue = getPref(PrefKey.AUDIO_VOLUME); + let nearestValue: number; + + if (amount > 0) { // Increase + nearestValue = ceilToNearest(currentValue, amount); + } else { // Decrease + nearestValue = floorToNearest(currentValue, -1 * amount); + } + + let newValue: number; + if (currentValue !== nearestValue) { + newValue = nearestValue; + } else { + newValue = currentValue + amount; + } + + newValue = setPref(PrefKey.AUDIO_VOLUME, newValue); + SoundShortcut.setGainNodeVolume(newValue); + + // Show toast + Toast.show(`${t('stream')} ❯ ${t('volume')}`, newValue + '%', {instant: true}); + BxEvent.dispatch(window, BxEvent.GAINNODE_VOLUME_CHANGED, { + volume: newValue, + }); + + return newValue; + } + + static setGainNodeVolume(value: number) { + STATES.currentStream.audioGainNode && (STATES.currentStream.audioGainNode.gain.value = value / 100); + } +} diff --git a/src/modules/ui/ui.ts b/src/modules/ui/ui.ts index d1a6dfa..ad5f2c8 100644 --- a/src/modules/ui/ui.ts +++ b/src/modules/ui/ui.ts @@ -11,6 +11,7 @@ import { t } from "@utils/translation"; import { VibrationManager } from "@modules/vibration-manager"; import { Screenshot } from "@/utils/screenshot"; import { ControllerShortcut } from "../controller-shortcut"; +import { SoundShortcut } from "../shortcuts/shortcut-sound"; export function localRedirect(path: string) { @@ -97,11 +98,20 @@ function setupQuickSettingsBar() { pref: PrefKey.AUDIO_VOLUME, label: t('volume'), onChange: (e: any, value: number) => { - STATES.currentStream.audioGainNode && (STATES.currentStream.audioGainNode.gain.value = value / 100); + SoundShortcut.setGainNodeVolume(value); }, params: { disabled: !getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL), }, + onMounted: ($elm: HTMLElement) => { + const $range = $elm.querySelector('input[type=range') as HTMLInputElement; + window.addEventListener(BxEvent.GAINNODE_VOLUME_CHANGED, e => { + $range.value = (e as any).volume; + BxEvent.dispatch($range, 'input', { + ignoreOnChange: true, + }); + }); + }, }, ], }, diff --git a/src/utils/bx-event.ts b/src/utils/bx-event.ts index a23a156..dc3cfe8 100644 --- a/src/utils/bx-event.ts +++ b/src/utils/bx-event.ts @@ -36,6 +36,7 @@ export enum BxEvent { MICROPHONE_STATE_CHANGED = 'bx-microphone-state-changed', CAPTURE_SCREENSHOT = 'bx-capture-screenshot', + GAINNODE_VOLUME_CHANGED = 'bx-gainnode-volume-changed', } export enum XcloudEvent { diff --git a/src/utils/preferences.ts b/src/utils/preferences.ts index 3f0a28c..9a9dc56 100644 --- a/src/utils/preferences.ts +++ b/src/utils/preferences.ts @@ -753,11 +753,13 @@ export class Preferences { return this.#prefs[key]; } - set(key: PrefKey, value: any) { + set(key: PrefKey, value: any): any { value = this.#validateValue(key, value); this.#prefs[key] = value; this.#updateStorage(); + + return value; } #updateStorage() { diff --git a/src/utils/settings.ts b/src/utils/settings.ts index 4cb06d8..8f61ced 100644 --- a/src/utils/settings.ts +++ b/src/utils/settings.ts @@ -183,7 +183,7 @@ export class SettingElement { $range.addEventListener('input', e => { value = parseInt((e.target as HTMLInputElement).value); $text.textContent = renderTextValue(value); - onChange && onChange(e, value); + !(e as any).ignoreOnChange && onChange && onChange(e, value); }); $wrapper.appendChild($range); diff --git a/src/utils/toast.ts b/src/utils/toast.ts index c76a158..c433dc3 100644 --- a/src/utils/toast.ts +++ b/src/utils/toast.ts @@ -15,7 +15,7 @@ export class Toast { static #timeout?: number | null; static #DURATION = 3000; - static show(msg: string, status?: string, options: Partial={}) { + static show(msg: string, status?: string, options: Partial = {}) { options = options || {}; const args = Array.from(arguments) as [string, string, ToastOptions]; @@ -43,7 +43,7 @@ export class Toast { // Get values from item const [msg, status, options] = Toast.#stack.shift()!; - if (options.html) { + if (options && options.html) { Toast.#$msg.innerHTML = msg; } else { Toast.#$msg.textContent = msg; diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 1a498b7..fa92337 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -77,3 +77,16 @@ export function renderString(str: string, obj: any){ return match; }); } + + +export function ceilToNearest(value: number, interval: number): number { + return Math.ceil(value / interval) * interval; +} + +export function floorToNearest(value: number, interval: number): number { + return Math.floor(value / interval) * interval; +} + +export function roundToNearest(value: number, interval: number): number { + return Math.round(value / interval) * interval; +}