mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
Implemented session data for audio
This commit is contained in:
parent
5e40d7abf1
commit
f17ee42d63
@ -52,6 +52,7 @@ export class AudioManager {
|
||||
#SRSClientUnitIDs: number[] = [];
|
||||
#syncInterval: number;
|
||||
#speechRecognition: boolean = true;
|
||||
#internalTextToSpeechSource: TextToSpeechSource;
|
||||
|
||||
constructor() {
|
||||
ConfigLoadedEvent.on((config: OlympusConfig) => {
|
||||
@ -161,23 +162,41 @@ export class AudioManager {
|
||||
newRadio?.setModulation(options.modulation);
|
||||
});
|
||||
} else {
|
||||
/* Add two default radios */
|
||||
this.addRadio();
|
||||
this.addRadio();
|
||||
/* Add two default radios and connect to the microphone*/
|
||||
let newRadio = this.addRadio();
|
||||
this.#sources.find((source) => source instanceof MicrophoneSource)?.connect(newRadio);
|
||||
this.#sources.find((source) => source instanceof TextToSpeechSource)?.connect(newRadio);
|
||||
|
||||
newRadio = this.addRadio();
|
||||
this.#sources.find((source) => source instanceof MicrophoneSource)?.connect(newRadio);
|
||||
this.#sources.find((source) => source instanceof TextToSpeechSource)?.connect(newRadio);
|
||||
}
|
||||
|
||||
let sessionFileSources = getApp().getSessionDataManager().getSessionData().fileSources;
|
||||
if (sessionFileSources) {
|
||||
/* Load file sources */
|
||||
sessionFileSources.forEach((options) => {
|
||||
this.addFileSource();
|
||||
});
|
||||
}
|
||||
|
||||
let sessionUnitSinks = getApp().getSessionDataManager().getSessionData().unitSinks;
|
||||
if (sessionUnitSinks) {
|
||||
/* Load session radios */
|
||||
/* Load unit sinks */
|
||||
sessionUnitSinks.forEach((options) => {
|
||||
let unit = getApp().getUnitsManager().getUnitByID(options.ID);
|
||||
if (unit) {
|
||||
let newSink = this.addUnitSink(unit);
|
||||
this.addUnitSink(unit);
|
||||
}
|
||||
});
|
||||
}
|
||||
let sessionFileSources = getApp().getSessionDataManager().getSessionData().fileSources;
|
||||
if (sessionFileSources && sessionFileSources.length > 0) getApp().setState(OlympusState.LOAD_FILES);
|
||||
|
||||
let sessionConnections = getApp().getSessionDataManager().getSessionData().connections;
|
||||
if (sessionConnections) {
|
||||
sessionConnections.forEach((connection) => {
|
||||
this.#sources[connection[0]]?.connect(this.#sinks[connection[1]]);
|
||||
})
|
||||
}
|
||||
|
||||
this.#running = true;
|
||||
AudioManagerStateChangedEvent.dispatch(this.#running);
|
||||
@ -190,6 +209,8 @@ export class AudioManager {
|
||||
this.#devices = devices;
|
||||
AudioManagerDevicesChangedEvent.dispatch(devices);
|
||||
});
|
||||
|
||||
this.#internalTextToSpeechSource = new TextToSpeechSource();
|
||||
}
|
||||
|
||||
stop() {
|
||||
@ -220,9 +241,9 @@ export class AudioManager {
|
||||
this.#endpoint = endpoint;
|
||||
}
|
||||
|
||||
addFileSource(file) {
|
||||
console.log(`Adding file source from ${file.name}`);
|
||||
const newSource = new FileSource(file);
|
||||
addFileSource() {
|
||||
console.log(`Adding file source`);
|
||||
const newSource = new FileSource();
|
||||
this.#sources.push(newSource);
|
||||
AudioSourcesChangedEvent.dispatch(getApp().getAudioManager().getSources());
|
||||
return newSource;
|
||||
@ -250,11 +271,10 @@ export class AudioManager {
|
||||
addRadio() {
|
||||
console.log("Adding new radio");
|
||||
const newRadio = new RadioSink();
|
||||
newRadio.speechDataAvailable = (blob) => this.#speechController.analyzeData(blob);
|
||||
newRadio.speechDataAvailable = (blob) => this.#speechController.analyzeData(blob, newRadio);
|
||||
this.#sinks.push(newRadio);
|
||||
/* Set radio name by default to be incremental number */
|
||||
newRadio.setName(`Radio ${this.#sinks.length}`);
|
||||
this.#sources.find((source) => source instanceof MicrophoneSource)?.connect(newRadio);
|
||||
AudioSinksChangedEvent.dispatch(this.#sinks);
|
||||
return newRadio;
|
||||
}
|
||||
@ -327,13 +347,17 @@ export class AudioManager {
|
||||
}
|
||||
|
||||
setSpeechRecognition(speechRecognition: boolean) {
|
||||
this.#speechRecognition = this.#speechRecognition;
|
||||
this.#speechRecognition = speechRecognition;
|
||||
}
|
||||
|
||||
getSpeechRecognition() {
|
||||
return this.#speechRecognition;
|
||||
}
|
||||
|
||||
getInternalTextToSpeechSource() {
|
||||
return this.#internalTextToSpeechSource;
|
||||
}
|
||||
|
||||
#syncRadioSettings() {
|
||||
/* Send the radio settings of each radio to the SRS backend */
|
||||
let message = {
|
||||
|
||||
@ -15,13 +15,12 @@ export class FileSource extends AudioSource {
|
||||
#restartTimeout: any;
|
||||
#looping = false;
|
||||
|
||||
constructor(file) {
|
||||
super();
|
||||
setFile(file: File) {
|
||||
this.#file = file;
|
||||
this.#filename = this.#file?.name;
|
||||
|
||||
this.setName(this.#file?.name ?? "N/A");
|
||||
|
||||
|
||||
/* Create the file reader and read the file from disk */
|
||||
var reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
@ -38,6 +37,8 @@ export class FileSource extends AudioSource {
|
||||
}
|
||||
};
|
||||
reader.readAsArrayBuffer(this.#file);
|
||||
|
||||
AudioSourcesChangedEvent.dispatch(getApp().getAudioManager().getSources());
|
||||
}
|
||||
|
||||
play() {
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
import { getApp } from "../olympusapp";
|
||||
import { blobToBase64 } from "../other/utils";
|
||||
import { AudioSource } from "./audiosource";
|
||||
import { RadioSink } from "./radiosink";
|
||||
|
||||
export class SpeechController {
|
||||
constructor() {}
|
||||
|
||||
analyzeData(blob: Blob) {
|
||||
analyzeData(blob: Blob, radio: RadioSink) {
|
||||
blobToBase64(blob)
|
||||
.then((base64) => {
|
||||
const requestOptions = {
|
||||
@ -23,23 +25,45 @@ export class SpeechController {
|
||||
throw new Error("Error saving profile");
|
||||
}
|
||||
})
|
||||
.then((text) => this.#executeCommand(text))
|
||||
.then((text) => this.#executeCommand(text.toLowerCase(), radio))
|
||||
.catch((error) => console.error(error)); // Handle errors
|
||||
})
|
||||
.catch((error) => console.error(error));
|
||||
}
|
||||
|
||||
#executeCommand(text) {
|
||||
playText(text, radio: RadioSink) {
|
||||
const textToSpeechSource = getApp()
|
||||
.getAudioManager()
|
||||
.getInternalTextToSpeechSource();
|
||||
|
||||
textToSpeechSource.connect(radio);
|
||||
textToSpeechSource.playText(text);
|
||||
radio.setPtt(true);
|
||||
textToSpeechSource.onMessageCompleted = () => {
|
||||
radio.setPtt(false);
|
||||
textToSpeechSource.disconnect(radio);
|
||||
}
|
||||
}
|
||||
|
||||
#executeCommand(text, radio) {
|
||||
console.log(`Received speech command: ${text}`);
|
||||
|
||||
if (text.indexOf("olympus") === 0 ) {
|
||||
this.#olympusCommand(text);
|
||||
this.#olympusCommand(text, radio);
|
||||
} else if (text.indexOf(getApp().getAWACSController()?.getCallsign().toLowerCase()) === 0) {
|
||||
getApp().getAWACSController()?.executeCommand(text);
|
||||
getApp().getAWACSController()?.executeCommand(text, radio);
|
||||
}
|
||||
}
|
||||
|
||||
#olympusCommand(text) {
|
||||
|
||||
#olympusCommand(text, radio) {
|
||||
if (text.indexOf("request straight") > 0 || text.indexOf("request straightin") > 0) {
|
||||
this.playText("Confirm you are on step 13, being a pussy?", radio);
|
||||
}
|
||||
else if (text.indexOf("bolter") > 0) {
|
||||
this.playText("What an idiot, I never boltered, 100% boarding rate", radio);
|
||||
}
|
||||
else if (text.indexOf("read back") > 0) {
|
||||
this.playText(text.replace("olympus", ""), radio);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,11 +12,12 @@ export class TextToSpeechSource extends AudioSource {
|
||||
#audioBuffer: AudioBuffer;
|
||||
#restartTimeout: any;
|
||||
#looping = false;
|
||||
onMessageCompleted: () => void = () => {};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.setName("Text to speech")
|
||||
this.setName("Text to speech");
|
||||
}
|
||||
|
||||
playText(text: string) {
|
||||
@ -36,7 +37,7 @@ export class TextToSpeechSource extends AudioSource {
|
||||
}
|
||||
}) // Parse the response
|
||||
.then((blob) => {
|
||||
return blob.arrayBuffer()
|
||||
return blob.arrayBuffer();
|
||||
})
|
||||
.then((contents) => {
|
||||
getApp()
|
||||
@ -76,7 +77,10 @@ export class TextToSpeechSource extends AudioSource {
|
||||
|
||||
if (this.#currentPosition > this.#duration) {
|
||||
this.#currentPosition = 0;
|
||||
if (!this.#looping) this.pause();
|
||||
if (!this.#looping) {
|
||||
this.pause();
|
||||
this.onMessageCompleted();
|
||||
}
|
||||
}
|
||||
|
||||
AudioSourcesChangedEvent.dispatch(getApp().getAudioManager().getSources());
|
||||
@ -133,4 +137,3 @@ export class TextToSpeechSource extends AudioSource {
|
||||
return this.#looping;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -286,8 +286,7 @@ export enum OlympusState {
|
||||
OPTIONS = "Options",
|
||||
AUDIO = "Audio",
|
||||
AIRBASE = "Airbase",
|
||||
GAME_MASTER = "Game master",
|
||||
LOAD_FILES = "Load files"
|
||||
GAME_MASTER = "Game master"
|
||||
}
|
||||
|
||||
export const NO_SUBSTATE = "No substate";
|
||||
|
||||
@ -2,6 +2,7 @@ import { getApp } from "../olympusapp";
|
||||
import { Coalition } from "../types/types";
|
||||
import { Unit } from "../unit/unit";
|
||||
import { bearing, coalitionToEnum, computeBearingRangeString, mToFt, rad2deg } from "../other/utils";
|
||||
import { TextToSpeechSource } from "../audio/texttospeechsource";
|
||||
|
||||
const trackStrings = ["North", "North-East", "East", "South-East", "South", "South-West", "West", "North-West", "North"];
|
||||
const relTrackStrings = ["hot", "flank right", "beam right", "cold", "cold", "cold", "beam left", "flank left", "hot"];
|
||||
@ -11,9 +12,11 @@ export class AWACSController {
|
||||
#callsign: string = "Magic";
|
||||
#referenceUnit: Unit;
|
||||
|
||||
constructor() {}
|
||||
constructor() {
|
||||
|
||||
executeCommand(text) {
|
||||
}
|
||||
|
||||
executeCommand(text, radio) {
|
||||
if (text.indexOf("request picture") > 0) {
|
||||
console.log("Requested AWACS picture");
|
||||
const readout = this.createPicture(true);
|
||||
|
||||
@ -33,6 +33,7 @@ export interface SessionData {
|
||||
radios?: { frequency: number; modulation: number }[];
|
||||
fileSources?: { filename: string; volume: number }[];
|
||||
unitSinks?: {ID: number}[];
|
||||
connections?: any[];
|
||||
}
|
||||
|
||||
export interface ProfileOptions {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { AudioSink } from "./audio/audiosink";
|
||||
import { FileSource } from "./audio/filesource";
|
||||
import { RadioSink } from "./audio/radiosink";
|
||||
import { UnitSink } from "./audio/unitsink";
|
||||
@ -26,10 +27,23 @@ export class SessionDataManager {
|
||||
.filter((sink) => sink instanceof UnitSink)
|
||||
.map((unitSink) => {
|
||||
return {
|
||||
ID: unitSink.getUnit().ID
|
||||
ID: unitSink.getUnit().ID,
|
||||
};
|
||||
});
|
||||
|
||||
this.#sessionData.connections = [];
|
||||
let counter = 0;
|
||||
let sources = getApp().getAudioManager().getSources();
|
||||
let sinks = getApp().getAudioManager().getSinks();
|
||||
sources.forEach((source, idx) => {
|
||||
counter++;
|
||||
source.getConnectedTo().forEach((sink) => {
|
||||
if (sinks.indexOf(sink as AudioSink) !== undefined) {
|
||||
this.#sessionData.connections?.push([idx, sinks.indexOf(sink as AudioSink)]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.#saveSessionData();
|
||||
}
|
||||
});
|
||||
@ -41,6 +55,20 @@ export class SessionDataManager {
|
||||
.map((fileSource) => {
|
||||
return { filename: fileSource.getFilename(), volume: fileSource.getVolume() };
|
||||
});
|
||||
|
||||
this.#sessionData.connections = [];
|
||||
let counter = 0;
|
||||
let sources = getApp().getAudioManager().getSources();
|
||||
let sinks = getApp().getAudioManager().getSinks();
|
||||
sources.forEach((source, idx) => {
|
||||
counter++;
|
||||
source.getConnectedTo().forEach((sink) => {
|
||||
if (sinks.indexOf(sink as AudioSink) !== undefined) {
|
||||
this.#sessionData.connections?.push([idx, sinks.indexOf(sink as AudioSink)]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.#saveSessionData();
|
||||
}
|
||||
});
|
||||
|
||||
@ -10,10 +10,10 @@ export function OlFrequencyInput(props: { value: number; className?: string; onC
|
||||
flex gap-2
|
||||
`}>
|
||||
<OlNumberInput
|
||||
min={1}
|
||||
min={0}
|
||||
max={400}
|
||||
onChange={(e) => {
|
||||
let newValue = Math.max(Math.min(Number(e.target.value), 400), 1) * 1000000;
|
||||
let newValue = Math.max(Math.min(Number(e.target.value), 400), 0) * 1000000;
|
||||
let decimalPart = frequency - Math.floor(frequency / 1000000) * 1000000;
|
||||
frequency = newValue + decimalPart;
|
||||
props.onChange(frequency);
|
||||
|
||||
@ -3,7 +3,10 @@ import React from "react";
|
||||
export function OlLabelToggle(props: { toggled: boolean | undefined; leftLabel: string; rightLabel: string; onClick: () => void }) {
|
||||
return (
|
||||
<button
|
||||
onClick={props.onClick}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
props.onClick();
|
||||
}}
|
||||
className={`
|
||||
relative flex h-10 w-[120px] flex-none cursor-pointer select-none
|
||||
flex-row content-center justify-between rounded-md border px-1 py-[5px]
|
||||
|
||||
@ -21,7 +21,10 @@ export function OlNumberInput(props: {
|
||||
<div className="relative flex max-w-[8rem] items-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={props.onDecrease}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
props.onDecrease();
|
||||
}}
|
||||
className={`
|
||||
h-10 rounded-s-lg bg-gray-100 p-3
|
||||
dark:bg-gray-700 dark:hover:bg-gray-600 dark:focus:ring-blue-700
|
||||
@ -45,6 +48,7 @@ export function OlNumberInput(props: {
|
||||
<input
|
||||
type="text"
|
||||
onChange={props.onChange}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
min={props.min}
|
||||
max={props.max}
|
||||
className={`
|
||||
@ -59,7 +63,10 @@ export function OlNumberInput(props: {
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={props.onIncrease}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
props.onIncrease();
|
||||
}}
|
||||
className={`
|
||||
h-10 rounded-e-lg bg-gray-100 p-3
|
||||
dark:bg-gray-700 dark:hover:bg-gray-600 dark:focus:ring-blue-500
|
||||
|
||||
@ -30,13 +30,14 @@ export function OlStateButton(props: {
|
||||
textColor = "#243141";
|
||||
}
|
||||
|
||||
const opacity = (hover && !props.checked) ? "AA" : "FF";
|
||||
const opacity = hover && !props.checked ? "AA" : "FF";
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
ref={buttonRef}
|
||||
onClick={() => {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
props.onClick();
|
||||
props.onClick ?? setHover(false);
|
||||
}}
|
||||
|
||||
@ -2,10 +2,16 @@ import React from "react";
|
||||
|
||||
export function OlToggle(props: { toggled: boolean | undefined; onClick: () => void }) {
|
||||
return (
|
||||
<div className="inline-flex cursor-pointer items-center" onClick={props.onClick}>
|
||||
<div
|
||||
className="inline-flex cursor-pointer items-center"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
props.onClick();
|
||||
}}
|
||||
>
|
||||
<button className="peer sr-only" />
|
||||
<div
|
||||
data-toggled={props.toggled === true? 'true': props.toggled === undefined? 'undefined': 'false'}
|
||||
data-toggled={props.toggled === true ? "true" : props.toggled === undefined ? "undefined" : "false"}
|
||||
className={`
|
||||
peer relative h-7 w-14 rounded-full bg-gray-200
|
||||
after:absolute after:start-[4px] after:top-0.5 after:h-6 after:w-6
|
||||
|
||||
@ -1,129 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Modal } from "./components/modal";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faArrowRight, faCheck, faUpload } from "@fortawesome/free-solid-svg-icons";
|
||||
import { getApp } from "../../olympusapp";
|
||||
import { OlympusState } from "../../constants/constants";
|
||||
import { SessionDataLoadedEvent } from "../../events";
|
||||
|
||||
export function FileSourceLoadPrompt(props: { open: boolean }) {
|
||||
const [files, setFiles] = useState([] as { filename: string; volume: number }[]);
|
||||
const [loaded, setLoaded] = useState([] as boolean[]);
|
||||
|
||||
useEffect(() => {
|
||||
SessionDataLoadedEvent.on((sessionData) => {
|
||||
if (getApp().getState() === OlympusState.LOAD_FILES) return; // TODO don't like this, is hacky Should avoid reading state directly
|
||||
if (sessionData.fileSources) {
|
||||
setFiles([...sessionData.fileSources]);
|
||||
setLoaded(
|
||||
sessionData.fileSources.map((file) => {
|
||||
return false;
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={props.open}
|
||||
className={`
|
||||
inline-flex h-fit max-h-[800px] w-[600px] overflow-y-auto scroll-smooth
|
||||
bg-white p-10
|
||||
dark:bg-olympus-800
|
||||
max-md:h-full max-md:max-h-full max-md:w-full max-md:rounded-none
|
||||
max-md:border-none
|
||||
`}
|
||||
>
|
||||
<div className="flex h-full w-full flex-col gap-12">
|
||||
<div className={`flex flex-col items-start gap-2`}>
|
||||
<span
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Please, select the files for the following audio sources
|
||||
</span>
|
||||
<span
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-gray-500
|
||||
`}
|
||||
>
|
||||
Browsers can't automatically load files from your computer, therefore you must click on the following buttons to select the original files for each
|
||||
audio file source.
|
||||
</span>
|
||||
<span
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-gray-500
|
||||
`}
|
||||
>
|
||||
If you don't want to reload your audio sources, press "Skip".
|
||||
</span>
|
||||
<div className="mt-4 w-full">
|
||||
{files.map((fileData, idx) => {
|
||||
return (
|
||||
<div
|
||||
className={`flex w-full content-center justify-between gap-4`}
|
||||
>
|
||||
<span className={`my-auto truncate text-white`}>{fileData.filename}</span>
|
||||
<button
|
||||
type="button"
|
||||
disabled={loaded[idx] === true || (idx > 0 && loaded[idx - 1] == false)}
|
||||
data-disabled={loaded[idx] === true || (idx > 0 && loaded[idx - 1] == false)}
|
||||
onClick={() => {
|
||||
var input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.click();
|
||||
input.onchange = (e: Event) => {
|
||||
let target = e.target as HTMLInputElement;
|
||||
if (target && target.files) {
|
||||
var file = target.files[0];
|
||||
getApp().getAudioManager().addFileSource(file).setVolume(fileData.volume);
|
||||
loaded[idx] = true;
|
||||
setLoaded([...loaded]);
|
||||
if (idx === loaded.length - 1) getApp().setState(OlympusState.IDLE);
|
||||
}
|
||||
};
|
||||
}}
|
||||
className={`
|
||||
mb-2 me-2 ml-auto flex cursor-pointer content-center
|
||||
items-center gap-2 rounded-sm bg-blue-600 px-5 py-2.5
|
||||
text-sm font-medium text-white
|
||||
data-[disabled="true"]:bg-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-800
|
||||
hover:bg-blue-700
|
||||
`}
|
||||
>
|
||||
{loaded[idx] ? "Loaded" : "Load"}
|
||||
<FontAwesomeIcon className={`my-auto`} icon={loaded[idx] ? faCheck : faUpload} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {getApp().setState(OlympusState.IDLE)}}
|
||||
className={`
|
||||
mb-2 me-2 ml-auto flex content-center items-center gap-2
|
||||
rounded-sm border-[1px] bg-blue-700 px-5 py-2.5 text-sm
|
||||
font-medium text-white
|
||||
dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400
|
||||
dark:hover:bg-gray-700 dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
>
|
||||
Skip
|
||||
<FontAwesomeIcon className={`my-auto`} icon={faArrowRight} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@ -202,18 +202,7 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
onClick={() => {
|
||||
var input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.click();
|
||||
input.onchange = (e: Event) => {
|
||||
let target = e.target as HTMLInputElement;
|
||||
if (target && target.files) {
|
||||
var file = target.files[0];
|
||||
getApp().getAudioManager().addFileSource(file);
|
||||
}
|
||||
};
|
||||
}}
|
||||
onClick={() => getApp().getAudioManager().addFileSource()}
|
||||
>
|
||||
Add audio source
|
||||
</button>
|
||||
|
||||
@ -18,11 +18,14 @@ export const RadioSinkPanel = forwardRef((props: { radio: RadioSink; shortcutKey
|
||||
<div
|
||||
data-receiving={props.radio.getReceiving()}
|
||||
className={`
|
||||
box-border flex flex-col content-center justify-between gap-2 rounded-md
|
||||
border-2 border-transparent bg-olympus-200/30 px-4 py-3
|
||||
box-border flex cursor-pointer flex-col content-center justify-between
|
||||
gap-2 rounded-md border-2 border-transparent bg-olympus-200/30 px-4 py-3
|
||||
data-[receiving='true']:border-white
|
||||
`}
|
||||
ref={ref}
|
||||
onClick={() => {
|
||||
setExpanded(!expanded);
|
||||
}}
|
||||
>
|
||||
<div className="flex content-center justify-between gap-2">
|
||||
<div
|
||||
|
||||
@ -8,6 +8,7 @@ import { OlRangeSlider } from "../../components/olrangeslider";
|
||||
import { FileSource } from "../../../audio/filesource";
|
||||
import { MicrophoneSource } from "../../../audio/microphonesource";
|
||||
import { TextToSpeechSource } from "../../../audio/texttospeechsource";
|
||||
import { FaUpload } from "react-icons/fa";
|
||||
|
||||
export const AudioSourcePanel = forwardRef((props: { source: AudioSource; onExpanded: () => void }, ref: ForwardedRef<HTMLDivElement>) => {
|
||||
const [meterLevel, setMeterLevel] = useState(0);
|
||||
@ -48,7 +49,47 @@ export const AudioSourcePanel = forwardRef((props: { source: AudioSource; onExpa
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full overflow-hidden">
|
||||
<span className={`my-auto truncate`}>{props.source.getName()}</span>
|
||||
<div className={`my-auto w-full truncate`}>
|
||||
{props.source.getName() === "" ? (
|
||||
props.source instanceof FileSource ? (
|
||||
<div
|
||||
className="flex w-full content-center justify-between"
|
||||
>
|
||||
<span className={`my-auto text-red-500`}>No file selected</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
var input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.click();
|
||||
input.onchange = (e: Event) => {
|
||||
let target = e.target as HTMLInputElement;
|
||||
if (target && target.files) {
|
||||
var file = target.files[0];
|
||||
(props.source as FileSource).setFile(file)
|
||||
|
||||
}
|
||||
};
|
||||
}}
|
||||
className={`
|
||||
flex cursor-pointer content-center items-center gap-2
|
||||
rounded-sm bg-blue-600 px-5 py-2.5 text-sm font-medium
|
||||
text-white
|
||||
data-[disabled="true"]:bg-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-800
|
||||
hover:bg-blue-700
|
||||
`}
|
||||
>
|
||||
<FaUpload className={`my-auto`} />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
"No name"
|
||||
)
|
||||
) : (
|
||||
props.source.getName()
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{!(props.source instanceof MicrophoneSource) && !(props.source instanceof TextToSpeechSource) && (
|
||||
<div
|
||||
@ -68,22 +109,22 @@ export const AudioSourcePanel = forwardRef((props: { source: AudioSource; onExpa
|
||||
<>
|
||||
{(props.source instanceof FileSource || props.source instanceof TextToSpeechSource) && (
|
||||
<div className="flex flex-col gap-2 rounded-md bg-olympus-400 p-2">
|
||||
{props.source instanceof TextToSpeechSource &&
|
||||
<input
|
||||
className={`
|
||||
block h-10 w-full border-[2px] bg-gray-50 py-2.5 text-center
|
||||
text-sm text-gray-900
|
||||
dark:border-gray-700 dark:bg-olympus-600 dark:text-white
|
||||
dark:placeholder-gray-400 dark:focus:border-blue-700
|
||||
dark:focus:ring-blue-700
|
||||
focus:border-blue-700 focus:ring-blue-500
|
||||
`}
|
||||
value={text}
|
||||
onChange={(ev) => {
|
||||
setText(ev.target.value);
|
||||
}}
|
||||
></input>
|
||||
}
|
||||
{props.source instanceof TextToSpeechSource && (
|
||||
<input
|
||||
className={`
|
||||
block h-10 w-full border-[2px] bg-gray-50 py-2.5 text-center
|
||||
text-sm text-gray-900
|
||||
dark:border-gray-700 dark:bg-olympus-600 dark:text-white
|
||||
dark:placeholder-gray-400 dark:focus:border-blue-700
|
||||
dark:focus:ring-blue-700
|
||||
focus:border-blue-700 focus:ring-blue-500
|
||||
`}
|
||||
value={text}
|
||||
onChange={(ev) => {
|
||||
setText(ev.target.value);
|
||||
}}
|
||||
></input>
|
||||
)}
|
||||
<div className="flex gap-4">
|
||||
<OlStateButton
|
||||
checked={false}
|
||||
@ -97,7 +138,8 @@ export const AudioSourcePanel = forwardRef((props: { source: AudioSource; onExpa
|
||||
<OlRangeSlider
|
||||
value={props.source.getDuration() > 0 ? (props.source.getCurrentPosition() / props.source.getDuration()) * 100 : 0}
|
||||
onChange={(ev) => {
|
||||
if (props.source instanceof FileSource || props.source instanceof TextToSpeechSource) props.source.setCurrentPosition(parseFloat(ev.currentTarget.value));
|
||||
if (props.source instanceof FileSource || props.source instanceof TextToSpeechSource)
|
||||
props.source.setCurrentPosition(parseFloat(ev.currentTarget.value));
|
||||
}}
|
||||
className="my-auto"
|
||||
/>
|
||||
@ -124,9 +166,10 @@ export const AudioSourcePanel = forwardRef((props: { source: AudioSource; onExpa
|
||||
flex-row border-gray-500
|
||||
`}
|
||||
>
|
||||
<div style={{ minWidth: `${meterLevel * 100}%` }} className={`
|
||||
rounded-full bg-gray-200
|
||||
`}></div>
|
||||
<div
|
||||
style={{ minWidth: `${meterLevel * 100}%` }}
|
||||
className={`rounded-full bg-gray-200`}
|
||||
></div>
|
||||
</div>
|
||||
<OlRangeSlider
|
||||
value={props.source.getVolume() * 100}
|
||||
|
||||
@ -11,7 +11,6 @@ import { MapHiddenTypes, MapOptions } from "../types/types";
|
||||
import { NO_SUBSTATE, OlympusState, OlympusSubState, OptionsSubstate, SpawnSubState, UnitControlSubState } from "../constants/constants";
|
||||
import { getApp, setupApp } from "../olympusapp";
|
||||
import { LoginModal } from "./modals/loginmodal";
|
||||
import { FileSourceLoadPrompt } from "./modals/filesourceloadprompt";
|
||||
|
||||
import { MiniMapPanel } from "./panels/minimappanel";
|
||||
import { UnitControlBar } from "./panels/unitcontrolbar";
|
||||
@ -70,8 +69,7 @@ export function UI() {
|
||||
<LoginModal open={appState === OlympusState.LOGIN} />
|
||||
<ProtectionPromptModal open={appState === OlympusState.UNIT_CONTROL && appSubState == UnitControlSubState.PROTECTION} />
|
||||
<KeybindModal open={appState === OlympusState.OPTIONS && appSubState === OptionsSubstate.KEYBIND} />
|
||||
<FileSourceLoadPrompt open={appState === OlympusState.LOAD_FILES}/>
|
||||
|
||||
|
||||
<div id="map-container" className="z-0 h-full w-screen" />
|
||||
|
||||
<MainMenu open={appState === OlympusState.MAIN_MENU} onClose={() => getApp().setState(OlympusState.IDLE)} />
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user