14 KiB
Folders and the library (redesign)
Folders everywhere: tags shown as filter chips
One idea of a folder for every area — Bücher, Zeitschriften, Hörbücher, Podcasts, Blogs (and the news' Ressorts, which already work this way):
- A thing can be in several folders (they work like tags); none is fine.
- Folders are filters, not rooms: a row of chips on top —
Alle · <folder> n · … · Ohne Ordner— tapping one filters in place; no "inside a folder" view, no back button. On a wide window the row wraps. - Assigning: mark one or more things (long press / ☐ select mode, or ⋮ on one), then "In Ordner …": a sheet with every folder of the area as checkboxes and "Neuer Ordner". No typing a name that must match.
- Managing (⋮ on the chip row → "Ordner verwalten"): rename, merge (rename onto an existing name), delete (the things stay, only the folder goes), reorder by dragging. The order is the chip order.
- Automatic groups stay automatic and look different from folders (no chip; a stack/group heading): audiobook series, magazine periodicals.
Where folders are kept
| Area | Field | Notes |
|---|---|---|
| E-books, magazines | encrypted meta folders: [names] |
folder (the first) kept for older clients; read folders or else [folder] |
| Audiobooks, podcasts | PodcastFeed.folders (JSON list) |
feed setting folders; folder (string) still accepted |
| Blogs | Source.folders (exists) |
|
| News | Source.category (one Ressort) |
already chips in Nachrichten |
The folder order per area: e-books in the encrypted user settings blob (names
are private), the others on the server (/podcasts/folders/, /feeds/folders/,
{order: [names]}).
Podcasts: a category's episodes, by time
The folder chips stand over „Folgen“ too: with one chosen it lists every episode of
that category (GET /podcasts/episodes/?folder=<name>, empty: the podcasts in none).
„Folgen“, „Neu“ and one podcast's page group their episodes by time — Heute, Gestern,
Diese Woche (the last 7 days), Diesen Monat (30 days), then the months — after the
Kino feed. In „Folgen“ (sort „Nach Zeitraum, seltene zuerst“, the default) and „Neu“
the podcasts that seldom come go first within a group: interval_days of each feed
in /podcasts/state/, the mean gap between its newest 10 episodes (one episode: its
age, at least 30 days, the same when all came within an hour; kept an hour). Heard is a filter chip, not an order.
Shared code: static/js/folders.js (diora-web, every platform)
DioraFolders.chips(el, {folders: [{name, count}], active, none: count|0, onPick(name|''|null), onManage()})
DioraFolders.assign({title, folders: [names], selected: [names], onDone(names)}) // the sheet
DioraFolders.manage({folders: [names], onRename(old, new), onDelete(name), onOrder(names)})
Each area keeps its own storage and calls these for the look and the dialogs.
The library (Bücher, Zeitschriften)
- Header: title, search (title, author), sort (zuletzt gelesen · Titel ·
Autor · hinzugefügt · kürzeste / längste zuerst), ⊞/☰ grid or list,
+(Buch hinzufügen, Aus Cloud). - Length (since 2026-10-25): the pages in the encrypted meta (
pages) — a PDF's pages, an EPUB's text by 1800 characters a page (bookPagesOfFile, 40-epub.js; the reader's own estimate). Set on upload and when a book is opened; for older books the device works it out once when a length sort is chosen ("Länge wird ermittelt … 3 von 80": file from the offline copy or the server, decrypted on the device). Unknown lengths come last; the cards show "· 320 S." while sorted by length. - Filter chips: Alle · Angefangen · Ungelesen · Gelesen — then the folders.
- Weiterlesen: one card on top (cover, title, "Seite 132 von 186 · gestern", ▶), not repeated in the list below.
- Grid (default on phone and desktop): covers with a progress line, title. List (default on e-ink / diora Reader): one row per book, small cover, title, author, % / ✓.
Covers, end-to-end encrypted
A client that has a book's file extracts its cover (EPUB: the OPF cover image;
PDF: page 1), scales it to 240 px wide JPEG, encrypts it with the user's key
(AES-GCM like the meta) and uploads it once: POST /books/<id>/cover/ {cover_ct, cover_iv}. GET /books/ lists has_cover; GET /books/<id>/cover/
returns {cover_ct, cover_iv}. Clients cache the decrypted image locally. The
server never sees a cover.
Editions
Two or more books can be editions of one work (the PDF scan and the EPUB, two
translations …): the same random key in each one's encrypted meta, work: "w…"
('' or missing = none). The list shows a work once — the edition read last, else an EPUB,
else the first added — with its formats on the cover ("EPUB · PDF", in the list view after
the author). Each edition keeps its own file, position and highlights.
- Joining: choose several books → "Als Ausgaben verbinden", or a book's ⋮ → "Als Ausgabe verbinden …" (other books listed, similar titles first, with a search).
- ⋮ → "Ausgaben (n) …" lists the editions: open one, "Lösen" sets one apart (the last one left is set apart with it), "Weitere Ausgabe verbinden …".
- For the apps' own book sheets:
window.bookEditions(id)opens that sheet,window.bookEditionsOf(id)returns the editions (the book alone if it has none).
Generated covers
A book without a cover (or one set to cover: 'generated' in its meta) gets one
drawn by static/js/gencover.js: an SVG, the same on every device, never stored.
A concept (motif, palettes, typeface) is chosen by score:
- words in the title (3), the shelf category (2) and the author (2, only at a
name's start). A stem of five letters or more counts anywhere (German
compounds), a shorter one at a word's start, one of three letters as a whole
word;
^/$in the list force a word's start / end. - the folders, by name (
FOLDER_THEMES): the first counts 2.5 — more than an author or shelf word, less than a title word — further ones 1.
A work of Marx or Engels (author, or a volume of the MEW series) is always a blue volume (blau: blue cloth, gold rules and capitals, the spine on the left with the volume number); no other book is.
Concepts: konstruktiv (Marxist and political titles), geometrie (Philosophie: rings, turning squares, triangle/circle/square), psyche (Psychoanalyse: two ellipses, the Borromean knot, a spiral), karte (Geschichte), kosmos (Science-Fiction), ornament (Fantasy), raster (Politik, Soziologie), schatten (Krimi: a red line, a lit doorway), grusel (Horror), klassiker (patterned paper with a title plate), sakral (Religion & Mythologie), spiel (Kinder & Jugend), code (Informatik), typo, wellen, botanik, berge, zellen, and since 2026-10-25 funk (talk and radio: waveform, mast, speech bubbles), musik (record, staff), kunst (colour fields, brushstrokes), essen (plate on a checked cloth, citrus), sport (track, pitch), geld (price chart, coins), reise (road, route with compass) — and bauhaus when nothing matches (shapes, quarter-circle tiles, a circle crossed by diagonals). Most concepts have two or three motifs. A hash of title and author picks motif, palette, layout and every detail.
Square ({square: true}, since 2026-10-25): audiobooks and podcasts without a picture, or
whose picture doesn't load (audiobooks.js; diora-android native-podcast.js art(), before the
initial letter). 300 × 300, three title lines at most; the band of the motif shown is centred
in what the text leaves free. Podcasts (podcast: true) nothing matches get funk, not
bauhaus; a Marx/Engels audiobook gets konstruktiv (the blue volume is a binding).
Export as XML (since 2026-10-24)
⇩ in the library's head (static/js/app/55-library-tools.js) saves diora-bibliothek-<date>.xml:
made on the device from the list it has decrypted (nothing new goes to the server), every book
and magazine issue with its diora metadata — title, author, file name, ISBN, pages, shelf
category, series and volume, work (editions), magazine mark, cover choice, folders —, the
reading position (<progress percent anchor read read-at last-read device furthest-percent furthest-anchor>), when it was uploaded, whether it has highlights, and an issue's articles with
their state. The folder order heads the file. Root <diora-library version="1">.
✦ beside it: ordering with Claude (docs/ai.md).
Eingang: books from a cloud folder by themselves (since 2026-10-24)
A cloud source (Einstellungen → Cloud-Verbindungen, WebDAV: accounts.WebDAVSource,
accounts/webdav.py) can be marked Eingang: its new books come into the library without
anyone importing them. The server can't do it alone — the books are encrypted with the key
on the devices — so a device does:
static/js/app/57-cloud-inbox.js: after the book list has loaded (at most every 5 minutes; only with the real key inlocalStorage, never on diora Reader) it asksGET /books/cloud/inbox/→{files: [{source, label, path, name, size, modified}], errors}: the.epub/.pdffiles of the Eingang sources (three folders deep) no device has taken. The server claims them for this device for ten minutes (another device asking meanwhile doesn't get them).- Each file: fetched through the server (
/books/cloud/<id>/fetch/), imported like any file (_importEbookBuffer,skipKnown: same title and author → left out), thenPOST /books/cloud/<id>/done/ {path, result}withimported,known,too_big(not offered again) orfailed(again once the claim ran out). - The server keeps them in
accounts.WebDAVSeen(source, path, size, modified): a file whose size changed counts as new. The folder itself is never changed.
The first such source: Shelfmark's e-book downloads on the Hetzner server, served read-only by
dufs (set up by the "Media" session; its notes: C:\Users\m\homelab-handoff\04-hoerbuecher.md).
Zeitschriften: das Heft und seine Artikel
An issue is a unit (the editors' selection and order, its date, its theme) and
so is each article in it. Neither is reduced to the other
(static/js/app/54-issues.js):
- The issue stays the document. It opens and reads through as before, in
the editors' order. The reader marks the article one is in: a running head
under the bar ("Schwerpunkt · Artikel 8 von 23 · Titel"), in the apps' footer
"Artikel 8/23", and a pause at each article's end (
aside.iss-end): "✓ gelesen", the issue's count, "Weiter im Heft" (the next article not read yet, skipping those read or skipped), "Zum Inhalt", "überspringen", "Doch nicht gelesen". In a PDF the pause is a bar that comes up when one turns from an article's last page onto the next. - The states determine each other. An article one was in and whose end one reached counts as read (a jump — from the contents, "Weiter" — reads nothing). The issue counts "7 von 23 gelesen · 2 übersprungen"; "Im Heft weiterlesen" goes to the article one was in, else the next one open. "Heft abschließen" marks the rest as skipped, never as read.
- In the library an issue shows one bar with a segment per article (read, skipped, open, the one one is in). A tap opens its contents: Rubriken, the articles with their state, a teaser and the reading time; the ⋮ / mark sets read / unread / skipped by hand.
The index comes from the file, on the device:
- EPUB: the table of contents, by file — an article is a file (the first entry pointing into it); the entries after it into the same file are its sections, whatever their depth (GegenStandpunkt puts some at the top level); an entry whose children point into other files is a Rubrik. A one-file EPUB: the leaves. No table of contents: each spine file with text. The teaser is the headline under the title (the headings before the first paragraph that the title doesn't say, not numbered sections), else the first paragraph.
- PDF: a real outline (word processors'
_Hlk…/_Toc…leftovers don't count); most magazines' PDFs have none, so mostly the contents page printed in the issue: among the first 16 pages the one with most numbered rows (numbers that merely count 1, 2, 3 … are a list, not a contents), plus its neighbours with entries. A row: the page number at the start ("14 Der Krieg gegen Palästina", Jacobin), or at the end, maybe with the author in a column before the title ("Nathan Sperber | Beyond Neoliberalism? | 23", NLR); lines under it in its type size continue the title, an author line ("Von …", a name) gives the author, lines in other type the teaser; a title in capitals may begin on the lines above its number, a lone word on top is its kicker (LuXemburg: GESPRÄCH, INFOGRAFIK); "Name: Titel" (manuskripte). Capitals in the contents' type are Rubriken. The printed numbers are mapped to the file's pages by the offset most titles agree on (found in the page's text, a headline in capitals preferred); running heads and the contents page's own head are dropped.
Reading times count 200 words a minute. It is kept in the encrypted meta as
issue: {v: 2, a: [{h, t, r, x, m, a?, p?, e?}]} (href, title, Rubrik, teaser,
minutes, author, a PDF's first and last page), or {v: 2, a: [], none: true}
for an issue without a usable table of contents (read as a whole). An index of
another version is built anew. Built in the
background for issues without one (not on e-ink: there when the issue is
opened, or it comes from another device).
The states are in the meta too: issueRead: {href: 'r' | 's'},
issueAt: href (the article one was in). A change goes to the server after
2.5 s; offline it waits in localStorage.diora_issue_pending and goes with the
next save (or when the connection is back).