DeadAir's pause on a slider, shown only while DeadAir is on (#183)

The four segmented buttons gave no sense that 0.2 to 0.5 s is one scale,
and stayed on screen with DeadAir off, where they did nothing. A range
input with a dot at each stop and Brisk/Relaxed at its ends says both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-11 00:14:50 +00:00
parent 089202f72b
commit 4ccf2e5511
4 changed files with 32 additions and 8 deletions

View File

@@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Choose the pause DeadAir leaves where it cuts a silence, from 0.2 to 0.5 seconds, for each show in its settings. It is 0.3 seconds unless you change it, up from 0.2, which made speech sound rushed (#182). - Choose the pause DeadAir leaves where it cuts a silence, from 0.2 to 0.5 seconds, for each show in its settings, on a slider from brisk to relaxed shown while DeadAir is on. It is 0.3 seconds unless you change it, up from 0.2, which made speech sound rushed (#182, #183).
### Changed ### Changed

View File

@@ -1782,8 +1782,8 @@ test('DeadAir is off until turned on for a show, then plays it shortened, with y
// A longer pause of your own (#182) saves less. // A longer pause of your own (#182) saves less.
const usual = (await entry()).enclosures[0].deadair; const usual = (await entry()).enclosures[0].deadair;
await page.locator('#content .acts [data-a="settings"]').click(); await page.locator('#content .acts [data-a="settings"]').click();
await expect(page.locator('[name=sdpause][value="300"]')).toBeChecked(); await expect(page.locator('#sdpause')).toHaveValue('300');
await page.locator('[name=sdpause][value="500"]').locator('xpath=..').click(); await page.locator('#sdpause').fill('500');
await page.locator('#ssave').click(); await page.locator('#ssave').click();
await expect.poll(async () => (await entry()).enclosures[0].deadair).toBeLessThan(usual); await expect.poll(async () => (await entry()).enclosures[0].deadair).toBeLessThan(usual);

View File

@@ -1114,6 +1114,23 @@ input[type=range]::-moz-range-thumb{width:12px;height:12px;border:0;border-radiu
.wcap{margin:0 0 16px;font-size:13px;line-height:1.55;color:var(--dim)} .wcap{margin:0 0 16px;font-size:13px;line-height:1.55;color:var(--dim)}
.wcap input[type=number]{display:inline-block;width:4.2em;margin:0 2px;padding:1px 4px;font-size:13px;text-align:center;font-variant-numeric:tabular-nums;color:var(--fg)} .wcap input[type=number]{display:inline-block;width:4.2em;margin:0 2px;padding:1px 4px;font-size:13px;text-align:center;font-variant-numeric:tabular-nums;color:var(--fg)}
.ffield{display:grid;gap:6px;margin-bottom:14px} .ffield{display:grid;gap:6px;margin-bottom:14px}
/* DeadAir's pause: four stops on one scale, a dot at each so it reads as four places to stop. */
.dpause{display:grid;gap:6px;margin-top:6px}
.dphead{display:flex;justify-content:space-between;align-items:baseline}
.dphead output{font-variant-numeric:tabular-nums;font-weight:600}
.dpslide{--t:22px;position:relative;height:var(--t)}
.dpslide::before,.dpslide::after{content:'';position:absolute;top:50%;pointer-events:none}
.dpslide::before{left:0;right:0;height:6px;margin-top:-3px;border-radius:3px;background:var(--raise)}
.dpslide::after{left:calc(var(--t)/2 - 3px);right:calc(var(--t)/2 - 3px);height:6px;margin-top:-3px;
background:radial-gradient(circle,var(--dim) 2px,transparent 2.5px) 0 0/6px 6px no-repeat,
radial-gradient(circle,var(--dim) 2px,transparent 2.5px) 33.333% 0/6px 6px no-repeat,
radial-gradient(circle,var(--dim) 2px,transparent 2.5px) 66.667% 0/6px 6px no-repeat,
radial-gradient(circle,var(--dim) 2px,transparent 2.5px) 100% 0/6px 6px no-repeat}
.dpslide input[type=range]{position:relative;z-index:1;width:100%;height:var(--t);margin:0;background:transparent}
.dpslide input[type=range]::-webkit-slider-thumb{width:var(--t);height:var(--t);box-shadow:0 0 0 3px var(--panel),0 1px 4px rgba(0,0,0,.3)}
.dpslide input[type=range]::-moz-range-thumb{width:var(--t);height:var(--t);box-shadow:0 0 0 3px var(--panel),0 1px 4px rgba(0,0,0,.3)}
.dpslide input[type=range]:focus-visible{outline:2px solid var(--accent);outline-offset:4px;border-radius:var(--t)}
.dpends{display:flex;justify-content:space-between;font-size:12.5px;color:var(--dim)}
.fdl+.ffield{margin-top:18px} .fdl+.ffield{margin-top:18px}
.flabel{font-size:14px;font-weight:500} .flabel{font-size:14px;font-weight:500}
.cap{margin:0;font-size:12.5px;line-height:1.45;color:var(--dim)} .cap{margin:0;font-size:12.5px;line-height:1.45;color:var(--dim)}
@@ -1350,6 +1367,8 @@ body:has(#player.on) #status{padding-bottom:3px}
.sw input:checked+i::after{transform:translateX(20px)} .sw input:checked+i::after{transform:translateX(20px)}
.seg label{padding:7px 4px;border-radius:8px;font-size:14px} .seg label{padding:7px 4px;border-radius:8px;font-size:14px}
.seg label:has(input:checked){box-shadow:0 1px 3px rgba(0,0,0,.22)} .seg label:has(input:checked){box-shadow:0 1px 3px rgba(0,0,0,.22)}
.dpslide{--t:28px}
.dpends{font-size:14px}
/* 16px or more, or Safari zooms the page in on it. */ /* 16px or more, or Safari zooms the page in on it. */
.wcap input[type=number]{font-size:16px;padding:3px 4px} .wcap input[type=number]{font-size:16px;padding:3px 4px}
.chips{background:var(--panel2);border-color:var(--edge);border-radius:10px} .chips{background:var(--panel2);border-color:var(--edge);border-radius:10px}

View File

@@ -531,10 +531,11 @@ function settingsModal(f, newUrl?: string){
<p class="cap">Cuts the dead air: long silences in ${isGroup?'these shows':'this show'} shrink to a beat, so <p class="cap">Cuts the dead air: long silences in ${isGroup?'these shows':'this show'} shrink to a beat, so
episodes finish sooner and every voice keeps its own pace. Leave it off for stories whose pauses episodes finish sooner and every voice keeps its own pace. Leave it off for stories whose pauses
are part of the telling.</p> are part of the telling.</p>
<span class="flabel" id="sdpausel">Pause it leaves</span> <div class="dpause" id="sdpausebox"${f.deadair?'':' hidden'}>
<div class="tabs seg" role="radiogroup" aria-labelledby="sdpausel">${[200,300,400,500].map(ms=> <div class="dphead"><label class="flabel" for="sdpause">Pause it leaves</label><output id="sdpauseout" for="sdpause"></output></div>
`<label><input type="radio" name="sdpause" value="${ms}"${(f.deadair_pause||300)===ms?' checked':''}>${ms/1000} s</label>`).join('')}</div> <div class="dpslide"><input type="range" id="sdpause" min="200" max="500" step="100" value="${f.deadair_pause||300}"></div>
<p class="cap">A longer pause sounds more relaxed and saves less time.</p></div> <div class="dpends" aria-hidden="true"><span>Brisk</span><span>Relaxed</span></div>
<p class="cap">A longer pause sounds more relaxed and saves less time.</p></div></div>
${isGroup?'':`<div class="ffield"><label class="flabel" for="sblockin">Hide items that mention</label> ${isGroup?'':`<div class="ffield"><label class="flabel" for="sblockin">Hide items that mention</label>
<div class="chips" id="sblock">${chipsHTML(f.blocked||[],'sblockin')}</div> <div class="chips" id="sblock">${chipsHTML(f.blocked||[],'sblockin')}</div>
<p class="cap">Whole words or phrases. Hidden from your list and not downloaded for you, along <p class="cap">Whole words or phrases. Hidden from your list and not downloaded for you, along
@@ -579,6 +580,10 @@ function settingsModal(f, newUrl?: string){
if(n) n.oninput=()=>{ newest=Math.max(1,Math.floor(Number(n.value))||1); draw(false); }; if(n) n.oninput=()=>{ newest=Math.max(1,Math.floor(Number(n.value))||1); draw(false); };
}; };
for(const r of $$('[name=swin]')) r.onchange=()=>{ mode=r.value; draw(true); }; for(const r of $$('[name=swin]')) r.onchange=()=>{ mode=r.value; draw(true); };
// The pause only matters with DeadAir on, so it is shown only then.
$('#sdead').onchange=e=>{ $('#sdpausebox').hidden=!e.target.checked; };
const pause=$('#sdpause'), pauseText=()=>{ const s=`${pause.value/1000} s`; $('#sdpauseout').textContent=s; pause.setAttribute('aria-valuetext',s); };
pause.oninput=pauseText; pauseText();
draw(true); draw(true);
// The feed's own newest items, each as tall as it is long, so the strip reads as this show. // The feed's own newest items, each as tall as it is long, so the strip reads as this show.
if(!isGroup) api(`/api/feeds/${encodeURIComponent(f.id)}/entries?limit=40`).then(page=>{ if(!isGroup) api(`/api/feeds/${encodeURIComponent(f.id)}/entries?limit=40`).then(page=>{
@@ -619,7 +624,7 @@ function settingsModal(f, newUrl?: string){
keywords:chipWords($('#skw')), keywords:chipWords($('#skw')),
max_new_per_check:mode==='default'?null:mode==='all'?0:newest, max_new_per_check:mode==='default'?null:mode==='all'?0:newest,
auto_download:$('#sauto').checked, allow_explicit:$('#sexp').checked, deadair:$('#sdead').checked, auto_download:$('#sauto').checked, allow_explicit:$('#sexp').checked, deadair:$('#sdead').checked,
deadair_pause:Number($('[name=sdpause]:checked').value)}; deadair_pause:Number($('#sdpause').value)};
if($('#sblock')) patch.blocked=chipWords($('#sblock')); if($('#sblock')) patch.blocked=chipWords($('#sblock'));
// The address is the same for everyone: an admin's to change, for a feed that moved, and the // The address is the same for everyone: an admin's to change, for a feed that moved, and the
// API refuses it from anyone else. The folder and category are not this dialog's at all. // API refuses it from anyone else. The folder and category are not this dialog's at all.