Compare commits
No commits in common. "868e979755c2d7efd569ca0a918556878cad65c5" and "c8fd139b44be4407a5b608c5f1fd67ce016ba08d" have entirely different histories.
868e979755
...
c8fd139b44
6 changed files with 21 additions and 177 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
132
docs/radio.md
132
docs/radio.md
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue