Commit Graph

20 Commits

Author SHA1 Message Date
e56bc9e8dd Save an episode without its dead air when DeadAir is on (#186)
The save button fetched the original while the player played it shortened.
/media/{id}/deadair sends the original's ID3 tag and then the pieces of the
person's plan one after another, read as it goes: MP3 and ADTS frames each
stand alone, so that is a file any player takes, and nothing is stored, as
with the playlist. The tag stops at the first kept frame, since what can
follow it, the Xing frame, gives the original's length.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 12:38:30 +00:00
d6c0e3e7a6 Show that DeadAir is still working on an episode (#176)
A new download in a show with DeadAir on plays at full length until its
copy is made, and nothing said so. Each file now carries
deadair_pending while DeadAir is on for its show and the copy is still
to come (not for one heard through, which it passes over), and the
episode's line and details say "Cutting dead air…". The maker sends a
deadair_done event when it finishes a file, and the page takes the list
again, so the line becomes how much shorter the episode is without a
reload.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 19:05:18 +00:00
00bc039964 DeadAir's wording: cut the dead air (#172)
The switch, the button's tooltip, the toasts and the time saved now say
what the name already pictures, the dead air cut, instead of explaining
it as silences shortened each time. The caption says the voice keeps its
own pace, which is the part people doubt, and names the shows to leave
out: stories whose pauses are part of the telling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 18:32:16 +00:00
827cc7489f DeadAir in the page: a switch per show, one beside the speed, and the time saved (#172)
A DeadAir switch in each show's settings, and the same switch beside
the speed for the show that is playing, drawn as a waveform with its gap
closed up, outlined when off and solid when on, with aria-pressed. A
file the server gives a copy for plays from /media/{id}/deadair. An
episode's line says how much shorter it is, and Settings says how much
DeadAir has saved you.

Turning it on or off mid-episode saves the place with a request it waits
for, not a beacon, then asks for the entry again from the new
GET /api/entries/{feed}/{guid}, whose position is in the clock of the
version it now plays, and carries on there. A copy not made yet plays
from the next time. Turning a show on wakes the copy maker, so the copy
is there in seconds rather than at its next pass.

The browser test plays a real MP3 with long silences, quiet.mp3, from a
show it adds and removes itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 18:18:41 +00:00
cb92d5e355 Say nothing in an empty list or an empty reader
Asked for: "Nothing here yet — try Scan now." and "Pick an item to read it." are gone. A search
that finds nothing still says so, as the words typed are the reason.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 12:50:22 +00:00
70c49921ab Keep a feed's and an item's buttons on one row (#146)
A feed's page has seven 44px buttons, which fit a 390px phone with nothing to spare: on a 360px
one, Unsubscribe went onto a second row by itself. Below that width an icon button now narrows
rather than the row breaking. The reader's buttons were items of its meta line itself, so a long
feed name took them onto the next line one at a time; they are one group now, which moves as one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 13:51:11 +00:00
95acde3046 Lay the Directory out as Apple's is, with the most subscribed in it (#124, #126, #128)
The Directory was one grid of every listed feed, 1,472 in production, under Podcasts and Blogs
tabs, a wall of about 30 category buttons and a sort menu, with a picked category's
subcategories appearing as a second row of buttons that looked like the first. It read as a
list to scroll, and the two rows were hard to tell apart.

It opens now on the ten most subscribed feeds, ranked, and the categories as tiles, each with
three of its shows' covers fanned in its corner on the tint its initials would get, so the
shows colour it and no theme's palette changes. A category has a page of its own: its most
subscribed, its subcategories with how many each holds, and its grid. See all is every feed,
as the grid was. Podcasts or Blogs holds across every page. Popular, the ten most subscribed,
had a place of its own in the feed list; it is the Directory's first section instead, and its
key, g p, is gone. /api/popular stays for scripts.

The search box finds a feed in the Directory by name (#126). It said "Search items…" there and
did nothing, since loadEntries returns early for a place that lists feeds.

A feed you do not subscribe to opens a page of its own (#128): its cover, category (a link to
that category's page), how many here subscribe, a subscribe button and its latest twenty items,
each with its title, linking to the post where it has one, its first lines, date and length.
Before, a click on it did nothing; only its + did. The items come from GET /api/directory/{id},
which answers only for a feed the Directory lists, so a guessed id reaches nothing private, and
carries no file or its address. The feed's own description would belong at the top, but the
database does not keep one.

Every tile and chart row takes the keyboard, as the feed list's rows do. Feed text made plain
keeps a space at a line break or a paragraph's end, which glued "2010)Recorded" together.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:19:38 +00:00
d238681ec1 Name an item without a title from its own text, not "(untitled)" (#116)
RSS 2.0 makes an item's title optional, and some blogs leave it out on purpose: Scripting News
titles almost none of its posts. Fifty rows of "(untitled)" said nothing about any of them.

entryName gives an item its title, or the opening of its text (HTML read through DOMParser, an
inert document that loads nothing; cut at a word near 120 characters), or its file's name, or
its show and date, with a flag for a name that is not a title. The list sets that one in the
regular weight, as the text it is rather than a heading; the reader leaves out the heading so
the post starts with itself; the player, the lock screen, Currently Listening, the native shell,
Share and the queued toast use the same name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 20:46:47 +00:00
a387012c69 Count as queued only what a scan will download on its own (#113)
Every file in state 'pending' counted as waiting to download: 4420 in production, across 12
shows. Since #97 a scan only downloads among a feed's newest max_new_per_check items, so those
were back-catalogue episodes no scan would take; the real queue was 0.

A new state, 'held': listed and downloadable by hand, but outside the feed's newest items, or of
a feed nothing downloads automatically. Db::hold_back moves a feed's waiting files between
'pending' and 'held' each time the feed is due, changed or not, and again after its items are
stored, so a new episode, a raised limit or auto-download turned on or off moves them. A held
file keeps its item's place among the newest, as a downloaded one does. 'pending' now means
queued, so ipx status, /api/status and the dashboard read true without changing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 19:47:52 +00:00
c7f13eea2f Send a changed feed's row to the page instead of it reloading the list (#105)
After a feed was checked, failed or downloaded a file, and after every item read, the page
fetched /api/feeds whole, about 60 ms for 160 rows, though one row had changed. The live event
stream now knows who is connected and, after an event that changes a feed, sends that person
its row (feed_row), built by the same code as the list (feed_rows, with Db::feed_list asked for
one feed). Marking an item read answers with the feed's row. The page puts the row in place
and redraws once a frame. A routine skip of a feed not due sends nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:41:43 +00:00
a5de4ae06c Toggle state in the icon's shape, not the theme's colours (#74)
4ed2d59 drew a pressed toggle in each theme's accent colour; the themes'
colours were not to change. Back as they were, pinned rows included. The
read button carries its state in its shape instead, as the pin does with
outline and solid: a tick when read, the envelope when not, where before
it showed the action (the envelope on a read item).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 23:38:59 +00:00
4ed2d59311 Toggles show their state the same way everywhere (#74, #75)
A sweep of all 24 theme palettes, measuring each icon's drawn colour,
found pinned in three colours: accent in the feed list, the text colour
on an item's row, and uncoloured on the toolbar, beside the title and on
the feed page. The read button showed the action (an envelope on a read
item) beside a pin showing the state. Now each toggle shows what is, with
aria-pressed, and a pressed one is the accent colour; Classic needs its
own rule, as its buttons set their colour at higher specificity. The
contrast test checks the accent on the button grounds, where it now draws.

Directory's Subscribe button carried the Subscribed tick; it is a plus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 23:36:23 +00:00
e16dace9c0 On the Unread tab, a swipe back goes to the item just read (#50)
selectEntry took each read item out of the list the moment you moved on
from it, so the item was not there for the back swipe (or k) to reach: it
went to the one before, or to the list if the item had been first.

Items read while turning from one to the next (a swipe, j and k) now stay
in the list until the reader closes or another item is picked from the
list, and a background refresh keeps them as it keeps the open one.
Picking a row still drops the item left behind at once, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:36:29 +00:00
f2fde8cc75 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>
2026-09-26 23:46:07 +00:00
380a552913 Share a feed, an item or a file (#48)
A share button in the feed's header, beside an item's "Open the original", and on each file.
It uses the Web Share API where the browser has it, which is the share sheet on a phone, and
copies the link where it does not. A file is shared by the publisher's address, not /media/,
which only someone signed in here can open.

Paid feeds carry the subscriber's access in their address, Patreon's in a token, so sharing one
gives the subscription away. An address that looks like that asks first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 13:17:52 +00:00
f09bb4a11c Revert pinned items rising to the top of their list
Sorting by the pin column, or the Pinned tab, was enough. order_sql loses its
pinned_first option, pinning no longer reloads the list, and the tests and
changelog line for #35 go. The NULLS FIRST/LAST ordering from the Postgres work
stays.

Closes #36.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 22:08:37 +00:00
c99e17bd80 A pinned item goes to the top of its list
order_sql takes pinned_first, which puts coalesce(s.flagged, 0) DESC ahead of
the chosen sort, so pins lead every list in whatever order is asked for and on
every page of it. Not when sorting by the pin column itself, where the direction
is the point, and not for Currently Listening. Pinning now asks for the list again
so the row moves at once, instead of redrawing it where it stood.

Closes #35.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 17:51:38 +00:00
42a1136e2e Every play button for what is playing shows pause, and pauses it
Only the player bar's button changed; the files pane's, the row's and the
toolbar's kept showing play while it played. play() now pauses when asked to play
what is already playing, which makes each of them a toggle, and syncPlayButtons()
repaints them on play, pause and ended and whenever the list or reader is drawn.

Closes #34.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 15:01:03 +00:00
53341264a7 On a phone, no "No files" box above an item that has none
The files sit over the text on a phone, so an item without any showed a box
saying so before its text. Nothing is shown now; the desktop files pane already
hid itself when empty.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 14:50:36 +00:00
26802d2b23 The page's script is TypeScript in web/src, built and minified with swc
- web/src/*.ts: the script that was inline in index.html and login.html, split along its
  existing sections. Still one scope, concatenated in order, not modules.
- web/build.mjs strips the types, puts the script in the page and minifies it with swc;
  build.rs runs it into OUT_DIR and web.rs include_str!s the result. 137 KB -> 106 KB.
- npx tsc -p . type-checks web/src, loosely; the handful of annotations it needed
  change no behaviour.
- The Docker build installs node and swc (npm ci --omit=dev).
- Two list requests racing no longer let the older one win, and switching tabs clears
  the selection it closes, which made a browser test flaky.

Closes #23, #24.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 12:41:34 +00:00