Swipe between items like turning pages (#52)

On a phone the reader is fixed over the item list, and the swipe from #49 moved it alone and
faded it to 40%, so the list showed through it and in the strip it uncovered, and again as the
next item slid in from the far side. Now a layer beside the reader, #dpeek, holds what is really
there: the next or previous item, drawn by the same detailHtml the reader uses, with 16px of the
page's background between them; "No more items" past the last; or, swiping right from the
first, a dimmer over the list that lifts as the reader, shadowed along its edge, is drawn off.
On release the swipe carries on from where the finger left it, over 120-250ms by how far is
left, and the neighbour becomes the reader in place; with reduced motion it switches at once.

A touch starting within 14px of the left edge is kept from Safari, which otherwise takes it as
Back and leaves the page mid-swipe. 14px because the back button starts at 16.

Offsets are rounded to whole pixels: at a fractional offset the seam between the reader and its
neighbour drew a stray light line.

The design had the list shift a third of the way across as it is uncovered, as iOS does. #detail
lives inside #shell, and a transform there makes it the containing block for the fixed reader,
so that part is left out; the dimmer and shadow carry it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-26 23:46:07 +00:00
parent a573a07ed5
commit f2fde8cc75
5 changed files with 113 additions and 28 deletions

View File

@@ -230,6 +230,19 @@ function showDetail(e){
}
// Nothing when there are no files: the pane that would say so is hidden above, and on a phone,
// where the files sit over the text, a box saying "No files" only pushed the text down.
const encs=e.enclosures.map(encBox).join('');
const narrow=!!window.matchMedia?.('(max-width:820px)')?.matches;
box.innerHTML=detailHtml(e);
if(files) files.innerHTML=narrow?'':`<div class="fhd">Files</div>${encs}`;
$('#dback').onclick=()=>showDetail(null);
for(const root of [box,files]) if(root) $$('button[data-a]',root).forEach(b=>
b.onclick=()=>epAction(b.dataset.a,e,null,b.dataset.enc?Number(b.dataset.enc):null));
syncPlayButtons();
}
/// What the reader shows for an item. Its own function because a phone also draws the next or
/// previous item beside the reader while it is swiped (gestures.ts).
function detailHtml(e){
const encs=e.enclosures.map(encBox).join('');
// A phone has no room for the files pane, so the files go above the text there instead.
// Below it, a long set of show notes pushed play and delete screens down, and on an iPhone
@@ -237,7 +250,8 @@ function showDetail(e){
const narrow=!!window.matchMedia?.('(max-width:820px)')?.matches;
const f=S.feeds.find(x=>x.id===e.feed_id);
const num=[e.season?`S${e.season}`:'',e.episode?`E${e.episode}`:''].filter(Boolean).join('');
box.innerHTML=`
// description was sanitized server-side with ammonia before it ever reached here
return `
<button class="btn ico" id="dback" title="Back to the items" aria-label="Back to the items">${ICON.left}</button>
<h3 class="dt">${esc(e.title||'(untitled)')}</h3>
<div class="dmeta">
@@ -254,12 +268,6 @@ function showDetail(e){
</div>
${narrow?encs:''}
<div class="dbody">${(e.description&&e.description.trim())||'<em>No show notes.</em>'}</div>`;
if(files) files.innerHTML=narrow?'':`<div class="fhd">Files</div>${encs}`;
// description was sanitized server-side with ammonia before it ever reached here
$('#dback').onclick=()=>showDetail(null);
for(const root of [box,files]) if(root) $$('button[data-a]',root).forEach(b=>
b.onclick=()=>epAction(b.dataset.a,e,null,b.dataset.enc?Number(b.dataset.enc):null));
syncPlayButtons();
}
/// One enclosure: a play button when the file is here, otherwise what it is and a way to get it.