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
- 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

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.
const usual = (await entry()).enclosures[0].deadair;
await page.locator('#content .acts [data-a="settings"]').click();
await expect(page.locator('[name=sdpause][value="300"]')).toBeChecked();
await page.locator('[name=sdpause][value="500"]').locator('xpath=..').click();
await expect(page.locator('#sdpause')).toHaveValue('300');
await page.locator('#sdpause').fill('500');
await page.locator('#ssave').click();
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 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}
/* 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}
.flabel{font-size:14px;font-weight:500}
.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)}
.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)}
.dpslide{--t:28px}
.dpends{font-size:14px}
/* 16px or more, or Safari zooms the page in on it. */
.wcap input[type=number]{font-size:16px;padding:3px 4px}
.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
episodes finish sooner and every voice keeps its own pace. Leave it off for stories whose pauses
are part of the telling.</p>
<span class="flabel" id="sdpausel">Pause it leaves</span>
<div class="tabs seg" role="radiogroup" aria-labelledby="sdpausel">${[200,300,400,500].map(ms=>
`<label><input type="radio" name="sdpause" value="${ms}"${(f.deadair_pause||300)===ms?' checked':''}>${ms/1000} s</label>`).join('')}</div>
<p class="cap">A longer pause sounds more relaxed and saves less time.</p></div>
<div class="dpause" id="sdpausebox"${f.deadair?'':' hidden'}>
<div class="dphead"><label class="flabel" for="sdpause">Pause it leaves</label><output id="sdpauseout" for="sdpause"></output></div>
<div class="dpslide"><input type="range" id="sdpause" min="200" max="500" step="100" value="${f.deadair_pause||300}"></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>
<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
@@ -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); };
};
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);
// 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=>{
@@ -619,7 +624,7 @@ function settingsModal(f, newUrl?: string){
keywords:chipWords($('#skw')),
max_new_per_check:mode==='default'?null:mode==='all'?0:newest,
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'));
// 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.