Förbättrat ljudsystemet
This commit is contained in:
101
src/app.js
101
src/app.js
@@ -15,7 +15,7 @@ import { getSessionsForEventHelper, getModeLabelHelper, normalizeStartModeHelper
|
||||
import { renderEventWorkspaceView } from "./event_workspace_controller.js";
|
||||
import { renderEventManagerMarkup } from "./event_manager_view.js";
|
||||
import { renderEventManagerView } from "./event_manager_controller.js";
|
||||
import { createDefaultAmmcConfigHelper, getManagedWsUrlHelper, loadAmmcConfigFromBackendHelper, saveAmmcConfigToBackendHelper, refreshAmmcStatusHelper, startManagedAmmcHelper, stopManagedAmmcHelper, applyPersistedStateHelper, hydrateFromBackendHelper, scheduleBackendSyncHelper, syncStateToBackendHelper, pingBackendHelper, checkAppVersionHelper, startAppVersionPollingHelper, startOverlaySyncHelper, startOverlayRotationHelper, startOverlayLiveRefreshHelper, ensureAudioContextHelper, playPassingBeepHelper, playFinishSirenHelper, playLeaderCueHelper, playStartCueHelper, playBestLapCueHelper, pushOverlayEventHelper, speakTextHelper, announcePassingHelper, announceRaceFinishedHelper, handleSessionTimerTickHelper, tickClockHelper } from "./runtime_services.js";
|
||||
import { createDefaultAmmcConfigHelper, getManagedWsUrlHelper, loadAmmcConfigFromBackendHelper, saveAmmcConfigToBackendHelper, refreshAmmcStatusHelper, startManagedAmmcHelper, stopManagedAmmcHelper, applyPersistedStateHelper, hydrateFromBackendHelper, scheduleBackendSyncHelper, syncStateToBackendHelper, pingBackendHelper, checkAppVersionHelper, startAppVersionPollingHelper, startOverlaySyncHelper, startOverlayRotationHelper, startOverlayLiveRefreshHelper, ensureAudioContextHelper, playPassingBeepHelper, playFinishSirenHelper, playLeaderCueHelper, playStartCueHelper, playBestLapCueHelper, pushOverlayEventHelper, getSpeechVoicesHelper, speakTextHelper, announcePassingHelper, announceRaceStartedHelper, announceRaceFinishedHelper, handleSessionTimerTickHelper, tickClockHelper } from "./runtime_services.js";
|
||||
import { connectDecoderHelper, disconnectDecoderHelper, processDecoderMessageHelper } from "./decoder_runtime.js";
|
||||
import { normalizeTrainingState as normalizeTrainingStateLogic, syncTrainingLedger as syncTrainingLedgerLogic, buildDriverTrainingStats as buildDriverTrainingStatsLogic, buildTrainingRecommendation as buildTrainingRecommendationLogic, getTrainingGoalProgress as getTrainingGoalProgressLogic, getNextTrainingMilestone as getNextTrainingMilestoneLogic } from "./training_logic.js";
|
||||
import { renderTrainingView } from "./training_views.js";
|
||||
@@ -141,10 +141,12 @@ const playFinishSiren = () => playFinishSirenHelper({ ensureAudioContext });
|
||||
const playLeaderCue = () => playLeaderCueHelper({ ensureAudioContext });
|
||||
const playStartCue = () => playStartCueHelper({ ensureAudioContext });
|
||||
const playBestLapCue = () => playBestLapCueHelper({ ensureAudioContext });
|
||||
const pushOverlayEvent = (type, label) => pushOverlayEventHelper(type, label, { uid, state, t, getOverlayEvents: () => overlayEvents, setOverlayEvents: (value) => { overlayEvents = value; }, getOverlayMode: () => overlayMode, getCurrentView: () => currentView, getOverlayViewMode: () => overlayViewMode, playLeaderCue, playPassingBeep, playFinishSiren, playStartCue, playBestLapCue });
|
||||
const speakText = (text) => speakTextHelper(text, { state, currentLanguage });
|
||||
const pushOverlayEvent = (type, label, options = {}) => pushOverlayEventHelper(type, label, { uid, state, t, getOverlayEvents: () => overlayEvents, setOverlayEvents: (value) => { overlayEvents = value; }, getOverlayMode: () => overlayMode, getCurrentView: () => currentView, getOverlayViewMode: () => overlayViewMode, playLeaderCue, playPassingBeep, playFinishSiren, playStartCue, playBestLapCue }, options);
|
||||
const getSpeechVoices = () => getSpeechVoicesHelper();
|
||||
const speakText = (text, options = {}) => speakTextHelper(text, { state, currentLanguage }, options);
|
||||
const announcePassing = (entry) => announcePassingHelper(entry, { state, t, playPassingBeep, speakText });
|
||||
const announceRaceFinished = () => announceRaceFinishedHelper({ state, t, getActiveSession, pushOverlayEvent, playFinishSiren });
|
||||
const announceRaceStarted = (session) => announceRaceStartedHelper(session, { state, t, pushOverlayEvent, playStartCue, speakText });
|
||||
const announceRaceFinished = () => announceRaceFinishedHelper({ state, t, getActiveSession, pushOverlayEvent, playFinishSiren, speakText });
|
||||
const handleSessionTimerTick = () => handleSessionTimerTickHelper({ getActiveSession, isUntimedSession, getSessionTiming, saveState, getLastFinishAnnouncementSessionId: () => lastFinishAnnouncementSessionId, setLastFinishAnnouncementSessionId: (value) => { lastFinishAnnouncementSessionId = value; }, announceRaceFinished });
|
||||
const tickClock = () => tickClockHelper({ dom, currentLanguage, handleSessionTimerTick, getActiveSession, getCurrentView: () => currentView, getOverlayMode: () => overlayMode, renderView, updateHeaderState });
|
||||
const buildTeamRaceResultsHtml = (event) => buildTeamRaceResultsHtmlHelper(event, { t, escapeHtml, resolveEventBranding, getClassName, buildPrintBrandBlock, buildTeamRaceStandings, renderTable, formatLap, renderTeamStintLog });
|
||||
@@ -196,7 +198,7 @@ const renderDrivers = () => renderDriversView({ state, dom, t, driverBrandFilter
|
||||
const renderCars = () => renderCarsView({ state, dom, t, carBrandFilter: () => carBrandFilter, setCarBrandFilter: (value) => { carBrandFilter = value; }, selectedCarEditId: () => selectedCarEditId, setSelectedCarEditId: (value) => { selectedCarEditId = value; }, uid, saveState, renderView, renderTable, escapeHtml, setFormError, bindModalShell, normalizeCar });
|
||||
const renderGuide = () => renderGuideView({ dom, t });
|
||||
const renderOverlay = () => renderOverlayPageView({ state, dom, t, escapeHtml, formatLap, formatCountdown, formatElapsedClock, formatPredictedLapDelta, getActiveSession, buildLeaderboard, ensureSessionResult, getSessionTiming, getVisiblePassings, resolveEventBranding, buildPracticeStandings, buildQualifyingStandings, buildFinalStandings, getOverlayModeLabel, getStatusLabel, getObsOverlayConfig, buildOverlayPanels, normalizeStartMode, renderPositionGrid, getEventName, getSessionTypeLabel, getStartModeLabel, renderRaceStandingsTableView, renderTeamOverlay, renderObsOverlay, renderOverlayLeaderboard, renderOverlaySidePanel, formatTeamActiveMemberLabel, getSessionGridEntries, overlayViewMode, overlayMode, publicOverlayMode, overlayEvents, overlayRotationIndex, openOverlayWindow, buildOverlayUrl });
|
||||
const renderTiming = () => renderTimingView({ state, dom, t, escapeHtml, formatLap, formatCountdown, formatElapsedClock, formatRaceClock, renderTable, uid, getActiveSession, ensureSessionResult, buildLeaderboard, getSessionTiming, getVisiblePassings, getEventName, getSessionTypeLabel, getStatusLabel, normalizeStartMode, getStartModeLabel, renderPositionGrid, connectDecoder, disconnectDecoder, applyCompetitorCorrection, invalidateCompetitorLastLap, restoreCompetitorLastInvalidLap, ensureAudioContext, validateTrackSessionForStart, pushOverlayEvent, saveState, updateHeaderState, renderView, normalizeDriver, normalizeCar, processDecoderMessage, getSelectedLeaderboardKey: () => selectedLeaderboardKey, setSelectedLeaderboardKey: (value) => { selectedLeaderboardKey = value; }, getQuickAddDraft: () => quickAddDraft, setQuickAddDraft: (value) => { quickAddDraft = value; }, getPassingValidationLabel, isCountedPassing, getCompetitorPassings, getManualCorrectionSummary, formatTeamActiveMemberLabel, lastFinishAnnouncementSessionId: () => lastFinishAnnouncementSessionId, setLastFinishAnnouncementSessionId: (value) => { lastFinishAnnouncementSessionId = value; }, lastOverlayLeaderKeyBySession, lastOverlayTop3BySession, overlayEvents: () => overlayEvents, setOverlayEvents: (value) => { overlayEvents = value; } });
|
||||
const renderTiming = () => renderTimingView({ state, dom, t, escapeHtml, formatLap, formatCountdown, formatElapsedClock, formatRaceClock, renderTable, uid, getActiveSession, ensureSessionResult, buildLeaderboard, getSessionTiming, getVisiblePassings, getEventName, getSessionTypeLabel, getStatusLabel, normalizeStartMode, getStartModeLabel, renderPositionGrid, connectDecoder, disconnectDecoder, applyCompetitorCorrection, invalidateCompetitorLastLap, restoreCompetitorLastInvalidLap, ensureAudioContext, validateTrackSessionForStart, announceRaceStarted, announceRaceFinished, saveState, updateHeaderState, renderView, normalizeDriver, normalizeCar, processDecoderMessage, getSelectedLeaderboardKey: () => selectedLeaderboardKey, setSelectedLeaderboardKey: (value) => { selectedLeaderboardKey = value; }, getQuickAddDraft: () => quickAddDraft, setQuickAddDraft: (value) => { quickAddDraft = value; }, getPassingValidationLabel, isCountedPassing, getCompetitorPassings, getManualCorrectionSummary, formatTeamActiveMemberLabel, lastFinishAnnouncementSessionId: () => lastFinishAnnouncementSessionId, setLastFinishAnnouncementSessionId: (value) => { lastFinishAnnouncementSessionId = value; }, lastOverlayLeaderKeyBySession, lastOverlayTop3BySession, overlayEvents: () => overlayEvents, setOverlayEvents: (value) => { overlayEvents = value; } });
|
||||
const renderJudging = () => renderJudgingView({ state, dom, t, escapeHtml, formatLap, renderTable, getActiveSession, ensureSessionResult, buildLeaderboard, getSessionTypeLabel, getJudgeFilteredRows, getJudgeFilteredLog, getCompetitorPassings, isCountedPassing, getPassingValidationLabel, undoJudgingAdjustment, applyCompetitorCorrection, invalidateCompetitorLastLap, restoreCompetitorLastInvalidLap, renderView, getSelectedJudgeKey: () => selectedJudgeKey, setSelectedJudgeKey: (value) => { selectedJudgeKey = value; }, getJudgingCompetitorFilter: () => judgingCompetitorFilter, setJudgingCompetitorFilter: (value) => { judgingCompetitorFilter = value; }, getJudgingLogFilter: () => judgingLogFilter, setJudgingLogFilter: (value) => { judgingLogFilter = value; } });
|
||||
const normalizeTrainingState = (training) => normalizeTrainingStateLogic(training, { uid });
|
||||
const syncTrainingLedger = () => {
|
||||
@@ -416,6 +418,9 @@ const TRANSLATIONS = {
|
||||
"drivers.brand_placeholder": "Team / märke (valfritt)",
|
||||
"drivers.brand_filter_placeholder": "Sök namn / transponder / brand",
|
||||
"drivers.transponder_placeholder": "Personlig transponder (valfritt)",
|
||||
"drivers.speech_name": "Uttalsnamn",
|
||||
"drivers.speech_name_placeholder": "Uttal för uppläsning (valfritt)",
|
||||
"drivers.speech_name_hint": "Skriv namnet som webbläsarrösten ska uttala det. Lämna tomt för vanligt förarnamn.",
|
||||
"drivers.add": "Lägg till förare",
|
||||
"drivers.title": "Förare",
|
||||
"cars.create": "Skapa bil",
|
||||
@@ -771,7 +776,11 @@ const TRANSLATIONS = {
|
||||
"timing.started": "Startad",
|
||||
"timing.remaining": "Nedräkning",
|
||||
"timing.elapsed": "Körtid",
|
||||
"timing.race_finished": "Race is finished",
|
||||
"timing.race_finished": "Racet är avslutat",
|
||||
"timing.race_started_voice": "Racet har startat",
|
||||
"timing.race_finished_voice": "Racet är avslutat",
|
||||
"timing.practice_started_voice": "Träningen har startat",
|
||||
"timing.practice_finished_voice": "Träningen är avslutad",
|
||||
"timing.follow_up": "Follow-up",
|
||||
"timing.follow_up_active": "Follow-up aktiv",
|
||||
"timing.no_active": "Ingen aktiv session vald.",
|
||||
@@ -883,9 +892,20 @@ const TRANSLATIONS = {
|
||||
"settings.passing_sound_off": "Av",
|
||||
"settings.passing_sound_beep": "Blipp",
|
||||
"settings.passing_sound_name": "Säg förarnamn",
|
||||
"settings.finish_voice": "Spela finish-siren",
|
||||
"settings.finish_voice": "Läs upp start och avslut",
|
||||
"settings.start_sound": "Startsignal",
|
||||
"settings.finish_sound": "Målsignal",
|
||||
"settings.speech_voice": "Talröst",
|
||||
"settings.system_default_voice": "Automatisk röst för valt språk",
|
||||
"settings.speech_rate": "Talhastighet",
|
||||
"settings.speech_pitch": "Tonhöjd",
|
||||
"settings.test_start": "Testa start",
|
||||
"settings.test_finish": "Testa mål",
|
||||
"settings.test_driver_voice": "Testa förarnamn",
|
||||
"settings.driver_voice_sample": "Testförare",
|
||||
"settings.test_audio": "Testa ljud",
|
||||
"settings.audio_note": "Browsern kräver oftast ett klick först innan ljud/tal tillåts.",
|
||||
"settings.voice_note": "Rösterna kommer från operativsystemet. På Windows ger Microsofts svenska röst oftast bäst uttal. Ange vid behov ett särskilt uttalsnamn under Förare.",
|
||||
"settings.branding": "Klubbinfo / PDF",
|
||||
"settings.club_name": "Klubbnamn",
|
||||
"settings.club_tagline": "Klubbtext",
|
||||
@@ -1008,6 +1028,12 @@ const TRANSLATIONS = {
|
||||
"guide.card_team_blurb": "Lagrace och endurance med team, stintlogg och summerade varv över lång tid.",
|
||||
"guide.card_training_blurb": "Förarutveckling, beständig varvhistorik, träningsmål och belöningar i mobilvänlig vy.",
|
||||
"guide.card_decoder_blurb": "AMMC, backend, WebSocket, Windows/Linux och lokal SQLite-lagring.",
|
||||
"guide.audio_title": "Start-, mål- och förarljud",
|
||||
"guide.audio_1": "1. Öppna Inställningar -> Ljud och aktivera webbläsarljud, startsignal, målsignal och uppläsning efter behov.",
|
||||
"guide.audio_2": "2. Välj en svensk systemröst och provlyssna med Testa start, Testa mål och Testa förarnamn. Webbläsaren kräver normalt att du klickar på sidan innan ljud tillåts.",
|
||||
"guide.audio_3": "3. Välj Blipp eller Säg förarnamn för passeringar. Om ett namn redan läses upp används ett kort blipp i stället så att röster inte staplas ovanpå varandra.",
|
||||
"guide.audio_4": "4. Om ett namn uttalas fel går du till Förare och anger Uttalsnamn, exempelvis en fonetisk stavning. Visningsnamnet ändras inte.",
|
||||
"guide.audio_5": "5. Ljudet spelas i webbläsaren på datorn där Tidtagning körs. Kontrollera därför rätt ljudutgång och håll den fliken öppen under sessionen.",
|
||||
"guide.sponsor_title": "Skapa Sponsor Event: 10 personer, 4 bilar",
|
||||
"guide.sponsor_1": "1. Lägg upp 4 bilar i sidan Bilar med unikt transponder-ID. Lägg gärna också in märke/modell i brandfältet.",
|
||||
"guide.sponsor_2": "2. Lägg upp 10 förare i sidan Förare. Du kan också spara team/märke i brandfältet och filtrera listan på det senare.",
|
||||
@@ -1343,6 +1369,9 @@ const TRANSLATIONS = {
|
||||
"drivers.brand_placeholder": "Team / brand (optional)",
|
||||
"drivers.brand_filter_placeholder": "Search name / transponder / brand",
|
||||
"drivers.transponder_placeholder": "Personal transponder (optional)",
|
||||
"drivers.speech_name": "Spoken name",
|
||||
"drivers.speech_name_placeholder": "Pronunciation for announcements (optional)",
|
||||
"drivers.speech_name_hint": "Enter the name as the browser voice should pronounce it. Leave blank to use the regular driver name.",
|
||||
"drivers.add": "Add Driver",
|
||||
"drivers.title": "Drivers",
|
||||
"cars.create": "Create Track Car",
|
||||
@@ -1699,6 +1728,10 @@ const TRANSLATIONS = {
|
||||
"timing.remaining": "Countdown",
|
||||
"timing.elapsed": "Elapsed",
|
||||
"timing.race_finished": "Race is finished",
|
||||
"timing.race_started_voice": "Race started",
|
||||
"timing.race_finished_voice": "Race finished",
|
||||
"timing.practice_started_voice": "Practice started",
|
||||
"timing.practice_finished_voice": "Practice finished",
|
||||
"timing.follow_up": "Follow-up",
|
||||
"timing.follow_up_active": "Follow-up active",
|
||||
"timing.no_active": "No active session selected.",
|
||||
@@ -1810,9 +1843,20 @@ const TRANSLATIONS = {
|
||||
"settings.passing_sound_off": "Off",
|
||||
"settings.passing_sound_beep": "Beep",
|
||||
"settings.passing_sound_name": "Speak driver name",
|
||||
"settings.finish_voice": "Play finish siren",
|
||||
"settings.finish_voice": "Announce start and finish",
|
||||
"settings.start_sound": "Start signal",
|
||||
"settings.finish_sound": "Finish signal",
|
||||
"settings.speech_voice": "Speech voice",
|
||||
"settings.system_default_voice": "Automatic voice for selected language",
|
||||
"settings.speech_rate": "Speech rate",
|
||||
"settings.speech_pitch": "Speech pitch",
|
||||
"settings.test_start": "Test start",
|
||||
"settings.test_finish": "Test finish",
|
||||
"settings.test_driver_voice": "Test driver name",
|
||||
"settings.driver_voice_sample": "Test driver",
|
||||
"settings.test_audio": "Test audio",
|
||||
"settings.audio_note": "Browsers usually require a click first before sound/speech is allowed.",
|
||||
"settings.voice_note": "Voices are provided by the operating system. Select a matching local voice and set a separate spoken name under Drivers when pronunciation needs help.",
|
||||
"settings.branding": "Club Info / PDF",
|
||||
"settings.club_name": "Club name",
|
||||
"settings.club_tagline": "Club tagline",
|
||||
@@ -1935,6 +1979,12 @@ const TRANSLATIONS = {
|
||||
"guide.card_team_blurb": "Team race and endurance with teams, stint logs and total laps over long sessions.",
|
||||
"guide.card_training_blurb": "Driver development, durable lap history, practice goals and rewards in a mobile-friendly view.",
|
||||
"guide.card_decoder_blurb": "AMMC, backend, WebSocket, Windows/Linux and local SQLite storage.",
|
||||
"guide.audio_title": "Start, finish, and driver audio",
|
||||
"guide.audio_1": "1. Open Settings -> Audio and enable browser audio, the start signal, finish signal, and voice announcements as required.",
|
||||
"guide.audio_2": "2. Select a matching system voice and preview it with Test start, Test finish, and Test driver name. The browser normally requires a click before audio is allowed.",
|
||||
"guide.audio_3": "3. Select Beep or Speak driver name for passings. If speech is already busy, a short beep is used instead so announcements do not overlap.",
|
||||
"guide.audio_4": "4. If a name is pronounced incorrectly, open Drivers and enter a phonetic Spoken name. The displayed driver name does not change.",
|
||||
"guide.audio_5": "5. Audio plays in the browser on the computer running Timing. Check that computer's audio output and keep the tab open during the session.",
|
||||
"guide.sponsor_title": "Create Sponsor Event: 10 drivers, 4 cars",
|
||||
"guide.sponsor_1": "1. Add 4 cars in Cars with unique transponder IDs. You can also store brand/model in the brand field.",
|
||||
"guide.sponsor_2": "2. Add 10 drivers in Drivers. You can also store team/brand in the brand field and filter by it later.",
|
||||
@@ -2200,6 +2250,7 @@ function normalizeDriver(driver = {}) {
|
||||
classId: String(driver?.classId || "").trim(),
|
||||
transponder: String(driver?.transponder || "").trim(),
|
||||
brand: String(driver?.brand || "").trim(),
|
||||
speechName: String(driver?.speechName || "").trim(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2340,6 +2391,11 @@ init();
|
||||
|
||||
async function init() {
|
||||
document.body.classList.toggle("overlay-mode", overlayMode);
|
||||
if ("speechSynthesis" in window) {
|
||||
window.speechSynthesis.addEventListener("voiceschanged", () => {
|
||||
if (currentView === "settings") renderView();
|
||||
});
|
||||
}
|
||||
seedDefaultData();
|
||||
applyTheme();
|
||||
renderNav();
|
||||
@@ -2408,6 +2464,18 @@ function seedDefaultData() {
|
||||
state.settings.finishVoiceEnabled = true;
|
||||
}
|
||||
|
||||
if (typeof state.settings.startSoundEnabled !== "boolean") {
|
||||
state.settings.startSoundEnabled = true;
|
||||
}
|
||||
|
||||
if (typeof state.settings.finishSoundEnabled !== "boolean") {
|
||||
state.settings.finishSoundEnabled = true;
|
||||
}
|
||||
|
||||
state.settings.speechVoiceUri = String(state.settings.speechVoiceUri || "");
|
||||
state.settings.speechRate = Math.max(0.65, Math.min(1.35, Number(state.settings.speechRate || 0.95) || 0.95));
|
||||
state.settings.speechPitch = Math.max(0.7, Math.min(1.3, Number(state.settings.speechPitch || 1) || 1));
|
||||
|
||||
if (typeof state.settings.speakerPassingCueEnabled !== "boolean") {
|
||||
state.settings.speakerPassingCueEnabled = false;
|
||||
}
|
||||
@@ -2489,6 +2557,11 @@ function loadState() {
|
||||
audioEnabled: parsed.settings?.audioEnabled !== false,
|
||||
passingSoundMode: parsed.settings?.passingSoundMode || "beep",
|
||||
finishVoiceEnabled: parsed.settings?.finishVoiceEnabled !== false,
|
||||
startSoundEnabled: parsed.settings?.startSoundEnabled !== false,
|
||||
finishSoundEnabled: parsed.settings?.finishSoundEnabled !== false,
|
||||
speechVoiceUri: String(parsed.settings?.speechVoiceUri || ""),
|
||||
speechRate: Math.max(0.65, Math.min(1.35, Number(parsed.settings?.speechRate || 0.95) || 0.95)),
|
||||
speechPitch: Math.max(0.7, Math.min(1.3, Number(parsed.settings?.speechPitch || 1) || 1)),
|
||||
speakerPassingCueEnabled: parsed.settings?.speakerPassingCueEnabled === true,
|
||||
speakerLeaderCueEnabled: parsed.settings?.speakerLeaderCueEnabled !== false,
|
||||
speakerFinishCueEnabled: parsed.settings?.speakerFinishCueEnabled !== false,
|
||||
@@ -2534,6 +2607,11 @@ function loadState() {
|
||||
audioEnabled: true,
|
||||
passingSoundMode: "beep",
|
||||
finishVoiceEnabled: true,
|
||||
startSoundEnabled: true,
|
||||
finishSoundEnabled: true,
|
||||
speechVoiceUri: "",
|
||||
speechRate: 0.95,
|
||||
speechPitch: 1,
|
||||
speakerPassingCueEnabled: false,
|
||||
speakerLeaderCueEnabled: true,
|
||||
speakerFinishCueEnabled: true,
|
||||
@@ -2720,8 +2798,8 @@ function buildDirectoryExportPayload() {
|
||||
|
||||
function buildDriversCsvRows() {
|
||||
return [
|
||||
["id", "name", "class", "brand", "transponder"],
|
||||
...state.drivers.map((driver) => [driver.id, driver.name, getClassName(driver.classId), driver.brand || "", driver.transponder || ""]),
|
||||
["id", "name", "class", "brand", "transponder", "spoken_name"],
|
||||
...state.drivers.map((driver) => [driver.id, driver.name, getClassName(driver.classId), driver.brand || "", driver.transponder || "", driver.speechName || ""]),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -2990,6 +3068,9 @@ function renderView() {
|
||||
connectDecoder,
|
||||
playPassingBeep,
|
||||
playFinishSiren,
|
||||
playStartCue,
|
||||
speakText,
|
||||
getSpeechVoices,
|
||||
openOverlayWindow,
|
||||
buildOverlayUrl,
|
||||
normalizeObsOverlaySettings,
|
||||
|
||||
Reference in New Issue
Block a user