Using a React base modal for the client/music volume change which uses a logarithmic volume bar
parent
09b636fd8d
commit
93fd5af1b8
|
@ -28,6 +28,7 @@ import * as hex from "tc-shared/crypto/hex";
|
|||
import { ClientEntry as ClientEntryView } from "./tree/Client";
|
||||
import * as React from "react";
|
||||
import {ChannelTreeEntry, ChannelTreeEntryEvents} from "tc-shared/ui/TreeEntry";
|
||||
import {spawnClientVolumeChange, spawnMusicBotVolumeChange} from "tc-shared/ui/modal/ModalChangeVolumeNew";
|
||||
|
||||
export enum ClientType {
|
||||
CLIENT_VOICE,
|
||||
|
@ -145,7 +146,8 @@ export interface ClientEvents extends ChannelTreeEntryEvents {
|
|||
client_properties: ClientProperties
|
||||
},
|
||||
notify_mute_state_change: { muted: boolean }
|
||||
notify_speak_state_change: { speaking: boolean }
|
||||
notify_speak_state_change: { speaking: boolean },
|
||||
"notify_audio_level_changed": { newValue: number },
|
||||
|
||||
"music_status_update": {
|
||||
player_buffered_index: number,
|
||||
|
@ -515,8 +517,8 @@ export class ClientEntry extends ChannelTreeEntry<ClientEvents> {
|
|||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: "client-change_nickname",
|
||||
name: (contextmenu.get_provider().html_format_enabled() ? "<b>" : "") +
|
||||
tr("Open text chat") +
|
||||
(contextmenu.get_provider().html_format_enabled() ? "</b>" : ""),
|
||||
tr("Open text chat") +
|
||||
(contextmenu.get_provider().html_format_enabled() ? "</b>" : ""),
|
||||
callback: () => {
|
||||
this.open_text_chat();
|
||||
}
|
||||
|
@ -657,15 +659,7 @@ export class ClientEntry extends ChannelTreeEntry<ClientEvents> {
|
|||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: "client-volume",
|
||||
name: tr("Change Volume"),
|
||||
callback: () => {
|
||||
spawnChangeVolume(this, true, this._audio_volume, undefined, volume => {
|
||||
this._audio_volume = volume;
|
||||
this.channelTree.client.settings.changeServer("volume_client_" + this.clientUid(), volume);
|
||||
if(this._audio_handle)
|
||||
this._audio_handle.set_volume(volume);
|
||||
//TODO: Update in info
|
||||
});
|
||||
}
|
||||
callback: () => spawnClientVolumeChange(this)
|
||||
},
|
||||
{
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
|
@ -933,6 +927,22 @@ export class ClientEntry extends ChannelTreeEntry<ClientEvents> {
|
|||
this._info_connection_promise_resolve = undefined;
|
||||
this._info_connection_promise_reject = undefined;
|
||||
}
|
||||
|
||||
setAudioVolume(value: number) {
|
||||
if(this._audio_volume == value)
|
||||
return;
|
||||
|
||||
this._audio_volume = value;
|
||||
|
||||
this.get_audio_handle()?.set_volume(value);
|
||||
this.channelTree.client.settings.changeServer("volume_client_" + this.clientUid(), value);
|
||||
|
||||
this.events.fire("notify_audio_level_changed", { newValue: value });
|
||||
}
|
||||
|
||||
getAudioVolume() {
|
||||
return this._audio_volume;
|
||||
}
|
||||
}
|
||||
|
||||
export class LocalClientEntry extends ClientEntry {
|
||||
|
@ -952,8 +962,8 @@ export class LocalClientEntry extends ClientEntry {
|
|||
...this.contextmenu_info(), {
|
||||
|
||||
name: (contextmenu.get_provider().html_format_enabled() ? "<b>" : "") +
|
||||
tr("Change name") +
|
||||
(contextmenu.get_provider().html_format_enabled() ? "</b>" : ""),
|
||||
tr("Change name") +
|
||||
(contextmenu.get_provider().html_format_enabled() ? "</b>" : ""),
|
||||
icon_class: "client-change_nickname",
|
||||
callback: () =>_self.openRename(),
|
||||
type: contextmenu.MenuEntryType.ENTRY
|
||||
|
@ -1106,8 +1116,8 @@ export class MusicClientEntry extends ClientEntry {
|
|||
contextmenu.spawn_context_menu(x, y,
|
||||
...this.contextmenu_info(), {
|
||||
name: (contextmenu.get_provider().html_format_enabled() ? "<b>" : "") +
|
||||
tr("Change bot name") +
|
||||
(contextmenu.get_provider().html_format_enabled() ? "</b>" : ""),
|
||||
tr("Change bot name") +
|
||||
(contextmenu.get_provider().html_format_enabled() ? "</b>" : ""),
|
||||
icon_class: "client-change_nickname",
|
||||
disabled: false,
|
||||
callback: () => {
|
||||
|
@ -1224,12 +1234,7 @@ export class MusicClientEntry extends ClientEntry {
|
|||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: "client-volume",
|
||||
name: tr("Change local volume"),
|
||||
callback: () => {
|
||||
spawnChangeVolume(this, true, this._audio_handle.get_volume(), undefined, volume => {
|
||||
this.channelTree.client.settings.changeServer("volume_client_" + this.clientUid(), volume);
|
||||
this._audio_handle.set_volume(volume);
|
||||
});
|
||||
}
|
||||
callback: () => spawnClientVolumeChange(this)
|
||||
},
|
||||
{
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
|
@ -1240,17 +1245,7 @@ export class MusicClientEntry extends ClientEntry {
|
|||
if(max_volume < 0)
|
||||
max_volume = 100;
|
||||
|
||||
spawnChangeVolume(this, false, this.properties.player_volume, max_volume / 100, value => {
|
||||
if(typeof(value) !== "number")
|
||||
return;
|
||||
|
||||
this.channelTree.client.serverConnection.send_command("clientedit", {
|
||||
clid: this.clientId(),
|
||||
player_volume: value,
|
||||
}).then(() => {
|
||||
//TODO: Update in info
|
||||
});
|
||||
});
|
||||
spawnMusicBotVolumeChange(this, max_volume / 100);
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -1274,11 +1269,11 @@ export class MusicClientEntry extends ClientEntry {
|
|||
callback: () => {
|
||||
const tag = $.spawn("div").append(formatMessage(tr("Do you really want to delete {0}"), this.createChatTag(false)));
|
||||
spawnYesNo(tr("Are you sure?"), $.spawn("div").append(tag), result => {
|
||||
if(result) {
|
||||
this.channelTree.client.serverConnection.send_command("musicbotdelete", {
|
||||
bot_id: this.properties.client_database_id
|
||||
});
|
||||
}
|
||||
if(result) {
|
||||
this.channelTree.client.serverConnection.send_command("musicbotdelete", {
|
||||
bot_id: this.properties.client_database_id
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
type: contextmenu.MenuEntryType.ENTRY
|
||||
|
@ -1294,7 +1289,7 @@ export class MusicClientEntry extends ClientEntry {
|
|||
handlePlayerInfo(json) {
|
||||
if(json) {
|
||||
const info = new MusicClientPlayerInfo();
|
||||
JSON.map_to(info, json);
|
||||
JSON.map_to(info, json);
|
||||
if(this._info_promise_resolve)
|
||||
this._info_promise_resolve(info);
|
||||
this._info_promise_reject = undefined;
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
@import "../../../css/static/mixin";
|
||||
|
||||
.container {
|
||||
width: 30em;
|
||||
min-width: 8em;
|
||||
flex-shrink: 1;
|
||||
max-width: 100%;
|
||||
|
||||
padding: 1em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
@include user-select(none);
|
||||
|
||||
> * {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
|
||||
.sliderContainer {
|
||||
margin-bottom: 1em;
|
||||
|
||||
.slider {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.value {
|
||||
margin-left: .5em;
|
||||
width: 3em;
|
||||
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
.reset {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.apply, .ok, .cancel {
|
||||
margin-left: 1em;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ import {sliderfy} from "tc-shared/ui/elements/Slider";
|
|||
import {createModal, Modal} from "tc-shared/ui/elements/Modal";
|
||||
import {ClientEntry} from "tc-shared/ui/client";
|
||||
import * as htmltags from "tc-shared/ui/htmltags";
|
||||
import {spawnReactModal} from "tc-shared/ui/react-elements/Modal";
|
||||
|
||||
let modal: Modal;
|
||||
export function spawnChangeVolume(client: ClientEntry, local: boolean, current: number, max: number | undefined, callback: (number) => void) {
|
||||
|
|
|
@ -0,0 +1,289 @@
|
|||
import {Modal, spawnReactModal} from "tc-shared/ui/react-elements/Modal";
|
||||
import * as React from "react";
|
||||
import {Slider} from "tc-shared/ui/react-elements/Slider";
|
||||
import {Button} from "tc-shared/ui/react-elements/Button";
|
||||
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||
import {EventHandler, ReactEventHandler, Registry} from "tc-shared/events";
|
||||
import {ClientEntry, MusicClientEntry} from "tc-shared/ui/client";
|
||||
const cssStyle = require("./ModalChangeVolume.scss");
|
||||
|
||||
export interface VolumeChangeEvents {
|
||||
"change-volume": {
|
||||
newValue: number,
|
||||
origin: "user-input" | "reset" | "unknown"
|
||||
}
|
||||
|
||||
"query-volume": {},
|
||||
"query-volume-response": {
|
||||
volume: number
|
||||
}
|
||||
|
||||
|
||||
"apply-volume": {
|
||||
newValue: number,
|
||||
origin: "user-input" | "reset" | "unknown"
|
||||
},
|
||||
"apply-volume-result": {
|
||||
newValue: number,
|
||||
success: boolean
|
||||
},
|
||||
|
||||
"close-modal": {}
|
||||
}
|
||||
|
||||
interface VolumeChangeModalState {
|
||||
state: "querying" | "applying" | "user-input";
|
||||
|
||||
volumeModifier: number;
|
||||
sliderValue: number;
|
||||
}
|
||||
|
||||
@ReactEventHandler(e => e.props.events)
|
||||
class VolumeChangeModal extends React.Component<{ clientName: string, remote: boolean, events: Registry<VolumeChangeEvents> }, VolumeChangeModalState> {
|
||||
private readonly refSlider = React.createRef<Slider>();
|
||||
|
||||
private originalValue: number;
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
volumeModifier: 1,
|
||||
sliderValue: 100,
|
||||
state: "querying"
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
this.props.events.fire("query-volume");
|
||||
}
|
||||
|
||||
render() {
|
||||
const db = Math.log2(this.state.volumeModifier) * 10;
|
||||
let valueString = db.toFixed(1) + "db";
|
||||
if(!valueString.startsWith("-") && valueString !== "0") valueString = "+" + valueString;
|
||||
|
||||
return (
|
||||
<div className={cssStyle.container}>
|
||||
<div className={cssStyle.info}>
|
||||
<a>Change value for client {this.props.clientName}</a>
|
||||
</div>
|
||||
<div className={cssStyle.sliderContainer}>
|
||||
<Slider
|
||||
minValue={0}
|
||||
maxValue={200}
|
||||
stepSize={1}
|
||||
|
||||
className={cssStyle.slider}
|
||||
tooltip={() => valueString}
|
||||
|
||||
onInput={value => this.onValueChanged(value)}
|
||||
|
||||
value={this.state.sliderValue}
|
||||
disabled={this.state.state !== "user-input"}
|
||||
|
||||
ref={this.refSlider}
|
||||
/>
|
||||
<a className={cssStyle.value}>{valueString}</a>
|
||||
</div>
|
||||
<div className={cssStyle.buttons}>
|
||||
<Button type={"small"} color={"blue"} className={cssStyle.reset} onClick={() => this.onResetClicked()} disabled={this.state.state !== "user-input" || this.state.sliderValue === 100}>
|
||||
<Translatable>Reset</Translatable>
|
||||
</Button>
|
||||
<Button type={"small"} color={"green"} className={cssStyle.apply} onClick={() => this.onApplyClick()} hidden={!this.props.remote} disabled={this.state.state !== "user-input" || this.originalValue === this.state.volumeModifier}>
|
||||
<Translatable>Apply</Translatable>
|
||||
</Button>
|
||||
<Button type={"small"} color={"red"} className={cssStyle.cancel} onClick={() => this.onCancelClick()}>
|
||||
<Translatable>Cancel</Translatable>
|
||||
</Button>
|
||||
<Button type={"small"} color={"green"} className={cssStyle.ok} onClick={() => this.onOkClick()}>
|
||||
<Translatable>Ok</Translatable>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private static slider2value(target: number) {
|
||||
if(target > 100) {
|
||||
/* between +0db and +20db */
|
||||
const value = (target - 100) * 20 / 100;
|
||||
return Math.pow(2, value / 10);
|
||||
} else if(target < 100) {
|
||||
/* between -30db and +0db */
|
||||
const value = 30 - target * 30 / 100;
|
||||
return Math.pow(2, -value / 10);
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static value2slider(value: number) {
|
||||
const db = Math.log2(value) * 10;
|
||||
if(db > 0) {
|
||||
return 100 + db * 100 / 20;
|
||||
} else if(db < 0) {
|
||||
return 100 + db * 100 / 30; /* db is negative */
|
||||
} else {
|
||||
return 100;
|
||||
}
|
||||
}
|
||||
|
||||
private onValueChanged(target: number) {
|
||||
this.props.events.fire("change-volume", {
|
||||
newValue: VolumeChangeModal.slider2value(target),
|
||||
origin: "user-input"
|
||||
});
|
||||
}
|
||||
|
||||
private onResetClicked() {
|
||||
this.props.events.fire("change-volume", { newValue: 1, origin: "reset" });
|
||||
}
|
||||
|
||||
private onApplyClick() {
|
||||
this.props.events.fire("apply-volume", {
|
||||
newValue: this.state.volumeModifier,
|
||||
origin: "user-input"
|
||||
});
|
||||
}
|
||||
|
||||
private onCancelClick() {
|
||||
this.props.events.fire("change-volume", { origin: "user-input", newValue: this.originalValue });
|
||||
this.props.events.fire("close-modal");
|
||||
}
|
||||
|
||||
private onOkClick() {
|
||||
if(this.props.remote && this.state.volumeModifier !== this.originalValue)
|
||||
this.props.events.fire("apply-volume", { origin: "user-input", newValue: this.originalValue });
|
||||
this.props.events.fire("close-modal");
|
||||
}
|
||||
|
||||
@EventHandler<VolumeChangeEvents>("change-volume")
|
||||
private handleVolumeChanged(event: VolumeChangeEvents["change-volume"]) {
|
||||
const sliderValue = VolumeChangeModal.value2slider(event.newValue);
|
||||
this.setState({
|
||||
volumeModifier: event.newValue,
|
||||
sliderValue: sliderValue
|
||||
});
|
||||
|
||||
if(event.origin !== "user-input")
|
||||
this.refSlider.current?.setState({ value: sliderValue });
|
||||
}
|
||||
|
||||
@EventHandler<VolumeChangeEvents>("query-volume")
|
||||
private handleVolumeQuery() {
|
||||
this.setState({
|
||||
state: "querying"
|
||||
});
|
||||
|
||||
this.refSlider.current?.setState({
|
||||
disabled: true
|
||||
});
|
||||
}
|
||||
|
||||
@EventHandler<VolumeChangeEvents>("apply-volume")
|
||||
private handleApplyVolume() {
|
||||
this.setState({
|
||||
state: "applying"
|
||||
});
|
||||
|
||||
this.refSlider.current?.setState({
|
||||
disabled: true
|
||||
});
|
||||
}
|
||||
|
||||
@EventHandler<VolumeChangeEvents>("query-volume-response")
|
||||
private handleVolumeQueryResponse(event: VolumeChangeEvents["query-volume-response"]) {
|
||||
const sliderValue = VolumeChangeModal.value2slider(event.volume);
|
||||
this.setState({
|
||||
volumeModifier: event.volume,
|
||||
sliderValue: sliderValue,
|
||||
state: "user-input"
|
||||
});
|
||||
|
||||
this.refSlider.current?.setState({
|
||||
value: sliderValue,
|
||||
disabled: false
|
||||
});
|
||||
|
||||
this.originalValue = event.volume;
|
||||
}
|
||||
|
||||
@EventHandler<VolumeChangeEvents>("apply-volume-result")
|
||||
private handleApplyVolumeResult(event: VolumeChangeEvents["apply-volume-result"]) {
|
||||
const sliderValue = VolumeChangeModal.value2slider(event.newValue);
|
||||
this.setState({
|
||||
volumeModifier: event.newValue,
|
||||
sliderValue: sliderValue,
|
||||
state: "user-input"
|
||||
});
|
||||
|
||||
this.refSlider.current?.setState({
|
||||
value: sliderValue,
|
||||
disabled: false
|
||||
});
|
||||
|
||||
this.originalValue = event.newValue;
|
||||
}
|
||||
}
|
||||
|
||||
export function spawnClientVolumeChange(client: ClientEntry) {
|
||||
const events = new Registry<VolumeChangeEvents>();
|
||||
|
||||
events.on("query-volume", () => {
|
||||
events.fire_async("query-volume-response", {
|
||||
volume: client.getAudioVolume()
|
||||
});
|
||||
});
|
||||
events.on("change-volume", event => {
|
||||
client.setAudioVolume(event.newValue);
|
||||
});
|
||||
|
||||
const modal = spawnReactModal(class extends Modal {
|
||||
renderBody() {
|
||||
return <VolumeChangeModal remote={false} clientName={client.clientNickName()} events={events} />;
|
||||
}
|
||||
|
||||
title(): string {
|
||||
return tr("Change local volume");
|
||||
}
|
||||
});
|
||||
|
||||
events.on("close-modal", event => modal.destroy());
|
||||
modal.show();
|
||||
return modal;
|
||||
}
|
||||
|
||||
export function spawnMusicBotVolumeChange(client: MusicClientEntry, maxValue: number) {
|
||||
//FIXME: Max value!
|
||||
const events = new Registry<VolumeChangeEvents>();
|
||||
|
||||
events.on("query-volume", () => {
|
||||
events.fire_async("query-volume-response", {
|
||||
volume: client.properties.player_volume
|
||||
});
|
||||
});
|
||||
events.on("apply-volume", event => {
|
||||
client.channelTree.client.serverConnection.send_command("clientedit", {
|
||||
clid: client.clientId(),
|
||||
player_volume: event.newValue,
|
||||
}).then(() => {
|
||||
events.fire("apply-volume-result", { newValue: client.properties.player_volume, success: true });
|
||||
}).catch(() => {
|
||||
events.fire("apply-volume-result", { newValue: client.properties.player_volume, success: false });
|
||||
});
|
||||
});
|
||||
|
||||
const modal = spawnReactModal(class extends Modal {
|
||||
renderBody() {
|
||||
return <VolumeChangeModal remote={true} clientName={client.clientNickName()} events={events} />;
|
||||
}
|
||||
|
||||
title(): string {
|
||||
return tr("Change remote volume");
|
||||
}
|
||||
});
|
||||
|
||||
events.on("close-modal", event => modal.destroy());
|
||||
modal.show();
|
||||
return modal;
|
||||
}
|
Loading…
Reference in New Issue