Compare commits
7 Commits
fix/wavelo
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 341cb6cfab | |||
|
|
5ac966572a | ||
| d02bf05a33 | |||
|
|
487c801bab | ||
| 1acbb569dd | |||
|
|
9862f751a2 | ||
| 3387ded386 |
573
public/app.js
573
public/app.js
@@ -138,6 +138,7 @@ const els = {
|
|||||||
wavelogKeyInput: document.getElementById("wavelogKeyInput"),
|
wavelogKeyInput: document.getElementById("wavelogKeyInput"),
|
||||||
wavelogKeySaveBtn: document.getElementById("wavelogKeySaveBtn"),
|
wavelogKeySaveBtn: document.getElementById("wavelogKeySaveBtn"),
|
||||||
wavelogKeyMessage: document.getElementById("wavelogKeyMessage"),
|
wavelogKeyMessage: document.getElementById("wavelogKeyMessage"),
|
||||||
|
wavelogInstanceLink: document.getElementById("wavelogInstanceLink"),
|
||||||
openwebrxTxStatePill: document.getElementById("openwebrxTxStatePill"),
|
openwebrxTxStatePill: document.getElementById("openwebrxTxStatePill"),
|
||||||
rotorCurrent: document.getElementById("rotorCurrent"),
|
rotorCurrent: document.getElementById("rotorCurrent"),
|
||||||
rotorCompass: document.getElementById("rotorCompass"),
|
rotorCompass: document.getElementById("rotorCompass"),
|
||||||
@@ -1505,6 +1506,7 @@ async function refreshCurrentUser() {
|
|||||||
const result = await api("/v1/me", { authRequired: true });
|
const result = await api("/v1/me", { authRequired: true });
|
||||||
state.user = result.user;
|
state.user = result.user;
|
||||||
state.wavelogApiKey = typeof result.wavelogApiKey === "string" ? result.wavelogApiKey : "";
|
state.wavelogApiKey = typeof result.wavelogApiKey === "string" ? result.wavelogApiKey : "";
|
||||||
|
state.wavelogBaseUrl = typeof result.wavelogBaseUrl === "string" ? result.wavelogBaseUrl : "";
|
||||||
applyWavelogKeyToInput();
|
applyWavelogKeyToInput();
|
||||||
renderUserSettingsAuthMethods();
|
renderUserSettingsAuthMethods();
|
||||||
await ensureLanguageFromUserPreference();
|
await ensureLanguageFromUserPreference();
|
||||||
@@ -1698,6 +1700,16 @@ function applyWavelogKeyToInput() {
|
|||||||
if (els.wavelogKeyInput) {
|
if (els.wavelogKeyInput) {
|
||||||
els.wavelogKeyInput.value = String(state.wavelogApiKey || "");
|
els.wavelogKeyInput.value = String(state.wavelogApiKey || "");
|
||||||
}
|
}
|
||||||
|
if (els.wavelogInstanceLink) {
|
||||||
|
const url = String(state.wavelogBaseUrl || "").trim();
|
||||||
|
if (url) {
|
||||||
|
els.wavelogInstanceLink.href = url;
|
||||||
|
els.wavelogInstanceLink.hidden = false;
|
||||||
|
} else {
|
||||||
|
els.wavelogInstanceLink.removeAttribute("href");
|
||||||
|
els.wavelogInstanceLink.hidden = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveWavelogKey() {
|
async function saveWavelogKey() {
|
||||||
@@ -1723,6 +1735,9 @@ async function openOpenWebRxSession() {
|
|||||||
const session = result && result.session ? result.session : null;
|
const session = result && result.session ? result.session : null;
|
||||||
if (result && typeof result.wavelogApiKey === "string") {
|
if (result && typeof result.wavelogApiKey === "string") {
|
||||||
state.wavelogApiKey = result.wavelogApiKey;
|
state.wavelogApiKey = result.wavelogApiKey;
|
||||||
|
if (typeof result.wavelogBaseUrl === "string") {
|
||||||
|
state.wavelogBaseUrl = result.wavelogBaseUrl;
|
||||||
|
}
|
||||||
applyWavelogKeyToInput();
|
applyWavelogKeyToInput();
|
||||||
}
|
}
|
||||||
const sessionTicket = session && session.ticket ? String(session.ticket) : "";
|
const sessionTicket = session && session.ticket ? String(session.ticket) : "";
|
||||||
@@ -1752,6 +1767,9 @@ async function openOpenWebRxExternal() {
|
|||||||
const session = result && result.session ? result.session : null;
|
const session = result && result.session ? result.session : null;
|
||||||
if (result && typeof result.wavelogApiKey === "string") {
|
if (result && typeof result.wavelogApiKey === "string") {
|
||||||
state.wavelogApiKey = result.wavelogApiKey;
|
state.wavelogApiKey = result.wavelogApiKey;
|
||||||
|
if (typeof result.wavelogBaseUrl === "string") {
|
||||||
|
state.wavelogBaseUrl = result.wavelogBaseUrl;
|
||||||
|
}
|
||||||
applyWavelogKeyToInput();
|
applyWavelogKeyToInput();
|
||||||
}
|
}
|
||||||
const sessionTicket = session && session.ticket ? String(session.ticket) : "";
|
const sessionTicket = session && session.ticket ? String(session.ticket) : "";
|
||||||
@@ -3766,141 +3784,154 @@ function maybeAttachMicrohamEqBuilder(settingsForm, settingFields, plugin) {
|
|||||||
|
|
||||||
const presets = {
|
const presets = {
|
||||||
flat: {
|
flat: {
|
||||||
hp: 250,
|
label: "Flat",
|
||||||
lp: 2800,
|
desc: "Neutrale Referenz: kaum Klangfärbung, ohne Dynamik und ohne Auto-Level. Ideal zum Vergleichen und Feintunen.",
|
||||||
mids: 0,
|
hp: 150, lp: 3000, mids: 0, presence: 0,
|
||||||
presence: 0,
|
deesserEnabled: false, deesserFreq: 2900, deesserCut: 0,
|
||||||
deesserEnabled: false,
|
compressorEnabled: false, compressorThreshold: -18, compressorRatio: 2.5,
|
||||||
deesserFreq: 5200,
|
gateEnabled: false, gateThreshold: -52, gateRelease: 180,
|
||||||
deesserCut: 0,
|
normalizerEnabled: false, normalizerPeak: 90, normalizerMaxGain: 12,
|
||||||
compressorEnabled: false,
|
limiter: true
|
||||||
compressorThreshold: -18,
|
},
|
||||||
compressorRatio: 2.5,
|
level: {
|
||||||
gateEnabled: false,
|
label: "Konstanter Pegel",
|
||||||
gateThreshold: -52,
|
desc: "Auto-Level: hält den Sendepegel über alle Browser und Mikrofone konstant (speechnorm). Noise Gate kappt Pausen, Kompressor aus – die Normalisierung bestimmt die Lautheit.",
|
||||||
gateRelease: 180,
|
hp: 200, lp: 2900, mids: 1.0, presence: 2.0,
|
||||||
|
deesserEnabled: false, deesserFreq: 2900, deesserCut: 0,
|
||||||
|
compressorEnabled: false, compressorThreshold: -18, compressorRatio: 2.5,
|
||||||
|
gateEnabled: true, gateThreshold: -50, gateRelease: 180,
|
||||||
|
normalizerEnabled: true, normalizerPeak: 92, normalizerMaxGain: 22,
|
||||||
limiter: true
|
limiter: true
|
||||||
},
|
},
|
||||||
dx: {
|
dx: {
|
||||||
hp: 360,
|
label: "DX",
|
||||||
lp: 2500,
|
desc: "Durchsetzungsstark: enges Sprachband, angehobene Präsenz, Kompressor plus Auto-Level für maximale Lesbarkeit im Pile-Up.",
|
||||||
mids: 1.0,
|
hp: 400, lp: 2700, mids: 2.0, presence: 3.5,
|
||||||
presence: 1.8,
|
deesserEnabled: true, deesserFreq: 2800, deesserCut: 2.5,
|
||||||
deesserEnabled: true,
|
compressorEnabled: true, compressorThreshold: -18, compressorRatio: 3.5,
|
||||||
deesserFreq: 5200,
|
gateEnabled: true, gateThreshold: -50, gateRelease: 150,
|
||||||
deesserCut: 2.0,
|
normalizerEnabled: true, normalizerPeak: 92, normalizerMaxGain: 18,
|
||||||
compressorEnabled: true,
|
|
||||||
compressorThreshold: -20,
|
|
||||||
compressorRatio: 3.0,
|
|
||||||
gateEnabled: false,
|
|
||||||
gateThreshold: -50,
|
|
||||||
gateRelease: 160,
|
|
||||||
limiter: true
|
limiter: true
|
||||||
},
|
},
|
||||||
ragchew: {
|
ragchew: {
|
||||||
hp: 300,
|
label: "Ragchew",
|
||||||
lp: 2800,
|
desc: "Natürlicher Klang: breiteres Band, sanfte Anhebung, milder Kompressor und moderates Auto-Level für entspanntes QSO.",
|
||||||
mids: 0.5,
|
hp: 150, lp: 3000, mids: 1.0, presence: 1.5,
|
||||||
presence: 1.0,
|
deesserEnabled: true, deesserFreq: 3000, deesserCut: 1.5,
|
||||||
deesserEnabled: true,
|
compressorEnabled: true, compressorThreshold: -22, compressorRatio: 2.2,
|
||||||
deesserFreq: 5000,
|
gateEnabled: true, gateThreshold: -55, gateRelease: 220,
|
||||||
deesserCut: 1.2,
|
normalizerEnabled: true, normalizerPeak: 88, normalizerMaxGain: 12,
|
||||||
compressorEnabled: true,
|
|
||||||
compressorThreshold: -22,
|
|
||||||
compressorRatio: 2.2,
|
|
||||||
gateEnabled: true,
|
|
||||||
gateThreshold: -55,
|
|
||||||
gateRelease: 220,
|
|
||||||
limiter: true
|
limiter: true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const parsed = parseMicrohamEqArgs(extraArgsField.element.value);
|
const parsed = parseMicrohamEqArgs(extraArgsField.element.value);
|
||||||
const initial = parsed || presets.flat;
|
const initial = Object.assign({}, presets.flat, parsed || {});
|
||||||
|
|
||||||
const wrap = document.createElement("div");
|
const wrap = document.createElement("div");
|
||||||
wrap.className = "plugin-eq-builder";
|
wrap.className = "plugin-eq-builder";
|
||||||
|
|
||||||
|
const head = document.createElement("div");
|
||||||
|
head.className = "eq-head";
|
||||||
|
const headText = document.createElement("div");
|
||||||
|
headText.className = "eq-head-text";
|
||||||
const title = document.createElement("strong");
|
const title = document.createElement("strong");
|
||||||
title.textContent = "TX Audio EQ";
|
title.className = "eq-head-title";
|
||||||
wrap.appendChild(title);
|
title.textContent = "TX Audio Equalizer";
|
||||||
|
|
||||||
const hint = document.createElement("small");
|
const hint = document.createElement("small");
|
||||||
hint.className = "muted";
|
hint.className = "eq-head-hint";
|
||||||
hint.textContent = parsed
|
hint.textContent = parsed
|
||||||
? "EQ aus aktuellen Extra-Args geladen"
|
? "Aus den aktuellen Extra-Args geladen."
|
||||||
: "Preset/Regler schreiben den FFmpeg-EQ in audioFfmpegExtraArgs";
|
: "Regler und Presets schreiben die FFmpeg-Kette in audioFfmpegExtraArgs.";
|
||||||
wrap.appendChild(hint);
|
headText.appendChild(title);
|
||||||
|
headText.appendChild(hint);
|
||||||
|
head.appendChild(headText);
|
||||||
|
wrap.appendChild(head);
|
||||||
|
|
||||||
const presetRow = document.createElement("div");
|
const presetBar = document.createElement("div");
|
||||||
presetRow.className = "plugin-eq-row";
|
presetBar.className = "eq-presets";
|
||||||
|
const presetOrder = ["flat", "level", "dx", "ragchew"];
|
||||||
const presetLabel = document.createElement("span");
|
const presetButtons = {};
|
||||||
presetLabel.className = "muted";
|
presetOrder.forEach((key) => {
|
||||||
presetLabel.textContent = "Preset";
|
const chip = document.createElement("button");
|
||||||
presetRow.appendChild(presetLabel);
|
chip.type = "button";
|
||||||
|
chip.className = "eq-chip" + (key === "level" ? " is-star" : "");
|
||||||
const presetSelect = document.createElement("select");
|
chip.dataset.preset = key;
|
||||||
presetSelect.innerHTML = [
|
chip.textContent = presets[key].label;
|
||||||
'<option value="flat">Flat - neutral</option>',
|
presetBar.appendChild(chip);
|
||||||
'<option value="dx">DX - durchsetzungsstark</option>',
|
presetButtons[key] = chip;
|
||||||
'<option value="ragchew">Ragchew - angenehmer Klang</option>'
|
});
|
||||||
].join("");
|
wrap.appendChild(presetBar);
|
||||||
presetRow.appendChild(presetSelect);
|
|
||||||
|
|
||||||
const presetBtn = document.createElement("button");
|
|
||||||
presetBtn.type = "button";
|
|
||||||
presetBtn.className = "ghost-btn";
|
|
||||||
presetBtn.textContent = "Preset anwenden";
|
|
||||||
presetRow.appendChild(presetBtn);
|
|
||||||
|
|
||||||
wrap.appendChild(presetRow);
|
|
||||||
|
|
||||||
const presetInfo = document.createElement("small");
|
const presetInfo = document.createElement("small");
|
||||||
presetInfo.className = "muted";
|
presetInfo.className = "eq-preset-info";
|
||||||
wrap.appendChild(presetInfo);
|
wrap.appendChild(presetInfo);
|
||||||
|
|
||||||
const hpControl = createEqSliderControl("Highpass (Hz)", 80, 600, 10, initial.hp);
|
// --- Section: Klangfilter (EQ) ---
|
||||||
const lpControl = createEqSliderControl("Lowpass (Hz)", 1800, 4000, 10, initial.lp);
|
const eqSec = createEqSection("Klangfilter", { subtitle: "Bandbreite und Frequenzgang der Stimme." });
|
||||||
const midsControl = createEqSliderControl("Mitten (dB)", -8, 12, 0.5, initial.mids);
|
const hpControl = createEqSliderControl("Highpass", 80, 600, 10, initial.hp, "Hz");
|
||||||
const presenceControl = createEqSliderControl("Presence (dB)", -8, 12, 0.5, initial.presence);
|
const lpControl = createEqSliderControl("Lowpass", 2000, 3200, 10, initial.lp, "Hz");
|
||||||
const deesserFreqControl = createEqSliderControl("De-Esser (Hz)", 3500, 8000, 100, initial.deesserFreq);
|
const midsControl = createEqSliderControl("Mitten · 1200 Hz", -8, 12, 0.5, initial.mids, "dB");
|
||||||
const deesserCutControl = createEqSliderControl("De-Esser Cut (dB)", 0, 8, 0.5, initial.deesserCut);
|
const presenceControl = createEqSliderControl("Presence · 2100 Hz", -8, 12, 0.5, initial.presence, "dB");
|
||||||
const compThresholdControl = createEqSliderControl("Compressor Threshold (dB)", -40, -6, 1, initial.compressorThreshold);
|
[hpControl, lpControl, midsControl, presenceControl].forEach((c) => eqSec.body.appendChild(c.row));
|
||||||
const compRatioControl = createEqSliderControl("Compressor Ratio", 1.2, 6, 0.1, initial.compressorRatio);
|
wrap.appendChild(eqSec.section);
|
||||||
const gateThresholdControl = createEqSliderControl("Gate Threshold (dB)", -70, -20, 1, initial.gateThreshold);
|
|
||||||
const gateReleaseControl = createEqSliderControl("Gate Release (ms)", 60, 500, 10, initial.gateRelease);
|
|
||||||
wrap.appendChild(hpControl.row);
|
|
||||||
wrap.appendChild(lpControl.row);
|
|
||||||
wrap.appendChild(midsControl.row);
|
|
||||||
wrap.appendChild(presenceControl.row);
|
|
||||||
const deesserEnabledRow = createEqCheckboxRow("De-Esser aktivieren", initial.deesserEnabled);
|
|
||||||
wrap.appendChild(deesserEnabledRow.row);
|
|
||||||
wrap.appendChild(deesserFreqControl.row);
|
|
||||||
wrap.appendChild(deesserCutControl.row);
|
|
||||||
const compressorEnabledRow = createEqCheckboxRow("Compressor aktivieren", initial.compressorEnabled);
|
|
||||||
wrap.appendChild(compressorEnabledRow.row);
|
|
||||||
wrap.appendChild(compThresholdControl.row);
|
|
||||||
wrap.appendChild(compRatioControl.row);
|
|
||||||
const gateEnabledRow = createEqCheckboxRow("Noise Gate aktivieren", initial.gateEnabled);
|
|
||||||
wrap.appendChild(gateEnabledRow.row);
|
|
||||||
wrap.appendChild(gateThresholdControl.row);
|
|
||||||
wrap.appendChild(gateReleaseControl.row);
|
|
||||||
|
|
||||||
const limiterRow = document.createElement("label");
|
// --- Section: De-Esser (toggle) ---
|
||||||
limiterRow.className = "plugin-eq-checkbox";
|
const deSec = createEqSection("De-Esser", {
|
||||||
const limiterInput = document.createElement("input");
|
subtitle: "Zähmt harte Zischlaute – wirkt nur innerhalb des Durchlassbands.",
|
||||||
limiterInput.type = "checkbox";
|
toggle: true,
|
||||||
limiterInput.checked = Boolean(initial.limiter);
|
toggleChecked: initial.deesserEnabled
|
||||||
limiterRow.appendChild(limiterInput);
|
});
|
||||||
const limiterText = document.createElement("span");
|
const deesserFreqControl = createEqSliderControl("Frequenz", 2200, 3400, 50, initial.deesserFreq, "Hz");
|
||||||
limiterText.textContent = "Limiter aktivieren (alimiter=0.95)";
|
const deesserCutControl = createEqSliderControl("Absenkung", 0, 8, 0.5, initial.deesserCut, "dB");
|
||||||
limiterRow.appendChild(limiterText);
|
[deesserFreqControl, deesserCutControl].forEach((c) => deSec.body.appendChild(c.row));
|
||||||
wrap.appendChild(limiterRow);
|
wrap.appendChild(deSec.section);
|
||||||
|
|
||||||
const preview = document.createElement("small");
|
// --- Section: Dynamik (compressor + gate + limiter) ---
|
||||||
preview.className = "muted";
|
const dynSec = createEqSection("Dynamik", { subtitle: "Kompression, Rauschsperre und Übersteuerungsschutz." });
|
||||||
wrap.appendChild(preview);
|
const compToggle = createEqToggle("Kompressor", initial.compressorEnabled);
|
||||||
|
compToggle.row.classList.add("eq-sub-toggle");
|
||||||
|
dynSec.body.appendChild(compToggle.row);
|
||||||
|
const compThresholdControl = createEqSliderControl("Threshold", -40, -6, 1, initial.compressorThreshold, "dB");
|
||||||
|
const compRatioControl = createEqSliderControl("Ratio", 1.2, 6, 0.1, initial.compressorRatio, ":1");
|
||||||
|
[compThresholdControl, compRatioControl].forEach((c) => dynSec.body.appendChild(c.row));
|
||||||
|
const gateToggle = createEqToggle("Noise Gate", initial.gateEnabled);
|
||||||
|
gateToggle.row.classList.add("eq-sub-toggle");
|
||||||
|
dynSec.body.appendChild(gateToggle.row);
|
||||||
|
const gateThresholdControl = createEqSliderControl("Gate Threshold", -70, -20, 1, initial.gateThreshold, "dB");
|
||||||
|
const gateReleaseControl = createEqSliderControl("Gate Release", 60, 500, 10, initial.gateRelease, "ms");
|
||||||
|
[gateThresholdControl, gateReleaseControl].forEach((c) => dynSec.body.appendChild(c.row));
|
||||||
|
const limiterToggle = createEqToggle("Limiter · alimiter=0.95", initial.limiter);
|
||||||
|
limiterToggle.row.classList.add("eq-sub-toggle");
|
||||||
|
dynSec.body.appendChild(limiterToggle.row);
|
||||||
|
wrap.appendChild(dynSec.section);
|
||||||
|
|
||||||
|
// --- Section: Pegel-Normalisierung (speechnorm, toggle) ---
|
||||||
|
const normSec = createEqSection("Pegel-Normalisierung", {
|
||||||
|
subtitle: "Auto-Level: gleicher Sendepegel aus jedem Browser und Mikrofon (speechnorm).",
|
||||||
|
toggle: true,
|
||||||
|
toggleChecked: initial.normalizerEnabled
|
||||||
|
});
|
||||||
|
const normPeakControl = createEqSliderControl("Ziel-Pegel", 50, 99, 1, initial.normalizerPeak, "%");
|
||||||
|
const normGainControl = createEqSliderControl("Max. Anhebung", 0, 30, 1, initial.normalizerMaxGain, "dB");
|
||||||
|
[normPeakControl, normGainControl].forEach((c) => normSec.body.appendChild(c.row));
|
||||||
|
const normNote = document.createElement("p");
|
||||||
|
normNote.className = "eq-note";
|
||||||
|
normNote.textContent = "Tipp: Für konstanten Pegel das Noise Gate aktiv lassen – sonst wird Grundrauschen in Sprechpausen mit angehoben.";
|
||||||
|
normSec.body.appendChild(normNote);
|
||||||
|
wrap.appendChild(normSec.section);
|
||||||
|
|
||||||
|
// --- Live preview of the generated chain ---
|
||||||
|
const previewWrap = document.createElement("div");
|
||||||
|
previewWrap.className = "eq-preview";
|
||||||
|
const previewLabel = document.createElement("span");
|
||||||
|
previewLabel.className = "eq-preview-label";
|
||||||
|
previewLabel.textContent = "FFmpeg-Kette";
|
||||||
|
const preview = document.createElement("code");
|
||||||
|
preview.className = "eq-preview-code";
|
||||||
|
previewWrap.appendChild(previewLabel);
|
||||||
|
previewWrap.appendChild(preview);
|
||||||
|
wrap.appendChild(previewWrap);
|
||||||
|
|
||||||
const saveButton = settingsForm.querySelector('button[type="submit"]');
|
const saveButton = settingsForm.querySelector('button[type="submit"]');
|
||||||
if (saveButton && saveButton.parentNode === settingsForm) {
|
if (saveButton && saveButton.parentNode === settingsForm) {
|
||||||
@@ -3909,55 +3940,51 @@ function maybeAttachMicrohamEqBuilder(settingsForm, settingFields, plugin) {
|
|||||||
settingsForm.appendChild(wrap);
|
settingsForm.appendChild(wrap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const syncDisabled = () => {
|
||||||
|
deesserFreqControl.setDisabled(!deSec.toggle.input.checked);
|
||||||
|
deesserCutControl.setDisabled(!deSec.toggle.input.checked);
|
||||||
|
deSec.section.classList.toggle("is-active", deSec.toggle.input.checked);
|
||||||
|
compThresholdControl.setDisabled(!compToggle.input.checked);
|
||||||
|
compRatioControl.setDisabled(!compToggle.input.checked);
|
||||||
|
gateThresholdControl.setDisabled(!gateToggle.input.checked);
|
||||||
|
gateReleaseControl.setDisabled(!gateToggle.input.checked);
|
||||||
|
normPeakControl.setDisabled(!normSec.toggle.input.checked);
|
||||||
|
normGainControl.setDisabled(!normSec.toggle.input.checked);
|
||||||
|
normSec.section.classList.toggle("is-active", normSec.toggle.input.checked);
|
||||||
|
};
|
||||||
|
|
||||||
const applyControlsToExtraArgs = () => {
|
const applyControlsToExtraArgs = () => {
|
||||||
const hp = Number(hpControl.number.value);
|
|
||||||
const lp = Number(lpControl.number.value);
|
|
||||||
const mids = Number(midsControl.number.value);
|
|
||||||
const presence = Number(presenceControl.number.value);
|
|
||||||
const deesserEnabled = Boolean(deesserEnabledRow.input.checked);
|
|
||||||
const deesserFreq = Number(deesserFreqControl.number.value);
|
|
||||||
const deesserCut = Number(deesserCutControl.number.value);
|
|
||||||
const compressorEnabled = Boolean(compressorEnabledRow.input.checked);
|
|
||||||
const compressorThreshold = Number(compThresholdControl.number.value);
|
|
||||||
const compressorRatio = Number(compRatioControl.number.value);
|
|
||||||
const gateEnabled = Boolean(gateEnabledRow.input.checked);
|
|
||||||
const gateThreshold = Number(gateThresholdControl.number.value);
|
|
||||||
const gateRelease = Number(gateReleaseControl.number.value);
|
|
||||||
const limiter = Boolean(limiterInput.checked);
|
|
||||||
const args = buildMicrohamEqExtraArgs({
|
const args = buildMicrohamEqExtraArgs({
|
||||||
hp,
|
hp: Number(hpControl.number.value),
|
||||||
lp,
|
lp: Number(lpControl.number.value),
|
||||||
mids,
|
mids: Number(midsControl.number.value),
|
||||||
presence,
|
presence: Number(presenceControl.number.value),
|
||||||
deesserEnabled,
|
deesserEnabled: Boolean(deSec.toggle.input.checked),
|
||||||
deesserFreq,
|
deesserFreq: Number(deesserFreqControl.number.value),
|
||||||
deesserCut,
|
deesserCut: Number(deesserCutControl.number.value),
|
||||||
compressorEnabled,
|
compressorEnabled: Boolean(compToggle.input.checked),
|
||||||
compressorThreshold,
|
compressorThreshold: Number(compThresholdControl.number.value),
|
||||||
compressorRatio,
|
compressorRatio: Number(compRatioControl.number.value),
|
||||||
gateEnabled,
|
gateEnabled: Boolean(gateToggle.input.checked),
|
||||||
gateThreshold,
|
gateThreshold: Number(gateThresholdControl.number.value),
|
||||||
gateRelease,
|
gateRelease: Number(gateReleaseControl.number.value),
|
||||||
limiter
|
normalizerEnabled: Boolean(normSec.toggle.input.checked),
|
||||||
|
normalizerPeak: Number(normPeakControl.number.value),
|
||||||
|
normalizerMaxGain: Number(normGainControl.number.value),
|
||||||
|
limiter: Boolean(limiterToggle.input.checked)
|
||||||
});
|
});
|
||||||
extraArgsField.element.value = args;
|
extraArgsField.element.value = args;
|
||||||
preview.textContent = `Generierte Extra-Args: ${args}`;
|
preview.textContent = args;
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatePresetInfo = () => {
|
const sliderControls = [
|
||||||
const selected = String(presetSelect.value || "flat");
|
hpControl, lpControl, midsControl, presenceControl,
|
||||||
if (selected === "dx") {
|
deesserFreqControl, deesserCutControl,
|
||||||
presetInfo.textContent = "DX: sprachfokussiert mit reduziertem Bass und moderater Praesenz, weniger kuenstlich abgestimmt.";
|
compThresholdControl, compRatioControl,
|
||||||
return;
|
gateThresholdControl, gateReleaseControl,
|
||||||
}
|
normPeakControl, normGainControl
|
||||||
if (selected === "ragchew") {
|
];
|
||||||
presetInfo.textContent = "Ragchew: natuerlicher Sprachklang mit leicht reduziertem Bass und sanfter Mitten/Praesenz-Anhebung.";
|
sliderControls.forEach((control) => {
|
||||||
return;
|
|
||||||
}
|
|
||||||
presetInfo.textContent = "Flat: neutrale Basis mit wenig Klangfaerbung, gut fuer Vergleich und Feintuning.";
|
|
||||||
};
|
|
||||||
|
|
||||||
const bindControlUpdate = (control) => {
|
|
||||||
control.range.addEventListener("input", () => {
|
control.range.addEventListener("input", () => {
|
||||||
control.number.value = control.range.value;
|
control.number.value = control.range.value;
|
||||||
applyControlsToExtraArgs();
|
applyControlsToExtraArgs();
|
||||||
@@ -3966,99 +3993,156 @@ function maybeAttachMicrohamEqBuilder(settingsForm, settingFields, plugin) {
|
|||||||
control.range.value = control.number.value;
|
control.range.value = control.number.value;
|
||||||
applyControlsToExtraArgs();
|
applyControlsToExtraArgs();
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
bindControlUpdate(hpControl);
|
|
||||||
bindControlUpdate(lpControl);
|
|
||||||
bindControlUpdate(midsControl);
|
|
||||||
bindControlUpdate(presenceControl);
|
|
||||||
bindControlUpdate(deesserFreqControl);
|
|
||||||
bindControlUpdate(deesserCutControl);
|
|
||||||
bindControlUpdate(compThresholdControl);
|
|
||||||
bindControlUpdate(compRatioControl);
|
|
||||||
bindControlUpdate(gateThresholdControl);
|
|
||||||
bindControlUpdate(gateReleaseControl);
|
|
||||||
limiterInput.addEventListener("change", applyControlsToExtraArgs);
|
|
||||||
deesserEnabledRow.input.addEventListener("change", applyControlsToExtraArgs);
|
|
||||||
compressorEnabledRow.input.addEventListener("change", applyControlsToExtraArgs);
|
|
||||||
gateEnabledRow.input.addEventListener("change", applyControlsToExtraArgs);
|
|
||||||
|
|
||||||
presetBtn.addEventListener("click", () => {
|
|
||||||
const selected = presets[presetSelect.value] || presets.flat;
|
|
||||||
hpControl.range.value = String(selected.hp);
|
|
||||||
hpControl.number.value = String(selected.hp);
|
|
||||||
lpControl.range.value = String(selected.lp);
|
|
||||||
lpControl.number.value = String(selected.lp);
|
|
||||||
midsControl.range.value = String(selected.mids);
|
|
||||||
midsControl.number.value = String(selected.mids);
|
|
||||||
presenceControl.range.value = String(selected.presence);
|
|
||||||
presenceControl.number.value = String(selected.presence);
|
|
||||||
deesserEnabledRow.input.checked = Boolean(selected.deesserEnabled);
|
|
||||||
deesserFreqControl.range.value = String(selected.deesserFreq);
|
|
||||||
deesserFreqControl.number.value = String(selected.deesserFreq);
|
|
||||||
deesserCutControl.range.value = String(selected.deesserCut);
|
|
||||||
deesserCutControl.number.value = String(selected.deesserCut);
|
|
||||||
compressorEnabledRow.input.checked = Boolean(selected.compressorEnabled);
|
|
||||||
compThresholdControl.range.value = String(selected.compressorThreshold);
|
|
||||||
compThresholdControl.number.value = String(selected.compressorThreshold);
|
|
||||||
compRatioControl.range.value = String(selected.compressorRatio);
|
|
||||||
compRatioControl.number.value = String(selected.compressorRatio);
|
|
||||||
gateEnabledRow.input.checked = Boolean(selected.gateEnabled);
|
|
||||||
gateThresholdControl.range.value = String(selected.gateThreshold);
|
|
||||||
gateThresholdControl.number.value = String(selected.gateThreshold);
|
|
||||||
gateReleaseControl.range.value = String(selected.gateRelease);
|
|
||||||
gateReleaseControl.number.value = String(selected.gateRelease);
|
|
||||||
limiterInput.checked = Boolean(selected.limiter);
|
|
||||||
applyControlsToExtraArgs();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
presetSelect.addEventListener("change", updatePresetInfo);
|
[deSec.toggle.input, compToggle.input, gateToggle.input, limiterToggle.input, normSec.toggle.input].forEach((input) => {
|
||||||
|
input.addEventListener("change", () => {
|
||||||
|
syncDisabled();
|
||||||
|
applyControlsToExtraArgs();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
preview.textContent = `Aktuelle Extra-Args: ${extraArgsField.element.value || "<leer>"}`;
|
const setActiveChip = (key) => {
|
||||||
updatePresetInfo();
|
presetOrder.forEach((k) => presetButtons[k].classList.toggle("is-active", k === key));
|
||||||
|
presetInfo.textContent = key && presets[key] ? presets[key].desc : "Eigene Einstellung – weicht von den Presets ab.";
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyPreset = (key) => {
|
||||||
|
const selected = presets[key] || presets.flat;
|
||||||
|
const set = (control, value) => {
|
||||||
|
control.range.value = String(value);
|
||||||
|
control.number.value = String(value);
|
||||||
|
};
|
||||||
|
set(hpControl, selected.hp);
|
||||||
|
set(lpControl, selected.lp);
|
||||||
|
set(midsControl, selected.mids);
|
||||||
|
set(presenceControl, selected.presence);
|
||||||
|
deSec.toggle.input.checked = Boolean(selected.deesserEnabled);
|
||||||
|
set(deesserFreqControl, selected.deesserFreq);
|
||||||
|
set(deesserCutControl, selected.deesserCut);
|
||||||
|
compToggle.input.checked = Boolean(selected.compressorEnabled);
|
||||||
|
set(compThresholdControl, selected.compressorThreshold);
|
||||||
|
set(compRatioControl, selected.compressorRatio);
|
||||||
|
gateToggle.input.checked = Boolean(selected.gateEnabled);
|
||||||
|
set(gateThresholdControl, selected.gateThreshold);
|
||||||
|
set(gateReleaseControl, selected.gateRelease);
|
||||||
|
limiterToggle.input.checked = Boolean(selected.limiter);
|
||||||
|
normSec.toggle.input.checked = Boolean(selected.normalizerEnabled);
|
||||||
|
set(normPeakControl, selected.normalizerPeak);
|
||||||
|
set(normGainControl, selected.normalizerMaxGain);
|
||||||
|
syncDisabled();
|
||||||
|
applyControlsToExtraArgs();
|
||||||
|
setActiveChip(key);
|
||||||
|
};
|
||||||
|
|
||||||
|
presetOrder.forEach((key) => {
|
||||||
|
presetButtons[key].addEventListener("click", () => applyPreset(key));
|
||||||
|
});
|
||||||
|
|
||||||
|
syncDisabled();
|
||||||
|
preview.textContent = extraArgsField.element.value || "–";
|
||||||
|
setActiveChip(parsed ? null : "flat");
|
||||||
}
|
}
|
||||||
|
|
||||||
function createEqSliderControl(labelText, min, max, step, value) {
|
function createEqSliderControl(labelText, min, max, step, value, unit) {
|
||||||
const row = document.createElement("div");
|
const row = document.createElement("div");
|
||||||
row.className = "plugin-eq-row";
|
row.className = "eq-control";
|
||||||
|
|
||||||
|
const head = document.createElement("div");
|
||||||
|
head.className = "eq-control-head";
|
||||||
const label = document.createElement("span");
|
const label = document.createElement("span");
|
||||||
label.className = "muted";
|
label.className = "eq-control-label";
|
||||||
label.textContent = labelText;
|
label.textContent = labelText;
|
||||||
row.appendChild(label);
|
head.appendChild(label);
|
||||||
|
|
||||||
|
const number = document.createElement("input");
|
||||||
|
number.type = "number";
|
||||||
|
number.className = "eq-control-value";
|
||||||
|
number.min = String(min);
|
||||||
|
number.max = String(max);
|
||||||
|
number.step = String(step);
|
||||||
|
number.value = String(value);
|
||||||
|
head.appendChild(number);
|
||||||
|
|
||||||
|
if (unit) {
|
||||||
|
const unitEl = document.createElement("span");
|
||||||
|
unitEl.className = "eq-control-unit";
|
||||||
|
unitEl.textContent = unit;
|
||||||
|
head.appendChild(unitEl);
|
||||||
|
}
|
||||||
|
row.appendChild(head);
|
||||||
|
|
||||||
const range = document.createElement("input");
|
const range = document.createElement("input");
|
||||||
range.type = "range";
|
range.type = "range";
|
||||||
|
range.className = "eq-range";
|
||||||
range.min = String(min);
|
range.min = String(min);
|
||||||
range.max = String(max);
|
range.max = String(max);
|
||||||
range.step = String(step);
|
range.step = String(step);
|
||||||
range.value = String(value);
|
range.value = String(value);
|
||||||
row.appendChild(range);
|
row.appendChild(range);
|
||||||
|
|
||||||
const number = document.createElement("input");
|
const setDisabled = (disabled) => {
|
||||||
number.type = "number";
|
const flag = Boolean(disabled);
|
||||||
number.min = String(min);
|
row.classList.toggle("is-disabled", flag);
|
||||||
number.max = String(max);
|
range.disabled = flag;
|
||||||
number.step = String(step);
|
number.disabled = flag;
|
||||||
number.value = String(value);
|
};
|
||||||
row.appendChild(number);
|
|
||||||
|
|
||||||
return { row, range, number };
|
return { row, range, number, setDisabled };
|
||||||
}
|
}
|
||||||
|
|
||||||
function createEqCheckboxRow(labelText, checked) {
|
function createEqToggle(labelText, checked) {
|
||||||
const row = document.createElement("label");
|
const row = document.createElement("label");
|
||||||
row.className = "plugin-eq-checkbox";
|
row.className = "eq-switch";
|
||||||
const input = document.createElement("input");
|
const input = document.createElement("input");
|
||||||
input.type = "checkbox";
|
input.type = "checkbox";
|
||||||
input.checked = Boolean(checked);
|
input.checked = Boolean(checked);
|
||||||
|
const track = document.createElement("span");
|
||||||
|
track.className = "eq-switch-track";
|
||||||
|
track.setAttribute("aria-hidden", "true");
|
||||||
|
const text = document.createElement("span");
|
||||||
|
text.className = "eq-switch-label";
|
||||||
|
text.textContent = labelText;
|
||||||
row.appendChild(input);
|
row.appendChild(input);
|
||||||
const label = document.createElement("span");
|
row.appendChild(track);
|
||||||
label.textContent = labelText;
|
row.appendChild(text);
|
||||||
row.appendChild(label);
|
|
||||||
return { row, input };
|
return { row, input };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createEqSection(titleText, options) {
|
||||||
|
const opts = options || {};
|
||||||
|
const section = document.createElement("section");
|
||||||
|
section.className = "eq-section";
|
||||||
|
|
||||||
|
const header = document.createElement("div");
|
||||||
|
header.className = "eq-section-head";
|
||||||
|
const title = document.createElement("h4");
|
||||||
|
title.className = "eq-section-title";
|
||||||
|
title.textContent = titleText;
|
||||||
|
header.appendChild(title);
|
||||||
|
|
||||||
|
let toggle = null;
|
||||||
|
if (opts.toggle) {
|
||||||
|
toggle = createEqToggle(opts.toggleLabel || "aktiv", opts.toggleChecked);
|
||||||
|
toggle.row.classList.add("eq-section-toggle");
|
||||||
|
header.appendChild(toggle.row);
|
||||||
|
}
|
||||||
|
section.appendChild(header);
|
||||||
|
|
||||||
|
if (opts.subtitle) {
|
||||||
|
const sub = document.createElement("p");
|
||||||
|
sub.className = "eq-section-sub";
|
||||||
|
sub.textContent = opts.subtitle;
|
||||||
|
section.appendChild(sub);
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = document.createElement("div");
|
||||||
|
body.className = "eq-section-body";
|
||||||
|
section.appendChild(body);
|
||||||
|
|
||||||
|
return { section, body, toggle };
|
||||||
|
}
|
||||||
|
|
||||||
function buildMicrohamEqExtraArgs({
|
function buildMicrohamEqExtraArgs({
|
||||||
hp,
|
hp,
|
||||||
lp,
|
lp,
|
||||||
@@ -4073,6 +4157,9 @@ function buildMicrohamEqExtraArgs({
|
|||||||
gateEnabled,
|
gateEnabled,
|
||||||
gateThreshold,
|
gateThreshold,
|
||||||
gateRelease,
|
gateRelease,
|
||||||
|
normalizerEnabled,
|
||||||
|
normalizerPeak,
|
||||||
|
normalizerMaxGain,
|
||||||
limiter
|
limiter
|
||||||
}) {
|
}) {
|
||||||
const filters = [
|
const filters = [
|
||||||
@@ -4082,7 +4169,7 @@ function buildMicrohamEqExtraArgs({
|
|||||||
`equalizer=f=2100:t=q:w=0.9:g=${Number(Number(presence || 0).toFixed(1))}`
|
`equalizer=f=2100:t=q:w=0.9:g=${Number(Number(presence || 0).toFixed(1))}`
|
||||||
];
|
];
|
||||||
if (deesserEnabled && Number(deesserCut || 0) > 0) {
|
if (deesserEnabled && Number(deesserCut || 0) > 0) {
|
||||||
const freq = Math.round(Number(deesserFreq) || 5200);
|
const freq = Math.round(Number(deesserFreq) || 2900);
|
||||||
const cut = Number(Number(-Math.abs(Number(deesserCut || 0))).toFixed(1));
|
const cut = Number(Number(-Math.abs(Number(deesserCut || 0))).toFixed(1));
|
||||||
filters.push(`equalizer=f=${freq}:t=q:w=1.1:g=${cut}`);
|
filters.push(`equalizer=f=${freq}:t=q:w=1.1:g=${cut}`);
|
||||||
}
|
}
|
||||||
@@ -4096,7 +4183,17 @@ function buildMicrohamEqExtraArgs({
|
|||||||
const gateThresholdDb = Number(Number(gateThreshold || -55).toFixed(1));
|
const gateThresholdDb = Number(Number(gateThreshold || -55).toFixed(1));
|
||||||
const gateThresholdLinear = Number(Math.pow(10, gateThresholdDb / 20).toFixed(6));
|
const gateThresholdLinear = Number(Math.pow(10, gateThresholdDb / 20).toFixed(6));
|
||||||
const release = Math.round(Number(gateRelease || 180));
|
const release = Math.round(Number(gateRelease || 180));
|
||||||
filters.push(`agate=threshold=${gateThresholdLinear}:release=${release}`);
|
filters.push(`agate=threshold=${gateThresholdLinear}:attack=5:release=${release}`);
|
||||||
|
}
|
||||||
|
// Adaptive level normalisation (speechnorm): lifts quiet inputs toward a target
|
||||||
|
// peak so every browser/microphone reaches the same TX level. Placed after the
|
||||||
|
// gate so background noise in pauses is cut first and not boosted.
|
||||||
|
if (normalizerEnabled) {
|
||||||
|
const peakPct = Math.min(99, Math.max(50, Math.round(Number(normalizerPeak || 90))));
|
||||||
|
const peak = Number((peakPct / 100).toFixed(2));
|
||||||
|
const gainDb = Math.min(30, Math.max(0, Math.round(Number(normalizerMaxGain || 12))));
|
||||||
|
const expansion = Math.max(1, Number(Math.pow(10, gainDb / 20).toFixed(2)));
|
||||||
|
filters.push(`speechnorm=p=${peak}:e=${expansion}:l=1`);
|
||||||
}
|
}
|
||||||
if (limiter) {
|
if (limiter) {
|
||||||
filters.push("alimiter=limit=0.95");
|
filters.push("alimiter=limit=0.95");
|
||||||
@@ -4131,10 +4228,37 @@ function parseMicrohamEqArgs(value) {
|
|||||||
const compressorEnabled = Boolean(compMatch);
|
const compressorEnabled = Boolean(compMatch);
|
||||||
const compressorThreshold = compMatch ? Number((20 * Math.log10(Number(compMatch[1]))).toFixed(1)) : -20;
|
const compressorThreshold = compMatch ? Number((20 * Math.log10(Number(compMatch[1]))).toFixed(1)) : -20;
|
||||||
const compressorRatio = compMatch ? Number(compMatch[2]) : 3;
|
const compressorRatio = compMatch ? Number(compMatch[2]) : 3;
|
||||||
const gateMatch = text.match(/agate\s*=\s*threshold\s*=\s*([0-9]+(?:\.[0-9]+)?)\s*:\s*release\s*=\s*([0-9]+(?:\.[0-9]+)?)/i);
|
const gateTokenMatch = text.match(/agate\s*=\s*([^",]+)/i);
|
||||||
const gateEnabled = Boolean(gateMatch);
|
const gateEnabled = Boolean(gateTokenMatch);
|
||||||
const gateThreshold = gateMatch ? Number((20 * Math.log10(Number(gateMatch[1]))).toFixed(1)) : -55;
|
let gateThreshold = -55;
|
||||||
const gateRelease = gateMatch ? Number(gateMatch[2]) : 180;
|
let gateRelease = 180;
|
||||||
|
if (gateTokenMatch) {
|
||||||
|
const gateStr = gateTokenMatch[1];
|
||||||
|
const gateThrMatch = gateStr.match(/(?:^|:)\s*threshold\s*=\s*([0-9]*\.?[0-9]+)/i);
|
||||||
|
const gateRelMatch = gateStr.match(/(?:^|:)\s*release\s*=\s*([0-9]*\.?[0-9]+)/i);
|
||||||
|
if (gateThrMatch) {
|
||||||
|
gateThreshold = Number((20 * Math.log10(Number(gateThrMatch[1]))).toFixed(1));
|
||||||
|
}
|
||||||
|
if (gateRelMatch) {
|
||||||
|
gateRelease = Number(gateRelMatch[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const normMatch = text.match(/speechnorm\s*=\s*([^",]+)/i);
|
||||||
|
const normalizerEnabled = Boolean(normMatch);
|
||||||
|
let normalizerPeak = 90;
|
||||||
|
let normalizerMaxGain = 12;
|
||||||
|
if (normMatch) {
|
||||||
|
const normStr = normMatch[1];
|
||||||
|
const peakMatch = normStr.match(/(?:^|:)\s*(?:peak|p)\s*=\s*([0-9]*\.?[0-9]+)/i);
|
||||||
|
const expMatch = normStr.match(/(?:^|:)\s*(?:expansion|e)\s*=\s*([0-9]*\.?[0-9]+)/i);
|
||||||
|
if (peakMatch) {
|
||||||
|
normalizerPeak = Math.round(Number(peakMatch[1]) * 100);
|
||||||
|
}
|
||||||
|
if (expMatch) {
|
||||||
|
const exp = Number(expMatch[1]);
|
||||||
|
normalizerMaxGain = exp > 0 ? Math.round(20 * Math.log10(exp)) : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
const limiter = /alimiter\s*=\s*limit\s*=\s*0\.95/i.test(text);
|
const limiter = /alimiter\s*=\s*limit\s*=\s*0\.95/i.test(text);
|
||||||
return {
|
return {
|
||||||
hp,
|
hp,
|
||||||
@@ -4150,6 +4274,9 @@ function parseMicrohamEqArgs(value) {
|
|||||||
gateEnabled,
|
gateEnabled,
|
||||||
gateThreshold,
|
gateThreshold,
|
||||||
gateRelease,
|
gateRelease,
|
||||||
|
normalizerEnabled,
|
||||||
|
normalizerPeak,
|
||||||
|
normalizerMaxGain,
|
||||||
limiter
|
limiter
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,7 @@
|
|||||||
"Key speichern": "Key speichern",
|
"Key speichern": "Key speichern",
|
||||||
"Frequenz und Mode der Station werden beim Bandwechsel an dein Wavelog-Logbuch gesendet.": "Frequenz und Mode der Station werden beim Bandwechsel an dein Wavelog-Logbuch gesendet.",
|
"Frequenz und Mode der Station werden beim Bandwechsel an dein Wavelog-Logbuch gesendet.": "Frequenz und Mode der Station werden beim Bandwechsel an dein Wavelog-Logbuch gesendet.",
|
||||||
"Wavelog API Key gespeichert.": "Wavelog API Key gespeichert.",
|
"Wavelog API Key gespeichert.": "Wavelog API Key gespeichert.",
|
||||||
|
"Wavelog öffnen": "Wavelog öffnen",
|
||||||
"Reservierungen": "Reservierungen",
|
"Reservierungen": "Reservierungen",
|
||||||
"Naechsten Slot reservieren": "Naechsten Slot reservieren",
|
"Naechsten Slot reservieren": "Naechsten Slot reservieren",
|
||||||
"SWR Uebersicht": "SWR Uebersicht",
|
"SWR Uebersicht": "SWR Uebersicht",
|
||||||
|
|||||||
@@ -56,6 +56,7 @@
|
|||||||
"Key speichern": "Save key",
|
"Key speichern": "Save key",
|
||||||
"Frequenz und Mode der Station werden beim Bandwechsel an dein Wavelog-Logbuch gesendet.": "The station's frequency and mode are sent to your Wavelog logbook on band change.",
|
"Frequenz und Mode der Station werden beim Bandwechsel an dein Wavelog-Logbuch gesendet.": "The station's frequency and mode are sent to your Wavelog logbook on band change.",
|
||||||
"Wavelog API Key gespeichert.": "Wavelog API key saved.",
|
"Wavelog API Key gespeichert.": "Wavelog API key saved.",
|
||||||
|
"Wavelog öffnen": "Open Wavelog",
|
||||||
"Reservierungen": "Reservations",
|
"Reservierungen": "Reservations",
|
||||||
"Naechsten Slot reservieren": "Reserve next slot",
|
"Naechsten Slot reservieren": "Reserve next slot",
|
||||||
"SWR Uebersicht": "SWR Overview",
|
"SWR Uebersicht": "SWR Overview",
|
||||||
|
|||||||
@@ -167,7 +167,10 @@
|
|||||||
<p class="muted">Session Key (Ticket): <code id="openwebrxSessionTicket">-</code></p>
|
<p class="muted">Session Key (Ticket): <code id="openwebrxSessionTicket">-</code></p>
|
||||||
</div>
|
</div>
|
||||||
<div id="wavelogKeyBox" class="stack">
|
<div id="wavelogKeyBox" class="stack">
|
||||||
<label for="wavelogKeyInput">Wavelog API Key (Rig-Key)</label>
|
<div class="section-head">
|
||||||
|
<label for="wavelogKeyInput">Wavelog API Key (Rig-Key)</label>
|
||||||
|
<a id="wavelogInstanceLink" target="_blank" rel="noopener noreferrer" hidden>Wavelog öffnen</a>
|
||||||
|
</div>
|
||||||
<input id="wavelogKeyInput" type="text" autocomplete="off" spellcheck="false" placeholder="Wavelog API Key" />
|
<input id="wavelogKeyInput" type="text" autocomplete="off" spellcheck="false" placeholder="Wavelog API Key" />
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button id="wavelogKeySaveBtn" type="button" class="ghost-btn">Key speichern</button>
|
<button id="wavelogKeySaveBtn" type="button" class="ghost-btn">Key speichern</button>
|
||||||
@@ -548,6 +551,12 @@
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<footer class="site-footer">
|
||||||
|
<a href="https://amateurradioclubgraz.at/imprint/" target="_blank" rel="noopener noreferrer">Impressum</a>
|
||||||
|
<span class="site-footer-sep" aria-hidden="true">·</span>
|
||||||
|
<a href="https://amateurradioclubgraz.at/datenschutz/" target="_blank" rel="noopener noreferrer">Datenschutz</a>
|
||||||
|
</footer>
|
||||||
|
|
||||||
<script src="/app.js" defer></script>
|
<script src="/app.js" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -762,24 +762,298 @@ button:disabled {
|
|||||||
|
|
||||||
.plugin-eq-builder {
|
.plugin-eq-builder {
|
||||||
border: 1px solid var(--surface-border);
|
border: 1px solid var(--surface-border);
|
||||||
border-radius: 10px;
|
border-radius: 16px;
|
||||||
padding: 0.65rem;
|
padding: 1rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.5rem;
|
gap: 0.9rem;
|
||||||
background: rgba(255, 255, 255, 0.02);
|
background:
|
||||||
|
radial-gradient(120% 140% at 0% 0%, color-mix(in srgb, var(--primary) 10%, transparent), transparent 55%),
|
||||||
|
color-mix(in srgb, var(--surface) 70%, transparent);
|
||||||
|
box-shadow: inset 0 1px 0 color-mix(in srgb, #fff 6%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.plugin-eq-row {
|
.eq-head {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: 130px 1fr 88px;
|
align-items: flex-start;
|
||||||
gap: 0.5rem;
|
gap: 0.6rem;
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.plugin-eq-checkbox {
|
.eq-head-text {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-head-title {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 1.05rem;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-head-hint {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-presets {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-chip {
|
||||||
|
appearance: none;
|
||||||
|
border: 1px solid var(--surface-border);
|
||||||
|
background: color-mix(in srgb, var(--surface) 55%, transparent);
|
||||||
|
color: var(--text);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.35rem 0.85rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color var(--transition), background var(--transition), transform var(--transition), box-shadow var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-chip:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
border-color: color-mix(in srgb, var(--primary) 55%, var(--surface-border));
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-chip.is-star::before {
|
||||||
|
content: "★ ";
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-chip.is-active {
|
||||||
|
background: var(--primary);
|
||||||
|
color: var(--primary-ink);
|
||||||
|
border-color: var(--primary);
|
||||||
|
box-shadow: 0 6px 18px color-mix(in srgb, var(--primary) 32%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-preset-info {
|
||||||
|
color: var(--muted);
|
||||||
|
min-height: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-section {
|
||||||
|
border: 1px solid var(--surface-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 0.75rem 0.85rem;
|
||||||
|
display: grid;
|
||||||
|
gap: 0.55rem;
|
||||||
|
background: color-mix(in srgb, var(--surface) 45%, transparent);
|
||||||
|
transition: border-color var(--transition), box-shadow var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-section.is-active {
|
||||||
|
border-color: color-mix(in srgb, var(--primary) 45%, var(--surface-border));
|
||||||
|
box-shadow: 0 0 0 1px color-mix(in srgb, var(--primary) 18%, transparent) inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-section-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
justify-content: space-between;
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-section-title {
|
||||||
|
margin: 0;
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-section-sub {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-section-body {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-note {
|
||||||
|
margin: 0.1rem 0 0;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--muted);
|
||||||
|
padding: 0.5rem 0.6rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: color-mix(in srgb, var(--primary) 8%, transparent);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--primary) 18%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-control {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.3rem;
|
||||||
|
transition: opacity var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-control.is-disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-control-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-control-label {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 0.86rem;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-control-value {
|
||||||
|
width: 72px;
|
||||||
|
text-align: right;
|
||||||
|
background: color-mix(in srgb, var(--surface) 70%, transparent);
|
||||||
|
border: 1px solid var(--surface-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--text);
|
||||||
|
padding: 0.2rem 0.4rem;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-control-unit {
|
||||||
|
width: 2.2rem;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-range {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 100%;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: color-mix(in srgb, var(--muted) 35%, transparent);
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-range::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--primary);
|
||||||
|
border: 2px solid var(--primary-ink);
|
||||||
|
box-shadow: 0 2px 8px color-mix(in srgb, var(--primary) 45%, transparent);
|
||||||
|
transition: transform var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-range::-webkit-slider-thumb:hover {
|
||||||
|
transform: scale(1.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-range::-moz-range-thumb {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--primary);
|
||||||
|
border: 2px solid var(--primary-ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-switch {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.55rem;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-switch input {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-switch-track {
|
||||||
|
position: relative;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 40px;
|
||||||
|
height: 22px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: color-mix(in srgb, var(--muted) 40%, transparent);
|
||||||
|
border: 1px solid var(--surface-border);
|
||||||
|
transition: background var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-switch-track::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fff;
|
||||||
|
transition: transform var(--transition);
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-switch input:checked + .eq-switch-track {
|
||||||
|
background: var(--primary);
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-switch input:checked + .eq-switch-track::after {
|
||||||
|
transform: translateX(18px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-switch input:focus-visible + .eq-switch-track {
|
||||||
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 35%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-switch-label {
|
||||||
|
font-size: 0.86rem;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-sub-toggle {
|
||||||
|
padding-top: 0.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-preview {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-preview-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-preview-code {
|
||||||
|
display: block;
|
||||||
|
font-family: "SFMono-Regular", "Consolas", "Liberation Mono", monospace;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--text);
|
||||||
|
background: color-mix(in srgb, var(--bg-0) 55%, transparent);
|
||||||
|
border: 1px solid var(--surface-border);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 0.6rem 0.7rem;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 540px) {
|
||||||
|
.eq-control-head {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dt {
|
dt {
|
||||||
@@ -885,3 +1159,24 @@ html[data-theme="light"] .message.domain-notice {
|
|||||||
display: grid;
|
display: grid;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Legal footer links (Impressum / Datenschutz -> homepage) */
|
||||||
|
.site-footer {
|
||||||
|
margin: 2rem auto 1.25rem;
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
.site-footer a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.site-footer a:hover,
|
||||||
|
.site-footer a:focus-visible {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.site-footer-sep {
|
||||||
|
margin: 0 0.5rem;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|||||||
@@ -360,7 +360,7 @@ async function routeRequest(req, res) {
|
|||||||
if (method === "GET" && url.pathname === "/v1/me") {
|
if (method === "GET" && url.pathname === "/v1/me") {
|
||||||
const auth = requireAuth(req, res);
|
const auth = requireAuth(req, res);
|
||||||
if (!auth) return;
|
if (!auth) return;
|
||||||
return sendJson(res, 200, { user: sanitizeUser(auth.user), wavelogApiKey: auth.user.wavelogApiKey || "", capabilities: userCapabilities(auth.user) });
|
return sendJson(res, 200, { user: sanitizeUser(auth.user), wavelogApiKey: auth.user.wavelogApiKey || "", wavelogBaseUrl: wavelogBaseUrlPublic(), capabilities: userCapabilities(auth.user) });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (method === "GET" && url.pathname === "/v1/auth/session/diag") {
|
if (method === "GET" && url.pathname === "/v1/auth/session/diag") {
|
||||||
@@ -3641,7 +3641,8 @@ async function handleOpenWebRxSessionIssue(res, user) {
|
|||||||
return sendJson(res, 200, {
|
return sendJson(res, 200, {
|
||||||
ok: true,
|
ok: true,
|
||||||
session: result,
|
session: result,
|
||||||
wavelogApiKey: user.wavelogApiKey || ""
|
wavelogApiKey: user.wavelogApiKey || "",
|
||||||
|
wavelogBaseUrl: wavelogBaseUrlPublic()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3719,6 +3720,12 @@ async function handleOpenWebRxBandSelect(res, user, body) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Public Wavelog base URL (for the UI link). Comes from runtime env only, so the
|
||||||
|
// private hostname is never baked into this (public) repo.
|
||||||
|
function wavelogBaseUrlPublic() {
|
||||||
|
return String(process.env.WAVELOG_BASE_URL || "").trim().replace(/\/+$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
// Push a frequency/mode to the active operator's Wavelog logbook (CAT API).
|
// Push a frequency/mode to the active operator's Wavelog logbook (CAT API).
|
||||||
// Best-effort: never throws into the caller's flow.
|
// Best-effort: never throws into the caller's flow.
|
||||||
async function sendWavelogCat(user, frequencyHz, mode) {
|
async function sendWavelogCat(user, frequencyHz, mode) {
|
||||||
|
|||||||
Reference in New Issue
Block a user