feat: quality slider in settings with color gradient
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:
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user