Compare commits

..

No commits in common. "868e979755c2d7efd569ca0a918556878cad65c5" and "c8fd139b44be4407a5b608c5f1fd67ce016ba08d" have entirely different histories.

6 changed files with 21 additions and 177 deletions

View file

@ -6,44 +6,29 @@ diora's areas are sorted by what one does, not where things come from:
|-------|----------|
| — | Home |
| Lesen | Bücher · Zeitschriften · Nachrichten · Blogs |
| Hören | Hörbücher · Podcasts · Radio · Ambient (· Fokus, web page only) |
| Projekte | Projekte (docs/projects.md; `side`) |
| — (foot) | Suche (docs/search.md; `footer`, `transient`) · Abos & Quellen (docs/sources.md; `footer`) |
| Hören | Hörbücher · Podcasts · Radio · Ambient |
| — | Abos & Quellen (managing all subscriptions; phase 2) |
- **Desktop / wide window (sidebar):** every section is its own entry, under
its group's heading. Nothing inside a section repeats the choice.
- **Phone (tab bar):** four tabs, **Home · Lesen · Hören · Projekte** (since 2026-10-19).
Lesen and Hören show a row of their sections at the top (segments); each group remembers
the section last used. The search is a magnifier in the bar on top (the apps' section bars,
the page's narrow bar), never a tab; Abos & Quellen is in the app menu. The mini player
stays below as it is.
- **Sidebar / wide bar:** `side` sections (Projekte) and then the `footer` ones (Suche,
Abos & Quellen) stand at its foot. A `transient` section (Suche) is never remembered as
the one to open next.
- **Phone (tab bar):** three tabs, **Home · Lesen · Hören**. Lesen and Hören
show a row of their sections at the top (segments); each group remembers
the section last used. The mini player stays below as it is.
- **diora Reader** (tolino, e-readers): only Lesen, without Home. **TV:** Hören.
Anything new goes into one of the groups. Managing (subscribing, settings,
folders) is in "Abos & Quellen" (docs/sources.md; done).
Anything new goes into one of the two groups. Managing (subscribing, settings,
folders) moves out of the everyday views into "Abos & Quellen" (phase 2).
## One definition: `static/js/nav.js` (diora-web)
Loaded by the page on every platform, before the other area scripts. It owns
the sections, which panel shows each, switching between them, the URL hash
and the remembered choice. The bars are drawn from it. **It is the one source for the
navigation.**
Underneath, `static/js/app/20-tabs.js` keeps `TOP_TABS`: the list of *panels*
(`#tab-<name>`) that `showTab(name)` can show — home, radio, focus, podcasts, books, feeds,
ambient, audiobooks, sources, suche, projekte. That is not a second navigation: nav.js
maps each section to one of these panels (several sections share one: Bücher/Zeitschriften
→ books, Nachrichten/Blogs → feeds) and calls `showTab`. A new area needs both: its panel in
`TOP_TABS` (and the template) and its section in nav.js.
and the remembered choice. The bars are drawn from it.
```
window.DioraNav = {
sections(), // [{id, group, label, icon, available}] in order (only those the page has)
groups(), // [{id: 'home'|'lesen'|'hoeren'|'projekte', label, icon, sections: [id…]}]
icon(name), // a section's or group's icon as SVG (also 'menu')
groups(), // [{id: 'home'|'lesen'|'hoeren', label, icon, sections: [id…]}]
current(), // the section id shown now
go(id), // show a section ('lesen'/'hoeren': that group's last section)
onChange(fn), // fn(sectionId) after every change
@ -61,10 +46,6 @@ window.DioraNav = {
| `podcasts` | hoeren | Podcasts | `#tab-podcasts` | |
| `radio` | hoeren | Radio | `#tab-radio` | |
| `ambient` | hoeren | Ambient | `#tab-ambient` | |
| `focus` | hoeren | Fokus | `#tab-focus` | web page only (`webOnly`) |
| `projekte` | projekte | Projekte | `#tab-projekte` | (projects.js) |
| `suche` | (foot) | Suche | `#tab-suche` | (search.js); never remembered |
| `sources` | (foot) | Abos & Quellen | `#tab-sources` | (sources.js) |
`go()` calls the page's `showTab(panel)` as before (so everything hooked onto
showTab keeps working) and then the panel's view hook:
@ -76,9 +57,8 @@ Hash: `#books`, `#magazines`, `#news`, `#blogs`, `#audiobooks`, … (old hashes
`#feeds`, `#radio/saved` keep working). Remembered: `localStorage`
`diora_nav_section` and `diora_nav_last_<group>`.
**Zeitschriften**: the books marked as magazines (encrypted meta `magazine`), or, unmarked,
those in a library folder called "Zeitschriften" (or "Magazine"); issues and articles:
docs/library.md.
**Zeitschriften** (phase 1): the books in a library folder called
"Zeitschriften" (or "Magazine"); a per-book mark follows with phase 3.
### The segment row (phone)
@ -91,9 +71,8 @@ restyle it.
| Where | Bar | Drawn by |
|-------|-----|----------|
| Android (phone) | `#nui-tabbar`: Home · Lesen · Hören · Projekte | native-radio.js, from `DioraNav.groups()` |
| Android tablet, desktop (`html.nui-sidebar`) | rail: Home, headings Lesen/Hören with their sections, at the foot Projekte · Suche · Abos & Quellen, Menü | native-radio.js, from `DioraNav.sections()` |
| Apps, every section | a bar on top (`.nui-tab-bar`; `.nui-section-bar` for the generic ones) with the section's name, its actions and the search magnifier | native-ui.js `SECTION_BARS`, native-radio.js, native-podcast.js |
| Android (phone) | `#nui-tabbar`: Home · Lesen · Hören | native-radio.js, from `DioraNav.groups()` |
| Android tablet, desktop (`html.nui-sidebar`) | rail: Home, headings Lesen/Hören with their sections, Menü | native-radio.js, from `DioraNav.sections()` |
| Web page | `#tabs` (narrow: groups + segments; wide: all sections under headings) | nav.js |
A tab/rail button calls `DioraNav.go(id)`; the active state comes from
@ -106,6 +85,6 @@ A tab/rail button calls `DioraNav.go(id)`; the active state comes from
audiobook mark in one place; the everyday views without managing.
3. Library (done): audiobook series are found in the titles ("Das Kapital –
Erster Band" …) and shown as one stack; audiobooks have folders
(`PodcastFeed.folders`, a list; `folder` is its first, for older clients); a book is marked as a magazine in its encrypted
(`PodcastFeed.folder`); a book is marked as a magazine in its encrypted
meta (`magazine: true/false`; unset: the "Zeitschriften" folder decides), and
Zeitschriften groups the issues by periodical.

View file

@ -3,7 +3,7 @@
Since 2026-10-12 the place to continue a book is **where it was last read, on any device**, not the
furthest place ever reached. The furthest place is still kept, as a hint ("Weiteste Stelle").
This file is the contract between the server (books/views.py `save_progress`), the web reader
(static/js/app/46-readpos.js, with the reader in 62-reader.js), diora-android (Ops.kt / SyncEngine.kt / native-*.js) and diora-desktop
(static/js/app.js), diora-android (Ops.kt / SyncEngine.kt / native-*.js) and diora-desktop
(src/main/ops.js / sync.js). They must apply the same rules.
## The state per book

View file

@ -1,132 +0,0 @@
# Radio, Ambient, Fokus
The oldest part of diora (it began as a radio player): internet radio with saved stations,
history and track titles, the generative Ambient player, and the web page's Fokus timer.
Written down 2026-10-24 from the code; it is the reference for the three apps.
## Server: the `radio` app
Models (`radio/models.py`):
| model | what |
|---|---|
| `SavedStation` | a user's station: name, url (unique per user), bitrate, country, tags, `favicon_url` (the logo), `is_favorite`, notes |
| `StationPlay` | one listening: station, started/ended; hour, weekday, weekend filled on save (for the recommendations) |
| `TrackHistory` | a track title heard (user, or `session_key` signed out), `scrobbled` |
| `FeaturedStation` | stations picked in the admin for everybody (order, active) |
| `FocusSession` | a finished Fokus round |
Also: `podcasts.PlayerState` with `kind='radio'` (the player sync between devices) and
`accounts.UserProfile.focus_station_url/_name` (the station the Fokus timer plays).
Endpoints (`radio/urls.py`):
- `GET /` — the page (`templates/radio/player.html`). The saved stations, the history (50) and
the featured stations come rendered into it (`#saved-tbody`, `#history-tbody`,
`.featured-section`) and as `INITIAL_SAVED` (with `play_count`), `INITIAL_FEATURED`,
`USER_FOCUS_STATION`. The apps take these parts from it (see below).
- `GET /radio/sse/?url=` — server-sent events with the track title, `data: {"track": …}` when it
changes, `{"error": "stream ended"}` at the end. The server opens the stream itself with
`Icy-MetaData: 1` and reads `StreamTitle` (`radio/icy.py`).
- `POST /radio/record/` — a track into the history; `scrobble: true` sends it to Last.fm when the
account has it linked (`radio/lastfm.py`; "Artist - Title", else the station as artist).
- `POST /radio/save/` (by url: create or update), `/radio/remove/<pk>/`, `/radio/favorite/<pk>/`,
`/radio/notes/<pk>/`; `GET /radio/history/` (last 200), `POST /radio/history/<pk>/delete/`.
- `POST /radio/play/start/` → `play_id`, `/radio/play/stop/`; `GET /radio/recommendations/` —
the five stations most played around this hour on this kind of day, with a German label.
- `POST /radio/import/` — an `.m3u`/`.m3u8` file into the saved stations (`#EXTINF` names) →
`{added, skipped}`.
- `POST /radio/focus/record/`, `GET /radio/focus/stats/`.
- `GET /radio/affiliate/?track=` — artwork/album from iTunes (an Amazon link only with
`AMAZON_AFFILIATE_ENABLED`).
- `GET /radio/creamfresh-stream/`, `POST /radio/creamfresh-feedback/` — the user's own station
(radio.creamfresh.xyz) through the server, which adds its login; 👍/👎 for the DJ.
- `GET /radio/stream-player/` — an old standalone player page; nothing links to it.
**Station search** is not on the server: the page asks radio-browser.info directly
(`de1.api.radio-browser.info/json/stations/search`, 50, by click count). A pasted `http(s)://`
address is offered to play or save as it is.
## Web page (`static/js/app/`)
| file | what |
|---|---|
| `01-core.js` | the globals: `audio` (one `<audio>`), `currentStation`, `currentTrack`, `sseSource`, `isPlaying`, `currentPlayId` |
| `10-radio.js` | `playStation(url, name, id)`, `stopPlayback`, `togglePlayStop`, the SSE (`startMetadataSSE` → `recordTrack`), volume (0–255, `diora_volume`), search, saving, favourites, recommendations, the creamfresh votes, Media Session |
| `16-stations.js` | mood chips (they search a tag), curated lists, featured stations, the occasional donation hint, notes |
| `29-m3u.js` | M3U import |
| `80-radio-sidebar.js` | the compact radio player of the sidebar |
| `24-ambient.js` | the Ambient tab (below) and `showRadioTab` |
| `14-focus.js` | Fokus (below) |
Sub-tabs `RADIO_SUB_TABS = ['search', 'saved', 'history']` (`20-tabs.js`), hash `#radio/saved`,
remembered in `diora_active_radio_tab`. On an https page an `http://` stream would be blocked:
the page opens it in a new tab — unless the host plays it (`window.DIORA_PLAYS_HTTP_STREAMS`,
set by the apps). No HLS: what `<audio>` plays.
## Ambient (`static/ambient/index.html`, "Drift")
No sound files: a generative WebAudio synthesizer. Modes (`MODES`): ambient music (chords,
bells, grains), noise colours, nature (rain, storm, sea, wind, stream, fire, crickets),
machines (fan, cabin, train), focus and sleep (binaural delta–beta, singing bowls,
heartbeat); each with two sliders and seeded presets in the URL hash.
The page holds it in an iframe, loaded when first needed (`24-ambient.js`); they talk by
`postMessage` (`{type: 'ambient', cmd: 'start'|'stop'|'mode'|'volume'}` in,
`{state: 'playing'|'stopped'}` out). Mode remembered in `diora_ambient_mode`.
Hooks for the apps: `__dioraAmbientStart/Stop/Cmd/Info/Playing`.
**One sound at a time:** Ambient starting stops the radio (`stopPlayback(false)`) and pauses a
podcast (`__dioraPausePlayer()`; Android: `podcastPause()`, pause only, never toggle); the
radio starting stops Ambient.
## Fokus (web page only)
A Pomodoro timer (25/5 min) in its own section (`nav.js`: `focus`, `webOnly`): it plays the
chosen focus station, records a `FocusSession` after each work phase, pauses the sound for the
break; with a do-not-disturb full-screen mode (`toggleDND`) and a statistics table.
## Android (diora-android)
- **One player** for radio, podcasts and audiobooks: `RadioService.kt`, a Media3
`MediaLibraryService` with one ExoPlayer (15 s connect / 20 s read timeout; diora's own
server gets the login headers — the creamfresh proxy). After a pause of more than 3 s a
station jumps back to live. Android Auto: Saved, Featured, Queue, voice search by name.
Resuming after a restart plays the last station. Radio always streams on mobile data (the
mobile-data rules are for episodes).
- `Radio.kt` — the page's controller: play, stop, `track(title)` (new metadata without
rebuffering), state to the page (`window.__dioraRadio`).
- `RadioPage.kt` — the saved stations, history and featured stations exist only in the
rendered `/`: the app fetches it with its login, cuts those parts out into `radio-page.json`
and splices them into its own page at markers that `tools/build_web.py` set
(`<!--diora:saved|history|featured-->`, `__DIORA_RADIO_*_JSON__`). So they show offline too.
- `LocalServer.kt` passes `/radio/sse/` through as a stream (padded with SSE comments: WebView
hands intercepted data over only when its buffer is full); POSTs go through `native.js`.
- `web-native/static/native-radio.js` — turns the page's `audio` into a remote of the native
player (`radioPlay`/`radioStop`, counters against stale echoes), shows errors ("Sender nicht
erreichbar"), draws the dock/rail (from `nav.js`), the mini player and the radio bar.
- `AmbientService.kt` — Ambient as a foreground media service (notification with Stopp, audio
focus, stops when headphones are pulled); started and stopped from the page
(`ambientState`). Widgets for Ambient and resuming (`CardWidgets.kt`).
- Sleep timer (`SleepTimer.kt`, `SleepRules.kt`): counts only playing time (Ambient too), fades
out over the last 10 s, shaking resets it, can stop after N episodes, can switch itself on at
night (22–6 h); one that counted nothing for 3 h is dropped.
- Data usage: the stream counts as `RADIO_STREAM`, `/radio/` and `/` as `RADIO_DATA`
(`DataUsage.kt`).
## Desktop (diora-desktop)
A copy of `native-radio.js`; `src/web/desktop.js` plays with a hidden `Audio()` of its own
(`DioraNative.radioPlay/Stop/State/Refresh/Track`; media keys: pause = stop) and re-reads `/`
like RadioPage.kt (at most every 20 s). `http://` streams go through the local router
(`/__stream?u=`, `src/main/router.js`) without their ICY data; the SSE is passed through with the
login; `backgroundThrottling: false` keeps the titles coming.
## Known weak spots
- `/radio/sse/` needs no login and connects to any address it is given — a way to make the
server fetch arbitrary URLs. Should require a session (or a saved/featured station).
- The login for the creamfresh stream is written into `radio/views.py`; it belongs in the
environment like the other secrets.
- Every listener's titles cost the server a second connection to the stream.
- No recording, no alarm; station logos only from radio-browser's favicon.

View file

@ -13,7 +13,7 @@ This is the plan for tidying it up without breaking anything.
* Whoever owns a file does its refactoring (web-native/* → the Android session;
app.js, home.js, templates, desktop → the desktop session); shared contracts live in docs/.
## The numbers (2026-10-14, before step 2; app.js is now `static/js/app/`)
## The numbers (2026-10-14)
| file | lines |
|---|---|
@ -37,8 +37,7 @@ Android Ops.kt), each tested separately or not at all.
* Shared test vectors for the reading-position rules (`docs/progress-cases.json`), run by
the Django tests, desktop's node tests and Android's OpsTest — one contract, checked
in all four places.
2. **Split app.js** (done: 656520f, sw v106 — `static/js/app/NN-*.js`, 33 files; the
template lists them in order) by its sections into plain scripts loaded in order (radio, focus,
2. **Split app.js** by its sections into plain scripts loaded in order (radio, focus,
ambient, crypto, epub, books, reading position, reader, tts, highlights, margin, …). They
stay classic scripts sharing globals, so the native layers that wrap `window.*` keep
working unchanged; template, sw.js SHELL and Android's build_web list them. No code

View file

@ -1,8 +1,8 @@
# Abos & Quellen (navigation phase 2)
One place to manage every subscription: podcasts, YouTube channels, audiobooks,
news sources and blogs. The everyday views (Strom, Merkliste, Wiedergabeliste — docs/podcasts.md —,
Nachrichten, Blogs, Hörbücher) show content; adding, changing and removing sources happens
news sources and blogs. The everyday views (Neu, Warteschlange, Nachrichten,
Blogs, Hörbücher) show content; adding, changing and removing sources happens
here (their own small entry points lead here).
## Where

View file

@ -2,10 +2,8 @@
diora has two colour schemes, **dark** (the old look) and **light**, chosen by the reader:
"Dunkel", "Hell" or "Automatisch" (as the operating system says). This file is the contract
between diora-web (app.css, home.js, feeds.js, templates, and static/podcasts/podcast-ui.css —
a copy of diora-android's native.css by tools/sync_podcast_ui.py), diora-android (native.css,
native-*.js; the apps ship diora-web's files too, through build_web.py) and diora-desktop
(desktop.css).
between diora-web (app.css, home.js, templates), diora-android (native.css, native-*.js,
podcast-ui.css, feeds.js) and diora-desktop (desktop.css).
## Switching