Using a React base modal for the client/music volume change which uses a logarithmic volume bar

canary
WolverinDEV 2020-05-20 20:47:48 +02:00
parent 09b636fd8d
commit 93fd5af1b8
4 changed files with 378 additions and 39 deletions

View File

@ -28,6 +28,7 @@ import * as hex from "tc-shared/crypto/hex";
import { ClientEntry as ClientEntryView } from "./tree/Client"; import { ClientEntry as ClientEntryView } from "./tree/Client";
import * as React from "react"; import * as React from "react";
import {ChannelTreeEntry, ChannelTreeEntryEvents} from "tc-shared/ui/TreeEntry"; import {ChannelTreeEntry, ChannelTreeEntryEvents} from "tc-shared/ui/TreeEntry";
import {spawnClientVolumeChange, spawnMusicBotVolumeChange} from "tc-shared/ui/modal/ModalChangeVolumeNew";
export enum ClientType { export enum ClientType {
CLIENT_VOICE, CLIENT_VOICE,
@ -145,7 +146,8 @@ export interface ClientEvents extends ChannelTreeEntryEvents {
client_properties: ClientProperties client_properties: ClientProperties
}, },
notify_mute_state_change: { muted: boolean } 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": { "music_status_update": {
player_buffered_index: number, player_buffered_index: number,
@ -515,8 +517,8 @@ export class ClientEntry extends ChannelTreeEntry<ClientEvents> {
type: contextmenu.MenuEntryType.ENTRY, type: contextmenu.MenuEntryType.ENTRY,
icon_class: "client-change_nickname", icon_class: "client-change_nickname",
name: (contextmenu.get_provider().html_format_enabled() ? "<b>" : "") + name: (contextmenu.get_provider().html_format_enabled() ? "<b>" : "") +
tr("Open text chat") + tr("Open text chat") +
(contextmenu.get_provider().html_format_enabled() ? "</b>" : ""), (contextmenu.get_provider().html_format_enabled() ? "</b>" : ""),
callback: () => { callback: () => {
this.open_text_chat(); this.open_text_chat();
} }
@ -657,15 +659,7 @@ export class ClientEntry extends ChannelTreeEntry<ClientEvents> {
type: contextmenu.MenuEntryType.ENTRY, type: contextmenu.MenuEntryType.ENTRY,
icon_class: "client-volume", icon_class: "client-volume",
name: tr("Change Volume"), name: tr("Change Volume"),
callback: () => { callback: () => spawnClientVolumeChange(this)
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
});
}
}, },
{ {
type: contextmenu.MenuEntryType.ENTRY, type: contextmenu.MenuEntryType.ENTRY,
@ -933,6 +927,22 @@ export class ClientEntry extends ChannelTreeEntry<ClientEvents> {
this._info_connection_promise_resolve = undefined; this._info_connection_promise_resolve = undefined;
this._info_connection_promise_reject = 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 { export class LocalClientEntry extends ClientEntry {
@ -952,8 +962,8 @@ export class LocalClientEntry extends ClientEntry {
...this.contextmenu_info(), { ...this.contextmenu_info(), {
name: (contextmenu.get_provider().html_format_enabled() ? "<b>" : "") + name: (contextmenu.get_provider().html_format_enabled() ? "<b>" : "") +
tr("Change name") + tr("Change name") +
(contextmenu.get_provider().html_format_enabled() ? "</b>" : ""), (contextmenu.get_provider().html_format_enabled() ? "</b>" : ""),
icon_class: "client-change_nickname", icon_class: "client-change_nickname",
callback: () =>_self.openRename(), callback: () =>_self.openRename(),
type: contextmenu.MenuEntryType.ENTRY type: contextmenu.MenuEntryType.ENTRY
@ -1106,8 +1116,8 @@ export class MusicClientEntry extends ClientEntry {
contextmenu.spawn_context_menu(x, y, contextmenu.spawn_context_menu(x, y,
...this.contextmenu_info(), { ...this.contextmenu_info(), {
name: (contextmenu.get_provider().html_format_enabled() ? "<b>" : "") + name: (contextmenu.get_provider().html_format_enabled() ? "<b>" : "") +
tr("Change bot name") + tr("Change bot name") +
(contextmenu.get_provider().html_format_enabled() ? "</b>" : ""), (contextmenu.get_provider().html_format_enabled() ? "</b>" : ""),
icon_class: "client-change_nickname", icon_class: "client-change_nickname",
disabled: false, disabled: false,
callback: () => { callback: () => {
@ -1224,12 +1234,7 @@ export class MusicClientEntry extends ClientEntry {
type: contextmenu.MenuEntryType.ENTRY, type: contextmenu.MenuEntryType.ENTRY,
icon_class: "client-volume", icon_class: "client-volume",
name: tr("Change local volume"), name: tr("Change local volume"),
callback: () => { callback: () => spawnClientVolumeChange(this)
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);
});
}
}, },
{ {
type: contextmenu.MenuEntryType.ENTRY, type: contextmenu.MenuEntryType.ENTRY,
@ -1240,17 +1245,7 @@ export class MusicClientEntry extends ClientEntry {
if(max_volume < 0) if(max_volume < 0)
max_volume = 100; max_volume = 100;
spawnChangeVolume(this, false, this.properties.player_volume, max_volume / 100, value => { spawnMusicBotVolumeChange(this, max_volume / 100);
if(typeof(value) !== "number")
return;
this.channelTree.client.serverConnection.send_command("clientedit", {
clid: this.clientId(),
player_volume: value,
}).then(() => {
//TODO: Update in info
});
});
} }
}, },
{ {
@ -1274,11 +1269,11 @@ export class MusicClientEntry extends ClientEntry {
callback: () => { callback: () => {
const tag = $.spawn("div").append(formatMessage(tr("Do you really want to delete {0}"), this.createChatTag(false))); 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 => { spawnYesNo(tr("Are you sure?"), $.spawn("div").append(tag), result => {
if(result) { if(result) {
this.channelTree.client.serverConnection.send_command("musicbotdelete", { this.channelTree.client.serverConnection.send_command("musicbotdelete", {
bot_id: this.properties.client_database_id bot_id: this.properties.client_database_id
}); });
} }
}); });
}, },
type: contextmenu.MenuEntryType.ENTRY type: contextmenu.MenuEntryType.ENTRY
@ -1294,7 +1289,7 @@ export class MusicClientEntry extends ClientEntry {
handlePlayerInfo(json) { handlePlayerInfo(json) {
if(json) { if(json) {
const info = new MusicClientPlayerInfo(); const info = new MusicClientPlayerInfo();
JSON.map_to(info, json); JSON.map_to(info, json);
if(this._info_promise_resolve) if(this._info_promise_resolve)
this._info_promise_resolve(info); this._info_promise_resolve(info);
this._info_promise_reject = undefined; this._info_promise_reject = undefined;

View File

@ -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;
}
}
}

View File

@ -4,6 +4,7 @@ import {sliderfy} from "tc-shared/ui/elements/Slider";
import {createModal, Modal} from "tc-shared/ui/elements/Modal"; import {createModal, Modal} from "tc-shared/ui/elements/Modal";
import {ClientEntry} from "tc-shared/ui/client"; import {ClientEntry} from "tc-shared/ui/client";
import * as htmltags from "tc-shared/ui/htmltags"; import * as htmltags from "tc-shared/ui/htmltags";
import {spawnReactModal} from "tc-shared/ui/react-elements/Modal";
let modal: Modal; let modal: Modal;
export function spawnChangeVolume(client: ClientEntry, local: boolean, current: number, max: number | undefined, callback: (number) => void) { export function spawnChangeVolume(client: ClientEntry, local: boolean, current: number, max: number | undefined, callback: (number) => void) {

View File

@ -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;
}