feat: quality slider in settings with color gradient
Some checks failed
Mirror to GitHub / mirror (push) Failing after 36s
Build Release Binaries / build-amd64 (push) Failing after 1m56s

Replace the quality dropdown with a range slider in the settings
panel. The slider goes from Auto (green) through Opus 24k, Opus 6k
(yellow), Codec2 3.2k (orange) to Codec2 1.2k (dark red). The
track uses a green-to-red gradient and the label color updates
to match the selected level. Removed the quality dropdown from
the connect screen — quality is now settings-only.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-04-07 17:50:46 +04:00
parent 85c2146760
commit 44f04b55e8
3 changed files with 134 additions and 26 deletions

View File

@@ -26,15 +26,6 @@
<label>Alias
<input id="alias" type="text" placeholder="your name" />
</label>
<label>Quality
<select id="quality">
<option value="auto">Auto</option>
<option value="good">Opus 24k</option>
<option value="degraded">Opus 6k</option>
<option value="codec2-3200">Codec2 3.2k</option>
<option value="catastrophic">Codec2 1.2k</option>
</select>
</label>
<div class="form-row">
<label class="checkbox">
<input id="os-aec" type="checkbox" checked />
@@ -100,15 +91,20 @@
</div>
<div class="settings-section">
<h3>Audio</h3>
<label>Quality
<select id="s-quality">
<option value="auto">Auto (adaptive)</option>
<option value="good">Good — Opus 24kbps</option>
<option value="degraded">Degraded — Opus 6kbps</option>
<option value="codec2-3200">Codec2 3200bps</option>
<option value="catastrophic">Catastrophic — Codec2 1200bps</option>
</select>
</label>
<div class="quality-control">
<div class="quality-header">
<span class="setting-label">QUALITY</span>
<span id="s-quality-label" class="quality-label">Auto</span>
</div>
<input id="s-quality" type="range" min="0" max="4" step="1" value="0" class="quality-slider" />
<div class="quality-ticks">
<span>Auto</span>
<span>Opus 24k</span>
<span>Opus 6k</span>
<span>C2 3.2k</span>
<span>C2 1.2k</span>
</div>
</div>
<label class="checkbox">
<input id="s-os-aec" type="checkbox" />
OS Echo Cancellation (macOS VoiceProcessingIO)

View File

@@ -8,7 +8,6 @@ const callScreen = document.getElementById("call-screen")!;
const roomInput = document.getElementById("room") as HTMLInputElement;
const aliasInput = document.getElementById("alias") as HTMLInputElement;
const osAecCheckbox = document.getElementById("os-aec") as HTMLInputElement;
const qualitySelect = document.getElementById("quality") as HTMLSelectElement;
const connectBtn = document.getElementById("connect-btn") as HTMLButtonElement;
const connectError = document.getElementById("connect-error")!;
const roomName = document.getElementById("room-name")!;
@@ -49,7 +48,30 @@ const sRoom = document.getElementById("s-room") as HTMLInputElement;
const sAlias = document.getElementById("s-alias") as HTMLInputElement;
const sOsAec = document.getElementById("s-os-aec") as HTMLInputElement;
const sAgc = document.getElementById("s-agc") as HTMLInputElement;
const sQuality = document.getElementById("s-quality") as HTMLSelectElement;
const sQuality = document.getElementById("s-quality") as HTMLInputElement;
const sQualityLabel = document.getElementById("s-quality-label")!;
// Quality slider config
const QUALITY_STEPS = ["auto", "good", "degraded", "codec2-3200", "catastrophic"];
const QUALITY_LABELS = ["Auto", "Opus 24k", "Opus 6k", "Codec2 3.2k", "Codec2 1.2k"];
const QUALITY_COLORS = ["#4ade80", "#4ade80", "#facc15", "#e97320", "#991b1b"];
function qualityToIndex(q: string): number {
const idx = QUALITY_STEPS.indexOf(q);
return idx >= 0 ? idx : 0;
}
function updateQualityUI(index: number) {
sQualityLabel.textContent = QUALITY_LABELS[index];
sQualityLabel.style.color = QUALITY_COLORS[index];
const pct = index / (QUALITY_STEPS.length - 1);
// Gradient: green at left → yellow middle → dark red at right
sQuality.style.background = `linear-gradient(90deg, #4ade80 0%, #facc15 40%, #e97320 70%, #991b1b 100%)`;
}
sQuality.addEventListener("input", () => {
updateQualityUI(parseInt(sQuality.value));
});
const sFingerprint = document.getElementById("s-fingerprint")!;
const sRecentRooms = document.getElementById("s-recent-rooms")!;
const sClearRecent = document.getElementById("s-clear-recent")!;
@@ -159,7 +181,6 @@ function applySettings() {
roomInput.value = s.room;
aliasInput.value = s.alias;
osAecCheckbox.checked = s.osAec;
qualitySelect.value = s.quality || "auto";
renderRecentRooms(s.recentRooms);
renderRelayButton();
}
@@ -380,7 +401,7 @@ async function doConnect() {
userDisconnected = false;
const s = loadSettings();
s.room = roomInput.value; s.alias = aliasInput.value; s.osAec = osAecCheckbox.checked; s.quality = qualitySelect.value;
s.room = roomInput.value; s.alias = aliasInput.value; s.osAec = osAecCheckbox.checked;
const room = roomInput.value.trim();
if (room) {
const entry: RecentRoom = { relay: relay.address, room };
@@ -392,7 +413,7 @@ async function doConnect() {
await invoke("connect", {
relay: relay.address, room: roomInput.value,
alias: aliasInput.value, osAec: osAecCheckbox.checked,
quality: qualitySelect.value,
quality: s.quality || "auto",
});
showCallScreen();
} catch (e: any) {
@@ -534,7 +555,10 @@ listen("call-event", (event: any) => {
// ── Settings ──
function openSettings() {
const s = loadSettings();
sRoom.value = s.room; sAlias.value = s.alias; sOsAec.checked = s.osAec; sQuality.value = s.quality || "auto";
sRoom.value = s.room; sAlias.value = s.alias; sOsAec.checked = s.osAec;
const qi = qualityToIndex(s.quality || "auto");
sQuality.value = String(qi);
updateQualityUI(qi);
sFingerprint.textContent = myFingerprint || "(loading...)";
renderSettingsRecentRooms(s.recentRooms);
settingsPanel.classList.remove("hidden");
@@ -569,9 +593,10 @@ settingsPanel.addEventListener("click", (e) => { if (e.target === settingsPanel)
settingsSave.addEventListener("click", () => {
const s = loadSettings();
s.room = sRoom.value; s.alias = sAlias.value; s.osAec = sOsAec.checked; s.quality = sQuality.value;
s.room = sRoom.value; s.alias = sAlias.value; s.osAec = sOsAec.checked;
s.quality = QUALITY_STEPS[parseInt(sQuality.value)] || "auto";
saveSettingsObj(s);
roomInput.value = s.room; aliasInput.value = s.alias; osAecCheckbox.checked = s.osAec; qualitySelect.value = s.quality;
roomInput.value = s.room; aliasInput.value = s.alias; osAecCheckbox.checked = s.osAec;
renderRecentRooms(s.recentRooms);
closeSettings();
});

View File

@@ -651,3 +651,90 @@ button.primary:disabled { opacity: 0.5; cursor: not-allowed; }
}
.secondary-btn:hover { border-color: var(--accent); color: var(--text); }
/* ── Quality slider ── */
.quality-control {
display: flex;
flex-direction: column;
gap: 6px;
padding: 4px 0;
}
.quality-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.quality-label {
font-size: 13px;
font-weight: 600;
padding: 2px 8px;
border-radius: 6px;
transition: all 0.2s;
}
.quality-slider {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 6px;
border-radius: 3px;
outline: none;
cursor: pointer;
transition: background 0.2s;
}
.quality-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--text);
border: 2px solid var(--bg);
box-shadow: 0 1px 4px rgba(0,0,0,0.4);
cursor: pointer;
transition: transform 0.1s;
}
.quality-slider::-webkit-slider-thumb:hover {
transform: scale(1.15);
}
.quality-ticks {
display: flex;
justify-content: space-between;
font-size: 9px;
color: var(--text-dim);
padding: 0 2px;
}
.form select {
background: var(--surface);
border: 1px solid #333;
border-radius: 8px;
padding: 10px 12px;
color: var(--text);
font-size: 15px;
outline: none;
transition: border-color 0.2s;
}
.form select:focus {
border-color: var(--accent);
}
.settings-section select {
background: var(--surface);
border: 1px solid #333;
border-radius: 8px;
padding: 8px 10px;
color: var(--text);
font-size: 14px;
outline: none;
}
.settings-section select:focus {
border-color: var(--accent);
}