diora-web/static/js/gencover.js

683 lines
44 KiB
JavaScript
Raw Normal View History

Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht) - Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“ (umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen (/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021, übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge (/feeds/folders/order/). - Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +; Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte; Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste. - Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie auf E-Ink. - Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor, Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie, Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster, Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau und Details – einzigartig, überall gleich, nichts gespeichert. - Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
/*
* Covers for books that have none (docs/library.md): drawn here, the same on every
* device, from what the book is called and where it stands — its title, author, shelf
* category and folders. Words in them point to a design concept (a motif, curated
* palettes, a typeface: Marx and Kapital to constructivism, Hegel and Dialektik to quiet
* geometry, Meer to waves, Krieg to map contours …), and so do the folders, by name
* (Klassiker to a patterned Insel-style cover, Horror to a bare tree before the moon …);
* a hash of title and author picks
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht) - Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“ (umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen (/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021, übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge (/feeds/folders/order/). - Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +; Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte; Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste. - Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie auf E-Ink. - Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor, Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie, Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster, Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau und Details – einzigartig, überall gleich, nichts gespeichert. - Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
* the palette, the layout and every detail of the motif, so no two books look alike and
* each always looks the same. Nothing is stored or sent: it's an SVG, made when needed.
*
* DioraGenCover.url(book, {gray}) a data: URL (book: {title, author, shelfTag, folders})
* DioraGenCover.theme(book) the concept chosen (for tests)
*
* Chrome-61-safe: no ?. / ??.
*/
(function () {
'use strict';
const W = 300, H = 450;
// ---- chance, the same for the same book ------------------------------------------------
function hash(s) {
let h = 2166136261;
for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); }
return h >>> 0;
}
function rng(seed) {
let a = seed >>> 0;
return function () {
a = (a + 0x6D2B79F5) >>> 0;
let t = a;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
const pick = (r, list) => list[Math.floor(r() * list.length)];
const between = (r, a, b) => a + r() * (b - a);
const f1 = n => Math.round(n * 10) / 10;
// ---- the concepts ---------------------------------------------------------------------------
// words: stems in the text (lower case, umlauts as they are). One of five letters or
// more counts anywhere (German compounds: "Massenpsychologie"); a shorter one only at
// the start of a word, and one of three letters only as a word of its own (plural
// allowed) — "gen" isn't in "Leiden", "war" not in "Gegenwart" or "Wardrobe".
// A leading ^ asks for a word start whatever the length ("stern", not in "Eastern"),
// a trailing $ for a word's end as well ("king", not "Kingdom").
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht) - Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“ (umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen (/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021, übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge (/feeds/folders/order/). - Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +; Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte; Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste. - Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie auf E-Ink. - Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor, Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie, Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster, Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau und Details – einzigartig, überall gleich, nichts gespeichert. - Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
// palettes: [background, ink, accent, second accent], curated to sit well together.
const SERIF = "Georgia, 'Times New Roman', serif";
const SANS = "'Helvetica Neue', Helvetica, Arial, sans-serif";
const NARROW = "'Arial Narrow', 'Roboto Condensed', 'Helvetica Neue', Arial, sans-serif";
const MONO = "'Courier New', Courier, monospace";
const THEMES = [
// The "blue volumes": only the works of Marx and Engels (see theme()), bound in blue cloth
// with gold, the spine on the left. It sets its own text (ownText), a volume label when the
// book has a number in a series.
{id: 'blau', font: SERIF, ownText: true, words: [],
palettes: [['#22305e', '#d4b25c', '#b89a52', '#161f40'], ['#1f3554', '#cfae5e', '#a98f55', '#14233a'],
['#1b2747', '#dcbf73', '#b29a62', '#111a33']]},
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht) - Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“ (umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen (/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021, übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge (/feeds/folders/order/). - Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +; Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte; Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste. - Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie auf E-Ink. - Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor, Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie, Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster, Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau und Details – einzigartig, überall gleich, nichts gespeichert. - Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
{id: 'konstruktiv', font: NARROW, upper: true,
words: ['marx', 'kapital', 'capital', 'arbeit', 'labor', 'labour', 'revolution', 'sozialis', 'socialis', 'kommun', 'communis',
'ökonom', 'econom', 'klasse', 'class', 'gewerkschaft', 'streik', 'strike', 'partei', 'lenin', 'luxemburg', 'engels',
'proletar', 'faschis', 'fascis', 'jacobin', 'gegenstandpunkt', 'demokratie', 'herrschaft', 'massen'],
palettes: [['#efe6d2', '#1b1b1b', '#c8322b', '#1b1b1b'], ['#1d1d1d', '#f2ead7', '#d23a2c', '#e8b23a'], ['#e9dfc8', '#22252a', '#b5302a', '#7a8a8f']]},
{id: 'geometrie', font: SERIF,
words: ['philosoph', 'hegel', 'kant', 'dialekt', 'dialect', 'vernunft', 'reason', 'geist', 'spirit', 'ethik', 'ethic',
'logik', 'logic', 'metaphys', 'phänomen', 'phenomen', 'denken', 'wahrheit', 'truth', 'sein', 'nichts', 'nothing',
'pragmati', 'pessimis', 'flatland', 'dimension', 'leviathan', 'republic', 'meditation', 'treatise', 'abhandlung',
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht) - Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“ (umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen (/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021, übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge (/feeds/folders/order/). - Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +; Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte; Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste. - Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie auf E-Ink. - Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor, Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie, Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster, Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau und Details – einzigartig, überall gleich, nichts gespeichert. - Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
'kritik', 'critique', 'theorie', 'theory', 'begriff', 'wissen'],
palettes: [['#f1ece1', '#2b2a27', '#b08a3e', '#6f7d72'], ['#26292e', '#ece4d4', '#c9a25a', '#8a9aa8'], ['#e7e1d6', '#3a3833', '#8c5a3c', '#5d6b74']]},
{id: 'psyche', font: SERIF,
words: ['lacan', 'freud', 'psycho', 'sex', 'begehren', 'desire', 'disavowal', 'verleugn', 'unbewusst', 'unconscious',
'comedy', 'komik', 'komöd', 'parallax', 'of the real', 'voice', 'stimme', 'žižek', 'zizek', 'zupan', 'dolar', 'traum', 'dream',
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht) - Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“ (umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen (/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021, übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge (/feeds/folders/order/). - Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +; Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte; Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste. - Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie auf E-Ink. - Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor, Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie, Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster, Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau und Details – einzigartig, überall gleich, nichts gespeichert. - Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
'odd one', 'antigone', 'let them rot', 'klavierspielerin'],
palettes: [['#f3e3d9', '#3d2440', '#c45b6a', '#5c8a8a'], ['#2c2236', '#f2dfd2', '#e07a6f', '#7fb3ae'], ['#e8ddeb', '#2e2a3d', '#8d4f8a', '#d99a6c']]},
{id: 'wellen', font: SERIF,
words: ['meer', 'sea', 'ozean', 'ocean', 'wasser', 'water', 'insel', 'island', 'fluss', 'river', 'flut', 'flood', 'deluge',
'sintflut', 'schiff', 'ship', 'welle', 'wave', 'strand', 'küste', 'coast', 'regen', 'rain', 'venedig', 'venice'],
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht) - Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“ (umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen (/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021, übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge (/feeds/folders/order/). - Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +; Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte; Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste. - Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie auf E-Ink. - Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor, Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie, Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster, Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau und Details – einzigartig, überall gleich, nichts gespeichert. - Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
palettes: [['#e5eef0', '#13324a', '#2f6f8f', '#8fb8c6'], ['#0f2a3d', '#e3edf0', '#4c95b0', '#a9d3dc'], ['#efe9dc', '#1f3b4d', '#3d7a8c', '#c98a5a']]},
{id: 'karte', font: SERIF, upper: true,
words: ['krieg', 'war', 'history', 'empire', 'imperium', 'reich', 'zerstörung', 'destruction', 'wages',
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht) - Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“ (umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen (/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021, übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge (/feeds/folders/order/). - Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +; Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte; Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste. - Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie auf E-Ink. - Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor, Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie, Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster, Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau und Details – einzigartig, überall gleich, nichts gespeichert. - Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
'nazi', 'staat', 'state', 'nation', 'kolonial', 'colonial', 'weltkrieg', 'schlacht', 'battle', 'grenze', 'border',
'seeing like', 'world-system', 'weltsystem'],
palettes: [['#e9e1cc', '#3b3a2c', '#8a4b2a', '#6d7a4f'], ['#2f3127', '#e6dcc4', '#b5763c', '#8e9a6b'], ['#ddd5bd', '#2c2a22', '#7a3b2e', '#5f6f63']]},
{id: 'kosmos', font: SANS, upper: true,
words: ['^stern', 'star', 'kosmos', 'cosmos', 'space', 'weltraum', 'planet', 'mond', 'moon', 'dune', 'nacht', 'night',
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht) - Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“ (umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen (/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021, übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge (/feeds/folders/order/). - Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +; Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte; Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste. - Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie auf E-Ink. - Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor, Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie, Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster, Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau und Details – einzigartig, überall gleich, nichts gespeichert. - Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
'universum', 'universe', 'zeit', 'time', 'licht', 'light', 'galax', 'messiah', 'science fiction', 'sci-fi', 'zukunft', 'future'],
palettes: [['#141833', '#ece6d3', '#e3b65b', '#7d8bd4'], ['#0e1424', '#dfe6f2', '#d9824a', '#6fa3c4'], ['#1f1a33', '#efe7da', '#c9a8e0', '#e3c46b']]},
{id: 'ornament', font: SERIF,
words: ['fantasy', 'drache', 'dragon', 'magie', 'magic', 'prophezei', 'prophecy', 'throne', 'thron', 'könig', 'king$',
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht) - Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“ (umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen (/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021, übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge (/feeds/folders/order/). - Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +; Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte; Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste. - Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie auf E-Ink. - Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor, Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie, Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster, Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau und Details – einzigartig, überall gleich, nichts gespeichert. - Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
'krone', 'crown', 'schwert', 'sword', 'warrior', 'krieger', 'cats', 'katze', 'legende', 'legend', 'märchen', 'fairy',
'zauber', 'spell', 'ice and fire', 'bundle'],
palettes: [['#1e2a2a', '#efe3c4', '#c69a3c', '#8f3b46'], ['#2b1d2e', '#f0e2c8', '#d3a13f', '#3f7a6d'], ['#f0e6d0', '#2d2433', '#8c3a4a', '#2f6b65']]},
{id: 'botanik', font: SERIF,
words: ['natur', 'nature', 'wald', 'forest', 'baum', 'tree', 'garten', 'garden', 'blume', 'flower', 'blatt', 'leaf',
'frühling', 'spring', 'sommer', 'summer', 'pflanze', 'plant', 'wurzel', 'root', 'gras', 'grass', 'pilz', 'mushroom'],
palettes: [['#eef0e4', '#23301f', '#4f7a43', '#b7a35a'], ['#203024', '#e8ecdc', '#86a86b', '#d6b56a'], ['#f2ece0', '#2b3326', '#6f8f5a', '#c27c5a']]},
{id: 'berge', font: SANS, upper: true,
words: ['berg', 'mountain', 'alpen', 'alps', 'gipfel', 'peak', 'zauberberg', 'hügel', 'landschaft', 'landscape',
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht) - Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“ (umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen (/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021, übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge (/feeds/folders/order/). - Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +; Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte; Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste. - Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie auf E-Ink. - Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor, Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie, Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster, Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau und Details – einzigartig, überall gleich, nichts gespeichert. - Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
'schnee', 'snow', 'winter', 'eis', 'ice', 'tal', 'valley', 'wandern'],
palettes: [['#e8ecef', '#22303b', '#5b7a8f', '#a9bccb'], ['#24323d', '#eef1f2', '#7f9fb3', '#c8d6de'], ['#efe9e0', '#2e3438', '#6f8590', '#c7a27a']]},
{id: 'zellen', font: SANS,
words: ['biolog', 'gehirn', 'brain', 'mensch', 'human', 'behave', 'verhalten', 'behavio', 'sucht', 'addict', 'enough',
'körper', 'body', 'medizin', 'medic', 'genet', 'genom', 'evolution', 'neuro', 'zelle', 'cell', 'leben', 'life', 'tier',
'animal', 'species', 'darwin', 'beagle', 'natural selection'],
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht) - Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“ (umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen (/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021, übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge (/feeds/folders/order/). - Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +; Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte; Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste. - Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie auf E-Ink. - Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor, Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie, Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster, Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau und Details – einzigartig, überall gleich, nichts gespeichert. - Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
palettes: [['#f1ebe4', '#26323a', '#e0765c', '#4f9a94'], ['#1f2b31', '#eef0ea', '#f08a6c', '#69b3aa'], ['#eaf0ee', '#22303a', '#d4634e', '#3f8f8a']]},
{id: 'raster', font: SANS,
words: ['stadt', 'city', 'urban', 'risiko', 'risk', 'gesellschaft', 'society', 'moderne', 'modern', 'system', 'analys',
'netz', 'network', 'technik', 'techn', 'smart', 'notes', 'notiz', 'daten', 'data', 'digital', 'maschine', 'machine',
'architektur', 'architect', 'hyperpolitik', 'politik', 'politic', 'revolution of', 'end of the'],
palettes: [['#ecebe7', '#1f2328', '#d1453b', '#2f5f8f'], ['#22262b', '#ebe8e1', '#e2b33c', '#4f86b8'], ['#f0ede6', '#2a2d31', '#3f6fa0', '#d9a03a']]},
{id: 'schatten', font: NARROW, upper: true,
words: ['tod', 'death', 'mord', 'murder', 'crime', 'verbrechen', 'schatten', 'shadow', 'dunkel', 'dark', 'angst', 'fear',
'serotonin', 'blut', 'blood', 'gift', 'poison', 'detektiv', 'detective', 'kommissar', 'thriller', 'krimi', 'sherlock',
'christie', 'lupin', 'burglar', 'einbrecher'],
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht) - Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“ (umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen (/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021, übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge (/feeds/folders/order/). - Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +; Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte; Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste. - Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie auf E-Ink. - Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor, Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie, Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster, Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau und Details – einzigartig, überall gleich, nichts gespeichert. - Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
palettes: [['#151515', '#ecebe6', '#c2302f', '#5a5a5a'], ['#ecebe6', '#151515', '#a8241f', '#7c7c7c'], ['#1c1f24', '#e4e0d8', '#d0a04a', '#55606b']]},
{id: 'typo', font: SERIF,
words: ['roman', 'novel', 'erzählung', 'stories', 'geschichten', 'gedicht', 'poem', 'poesie', 'poetry', 'brief', 'letters',
'tagebuch', 'diary', 'molloy', 'beckett', 'manuskripte', 'literatur', 'literature', 'review', 'zeitschrift'],
palettes: [['#f2ede3', '#211f1c', '#9b3b2f', '#7d7466'], ['#24211e', '#f0e9dc', '#d39b5f', '#8f8578'], ['#e9e4f0', '#26222e', '#5b4a8a', '#a3909a']]},
// A patterned paper with a title plate, like the Insel-Bücherei.
{id: 'klassiker', font: SERIF, layouts: ['plate'], frame: true,
words: ['klassik', 'classic', 'sämtliche werke', 'gesammelte werke', 'works of'],
palettes: [['#e9dcc0', '#2a2620', '#a8452f', '#c9a86a'], ['#d9e1d6', '#22302a', '#3f6b57', '#b58a4a'], ['#e4d9e6', '#2c2433', '#6d3f73', '#c09a5a'],
['#dfe4ea', '#1f2733', '#2f557a', '#c5953f'], ['#f0e3d3', '#2d2420', '#7e2f3a', '#3f6f74']]},
{id: 'sakral', font: SERIF, upper: true,
words: ['bibel', 'bible', 'gott', 'koran', 'quran', 'edda', 'mytholog', 'myth', 'heilig', 'holy', 'sacred', 'kirche',
'church', 'religio', 'evangel', 'gospel', 'testament', 'septuaginta', 'kojiki', 'heldensage', 'sagen', 'beowulf',
'götter', 'divine', 'paradise', 'paradies', 'hölle', 'hell'],
palettes: [['#1d2440', '#efe4c8', '#c9a245', '#7d4b8a'], ['#3a1c1f', '#f1e5cc', '#d1a548', '#4f7a8a'], ['#efe6d2', '#2b2218', '#9a6b22', '#7a2e2a']]},
{id: 'grusel', font: SERIF, upper: true,
words: ['horror', 'grusel', 'gothic', 'vampir', 'dracula', 'gespenst', 'ghost', 'spuk', 'haunt', 'cthulhu', 'monster',
'frankenstein', 'sleepy hollow', 'borderland', 'poe', 'shining', 'scream', 'lovecraft'],
palettes: [['#16141a', '#e8e2d6', '#9e2a2b', '#d9d2bf', '#08070a'], ['#1d2320', '#e3e6dc', '#7d9a6a', '#c9cfb5', '#090c0a'],
['#e3dccd', '#1a1718', '#8c1c1c', '#f4efe4', '#1a1718']]},
{id: 'spiel', font: SANS,
words: ['kinder', 'children', 'kids', 'jugend', 'abenteuer', 'adventure', 'alice', 'looking-glass', 'wonderland',
'treasure', 'schatz', 'green gables', 'narnia', 'wardrobe', 'coraline', 'unendliche'],
palettes: [['#fdf3e1', '#2d2a26', '#f2a23a', '#4fa3a5'], ['#e8f4f1', '#23343a', '#ef7b5a', '#f4c54b'], ['#2f3e6b', '#fff6e6', '#f6c445', '#ef8a6f'],
['#f6ecf2', '#2c2433', '#e0607e', '#5bb08f']]},
{id: 'code', font: MONO,
words: ['program', 'comput', 'informatik', 'software', 'algorithm', 'linux', 'unix', 'python', 'javascript',
'rhcsa', 'red hat', 'interpretation of computer'],
palettes: [['#1e2229', '#e6e1d6', '#e5a93d', '#6fb3c9'], ['#f4f1ea', '#22262c', '#c2453a', '#3d7ab0'], ['#0f1a14', '#d8eadc', '#5fd08a', '#e0c068']]},
];
// Folders say what a book is when its title doesn't: by name (lower case, a part of it).
const FOLDER_THEMES = [
['marx', 'konstruktiv'], ['ökonom', 'konstruktiv'], ['ljubljana', 'psyche'], ['psychoanaly', 'psyche'],
['kritische theorie', 'geometrie'], ['philosoph', 'geometrie'], ['poststruktur', 'raster'], ['zeitdiagnos', 'raster'],
['politik', 'raster'], ['soziolog', 'raster'], ['gesellschaft', 'raster'], ['shadowrun', 'raster'],
['psycholog', 'zellen'], ['wissenschaft', 'zellen'], ['biolog', 'zellen'], ['geschicht', 'karte'],
['informatik', 'code'], ['schreib', 'typo'], ['gegenwartslit', 'typo'], ['roman', 'typo'], ['zeitschrift', 'typo'],
['religion', 'sakral'], ['mytholog', 'sakral'], ['klassiker', 'klassiker'], ['science', 'kosmos'], ['sci-fi', 'kosmos'],
['fantasy', 'ornament'], ['krimi', 'schatten'], ['thriller', 'schatten'], ['warhammer', 'schatten'], ['horror', 'grusel'],
['kinder', 'spiel'], ['jugend', 'spiel'],
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht) - Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“ (umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen (/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021, übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge (/feeds/folders/order/). - Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +; Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte; Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste. - Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie auf E-Ink. - Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor, Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie, Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster, Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau und Details – einzigartig, überall gleich, nichts gespeichert. - Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
];
const DEFAULT = {id: 'bauhaus', font: SANS,
palettes: [['#efe9dd', '#24262a', '#d0533f', '#3f7fb0'], ['#26282d', '#efe8da', '#e3b04b', '#5b9aa8'], ['#e8ecef', '#22272c', '#c8553d', '#6b8f71'],
['#f3e9e2', '#2a2523', '#b84a62', '#4c7a8a']]};
// ---- which concept: the words, weighted by where they stand -------------------------------
function norm(s) { return String(s || '').toLowerCase(); }
const LETTER = 'a-z0-9äöüßàâçéèêëîïôûùÿñæœ';
function matcher(w, atStart) {
const start = atStart || w.charAt(0) === '^';
const whole = w.slice(-1) === '$';
const stem = w.replace(/^\^|\$$/g, '').replace(/[.*+?()[\]{}|\\]/g, '\\$&');
if (!start && !whole && stem.length >= 5) return s => s.indexOf(stem) >= 0;
const re = new RegExp('(^|[^' + LETTER + '])' + stem + (whole || stem.length <= 3 ? '(s|e|en)?($|[^' + LETTER + '])' : ''));
return s => re.test(s);
}
// In a name only at a word's start: "light" isn't in "Lightner".
THEMES.forEach(t => { t.match = t.words.map(w => matcher(w)); t.matchName = t.words.map(w => matcher(w, true)); });
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht) - Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“ (umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen (/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021, übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge (/feeds/folders/order/). - Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +; Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte; Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste. - Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie auf E-Ink. - Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor, Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie, Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster, Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau und Details – einzigartig, überall gleich, nichts gespeichert. - Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
function scored(book) {
// The title says most about a book; author and shelf category help. The first folder
// outweighs a word in the author or shelf category (not one in the title), further ones tip the scales.
const fields = [[norm(book.title), 3, 'match'], [norm(book.shelfTag), 2, 'match'], [norm(book.author), 2, 'matchName']];
const folders = (book.folders || (book.folder ? [book.folder] : [])).map(norm);
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht) - Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“ (umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen (/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021, übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge (/feeds/folders/order/). - Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +; Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte; Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste. - Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie auf E-Ink. - Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor, Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie, Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster, Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau und Details – einzigartig, überall gleich, nichts gespeichert. - Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
return THEMES.map(t => {
let score = 0;
fields.forEach(f => { if (f[0]) t[f[2]].forEach(m => { if (m(f[0])) score += f[1]; }); });
folders.forEach((f, i) => {
if (FOLDER_THEMES.some(ft => ft[1] === t.id && f.indexOf(ft[0]) >= 0)) score += i ? 1 : 2.5;
});
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht) - Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“ (umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen (/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021, übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge (/feeds/folders/order/). - Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +; Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte; Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste. - Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie auf E-Ink. - Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor, Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie, Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster, Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau und Details – einzigartig, überall gleich, nichts gespeichert. - Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
return {t, score};
}).filter(x => x.score > 0).sort((a, b) => b.score - a.score);
}
// A work of Marx or Engels (by its author, or a volume of the MEW) is a blue volume,
// whatever else the words say; nothing else is.
const MARX_ENGELS = /(^|[^a-zäöü])(marx|engels)($|[^a-zäöü])/i;
const BLAU = THEMES.filter(t => t.id === 'blau')[0];
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht) - Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“ (umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen (/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021, übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge (/feeds/folders/order/). - Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +; Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte; Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste. - Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie auf E-Ink. - Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor, Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie, Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster, Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau und Details – einzigartig, überall gleich, nichts gespeichert. - Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
function theme(book) {
if (MARX_ENGELS.test(book.author || '') || /mew|marx.engels.werke/i.test(book.series || '')) return BLAU;
const s = scored(book).filter(x => x.t !== BLAU);
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht) - Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“ (umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen (/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021, übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge (/feeds/folders/order/). - Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +; Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte; Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste. - Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie auf E-Ink. - Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor, Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie, Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster, Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau und Details – einzigartig, überall gleich, nichts gespeichert. - Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
return s.length ? s[0].t : DEFAULT;
}
// ---- motifs: each draws into the whole cover, the text gets its own space later -----------
const M = {
konstruktiv(r, p) {
let s = '';
const a = pick(r, [-34, -28, 28, 34, -45]);
s += `<g transform="rotate(${a} 150 225)">`;
for (let i = 0; i < 3; i++) {
const y = between(r, 60, 380), h = between(r, 14, 46);
s += `<rect x="-120" y="${f1(y)}" width="540" height="${f1(h)}" fill="${i ? p[1] : p[2]}" opacity="${i ? 0.9 : 1}"/>`;
}
s += '</g>';
s += `<circle cx="${f1(between(r, 70, 230))}" cy="${f1(between(r, 120, 320))}" r="${f1(between(r, 38, 78))}" fill="${p[2]}"/>`;
s += `<rect x="${f1(between(r, 20, 200))}" y="${f1(between(r, 40, 360))}" width="${f1(between(r, 30, 80))}" height="${f1(between(r, 30, 80))}" fill="none" stroke="${p[1]}" stroke-width="6"/>`;
return s;
},
geometrie(r, p) {
const v = r();
if (v < 0.33) { // nested squares, turning
const cx = between(r, 110, 190), cy = between(r, 220, 320), n = 5 + Math.floor(r() * 4), turn = between(r, 6, 12);
let s = '';
for (let i = n; i >= 1; i--) {
const d = i * between(r, 13, 17);
s += `<rect x="${f1(cx - d)}" y="${f1(cy - d)}" width="${f1(d * 2)}" height="${f1(d * 2)}" fill="none" stroke="${i === 2 ? p[2] : p[1]}" ` +
`stroke-width="${i === 2 ? 2.2 : 1}" opacity="${i === 2 ? 0.9 : 0.5}" transform="rotate(${f1(i * turn)} ${f1(cx)} ${f1(cy)})"/>`;
}
return s + `<circle cx="${f1(cx)}" cy="${f1(cy)}" r="${f1(between(r, 6, 11))}" fill="${p[2]}"/>`;
}
if (v < 0.6) { // triangle, circle and square, drawn
const y = between(r, 250, 330), sz = between(r, 46, 60), xs = [70, 150, 230];
const order = [0, 1, 2].sort(() => r() - 0.5), filled = Math.floor(r() * 3);
let s = `<line x1="20" y1="${f1(y + sz + 14)}" x2="${W - 20}" y2="${f1(y + sz + 14)}" stroke="${p[1]}" stroke-width="1" opacity=".5"/>`;
order.forEach((kind, i) => {
const x = xs[i], att = i === filled ? `fill="${p[2]}" stroke="none"` : `fill="none" stroke="${p[1]}" stroke-width="1.6"`;
if (kind === 0) s += `<circle cx="${x}" cy="${f1(y + sz / 2)}" r="${f1(sz / 2)}" ${att}/>`;
else if (kind === 1) s += `<rect x="${f1(x - sz / 2)}" y="${f1(y)}" width="${f1(sz)}" height="${f1(sz)}" ${att}/>`;
else s += `<path d="M${x} ${f1(y)} L${f1(x + sz / 2)} ${f1(y + sz)} L${f1(x - sz / 2)} ${f1(y + sz)} Z" ${att}/>`;
});
return s + `<circle cx="${f1(between(r, 40, 260))}" cy="${f1(y - between(r, 40, 80))}" r="3" fill="${p[3]}"/>`;
}
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht) - Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“ (umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen (/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021, übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge (/feeds/folders/order/). - Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +; Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte; Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste. - Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie auf E-Ink. - Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor, Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie, Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster, Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau und Details – einzigartig, überall gleich, nichts gespeichert. - Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
const cx = between(r, 100, 200), cy = between(r, 170, 300);
let s = '';
const n = 4 + Math.floor(r() * 5);
for (let i = 1; i <= n; i++) s += `<circle cx="${f1(cx)}" cy="${f1(cy)}" r="${f1(i * between(r, 14, 22))}" fill="none" stroke="${i % 3 ? p[1] : p[2]}" stroke-width="${i % 3 ? 1 : 2.4}" opacity="${i % 3 ? 0.55 : 0.9}"/>`;
const phi = 1.618;
s += `<line x1="0" y1="${f1(H / phi)}" x2="${W}" y2="${f1(H / phi)}" stroke="${p[1]}" stroke-width="1" opacity=".5"/>`;
s += `<line x1="${f1(W / phi)}" y1="0" x2="${f1(W / phi)}" y2="${H}" stroke="${p[1]}" stroke-width="1" opacity=".5"/>`;
s += `<circle cx="${f1(cx + between(r, -40, 40))}" cy="${f1(cy + between(r, -40, 40))}" r="${f1(between(r, 8, 18))}" fill="${p[2]}"/>`;
return s;
},
psyche(r, p) {
const v = r();
if (v < 0.35) { // the Borromean knot: three rings, no two linked
const cx = between(r, 125, 175), cy = between(r, 250, 300), rr = between(r, 52, 64), d = rr * 0.62;
let s = '';
[[-90, p[2], 0.75], [30, p[3], 0.6], [150, p[1], 0]].forEach(([a, c, o], i) => {
const x = cx + Math.cos(a * Math.PI / 180) * d, y = cy + Math.sin(a * Math.PI / 180) * d;
s += `<circle cx="${f1(x)}" cy="${f1(y)}" r="${f1(rr)}" fill="${c}" fill-opacity="${o}" stroke="${p[1]}" stroke-width="${i === 2 ? 2.4 : 1.2}"/>`;
});
return s;
}
if (v < 0.6) { // a spiral, over a divided ground
const cx = between(r, 120, 180), cy = between(r, 240, 300), turns = between(r, 3, 4.5), grow = between(r, 4.5, 6.5);
let d = '';
for (let k = 0; k <= turns * 36; k++) {
const a = k / 36 * Math.PI * 2, rad = grow * a;
d += (k ? ' L' : 'M') + f1(cx + Math.cos(a) * rad) + ' ' + f1(cy + Math.sin(a) * rad);
}
return `<rect x="0" y="${f1(cy)}" width="${W}" height="${f1(H - cy)}" fill="${p[3]}" opacity=".3"/>` +
`<path d="${d}" fill="none" stroke="${p[2]}" stroke-width="2.4" stroke-linecap="round"/>` +
`<circle cx="${f1(cx)}" cy="${f1(cy)}" r="4" fill="${p[1]}"/>`;
}
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht) - Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“ (umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen (/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021, übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge (/feeds/folders/order/). - Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +; Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte; Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste. - Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie auf E-Ink. - Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor, Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie, Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster, Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau und Details – einzigartig, überall gleich, nichts gespeichert. - Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
let s = `<rect x="${W / 2}" y="0" width="${W / 2}" height="${H}" fill="${p[3]}" opacity=".35"/>`;
const cy = between(r, 170, 290);
const rx = between(r, 60, 95), ry = between(r, 80, 130);
s += `<ellipse cx="${f1(W / 2 - rx * 0.45)}" cy="${f1(cy)}" rx="${f1(rx)}" ry="${f1(ry)}" fill="${p[2]}" opacity=".85"/>`;
s += `<ellipse cx="${f1(W / 2 + rx * 0.45)}" cy="${f1(cy)}" rx="${f1(rx)}" ry="${f1(ry)}" fill="none" stroke="${p[1]}" stroke-width="2.5"/>`;
s += `<line x1="${W / 2}" y1="40" x2="${W / 2}" y2="${H - 40}" stroke="${p[1]}" stroke-width="1" opacity=".6"/>`;
return s;
},
wellen(r, p) {
let s = '';
const n = 6 + Math.floor(r() * 5);
const amp = between(r, 6, 16), len = between(r, 60, 120);
for (let i = 0; i < n; i++) {
const y0 = 120 + i * (300 / n);
let d = `M0 ${f1(y0)}`;
for (let x = 0; x <= W; x += 10) d += ` L${x} ${f1(y0 + Math.sin((x / len) * Math.PI * 2 + i * 0.7) * amp)}`;
d += ` L${W} ${H} L0 ${H} Z`;
s += `<path d="${d}" fill="${i % 2 ? p[2] : p[3]}" opacity="${f1(0.18 + i * 0.07)}"/>`;
}
return s;
},
karte(r, p) {
let s = '';
const cx = between(r, 80, 220), cy = between(r, 160, 320);
for (let i = 1; i <= 9; i++) {
let d = '';
for (let k = 0; k <= 36; k++) {
const a = (k / 36) * Math.PI * 2;
const rad = i * 15 * (1 + 0.18 * Math.sin(a * 3 + i) + 0.1 * Math.cos(a * 5 + r()));
d += (k ? ' L' : 'M') + f1(cx + Math.cos(a) * rad) + ' ' + f1(cy + Math.sin(a) * rad * 1.15);
}
s += `<path d="${d} Z" fill="none" stroke="${i % 4 ? p[1] : p[2]}" stroke-width="${i % 4 ? 0.8 : 1.8}" opacity="${i % 4 ? 0.45 : 0.9}"/>`;
}
s += `<g stroke="${p[2]}" stroke-width="1.4"><line x1="${f1(cx)}" y1="${f1(cy - 30)}" x2="${f1(cx)}" y2="${f1(cy + 30)}"/><line x1="${f1(cx - 30)}" y1="${f1(cy)}" x2="${f1(cx + 30)}" y2="${f1(cy)}"/></g>`;
return s;
},
kosmos(r, p) {
let s = '';
for (let i = 0; i < 70; i++) s += `<circle cx="${f1(r() * W)}" cy="${f1(r() * H)}" r="${f1(between(r, 0.4, 1.5))}" fill="${p[1]}" opacity="${f1(between(r, 0.3, 0.9))}"/>`;
const cx = between(r, 90, 210), cy = between(r, 170, 290), pr = between(r, 30, 55);
for (let i = 1; i <= 3; i++) s += `<ellipse cx="${f1(cx)}" cy="${f1(cy)}" rx="${f1(pr + i * 28)}" ry="${f1((pr + i * 28) * 0.38)}" fill="none" stroke="${p[3]}" stroke-width="1" opacity=".6" transform="rotate(-18 ${f1(cx)} ${f1(cy)})"/>`;
s += `<circle cx="${f1(cx)}" cy="${f1(cy)}" r="${f1(pr)}" fill="${p[2]}"/>`;
s += `<circle cx="${f1(cx + pr * 0.35)}" cy="${f1(cy - pr * 0.2)}" r="${f1(pr)}" fill="${p[0]}" opacity=".35"/>`;
return s;
},
ornament(r, p) {
const cx = W / 2, cy = between(r, 190, 260), n = pick(r, [8, 10, 12, 16]);
let s = `<g transform="translate(${cx} ${f1(cy)})">`;
const len = between(r, 70, 105), wid = between(r, 18, 30);
for (let i = 0; i < n; i++) {
s += `<path d="M0 0 C ${f1(wid)} ${f1(-len * 0.4)} ${f1(wid * 0.5)} ${f1(-len * 0.8)} 0 ${f1(-len)} C ${f1(-wid * 0.5)} ${f1(-len * 0.8)} ${f1(-wid)} ${f1(-len * 0.4)} 0 0 Z" ` +
`fill="${i % 2 ? p[2] : p[3]}" opacity=".85" transform="rotate(${f1(i * 360 / n)})"/>`;
}
s += `<circle r="${f1(between(r, 12, 20))}" fill="${p[1]}"/><circle r="${f1(len + 12)}" fill="none" stroke="${p[2]}" stroke-width="1.5"/></g>`;
s += `<rect x="14" y="14" width="${W - 28}" height="${H - 28}" fill="none" stroke="${p[2]}" stroke-width="1.5" opacity=".8"/>`;
return s;
},
botanik(r, p) {
let s = '';
const n = 5 + Math.floor(r() * 4);
for (let i = 0; i < n; i++) {
const x = between(r, 40, 260), y = between(r, 150, 420), len = between(r, 60, 130), a = between(r, -50, 50);
s += `<g transform="translate(${f1(x)} ${f1(y)}) rotate(${f1(a)})">` +
`<path d="M0 0 C ${f1(len * 0.35)} ${f1(-len * 0.35)} ${f1(len * 0.3)} ${f1(-len * 0.8)} 0 ${f1(-len)} C ${f1(-len * 0.3)} ${f1(-len * 0.8)} ${f1(-len * 0.35)} ${f1(-len * 0.35)} 0 0 Z" fill="${i % 3 ? p[2] : p[3]}" opacity=".8"/>` +
`<line x1="0" y1="0" x2="0" y2="${f1(-len * 0.95)}" stroke="${p[0]}" stroke-width="1.2" opacity=".7"/></g>`;
}
return s;
},
berge(r, p) {
let s = `<circle cx="${f1(between(r, 60, 240))}" cy="${f1(between(r, 120, 190))}" r="${f1(between(r, 22, 38))}" fill="${p[3]}"/>`;
for (let i = 0; i < 4; i++) {
const base = 260 + i * 45;
let d = `M0 ${H} L0 ${base}`;
for (let x = 0; x <= W; x += 30) d += ` L${x} ${f1(base - between(r, 10, 90 - i * 15))}`;
d += ` L${W} ${base} L${W} ${H} Z`;
s += `<path d="${d}" fill="${i % 2 ? p[1] : p[2]}" opacity="${f1(0.35 + i * 0.18)}"/>`;
}
return s;
},
zellen(r, p) {
let s = '';
for (let i = 0; i < 16; i++) {
const x = between(r, 20, 280), y = between(r, 140, 430), rad = between(r, 12, 42);
s += `<circle cx="${f1(x)}" cy="${f1(y)}" r="${f1(rad)}" fill="${i % 3 ? p[3] : p[2]}" opacity="${f1(between(r, 0.25, 0.75))}"/>`;
if (i % 4 === 0) s += `<circle cx="${f1(x)}" cy="${f1(y)}" r="${f1(rad * 0.3)}" fill="${p[1]}" opacity=".7"/>`;
}
return s;
},
raster(r, p) {
if (r() < 0.45) { // or a skyline
let s = '';
let x = 10;
while (x < W - 10) {
const w = between(r, 18, 46), h = between(r, 60, 230), v = r();
s += `<rect x="${f1(x)}" y="${f1(H - 20 - h)}" width="${f1(w)}" height="${f1(h)}" fill="${v < 0.15 ? p[2] : v < 0.25 ? p[3] : p[1]}" opacity="${v < 0.25 ? 0.95 : f1(0.12 + v * 0.18)}"/>`;
for (let wy = H - 30 - h + 12; wy < H - 32; wy += 14) {
if (r() < 0.4) s += `<rect x="${f1(x + 5)}" y="${f1(wy)}" width="${f1(Math.max(3, w - 10))}" height="3" fill="${p[0]}" opacity=".5"/>`;
}
x += w + between(r, 2, 6);
}
return s;
}
let s = '';
const cols = 4 + Math.floor(r() * 3), rows = 5 + Math.floor(r() * 3), gap = 6;
const cw = (W - 40 - gap * (cols - 1)) / cols, rh = (H - 200 - gap * (rows - 1)) / rows;
for (let i = 0; i < cols; i++) for (let j = 0; j < rows; j++) {
const v = r();
const fill = v < 0.12 ? p[2] : v < 0.2 ? p[3] : p[1];
s += `<rect x="${f1(20 + i * (cw + gap))}" y="${f1(180 + j * (rh + gap))}" width="${f1(cw)}" height="${f1(rh)}" fill="${fill}" opacity="${v < 0.2 ? 0.95 : f1(0.08 + v * 0.12)}"/>`;
}
return s;
},
schatten(r, p) {
if (r() < 0.5) { // light through a doorway, falling across the floor
const x = between(r, 90, 170), w = between(r, 44, 64), top = between(r, 150, 210), floor = between(r, 330, 360);
return `<rect x="0" y="${f1(floor)}" width="${W}" height="${f1(H - floor)}" fill="${p[3]}" opacity=".25"/>` +
`<rect x="${f1(x)}" y="${f1(top)}" width="${f1(w)}" height="${f1(floor - top)}" fill="${p[1]}" opacity=".9"/>` +
`<path d="M${f1(x)} ${f1(floor)} L${f1(x + w)} ${f1(floor)} L${f1(x + w + between(r, 60, 120))} ${H} L${f1(x - between(r, 10, 40))} ${H} Z" fill="${p[1]}" opacity=".35"/>` +
`<rect x="${f1(x + w * 0.3)}" y="${f1(floor - between(r, 60, 80))}" width="${f1(w * 0.18)}" height="${f1(between(r, 60, 80))}" fill="${p[2]}"/>`;
}
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht) - Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“ (umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen (/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021, übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge (/feeds/folders/order/). - Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +; Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte; Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste. - Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie auf E-Ink. - Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor, Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie, Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster, Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau und Details – einzigartig, überall gleich, nichts gespeichert. - Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
const x = between(r, 60, 240);
return `<rect x="${f1(x)}" y="0" width="${f1(between(r, 3, 8))}" height="${H}" fill="${p[2]}"/>` +
`<circle cx="${f1(x)}" cy="${f1(between(r, 200, 330))}" r="${f1(between(r, 50, 90))}" fill="${p[3]}" opacity=".35"/>`;
},
typo(r, p, book) {
const ch = (book.title || '?').replace(/^[^A-Za-zÄÖÜäöüß0-9]+/, '').charAt(0).toUpperCase() || '?';
return `<text x="${f1(between(r, 20, 60))}" y="${f1(between(r, 380, 430))}" font-family="${SERIF}" font-size="${f1(between(r, 240, 320))}" ` +
`fill="${p[2]}" opacity=".9">${esc(ch)}</text>` +
`<line x1="20" y1="${f1(between(r, 120, 160))}" x2="${W - 20}" y2="${f1(between(r, 120, 160))}" stroke="${p[1]}" stroke-width="1" opacity=".5"/>`;
},
blau(r, p, book) {
const gold = p[1], dim = p[2], deep = p[3];
// Cloth: a fine weave, light from the upper left, the hinge along the spine.
let s = `<defs><pattern id="wv" width="3" height="3" patternUnits="userSpaceOnUse">` +
`<path d="M0 .5H3" stroke="${deep}" stroke-width="1" opacity=".5"/><path d="M.5 0V3" stroke="${gold}" stroke-width=".6" opacity=".06"/></pattern>` +
`<radialGradient id="lt" cx="30%" cy="18%" r="85%"><stop offset="0" stop-color="#ffffff" stop-opacity=".09"/>` +
`<stop offset="1" stop-color="#ffffff" stop-opacity="0"/></radialGradient></defs>` +
`<rect width="${W}" height="${H}" fill="url(#wv)"/><rect width="${W}" height="${H}" fill="url(#lt)"/>`;
// The spine, as on the shelf: gold bands top and bottom, a field with the volume, a groove at the hinge.
const SP = 46;
s += `<rect x="0" y="0" width="${SP}" height="${H}" fill="${deep}" opacity=".55"/>` +
`<rect x="${SP}" y="0" width="3" height="${H}" fill="${deep}"/><rect x="${SP + 3}" y="0" width="1" height="${H}" fill="${gold}" opacity=".16"/>`;
[[26, 1.6], [31, .7], [58, .7], [63, 1.6], [H - 68, 1.6], [H - 63, .7], [H - 34, .7], [H - 29, 1.6]].forEach(b => {
s += `<rect x="5" y="${b[0]}" width="${SP - 10}" height="${b[1]}" fill="${gold}"/>`;
});
s += `<path d="M${SP / 2} 40 l4 5 -4 5 -4 -5Z" fill="${gold}"/>`;
const vol = typeof book.seriesIndex === 'number' ? String(book.seriesIndex) : '';
s += `<rect x="8" y="104" width="${SP - 16}" height="44" fill="none" stroke="${gold}" stroke-width="1"/>`;
if (vol) s += `<text x="${SP / 2}" y="132" text-anchor="middle" font-family="${SERIF}" font-size="${vol.length > 3 ? 10 : 14}" fill="${gold}">${esc(vol)}</text>`;
else s += `<path d="M${SP / 2} 118 l5 6 -5 6 -5 -6Z" fill="${gold}"/>`;
s += `<rect x="12" y="170" width="${SP - 24}" height=".7" fill="${gold}" opacity=".7"/><rect x="12" y="${H - 104}" width="${SP - 24}" height=".7" fill="${gold}" opacity=".7"/>`;
const x0 = SP + 22, x1 = W - 22;
const rule = y => `<rect x="${x0}" y="${y}" width="${x1 - x0}" height="1.6" fill="${gold}"/>` +
`<rect x="${x0}" y="${y + 5}" width="${x1 - x0}" height=".8" fill="${gold}" opacity=".8"/>`;
s += rule(52) + rule(H - 64);
const cx = (x0 + x1) / 2;
const line = (t, y, size, o) => `<text x="${cx}" y="${f1(y)}" text-anchor="middle" font-family="${o && o.sans ? SANS : SERIF}" ` +
`font-size="${f1(size)}" letter-spacing="${o && o.ls != null ? o.ls : 1}" fill="${o && o.dim ? dim : gold}">${esc(t)}</text>`;
const caps = t => String(t || '').toUpperCase();
const fit = (t, max, lsp) => Math.max(7, Math.min(max, Math.floor(((x1 - x0) / Math.max(1, t.length) - lsp) / 0.72)));
const em = 0.74 * 248 / (x1 - x0); // lines() packs into 248 px; the front is narrower
const ix = typeof book.seriesIndex === 'number' ? book.seriesIndex : null;
const werke = /\bmew\b|marx.engels|werke/i.test(book.series || '');
const label = (y) => {
const n = String(ix);
return `<rect x="${f1(cx - 58)}" y="${y}" width="116" height="70" fill="none" stroke="${gold}" stroke-width="1.4"/>` +
`<rect x="${f1(cx - 54)}" y="${y + 4}" width="108" height="62" fill="none" stroke="${gold}" stroke-width=".6" opacity=".7"/>` +
line('BAND', y + 22, 10, {ls: 3, sans: true}) + line(n, y + 56, n.length > 3 ? 26 : 32, {ls: 1});
};
if (werke) {
const a = 'KARL MARX · FRIEDRICH ENGELS';
s += line(a, 92, fit(a, 11, 2), {ls: 2, sans: true});
s += line('WERKE', 160, 34, {ls: 8});
if (ix != null) s += label(190);
const t = caps(mainTitle(book.title));
const ls = lines(t, 14, 4, em);
ls.forEach((l, i) => { s += line(l, (ix != null ? 300 : 230) + i * 18, 14, {ls: 1, dim: true}); });
return s;
}
const author = caps(String(book.author || '').split(';')[0].replace(/\s*,\s*/g, ' · '));
if (author) s += line(author, 96, fit(author, 12, 2), {ls: 2, sans: true});
const t = caps(mainTitle(book.title));
const longest = t.split(/\s+/).reduce((n, w) => Math.max(n, w.length), 0);
const size = Math.max(14, Math.min(t.length <= 12 ? 28 : t.length <= 24 ? 24 : t.length <= 40 ? 20 : 17,
Math.floor((x1 - x0) / (longest * 0.74))));
const ls = lines(t, size, 5, em);
const lh = size * 1.2, h = ls.length * lh;
const y0 = (ix != null ? 200 : 228) - h / 2 + size * 0.8;
ls.forEach((l, i) => { s += line(l, y0 + i * lh, size, {ls: 1}); });
if (ix != null) s += label(296);
return s;
},
klassiker(r, p) {
// An all-over pattern (the paper), the plate for the text goes over it.
const kind = Math.floor(r() * 5), sz = pick(r, [18, 22, 26]);
let tile;
if (kind === 0) { // lattice of diamonds
tile = `<path d="M0 ${sz / 2} L${sz / 2} 0 L${sz} ${sz / 2} L${sz / 2} ${sz} Z" fill="none" stroke="${p[2]}" stroke-width="1.4"/>` +
`<circle cx="${sz / 2}" cy="${sz / 2}" r="${f1(sz / 9)}" fill="${p[3]}"/>`;
} else if (kind === 1) { // scales
tile = `<path d="M0 ${sz} A${sz / 2} ${sz / 2} 0 0 1 ${sz} ${sz} M${-sz / 2} ${sz / 2} A${sz / 2} ${sz / 2} 0 0 1 ${sz / 2} ${sz / 2} ` +
`M${sz / 2} ${sz / 2} A${sz / 2} ${sz / 2} 0 0 1 ${sz * 1.5} ${sz / 2}" fill="none" stroke="${p[2]}" stroke-width="1.3"/>`;
} else if (kind === 2) { // dots, offset rows
tile = `<circle cx="${sz / 4}" cy="${sz / 4}" r="${f1(sz / 7)}" fill="${p[2]}"/><circle cx="${sz * 0.75}" cy="${sz * 0.75}" r="${f1(sz / 7)}" fill="${p[3]}"/>`;
} else if (kind === 3) { // little stars
const c = sz / 2, a = sz / 3, b = sz / 10;
tile = `<path d="M${c} ${f1(c - a)} L${f1(c + b)} ${f1(c - b)} L${f1(c + a)} ${c} L${f1(c + b)} ${f1(c + b)} L${c} ${f1(c + a)} L${f1(c - b)} ${f1(c + b)} L${f1(c - a)} ${c} L${f1(c - b)} ${f1(c - b)} Z" fill="${p[2]}"/>` +
`<circle cx="0" cy="0" r="1.6" fill="${p[3]}"/><circle cx="${sz}" cy="0" r="1.6" fill="${p[3]}"/><circle cx="0" cy="${sz}" r="1.6" fill="${p[3]}"/><circle cx="${sz}" cy="${sz}" r="1.6" fill="${p[3]}"/>`;
} else { // stripes with a beading
tile = `<rect x="0" y="0" width="${f1(sz / 3)}" height="${sz}" fill="${p[2]}" opacity=".8"/><circle cx="${f1(sz * 0.67)}" cy="${sz / 2}" r="${f1(sz / 9)}" fill="${p[3]}"/>`;
}
return `<defs><pattern id="pt" width="${sz}" height="${sz}" patternUnits="userSpaceOnUse">${tile}</pattern></defs>` +
`<rect width="${W}" height="${H}" fill="url(#pt)" opacity=".75"/>`;
},
sakral(r, p) {
// A pointed arch with a rose window, nested; light from above.
const cx = W / 2, w = between(r, 170, 210), ys = between(r, 300, 330);
let s = `<rect width="${W}" height="${H}" fill="${p[3]}" opacity=".12"/>`;
for (let i = 0; i < 3; i++) {
const ww = w - i * 34, x0 = cx - ww / 2, x1 = cx + ww / 2, apex = ys - ww * 0.866;
s += `<path d="M${f1(x0)} ${H} L${f1(x0)} ${f1(ys)} A${f1(ww)} ${f1(ww)} 0 0 1 ${f1(cx)} ${f1(apex)} A${f1(ww)} ${f1(ww)} 0 0 1 ${f1(x1)} ${f1(ys)} L${f1(x1)} ${H}" ` +
`fill="${i === 2 ? p[2] : 'none'}" fill-opacity="${i === 2 ? 0.18 : 0}" stroke="${p[2]}" stroke-width="${i ? 1.2 : 2.4}" opacity="${i ? 0.8 : 1}"/>`;
}
const ry = ys - w * 0.3, rr = between(r, 30, 42), n = pick(r, [8, 12, 16]);
s += `<circle cx="${cx}" cy="${f1(ry)}" r="${f1(rr)}" fill="none" stroke="${p[2]}" stroke-width="1.6"/>`;
for (let i = 0; i < n; i++) {
const a = i * Math.PI * 2 / n;
s += `<line x1="${cx}" y1="${f1(ry)}" x2="${f1(cx + Math.cos(a) * rr)}" y2="${f1(ry + Math.sin(a) * rr)}" stroke="${p[2]}" stroke-width="1"/>` +
`<circle cx="${f1(cx + Math.cos(a) * rr * 0.62)}" cy="${f1(ry + Math.sin(a) * rr * 0.62)}" r="${f1(rr * 0.16)}" fill="${i % 2 ? p[3] : p[2]}" opacity=".85"/>`;
}
s += `<line x1="${cx}" y1="${f1(ry + rr)}" x2="${cx}" y2="${H}" stroke="${p[2]}" stroke-width="1.2" opacity=".8"/>`;
return s;
},
grusel(r, p) {
// A bare tree before a big moon.
const tree = p[4] || p[1];
const mx = between(r, 90, 210), my = between(r, 170, 250), mr = between(r, 70, 100);
let s = `<circle cx="${f1(mx)}" cy="${f1(my)}" r="${f1(mr)}" fill="${p[3]}" opacity=".9"/>`;
s += `<circle cx="${f1(mx + mr * 0.3)}" cy="${f1(my - mr * 0.2)}" r="${f1(mr * 0.18)}" fill="${p[0]}" opacity=".08"/>`;
let lines = '';
const branch = (x, y, a, len, w, d) => {
const x2 = x + Math.cos(a) * len, y2 = y + Math.sin(a) * len;
lines += `<line x1="${f1(x)}" y1="${f1(y)}" x2="${f1(x2)}" y2="${f1(y2)}" stroke-width="${f1(w)}"/>`;
if (d <= 0) return;
const spread = between(r, 0.3, 0.6);
branch(x2, y2, a - spread + between(r, -0.15, 0.15), len * between(r, 0.66, 0.8), w * 0.68, d - 1);
branch(x2, y2, a + spread + between(r, -0.15, 0.15), len * between(r, 0.66, 0.8), w * 0.68, d - 1);
};
const tx = between(r, 70, 230);
branch(tx, H - 30, -Math.PI / 2 + between(r, -0.15, 0.15), between(r, 85, 110), 9, 6);
s += `<g stroke="${tree}" stroke-linecap="round">${lines}</g>`;
s += `<path d="M0 ${H} L0 ${H - 34} Q${W / 2} ${f1(H - between(r, 50, 70))} ${W} ${H - 30} L${W} ${H} Z" fill="${tree}"/>`;
s += `<rect x="0" y="${H - 8}" width="${W}" height="8" fill="${p[2]}"/>`;
return s;
},
spiel(r, p) {
// Rolling hills, a sun, a few bright dots.
let s = `<circle cx="${f1(between(r, 70, 230))}" cy="${f1(between(r, 180, 240))}" r="${f1(between(r, 34, 50))}" fill="${p[2]}"/>`;
for (let i = 0; i < 3; i++) {
const cx = between(r, -40, 340), rad = between(r, 140, 220);
s += `<circle cx="${f1(cx)}" cy="${f1(H + rad * 0.45 - i * 30)}" r="${f1(rad)}" fill="${i % 2 ? p[3] : p[1]}" opacity="${f1(0.18 + i * 0.22)}"/>`;
}
for (let i = 0; i < 9; i++) {
s += `<circle cx="${f1(r() * W)}" cy="${f1(between(r, 140, 320))}" r="${f1(between(r, 3, 8))}" fill="${pick(r, [p[2], p[3]])}" opacity=".85"/>`;
}
return s;
},
code(r, p) {
// An editor's minimap: indented lines of tokens, some of them highlighted.
let s = '', indent = 0;
for (let y = 150; y < H - 24; y += 13) {
if (r() < 0.12) continue; // an empty line
indent = Math.max(0, Math.min(4, indent + pick(r, [-1, 0, 0, 1])));
let x = 26 + indent * 16;
const n = 1 + Math.floor(r() * 3);
for (let k = 0; k < n && x < W - 30; k++) {
const w = Math.min(between(r, 14, 70), W - 26 - x), v = r();
s += `<rect x="${f1(x)}" y="${y}" width="${f1(w)}" height="6" rx="3" fill="${v < 0.2 ? p[2] : v < 0.35 ? p[3] : p[1]}" opacity="${v < 0.35 ? 0.95 : 0.3}"/>`;
x += w + 6;
}
}
s += `<rect x="12" y="140" width="3" height="${H - 160}" fill="${p[2]}" opacity=".7"/>`;
return s;
},
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht) - Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“ (umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen (/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021, übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge (/feeds/folders/order/). - Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +; Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte; Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste. - Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie auf E-Ink. - Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor, Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie, Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster, Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau und Details – einzigartig, überall gleich, nichts gespeichert. - Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
bauhaus(r, p) {
let s = '';
for (let i = 0; i < 4; i++) {
const kind = Math.floor(r() * 3), x = between(r, 30, 270), y = between(r, 150, 410), size = between(r, 40, 110);
const fill = pick(r, [p[2], p[3], p[1]]);
if (kind === 0) s += `<circle cx="${f1(x)}" cy="${f1(y)}" r="${f1(size / 2)}" fill="${fill}" opacity=".9"/>`;
else if (kind === 1) s += `<rect x="${f1(x - size / 2)}" y="${f1(y - size / 2)}" width="${f1(size)}" height="${f1(size)}" fill="${fill}" opacity=".9" transform="rotate(${f1(between(r, -20, 20))} ${f1(x)} ${f1(y)})"/>`;
else s += `<path d="M${f1(x)} ${f1(y - size / 2)} L${f1(x + size / 2)} ${f1(y + size / 2)} L${f1(x - size / 2)} ${f1(y + size / 2)} Z" fill="${fill}" opacity=".9"/>`;
}
return s;
},
};
// ---- the text -------------------------------------------------------------------------------------
function esc(s) {
return String(s).replace(/[&<>"']/g, c => ({'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&apos;'}[c]));
}
// The title's lines, for a size that fits; a word too long is broken.
function lines(text, size, max, em) {
const per = Math.max(4, Math.floor(248 / (size * (em || 0.56))));
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht) - Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“ (umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen (/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021, übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge (/feeds/folders/order/). - Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +; Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte; Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste. - Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie auf E-Ink. - Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor, Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie, Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster, Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau und Details – einzigartig, überall gleich, nichts gespeichert. - Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
const words = String(text).split(/\s+/).filter(Boolean);
const out = [];
let cur = '';
words.forEach(w => {
while (w.length > per) { if (cur) { out.push(cur); cur = ''; } out.push(w.slice(0, per - 1) + '‑'); w = w.slice(per - 1); }
if (!cur) cur = w;
else if ((cur + ' ' + w).length <= per) cur += ' ' + w;
else { out.push(cur); cur = w; }
});
if (cur) out.push(cur);
if (out.length > max) { out.length = max; out[max - 1] = out[max - 1].replace(/\s*\S*$/, '') + ' …'; }
return out;
}
// Only the main title ("Seeing Like a State: How …" → "Seeing Like a State").
function mainTitle(t) {
let s = String(t || '').split(/\s[-:–—•·\/]\s|:\s/)[0].trim();
// "Der Zauberberg. Erster Band" → "Der Zauberberg", but not at an initial ("George R. R. Martin").
const re = /(\S+)\.\s+(?=[A-ZÄÖÜ])/g;
let m;
while ((m = re.exec(s))) {
if (m[1].length >= 3) { s = s.slice(0, m.index + m[1].length); break; }
}
return s.length >= 3 ? s : String(t || '?');
}
function textBlock(r, th, p, book) {
const title = mainTitle(book.title);
const upper = th.upper;
const t = upper ? title.toUpperCase() : title;
const len = t.length;
const longest = t.split(/\s+/).reduce((n, w) => Math.max(n, w.length), 0);
// The longest word stays whole: the type gets smaller rather than the word broken.
// Capitals take more room than small letters (about .72 of the size against .56).
const em = upper ? 0.74 : 0.58;
const size = Math.max(14, Math.min(len <= 10 ? 40 : len <= 18 ? 34 : len <= 30 ? 28 : len <= 48 ? 23 : 19,
Math.floor(248 / (longest * em))));
const ls = lines(t, size, 5, upper ? 0.72 : 0.56);
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht) - Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“ (umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen (/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021, übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge (/feeds/folders/order/). - Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +; Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte; Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste. - Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie auf E-Ink. - Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor, Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie, Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster, Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau und Details – einzigartig, überall gleich, nichts gespeichert. - Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
const lh = size * 1.12;
const layout = pick(r, th.layouts || ['top', 'top', 'bottom', 'plate']);
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht) - Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“ (umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen (/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021, übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge (/feeds/folders/order/). - Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +; Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte; Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste. - Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie auf E-Ink. - Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor, Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie, Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster, Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau und Details – einzigartig, überall gleich, nichts gespeichert. - Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
const author = String(book.author || '').split(/[;,]/)[0].trim();
const blockH = ls.length * lh + (author ? 30 : 0) + 40;
let y0, s = '';
if (layout === 'top') {
y0 = 34;
s += `<rect x="0" y="0" width="${W}" height="${f1(blockH)}" fill="${p[0]}" opacity=".94"/>`;
} else if (layout === 'bottom') {
y0 = H - blockH + 20;
s += `<rect x="0" y="${f1(H - blockH)}" width="${W}" height="${f1(blockH)}" fill="${p[0]}" opacity=".94"/>`;
} else {
y0 = 70;
s += `<rect x="18" y="${f1(y0 - 34)}" width="${W - 36}" height="${f1(blockH)}" rx="3" fill="${p[0]}" opacity=".95"/>`;
if (th.frame) s += `<rect x="24" y="${f1(y0 - 28)}" width="${W - 48}" height="${f1(blockH - 12)}" rx="2" fill="none" stroke="${p[1]}" stroke-width="1" opacity=".6"/>`;
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht) - Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“ (umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen (/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021, übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge (/feeds/folders/order/). - Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +; Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte; Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste. - Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie auf E-Ink. - Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor, Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie, Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster, Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau und Details – einzigartig, überall gleich, nichts gespeichert. - Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
}
const x = 26;
ls.forEach((l, i) => {
s += `<text x="${x}" y="${f1(y0 + size * 0.85 + i * lh)}" font-family="${th.font}" font-size="${size}" font-weight="${th.font === SERIF ? 400 : 700}" ` +
`fill="${p[1]}" letter-spacing="${upper ? 0.5 : 0}">${esc(l)}</text>`;
});
if (author) {
// A long name closes up and gets smaller rather than running off the cover.
const spacing = author.length > 22 ? 1 : 2;
const asz = Math.max(8, Math.min(12, Math.floor((240 / author.length - spacing) / 0.7)));
s += `<text x="${x}" y="${f1(y0 + ls.length * lh + 22)}" font-family="${SANS}" font-size="${asz}" letter-spacing="${spacing}" fill="${p[2]}">${esc(author.toUpperCase())}</text>`;
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht) - Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“ (umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen (/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021, übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge (/feeds/folders/order/). - Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +; Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte; Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste. - Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie auf E-Ink. - Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor, Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie, Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster, Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau und Details – einzigartig, überall gleich, nichts gespeichert. - Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
}
return s;
}
// ---- the cover ---------------------------------------------------------------------------------------
function gray(hex) {
const n = parseInt(hex.slice(1), 16);
const y = Math.round(0.3 * (n >> 16) + 0.59 * ((n >> 8) & 255) + 0.11 * (n & 255));
const h = (y < 16 ? '0' : '') + y.toString(16);
return '#' + h + h + h;
}
function svg(book, opts) {
const th = theme(book);
const r = rng(hash(norm(book.title) + '|' + norm(book.author)));
let p = pick(r, th.palettes);
if (opts && opts.gray) p = p.map(gray);
const motif = (M[th.id] || M.bauhaus)(r, p, book);
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}">` +
`<rect width="${W}" height="${H}" fill="${p[0]}"/>${motif}${th.ownText ? '' : textBlock(r, th, p, book)}</svg>`;
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht) - Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“ (umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen (/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021, übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge (/feeds/folders/order/). - Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +; Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte; Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste. - Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie auf E-Ink. - Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor, Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie, Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster, Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau und Details – einzigartig, überall gleich, nichts gespeichert. - Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
}
const cache = new Map();
function url(book, opts) {
const key = (book.title || '') + '|' + (book.author || '') + '|' + (book.shelfTag || '') + '|' +
((book.folders || []).join(',')) + '|' + (book.series || '') + '|' + (book.seriesIndex == null ? '' : book.seriesIndex) + '|' +
(opts && opts.gray ? 1 : 0);
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht) - Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“ (umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen (/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021, übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge (/feeds/folders/order/). - Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +; Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte; Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste. - Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie auf E-Ink. - Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor, Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie, Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster, Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau und Details – einzigartig, überall gleich, nichts gespeichert. - Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
if (!cache.has(key)) cache.set(key, 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg(book, opts)));
return cache.get(key);
}
window.DioraGenCover = {url, svg, theme: book => theme(book).id};
})();