Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
/ *
* The feed reader ( the server side is the feeds app , /feeds/ … ) : the "Feeds" tab
* with two parts that work differently on purpose —
* Nachrichten : stories ( the same event from several sources is one card ) ,
* the "Seit letztem Mal" briefing on top , old news simply goes away ;
* Blogs : a plain list of unread articles , per blog if wanted .
* Articles open in the page ( full text from the server ) , the browser only on request .
* Runs in the web page , the desktop app and both Android apps ( in diora Reader ,
* which has no tab bar , it brings a small Bücher / Feeds bar ) . Chrome - 61 - safe :
* the e - reader app transpiles it ( compat61 ) , so no ? . / ? ? here .
* /
( function ( ) {
'use strict' ;
const tab = document . getElementById ( 'tab-feeds' ) ;
if ( ! tab ) return ;
const READER _APP = document . documentElement . classList . contains ( 'nui-reader-app' ) ;
const st = {
view : 'news' , // news | blogs | manage
state : null , // /feeds/state/
briefing : null ,
stories : [ ] , storiesMore : false ,
category : '' ,
2026-10-19 17:07:51 +02:00
topics : [ ] , // the day's topics (/feeds/news/ first page): [{label, ids}]
topic : null , // the one shown, or null
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
items : [ ] , itemsMore : false ,
blogSource : '' , blogUnread : true ,
Blog-Feed nach Kino: Abschnitte, Favoriten, Seltene zuerst, Ausblenden, NEU
- Abschnitte Heute/Gestern/Diese Woche/Diesen Monat/Älter; je Abschnitt bis zu
5 Posts je Favoriten-Blog zuerst, dann „Seltene zuerst“ (mittlerer Abstand der
letzten 10 Posts, ein Post = 30 Tage) oder chronologisch
- Blog „im Hauptfeed ausblenden“: nicht unter Alle und nicht gezählt, im Ordner da
- NEU (jünger als 24 h, ungelesen) am Post, als Zahl am Tab, Punkt am Ordner
- Chips Ungelesen/Alle, ★ Favoriten, Seltene zuerst; Schalter je Blog
- Lesezeit (Wörter/230) an jedem Post; words wird beim Speichern gezählt
- sw v93
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:32:15 +02:00
blogFav : false , // main feed: only the favorite blogs
rareFirst : true , // within a day: blogs that seldom post first
2026-10-04 20:54:29 +02:00
blogFolder : null , // null = all blogs, '' = blogs without a folder, else a folder name
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
found : null , // discover results
opml : null , // parsed OPML import
editing : null , // source id being edited
loading : false ,
error : '' ,
} ;
try {
const saved = localStorage . getItem ( 'diora_feeds_view' ) ;
if ( saved === 'news' || saved === 'blogs' ) st . view = saved ;
Blog-Feed nach Kino: Abschnitte, Favoriten, Seltene zuerst, Ausblenden, NEU
- Abschnitte Heute/Gestern/Diese Woche/Diesen Monat/Älter; je Abschnitt bis zu
5 Posts je Favoriten-Blog zuerst, dann „Seltene zuerst“ (mittlerer Abstand der
letzten 10 Posts, ein Post = 30 Tage) oder chronologisch
- Blog „im Hauptfeed ausblenden“: nicht unter Alle und nicht gezählt, im Ordner da
- NEU (jünger als 24 h, ungelesen) am Post, als Zahl am Tab, Punkt am Ordner
- Chips Ungelesen/Alle, ★ Favoriten, Seltene zuerst; Schalter je Blog
- Lesezeit (Wörter/230) an jedem Post; words wird beim Speichern gezählt
- sw v93
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:32:15 +02:00
if ( localStorage . getItem ( 'diora_feeds_rare' ) === '0' ) st . rareFirst = false ;
if ( localStorage . getItem ( 'diora_feeds_unread' ) === '0' ) st . blogUnread = false ;
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
} catch ( e ) { }
const esc = s => String ( s == null ? '' : s ) . replace ( /&/g , '&' ) . replace ( /</g , '<' )
. replace ( />/g , '>' ) . replace ( /"/g , '"' ) ;
async function api ( path , body ) {
const opts = { credentials : 'same-origin' , headers : { } } ;
if ( body !== undefined ) {
opts . method = 'POST' ;
opts . headers [ 'Content-Type' ] = 'application/json' ;
opts . body = JSON . stringify ( body ) ;
}
const r = await fetch ( path , opts ) ;
let data = null ;
try { data = await r . json ( ) ; } catch ( e ) { }
if ( ! r . ok ) throw new Error ( ( data && data . error ) || ( 'Fehler ' + r . status ) ) ;
return data ;
}
// ---- time
function when ( iso ) {
if ( ! iso ) return '' ;
const d = new Date ( iso ) ;
const mins = Math . round ( ( Date . now ( ) - d . getTime ( ) ) / 60000 ) ;
if ( mins < 1 ) return 'gerade eben' ;
if ( mins < 60 ) return 'vor ' + mins + ' Min.' ;
if ( mins < 6 * 60 ) return 'vor ' + Math . round ( mins / 60 ) + ' Std.' ;
const hm = ( '0' + d . getHours ( ) ) . slice ( - 2 ) + ':' + ( '0' + d . getMinutes ( ) ) . slice ( - 2 ) ;
const day = dayLabel ( d ) ;
return ( day === 'Heute' ? '' : day + ', ' ) + hm ;
}
function dayLabel ( d ) {
const today = new Date ( ) ; today . setHours ( 0 , 0 , 0 , 0 ) ;
const that = new Date ( d . getTime ( ) ) ; that . setHours ( 0 , 0 , 0 , 0 ) ;
const diff = Math . round ( ( today - that ) / 86400000 ) ;
if ( diff === 0 ) return 'Heute' ;
if ( diff === 1 ) return 'Gestern' ;
return d . toLocaleDateString ( 'de-DE' , { weekday : 'short' , day : 'numeric' , month : 'short' } ) ;
}
// ---- loading
async function loadState ( ) {
st . state = await api ( '/feeds/state/' ) ;
}
async function loadNews ( more ) {
const q = [ ] ;
2026-10-19 17:07:51 +02:00
if ( st . topic ) q . push ( 'ids=' + st . topic . ids . join ( ',' ) ) ;
else {
if ( more && st . stories . length ) q . push ( 'before=' + encodeURIComponent ( st . stories [ st . stories . length - 1 ] . last _at ) ) ;
if ( st . category ) q . push ( 'category=' + encodeURIComponent ( st . category ) ) ;
}
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
const [ news , briefing ] = await Promise . all ( [
api ( '/feeds/news/' + ( q . length ? '?' + q . join ( '&' ) : '' ) ) ,
more ? Promise . resolve ( st . briefing ) : api ( '/feeds/briefing/' ) ,
] ) ;
st . stories = more ? st . stories . concat ( news . stories ) : news . stories ;
st . storiesMore = news . more ;
st . briefing = briefing ;
2026-10-19 17:07:51 +02:00
if ( news . topics ) st . topics = news . topics ;
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
}
async function loadBlogs ( more ) {
const q = [ ] ;
if ( st . blogSource ) q . push ( 'source=' + st . blogSource ) ;
2026-10-04 20:54:29 +02:00
else if ( st . blogFolder !== null ) q . push ( 'folder=' + encodeURIComponent ( st . blogFolder ) ) ;
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
if ( st . blogUnread ) q . push ( 'unread=1' ) ;
Blog-Feed nach Kino: Abschnitte, Favoriten, Seltene zuerst, Ausblenden, NEU
- Abschnitte Heute/Gestern/Diese Woche/Diesen Monat/Älter; je Abschnitt bis zu
5 Posts je Favoriten-Blog zuerst, dann „Seltene zuerst“ (mittlerer Abstand der
letzten 10 Posts, ein Post = 30 Tage) oder chronologisch
- Blog „im Hauptfeed ausblenden“: nicht unter Alle und nicht gezählt, im Ordner da
- NEU (jünger als 24 h, ungelesen) am Post, als Zahl am Tab, Punkt am Ordner
- Chips Ungelesen/Alle, ★ Favoriten, Seltene zuerst; Schalter je Blog
- Lesezeit (Wörter/230) an jedem Post; words wird beim Speichern gezählt
- sw v93
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:32:15 +02:00
if ( st . blogFav && ! st . blogSource && st . blogFolder === null ) q . push ( 'fav=1' ) ;
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
if ( more && st . items . length ) q . push ( 'before=' + encodeURIComponent ( st . items [ st . items . length - 1 ] . published ) ) ;
const data = await api ( '/feeds/blogs/' + ( q . length ? '?' + q . join ( '&' ) : '' ) ) ;
st . items = more ? st . items . concat ( data . items ) : data . items ;
st . itemsMore = data . more ;
}
async function load ( more ) {
st . loading = true ;
st . error = '' ;
render ( ) ;
try {
if ( ! more ) await loadState ( ) ;
if ( st . view === 'news' ) await loadNews ( more ) ;
else if ( st . view === 'blogs' ) await loadBlogs ( more ) ;
} catch ( e ) {
st . error = e . message === 'Failed to fetch' ? 'Keine Verbindung.' : e . message ;
}
st . loading = false ;
render ( ) ;
}
// ---- rendering
function sources ( kind ) {
return ( ( st . state && st . state . sources ) || [ ] ) . filter ( s => ! kind || s . kind === kind ) ;
}
function render ( ) {
const s = st . state ;
const newsN = s ? s . news _new : 0 ;
Blog-Feed nach Kino: Abschnitte, Favoriten, Seltene zuerst, Ausblenden, NEU
- Abschnitte Heute/Gestern/Diese Woche/Diesen Monat/Älter; je Abschnitt bis zu
5 Posts je Favoriten-Blog zuerst, dann „Seltene zuerst“ (mittlerer Abstand der
letzten 10 Posts, ein Post = 30 Tage) oder chronologisch
- Blog „im Hauptfeed ausblenden“: nicht unter Alle und nicht gezählt, im Ordner da
- NEU (jünger als 24 h, ungelesen) am Post, als Zahl am Tab, Punkt am Ordner
- Chips Ungelesen/Alle, ★ Favoriten, Seltene zuerst; Schalter je Blog
- Lesezeit (Wörter/230) an jedem Post; words wird beim Speichern gezählt
- sw v93
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:32:15 +02:00
const blogN = s ? s . blogs _new : 0 ; // new = younger than a day, unread (hidden blogs not counted)
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
let body ;
if ( st . view === 'manage' ) body = renderManage ( ) ;
else if ( ! s && st . loading ) body = '<p class="fd-note">Lädt …</p>' ;
else if ( st . view === 'news' ) body = renderNews ( ) ;
else body = renderBlogs ( ) ;
tab . innerHTML =
'<div class="fd">' +
'<div class="fd-top">' +
( st . view === 'manage'
? '<button type="button" class="fd-back" data-act="close-manage">‹ Zurück</button><h2 class="fd-h">Feeds verwalten</h2>'
: '<div class="fd-seg">' +
'<button type="button" data-view="news" class="' + ( st . view === 'news' ? 'on' : '' ) + '">Nachrichten' +
( newsN ? ' <span class="fd-count">' + newsN + '</span>' : '' ) + '</button>' +
'<button type="button" data-view="blogs" class="' + ( st . view === 'blogs' ? 'on' : '' ) + '">Blogs' +
( blogN ? ' <span class="fd-count">' + blogN + '</span>' : '' ) + '</button>' +
'</div>' +
'<div class="fd-actions">' +
'<button type="button" class="fd-icon" data-act="refresh" title="Jetzt abrufen" aria-label="Jetzt abrufen">' + ( st . loading ? '…' : '↻' ) + '</button>' +
'<button type="button" class="fd-icon" data-act="manage" title="Feeds verwalten" aria-label="Feeds verwalten">⚙</button>' +
'</div>' ) +
'</div>' +
( st . error ? '<p class="fd-error">' + esc ( st . error ) + '</p>' : '' ) +
body + '</div>' ;
}
function storyMeta ( x ) {
const lead = x . lead || { } ;
const others = x . sources - 1 ;
return esc ( lead . source _title ) + ( others > 0 ? ' <span class="fd-more-src">+' + others + '</span>' : '' ) +
' · ' + esc ( when ( x . last _at ) ) ;
}
function storyRow ( x ) {
const lead = x . lead || { } ;
return '<button type="button" class="fd-row' + ( x . unread ? '' : ' fd-read' ) + '" data-story="' + x . id + '">' +
'<span class="fd-row-text"><span class="fd-row-title">' + esc ( lead . title ) + '</span>' +
'<span class="fd-row-meta">' + storyMeta ( x ) + '</span></span>' +
( x . sources > 1 ? '<span class="fd-badge" title="' + x . sources + ' Quellen">' + x . sources + '</span>' : '' ) +
'</button>' ;
}
function renderBriefing ( ) {
const b = st . briefing ;
if ( ! b || ! b . total ) return '' ;
let h = '<section class="fd-brief">' +
'<div class="fd-brief-head"><h2>Seit letztem Mal</h2>' +
'<span class="fd-note">' + b . total + ( b . total === 1 ? ' Meldung' : ' Meldungen' ) + ' seit ' + esc ( when ( b . since ) ) + '</span></div>' ;
if ( b . top . length ) {
h += '<h3>Top-Themen</h3>' + b . top . map ( storyRow ) . join ( '' ) ;
}
b . sections . forEach ( sec => {
h += '<h3>' + esc ( sec . category ) + ' <small>' + sec . count + '</small></h3>' + sec . stories . map ( storyRow ) . join ( '' ) ;
if ( sec . count > sec . stories . length ) {
h += '<button type="button" class="fd-link" data-cat="' + esc ( sec . category ) + '">' +
( sec . count - sec . stories . length ) + ' weitere in ' + esc ( sec . category ) + ' › </button>' ;
}
} ) ;
h += '<button type="button" class="btn btn-primary fd-seen" data-act="seen">Alles gelesen ✓</button></section>' ;
return h ;
}
function renderNews ( ) {
if ( ! sources ( 'news' ) . length ) {
return '<div class="fd-empty"><p>Noch keine Nachrichtenquellen.</p>' +
'<button type="button" class="btn btn-primary" data-act="manage">Feed hinzufügen</button></div>' ;
}
const cats = ( st . state . categories || [ ] ) ;
let h = renderBriefing ( ) ;
if ( cats . length > 1 ) {
h += '<div class="fd-chips">' +
'<button type="button" class="fd-chip' + ( st . category ? '' : ' on' ) + '" data-cat="">Alle</button>' +
cats . map ( c => '<button type="button" class="fd-chip' + ( st . category === c ? ' on' : '' ) + '" data-cat="' + esc ( c ) + '">' + esc ( c ) + '</button>' ) . join ( '' ) +
'</div>' ;
}
2026-10-19 17:07:51 +02:00
if ( st . topics . length || st . topic ) {
// Topics: stories of the last days about one matter (feeds/textproc.py).
const list = st . topic && ! st . topics . some ( t => t . label === st . topic . label ) ? [ st . topic ] . concat ( st . topics ) : st . topics ;
h += '<div class="fd-chips fd-topics"><span class="fd-note">Themen</span>' +
list . map ( ( t , n ) => '<button type="button" class="fd-chip' + ( st . topic && st . topic . label === t . label ? ' on' : '' ) +
'" data-topic="' + n + '">' + esc ( t . label ) + ' <small>' + t . ids . length + '</small></button>' ) . join ( '' ) + '</div>' ;
st . topicList = list ;
}
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
if ( ! st . stories . length ) return h + '<p class="fd-note">' + ( st . loading ? 'Lädt …' : 'Gerade keine Nachrichten.' ) + '</p>' ;
let day = '' ;
st . stories . forEach ( x => {
const d = dayLabel ( new Date ( x . last _at ) ) ;
if ( d !== day ) {
day = d ;
h += '<h3 class="fd-day">' + esc ( d ) + '</h3>' ;
}
const lead = x . lead || { } ;
h += '<button type="button" class="fd-card' + ( x . unread ? '' : ' fd-read' ) + '" data-story="' + x . id + '">' +
( lead . image _url ? '<img class="fd-thumb" loading="lazy" alt="" src="' + esc ( lead . image _url ) + '">' : '' ) +
'<span class="fd-card-text"><span class="fd-card-title">' + esc ( lead . title ) + '</span>' +
( lead . summary ? '<span class="fd-card-sum">' + esc ( lead . summary ) + '</span>' : '' ) +
'<span class="fd-row-meta">' + storyMeta ( x ) + '</span></span></button>' ;
} ) ;
if ( st . storiesMore ) h += '<button type="button" class="btn fd-loadmore" data-act="more">Ältere laden</button>' ;
return h ;
}
Blog-Feed nach Kino: Abschnitte, Favoriten, Seltene zuerst, Ausblenden, NEU
- Abschnitte Heute/Gestern/Diese Woche/Diesen Monat/Älter; je Abschnitt bis zu
5 Posts je Favoriten-Blog zuerst, dann „Seltene zuerst“ (mittlerer Abstand der
letzten 10 Posts, ein Post = 30 Tage) oder chronologisch
- Blog „im Hauptfeed ausblenden“: nicht unter Alle und nicht gezählt, im Ordner da
- NEU (jünger als 24 h, ungelesen) am Post, als Zahl am Tab, Punkt am Ordner
- Chips Ungelesen/Alle, ★ Favoriten, Seltene zuerst; Schalter je Blog
- Lesezeit (Wörter/230) an jedem Post; words wird beim Speichern gezählt
- sw v93
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:32:15 +02:00
// ---- blogs: days, favorites first, rare blogs first (after Kino's feed)
const NEW _MS = 24 * 3600000 ;
const FAV _PER _DAY = 5 ; // posts of one favorite blog that go first per section
2026-10-04 21:54:04 +02:00
function inFolderOf ( b , f ) {
const list = b . folders || [ ] ;
if ( f === '' ) return ! list . length ;
const k = f . toLowerCase ( ) ;
return list . some ( x => x . toLowerCase ( ) === k ) ;
}
Blog-Feed nach Kino: Abschnitte, Favoriten, Seltene zuerst, Ausblenden, NEU
- Abschnitte Heute/Gestern/Diese Woche/Diesen Monat/Älter; je Abschnitt bis zu
5 Posts je Favoriten-Blog zuerst, dann „Seltene zuerst“ (mittlerer Abstand der
letzten 10 Posts, ein Post = 30 Tage) oder chronologisch
- Blog „im Hauptfeed ausblenden“: nicht unter Alle und nicht gezählt, im Ordner da
- NEU (jünger als 24 h, ungelesen) am Post, als Zahl am Tab, Punkt am Ordner
- Chips Ungelesen/Alle, ★ Favoriten, Seltene zuerst; Schalter je Blog
- Lesezeit (Wörter/230) an jedem Post; words wird beim Speichern gezählt
- sw v93
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:32:15 +02:00
function isNew ( i , src ) {
return ! i . read && Date . now ( ) - new Date ( i . published ) . getTime ( ) < NEW _MS && ! ( src && src . hidden ) ;
}
function readingTime ( i ) {
return i . words >= 120 ? Math . max ( 1 , Math . round ( i . words / 230 ) ) + ' Min.' : '' ;
}
function section ( iso ) {
const d = new Date ( iso ) ;
const label = dayLabel ( d ) ;
if ( label === 'Heute' || label === 'Gestern' ) return label ;
const days = ( Date . now ( ) - d . getTime ( ) ) / 86400000 ;
if ( days < 7 ) return 'Diese Woche' ;
if ( days < 31 ) return 'Diesen Monat' ;
return 'Älter' ;
}
// Posts into sections (newest first); in each, up to FAV_PER_DAY posts per
// favorite blog first, then the rest — blogs that post seldom ahead of the
// busy ones ("Seltene zuerst"), or simply by time.
function blogSections ( items , byId ) {
const order = [ 'Heute' , 'Gestern' , 'Diese Woche' , 'Diesen Monat' , 'Älter' ] ;
const groups = { } ;
items . forEach ( i => { ( groups [ section ( i . published ) ] = groups [ section ( i . published ) ] || [ ] ) . push ( i ) ; } ) ;
const newest = ( a , b ) => new Date ( b . published ) - new Date ( a . published ) ;
return order . filter ( k => groups [ k ] ) . map ( k => {
const list = groups [ k ] . slice ( ) . sort ( newest ) ;
const favCount = { } ;
const fav = [ ] ;
const rest = [ ] ;
list . forEach ( i => {
const src = byId [ i . source ] ;
if ( src && src . fav && ( favCount [ i . source ] || 0 ) < FAV _PER _DAY ) {
favCount [ i . source ] = ( favCount [ i . source ] || 0 ) + 1 ;
fav . push ( i ) ;
} else {
rest . push ( i ) ;
}
} ) ;
if ( st . rareFirst ) {
const gap = i => { const src = byId [ i . source ] ; return ( src && src . interval _days ) || 0 ; } ;
rest . sort ( ( a , b ) => ( gap ( b ) - gap ( a ) ) || newest ( a , b ) ) ;
}
return { label : k , items : fav . concat ( rest ) } ;
} ) ;
}
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
function renderBlogs ( ) {
const blogs = sources ( 'blog' ) ;
if ( ! blogs . length ) {
return '<div class="fd-empty"><p>Noch keine Blogs abonniert.</p>' +
'<button type="button" class="btn btn-primary" data-act="manage">Feed hinzufügen</button></div>' ;
}
Blog-Feed nach Kino: Abschnitte, Favoriten, Seltene zuerst, Ausblenden, NEU
- Abschnitte Heute/Gestern/Diese Woche/Diesen Monat/Älter; je Abschnitt bis zu
5 Posts je Favoriten-Blog zuerst, dann „Seltene zuerst“ (mittlerer Abstand der
letzten 10 Posts, ein Post = 30 Tage) oder chronologisch
- Blog „im Hauptfeed ausblenden“: nicht unter Alle und nicht gezählt, im Ordner da
- NEU (jünger als 24 h, ungelesen) am Post, als Zahl am Tab, Punkt am Ordner
- Chips Ungelesen/Alle, ★ Favoriten, Seltene zuerst; Schalter je Blog
- Lesezeit (Wörter/230) an jedem Post; words wird beim Speichern gezählt
- sw v93
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:32:15 +02:00
const byId = { } ;
blogs . forEach ( b => { byId [ b . id ] = b ; } ) ;
// Folders (a blog's category): chips with their unread count, a dot when something is new.
2026-10-04 20:54:29 +02:00
const folders = ( st . state . folders || [ ] ) ;
2026-10-04 21:54:04 +02:00
const inFolder = f => blogs . filter ( b => inFolderOf ( b , f ) ) ;
Blog-Feed nach Kino: Abschnitte, Favoriten, Seltene zuerst, Ausblenden, NEU
- Abschnitte Heute/Gestern/Diese Woche/Diesen Monat/Älter; je Abschnitt bis zu
5 Posts je Favoriten-Blog zuerst, dann „Seltene zuerst“ (mittlerer Abstand der
letzten 10 Posts, ein Post = 30 Tage) oder chronologisch
- Blog „im Hauptfeed ausblenden“: nicht unter Alle und nicht gezählt, im Ordner da
- NEU (jünger als 24 h, ungelesen) am Post, als Zahl am Tab, Punkt am Ordner
- Chips Ungelesen/Alle, ★ Favoriten, Seltene zuerst; Schalter je Blog
- Lesezeit (Wörter/230) an jedem Post; words wird beim Speichern gezählt
- sw v93
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:32:15 +02:00
const sum = ( list , key ) => list . reduce ( ( n , b ) => n + b [ key ] , 0 ) ;
const visible = blogs . filter ( b => ! b . hidden ) ;
2026-10-04 20:54:29 +02:00
let h = '' ;
if ( folders . length ) {
Blog-Feed nach Kino: Abschnitte, Favoriten, Seltene zuerst, Ausblenden, NEU
- Abschnitte Heute/Gestern/Diese Woche/Diesen Monat/Älter; je Abschnitt bis zu
5 Posts je Favoriten-Blog zuerst, dann „Seltene zuerst“ (mittlerer Abstand der
letzten 10 Posts, ein Post = 30 Tage) oder chronologisch
- Blog „im Hauptfeed ausblenden“: nicht unter Alle und nicht gezählt, im Ordner da
- NEU (jünger als 24 h, ungelesen) am Post, als Zahl am Tab, Punkt am Ordner
- Chips Ungelesen/Alle, ★ Favoriten, Seltene zuerst; Schalter je Blog
- Lesezeit (Wörter/230) an jedem Post; words wird beim Speichern gezählt
- sw v93
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:32:15 +02:00
const chip = ( value , label , list ) => '<button type="button" class="fd-chip' + ( st . blogFolder === value ? ' on' : '' ) +
'" data-folder="' + ( value === null ? '*' : esc ( value ) ) + '">' +
( sum ( list , 'new' ) ? '<span class="fd-dot"></span>' : '' ) + esc ( label ) +
( sum ( list , 'unread' ) ? ' <small>' + sum ( list , 'unread' ) + '</small>' : '' ) + '</button>' ;
h += '<div class="fd-chips">' + chip ( null , 'Alle' , visible ) +
folders . map ( f => chip ( f , f , inFolder ( f ) ) ) . join ( '' ) +
2026-10-16 20:56:19 +02:00
( blogs . some ( b => ! ( b . folders || [ ] ) . length ) ? chip ( '' , 'Ohne Ordner' , inFolder ( '' ) ) : '' ) +
( window . DioraFolders ? '<button type="button" class="fd-chip" data-act="folders-manage" title="Ordner verwalten" aria-label="Ordner verwalten">⋯</button>' : '' ) +
'</div>' ;
Blog-Feed nach Kino: Abschnitte, Favoriten, Seltene zuerst, Ausblenden, NEU
- Abschnitte Heute/Gestern/Diese Woche/Diesen Monat/Älter; je Abschnitt bis zu
5 Posts je Favoriten-Blog zuerst, dann „Seltene zuerst“ (mittlerer Abstand der
letzten 10 Posts, ein Post = 30 Tage) oder chronologisch
- Blog „im Hauptfeed ausblenden“: nicht unter Alle und nicht gezählt, im Ordner da
- NEU (jünger als 24 h, ungelesen) am Post, als Zahl am Tab, Punkt am Ordner
- Chips Ungelesen/Alle, ★ Favoriten, Seltene zuerst; Schalter je Blog
- Lesezeit (Wörter/230) an jedem Post; words wird beim Speichern gezählt
- sw v93
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:32:15 +02:00
}
const main = st . blogFolder === null ;
const shown = main ? blogs : inFolder ( st . blogFolder ) ;
h += '<div class="fd-chips fd-modes">' +
'<button type="button" class="fd-chip' + ( st . blogUnread ? ' on' : '' ) + '" data-act="mode-unread">Ungelesen</button>' +
'<button type="button" class="fd-chip' + ( st . blogUnread ? '' : ' on' ) + '" data-act="mode-all">Alle</button>' +
( main ? '<button type="button" class="fd-chip' + ( st . blogFav ? ' on' : '' ) + '" data-act="mode-fav">★ Favoriten</button>' : '' ) +
'<button type="button" class="fd-chip' + ( st . rareFirst ? ' on' : '' ) + '" data-act="mode-rare" title="Blogs, die selten schreiben, zuerst">Seltene zuerst</button>' +
'</div>' ;
2026-10-04 20:54:29 +02:00
h += '<div class="fd-filter">' +
'<select class="search-input fd-select" data-act="blog-source"><option value="">' +
Blog-Feed nach Kino: Abschnitte, Favoriten, Seltene zuerst, Ausblenden, NEU
- Abschnitte Heute/Gestern/Diese Woche/Diesen Monat/Älter; je Abschnitt bis zu
5 Posts je Favoriten-Blog zuerst, dann „Seltene zuerst“ (mittlerer Abstand der
letzten 10 Posts, ein Post = 30 Tage) oder chronologisch
- Blog „im Hauptfeed ausblenden“: nicht unter Alle und nicht gezählt, im Ordner da
- NEU (jünger als 24 h, ungelesen) am Post, als Zahl am Tab, Punkt am Ordner
- Chips Ungelesen/Alle, ★ Favoriten, Seltene zuerst; Schalter je Blog
- Lesezeit (Wörter/230) an jedem Post; words wird beim Speichern gezählt
- sw v93
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:32:15 +02:00
( main ? 'Alle Blogs' : 'Alle in ' + esc ( st . blogFolder || 'Ohne Ordner' ) ) + '</option>' +
2026-10-04 20:54:29 +02:00
shown . map ( b => '<option value="' + b . id + '"' + ( String ( b . id ) === String ( st . blogSource ) ? ' selected' : '' ) + '>' +
Blog-Feed nach Kino: Abschnitte, Favoriten, Seltene zuerst, Ausblenden, NEU
- Abschnitte Heute/Gestern/Diese Woche/Diesen Monat/Älter; je Abschnitt bis zu
5 Posts je Favoriten-Blog zuerst, dann „Seltene zuerst“ (mittlerer Abstand der
letzten 10 Posts, ein Post = 30 Tage) oder chronologisch
- Blog „im Hauptfeed ausblenden“: nicht unter Alle und nicht gezählt, im Ordner da
- NEU (jünger als 24 h, ungelesen) am Post, als Zahl am Tab, Punkt am Ordner
- Chips Ungelesen/Alle, ★ Favoriten, Seltene zuerst; Schalter je Blog
- Lesezeit (Wörter/230) an jedem Post; words wird beim Speichern gezählt
- sw v93
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:32:15 +02:00
( b . fav ? '★ ' : '' ) + esc ( b . title ) + ( b . unread ? ' (' + b . unread + ')' : '' ) + ( b . hidden ? ' – ausgeblendet' : '' ) +
'</option>' ) . join ( '' ) + '</select>' +
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
( st . items . some ( i => ! i . read ) ? '<button type="button" class="btn fd-allread" data-act="blogs-read">Alle gelesen</button>' : '' ) +
'</div>' ;
Blog-Feed nach Kino: Abschnitte, Favoriten, Seltene zuerst, Ausblenden, NEU
- Abschnitte Heute/Gestern/Diese Woche/Diesen Monat/Älter; je Abschnitt bis zu
5 Posts je Favoriten-Blog zuerst, dann „Seltene zuerst“ (mittlerer Abstand der
letzten 10 Posts, ein Post = 30 Tage) oder chronologisch
- Blog „im Hauptfeed ausblenden“: nicht unter Alle und nicht gezählt, im Ordner da
- NEU (jünger als 24 h, ungelesen) am Post, als Zahl am Tab, Punkt am Ordner
- Chips Ungelesen/Alle, ★ Favoriten, Seltene zuerst; Schalter je Blog
- Lesezeit (Wörter/230) an jedem Post; words wird beim Speichern gezählt
- sw v93
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:32:15 +02:00
const one = st . blogSource ? byId [ st . blogSource ] : null ;
if ( one ) {
// The selected blog's switches.
h += '<div class="fd-blogbar">' +
'<button type="button" class="fd-chip' + ( one . fav ? ' on' : '' ) + '" data-flag="fav" data-id="' + one . id + '">' + ( one . fav ? '★ Favorit' : '☆ Favorit' ) + '</button>' +
'<button type="button" class="fd-chip' + ( one . hidden ? ' on' : '' ) + '" data-flag="hidden" data-id="' + one . id + '" title="Nicht unter „Alle“, nur im Ordner">' +
( one . hidden ? 'Im Hauptfeed ausgeblendet' : 'Im Hauptfeed ausblenden' ) + '</button>' +
2026-10-04 21:40:27 +02:00
'<button type="button" class="fd-chip' + ( one . notify ? ' on' : '' ) + '" data-flag="notify" data-id="' + one . id + '" title="Benachrichtigung bei neuen Posts (App)">' +
( one . notify ? '🔔 Glocke an' : '🔕 Glocke' ) + '</button>' +
2026-10-16 20:56:19 +02:00
( window . DioraFolders ? '<button type="button" class="fd-chip" data-act="blog-folders" data-id="' + one . id + '">▤ In Ordner …</button>' : '' ) +
Blog-Feed nach Kino: Abschnitte, Favoriten, Seltene zuerst, Ausblenden, NEU
- Abschnitte Heute/Gestern/Diese Woche/Diesen Monat/Älter; je Abschnitt bis zu
5 Posts je Favoriten-Blog zuerst, dann „Seltene zuerst“ (mittlerer Abstand der
letzten 10 Posts, ein Post = 30 Tage) oder chronologisch
- Blog „im Hauptfeed ausblenden“: nicht unter Alle und nicht gezählt, im Ordner da
- NEU (jünger als 24 h, ungelesen) am Post, als Zahl am Tab, Punkt am Ordner
- Chips Ungelesen/Alle, ★ Favoriten, Seltene zuerst; Schalter je Blog
- Lesezeit (Wörter/230) an jedem Post; words wird beim Speichern gezählt
- sw v93
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:32:15 +02:00
'<button type="button" class="fd-chip" data-edit-open="' + one . id + '">⚙</button></div>' ;
}
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
if ( ! st . items . length ) return h + '<p class="fd-note">' + ( st . loading ? 'Lädt …' : ( st . blogUnread ? 'Alles gelesen.' : 'Keine Artikel.' ) ) + '</p>' ;
2026-10-04 21:54:04 +02:00
const reading = st . items . filter ( i => ! i . read && i . progress >= 0.05 && i . progress < 0.9 )
. sort ( ( a , b ) => b . progress - a . progress ) . slice ( 0 , 3 ) ;
if ( reading . length ) {
h += '<h3 class="fd-day">Weiterlesen</h3>' + reading . map ( i =>
'<button type="button" class="fd-post fd-continue" data-item="' + i . id + '">' +
'<span class="fd-card-title">' + esc ( i . title ) + '</span>' +
'<span class="fd-row-meta">' + esc ( i . source _title ) + ' · ' + Math . round ( i . progress * 100 ) + ' % gelesen</span>' +
'<span class="fd-prog"><span style="width:' + Math . round ( i . progress * 100 ) + '%"></span></span></button>' ) . join ( '' ) ;
}
Blog-Feed nach Kino: Abschnitte, Favoriten, Seltene zuerst, Ausblenden, NEU
- Abschnitte Heute/Gestern/Diese Woche/Diesen Monat/Älter; je Abschnitt bis zu
5 Posts je Favoriten-Blog zuerst, dann „Seltene zuerst“ (mittlerer Abstand der
letzten 10 Posts, ein Post = 30 Tage) oder chronologisch
- Blog „im Hauptfeed ausblenden“: nicht unter Alle und nicht gezählt, im Ordner da
- NEU (jünger als 24 h, ungelesen) am Post, als Zahl am Tab, Punkt am Ordner
- Chips Ungelesen/Alle, ★ Favoriten, Seltene zuerst; Schalter je Blog
- Lesezeit (Wörter/230) an jedem Post; words wird beim Speichern gezählt
- sw v93
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:32:15 +02:00
blogSections ( st . items , byId ) . forEach ( sec => {
h += '<h3 class="fd-day">' + esc ( sec . label ) + '</h3>' ;
h += sec . items . map ( i => {
const src = byId [ i . source ] ;
const rt = readingTime ( i ) ;
return '<button type="button" class="fd-post' + ( i . read ? ' fd-read' : '' ) + ( src && src . hidden ? ' fd-hidden' : '' ) + '" data-item="' + i . id + '">' +
'<span class="fd-card-title">' + ( isNew ( i , src ) ? '<span class="fd-new">NEU</span> ' : '' ) + ( i . starred ? '★ ' : '' ) + esc ( i . title ) + '</span>' +
'<span class="fd-row-meta">' + ( src && src . fav ? '★ ' : '' ) + esc ( i . source _title ) + ' · ' + esc ( when ( i . published ) ) + ( rt ? ' · ' + rt : '' ) + '</span>' +
( i . summary ? '<span class="fd-card-sum">' + esc ( i . summary ) + '</span>' : '' ) +
2026-10-04 21:54:04 +02:00
( ! i . read && i . progress > 0.02 ? '<span class="fd-prog"><span style="width:' + Math . round ( i . progress * 100 ) + '%"></span></span>' : '' ) +
Blog-Feed nach Kino: Abschnitte, Favoriten, Seltene zuerst, Ausblenden, NEU
- Abschnitte Heute/Gestern/Diese Woche/Diesen Monat/Älter; je Abschnitt bis zu
5 Posts je Favoriten-Blog zuerst, dann „Seltene zuerst“ (mittlerer Abstand der
letzten 10 Posts, ein Post = 30 Tage) oder chronologisch
- Blog „im Hauptfeed ausblenden“: nicht unter Alle und nicht gezählt, im Ordner da
- NEU (jünger als 24 h, ungelesen) am Post, als Zahl am Tab, Punkt am Ordner
- Chips Ungelesen/Alle, ★ Favoriten, Seltene zuerst; Schalter je Blog
- Lesezeit (Wörter/230) an jedem Post; words wird beim Speichern gezählt
- sw v93
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:32:15 +02:00
'</button>' ;
} ) . join ( '' ) ;
} ) ;
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
if ( st . itemsMore ) h += '<button type="button" class="btn fd-loadmore" data-act="more">Ältere laden</button>' ;
return h ;
}
function kindSelect ( name , value ) {
return '<select class="search-input" name="' + name + '">' +
'<option value="news"' + ( value === 'news' ? ' selected' : '' ) + '>Nachrichten</option>' +
'<option value="blog"' + ( value === 'blog' ? ' selected' : '' ) + '>Blog</option></select>' ;
}
function categoryInput ( value ) {
2026-10-04 20:54:29 +02:00
const cats = ( ( st . state && st . state . categories ) || [ ] ) . concat ( ( st . state && st . state . folders ) || [ ] ) ;
return '<input class="search-input" name="category" list="fd-cats" placeholder="z. B. Politik / Philosophie" value="' + esc ( value || '' ) + '">' +
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
'<datalist id="fd-cats">' + cats . map ( c => '<option value="' + esc ( c ) + '">' ) . join ( '' ) + '</datalist>' ;
}
function renderManage ( ) {
const s = st . state || { sources : [ ] , settings : { } } ;
let h = '<section class="fd-box"><h3>Feed hinzufügen</h3>' +
'<form class="fd-add" data-form="discover"><input class="search-input" name="url" type="url" inputmode="url" ' +
'placeholder="Adresse einer Seite oder eines Feeds" required>' +
'<button class="btn btn-primary" type="submit">Suchen</button></form>' ;
if ( st . found ) {
h += st . found . map ( ( f , n ) =>
'<form class="fd-found" data-form="subscribe" data-n="' + n + '">' +
'<div class="fd-found-title">' + esc ( f . title ) + '</div><div class="fd-note fd-url">' + esc ( f . url ) + '</div>' +
( f . samples . length ? '<ul class="fd-samples">' + f . samples . slice ( 0 , 3 ) . map ( t => '<li>' + esc ( t ) + '</li>' ) . join ( '' ) + '</ul>' : '' ) +
( f . subscribed ? '<p class="fd-note">Schon abonniert.</p>' :
'<div class="fd-grid"><label>Art ' + kindSelect ( 'kind' , f . suggested _kind ) + '</label>' +
2026-10-04 20:54:29 +02:00
'<label class="fd-cat-field">' + ( f . suggested _kind === 'news' ? 'Ressort' : 'Ordner' ) + ' ' + categoryInput ( '' ) + '</label></div>' +
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
'<button class="btn btn-primary" type="submit">Abonnieren</button>' ) +
'</form>' ) . join ( '' ) ;
}
h += '</section>' ;
const news = s . sources . filter ( x => x . kind === 'news' ) ;
const blogs = s . sources . filter ( x => x . kind === 'blog' ) ;
const group = ( title , list ) => ! list . length ? '' :
'<section class="fd-box"><h3>' + title + ' <small>' + list . length + '</small></h3>' +
list . map ( x => x . id === st . editing ? editForm ( x ) :
'<button type="button" class="fd-src" data-edit="' + x . id + '">' +
2026-10-04 21:40:27 +02:00
'<span class="fd-src-title">' + ( x . fav ? '★ ' : '' ) + ( x . notify ? '🔔 ' : '' ) + esc ( x . title ) + ( x . hidden ? ' <small class="fd-note">ausgeblendet</small>' : '' ) + '</span>' +
2026-10-04 21:54:04 +02:00
'<span class="fd-row-meta">' + ( x . kind === 'news' ? esc ( x . category || 'Allgemein' ) + ' · ' : ( ( x . folders || [ ] ) . length ? esc ( x . folders . join ( ', ' ) ) + ' · ' : '' ) ) +
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
( x . unread ? x . unread + ' ungelesen' : '' ) +
( x . error _count >= 3 ? ' <span class="fd-warn" title="' + esc ( x . last _error ) + '">⚠ Abruf schlägt fehl</span>' : '' ) +
'</span></button>' ) . join ( '' ) + '</section>' ;
h += group ( 'Nachrichten' , news . sort ( ( a , b ) => ( a . category || '' ) . localeCompare ( b . category || '' ) || a . title . localeCompare ( b . title ) ) ) ;
2026-10-04 21:54:04 +02:00
const firstFolder = b => ( ( b . folders || [ ] ) [ 0 ] || '~' ) . toLowerCase ( ) ;
h += group ( 'Blogs' , blogs . sort ( ( a , b ) => firstFolder ( a ) . localeCompare ( firstFolder ( b ) ) || a . title . localeCompare ( b . title ) ) ) ;
// The folders themselves: rename, dissolve (the blogs stay).
const folders = ( s . folders || [ ] ) ;
if ( folders . length ) {
h += '<section class="fd-box"><h3>Ordner <small>' + folders . length + '</small></h3>' + folders . map ( f =>
'<div class="fd-folder-row"><span>' + esc ( f ) + ' <small class="fd-note">' + blogs . filter ( b => inFolderOf ( b , f ) ) . length + ' Blogs</small></span>' +
'<span class="fd-buttons"><button type="button" class="btn" data-act="folder-rename" data-name="' + esc ( f ) + '">Umbenennen</button>' +
'<button type="button" class="btn" data-act="folder-dissolve" data-name="' + esc ( f ) + '">Auflösen</button></span></div>' ) . join ( '' ) +
'<p class="fd-note">Tipp: lange auf einen Ordner in der Blog-Ansicht drücken (oder Rechtsklick) öffnet dasselbe.</p></section>' ;
}
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
h += '<section class="fd-box"><h3>Import & Einstellungen</h3>' +
'<label class="btn fd-file">OPML importieren …<input type="file" accept=".opml,.xml,text/xml,text/x-opml" data-act="opml" hidden></label> ' +
( window . DioraNative ? '' : '<a class="btn" href="/feeds/export.opml">OPML exportieren</a>' ) ;
if ( st . opml ) h += renderOpml ( ) ;
const keep = ( s . settings && s . settings . news _keep _days ) || 2 ;
h += '<label class="fd-keep">Nachrichten behalten: <select class="search-input" data-act="keep">' +
[ 1 , 2 , 3 , 5 , 7 , 14 ] . map ( d => '<option value="' + d + '"' + ( d === keep ? ' selected' : '' ) + '>' + d + ( d === 1 ? ' Tag' : ' Tage' ) + '</option>' ) . join ( '' ) +
'</select></label><p class="fd-note">Ältere Nachrichten verschwinden von selbst (gemerkte bleiben). Blogartikel bleiben, bis sie gelesen sind.</p></section>' ;
return h ;
}
function editForm ( x ) {
return '<form class="fd-edit" data-form="edit" data-id="' + x . id + '">' +
'<label>Name <input class="search-input" name="title" value="' + esc ( x . title ) + '"></label>' +
'<div class="fd-note fd-url">' + esc ( x . url ) + '</div>' +
( x . last _error ? '<p class="fd-warn">Letzter Fehler: ' + esc ( x . last _error ) + '</p>' : '' ) +
'<div class="fd-grid"><label>Art ' + kindSelect ( 'kind' , x . kind ) + '</label>' +
2026-10-04 21:54:04 +02:00
( x . kind === 'news' ? '<label class="fd-cat-field">Ressort ' + categoryInput ( x . category ) + '</label>' : folderPicker ( x ) ) +
Blog-Feed nach Kino: Abschnitte, Favoriten, Seltene zuerst, Ausblenden, NEU
- Abschnitte Heute/Gestern/Diese Woche/Diesen Monat/Älter; je Abschnitt bis zu
5 Posts je Favoriten-Blog zuerst, dann „Seltene zuerst“ (mittlerer Abstand der
letzten 10 Posts, ein Post = 30 Tage) oder chronologisch
- Blog „im Hauptfeed ausblenden“: nicht unter Alle und nicht gezählt, im Ordner da
- NEU (jünger als 24 h, ungelesen) am Post, als Zahl am Tab, Punkt am Ordner
- Chips Ungelesen/Alle, ★ Favoriten, Seltene zuerst; Schalter je Blog
- Lesezeit (Wörter/230) an jedem Post; words wird beim Speichern gezählt
- sw v93
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:32:15 +02:00
( x . kind === 'blog' ? '<label class="fd-check"><input type="checkbox" name="fav"' + ( x . fav ? ' checked' : '' ) + '> Favorit</label>' +
2026-10-04 21:40:27 +02:00
'<label class="fd-check"><input type="checkbox" name="hidden"' + ( x . hidden ? ' checked' : '' ) + '> Im Hauptfeed ausblenden (nur im Ordner)</label>' +
'<label class="fd-check"><input type="checkbox" name="notify"' + ( x . notify ? ' checked' : '' ) + '> Benachrichtigen bei neuen Posts</label>' : '' ) +
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
'<label>Ganzer Artikel <select class="search-input" name="fulltext">' +
[ [ 'auto' , 'Automatisch' ] , [ 'always' , 'Immer holen' ] , [ 'never' , 'Nur Feed-Text' ] ] . map ( o =>
'<option value="' + o [ 0 ] + '"' + ( x . fulltext === o [ 0 ] ? ' selected' : '' ) + '>' + o [ 1 ] + '</option>' ) . join ( '' ) +
'</select></label></div>' +
'<div class="fd-buttons"><button class="btn btn-primary" type="submit">Speichern</button>' +
'<button class="btn" type="button" data-act="cancel-edit">Abbrechen</button>' +
2026-10-04 21:24:59 +02:00
'<button class="btn btn-danger" type="button" data-act="delete" data-id="' + x . id + '">Abbestellen</button></div></form>' +
loginForm ( x ) ;
}
2026-10-04 21:54:04 +02:00
function folderPicker ( x ) {
const all = ( st . state && st . state . folders ) || [ ] ;
return '<fieldset class="fd-folders"><legend>Ordner</legend>' +
all . map ( f => '<label class="fd-check"><input type="checkbox" name="folder" value="' + esc ( f ) + '"' +
( inFolderOf ( x , f ) ? ' checked' : '' ) + '> ' + esc ( f ) + '</label>' ) . join ( '' ) +
'<input class="search-input" name="newfolder" maxlength="40" placeholder="Neuer Ordner"></fieldset>' ;
}
2026-10-04 21:24:59 +02:00
// A paywalled site (junge Welt …): the subscriber login, kept encrypted on the
// server, which then fetches whole articles. The password never comes back.
function loginForm ( x ) {
const l = x . login ;
if ( ! l ) return '' ;
let h = '<form class="fd-edit fd-login" data-form="login" data-id="' + x . id + '">' +
'<h4>Abo-Anmeldung bei ' + esc ( l . site ) + '</h4>' ;
if ( l . username ) {
h += '<p>' + ( l . ok ? '✓ Angemeldet als ' : '⚠ Anmeldung klappt nicht: ' ) + esc ( l . ok ? l . username : ( l . error || l . username ) ) + '</p>' +
'<div class="fd-buttons"><button class="btn" type="button" data-act="login-delete" data-id="' + x . id + '">Zugangsdaten löschen</button></div>' ;
if ( l . ok ) return h + '</form>' ;
h += '<p class="fd-note">Neu eingeben:</p>' ;
}
return h +
'<label>Benutzername <input class="search-input" name="username" autocomplete="off" autocapitalize="none" required></label>' +
'<label>Passwort <input class="search-input" name="password" type="password" autocomplete="new-password" required></label>' +
'<p class="fd-note">Das Passwort liegt verschlüsselt auf dem diora-Server und wird nie wieder angezeigt. ' +
'Damit holt der Server die ganzen Artikel statt nur des freien Teils.</p>' +
'<div class="fd-buttons"><button class="btn btn-primary" type="submit">Anmelden</button></div></form>' ;
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
}
function renderOpml ( ) {
const groups = st . opml ;
const n = groups . reduce ( ( a , g ) => a + ( g . kind === 'skip' ? 0 : g . feeds . length ) , 0 ) ;
return '<div class="fd-opml"><p class="fd-note">Was ist was? Pro Ordner:</p>' +
groups . map ( ( g , i ) =>
'<div class="fd-opml-row"><span>' + esc ( g . name || 'Ohne Ordner' ) + ' <small>(' + g . feeds . length + ')</small></span>' +
'<select class="search-input" data-opml="' + i + '">' +
[ [ 'news' , 'Nachrichten' ] , [ 'blog' , 'Blogs' ] , [ 'skip' , 'Nicht übernehmen' ] ] . map ( o =>
'<option value="' + o [ 0 ] + '"' + ( g . kind === o [ 0 ] ? ' selected' : '' ) + '>' + o [ 1 ] + '</option>' ) . join ( '' ) +
'</select></div>' ) . join ( '' ) +
'<button type="button" class="btn btn-primary" data-act="opml-import"' + ( n ? '' : ' disabled' ) + '>' + n + ' Feeds übernehmen</button> ' +
'<button type="button" class="btn" data-act="opml-cancel">Abbrechen</button></div>' ;
}
// ---- OPML
function parseOpml ( text ) {
const doc = new DOMParser ( ) . parseFromString ( text , 'text/xml' ) ;
const groups = { } ;
const order = [ ] ;
Array . prototype . forEach . call ( doc . querySelectorAll ( 'outline[xmlUrl], outline[xmlurl]' ) , o => {
const url = o . getAttribute ( 'xmlUrl' ) || o . getAttribute ( 'xmlurl' ) ;
const parent = o . parentNode && o . parentNode . nodeName . toLowerCase ( ) === 'outline' ? o . parentNode : null ;
const name = parent ? ( parent . getAttribute ( 'text' ) || parent . getAttribute ( 'title' ) || '' ) : '' ;
if ( ! groups [ name ] ) {
groups [ name ] = { name , feeds : [ ] ,
kind : /nachricht|news|politik|zeitung|presse|aktuell|welt|wirtschaft|tech/i . test ( name ) ? 'news' : 'blog' } ;
order . push ( name ) ;
}
groups [ name ] . feeds . push ( { url , title : o . getAttribute ( 'title' ) || o . getAttribute ( 'text' ) || url } ) ;
} ) ;
return order . map ( n => groups [ n ] ) ;
}
// ---- article view
let reader = null ; // the overlay element
let readerList = [ ] ; // what "next" walks through (blog items)
let readerStory = null ;
let readerItem = null ;
function fontSize ( ) {
let v = 18 ;
try { v = parseInt ( localStorage . getItem ( 'diora_feeds_font' ) || '18' , 10 ) || 18 ; } catch ( e ) { }
return Math . max ( 14 , Math . min ( 26 , v ) ) ;
}
function ensureReader ( ) {
if ( reader ) return reader ;
reader = document . createElement ( 'div' ) ;
reader . id = 'fd-article' ;
reader . hidden = true ;
reader . addEventListener ( 'click' , onReaderClick ) ;
2026-10-04 21:54:04 +02:00
reader . addEventListener ( 'scroll' , ( ) => {
clearTimeout ( progressTimer ) ;
progressTimer = setTimeout ( saveProgress , 1200 ) ;
} , { passive : true } ) ;
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
document . body . appendChild ( reader ) ;
return reader ;
}
function readerOpen ( ) {
return ! ! ( reader && ! reader . hidden ) ;
}
function closeReader ( ) {
if ( ! reader ) return ;
2026-10-04 21:54:04 +02:00
saveProgress ( ) ;
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
reader . hidden = true ;
reader . innerHTML = '' ;
document . documentElement . classList . remove ( 'fd-reading' ) ;
readerItem = readerStory = null ;
render ( ) ;
}
2026-10-04 21:54:04 +02:00
// How far the open article is read (0-1); saved now and then and when leaving it.
let progressSent = 0 ;
let progressTimer = null ;
function readProgress ( ) {
if ( ! reader || reader . hidden ) return 0 ;
const room = reader . scrollHeight - reader . clientHeight ;
return room <= 40 ? 1 : Math . min ( 1 , reader . scrollTop / room ) ;
}
function saveProgress ( ) {
const i = readerItem ;
if ( ! i ) return ;
const p = Math . round ( readProgress ( ) * 100 ) / 100 ;
if ( Math . abs ( p - progressSent ) < 0.02 && ! ( p >= 0.9 && ! i . read ) ) return ;
progressSent = p ;
i . progress = p ;
st . items . forEach ( x => { if ( x . id === i . id ) x . progress = p ; } ) ;
if ( p >= 0.9 && ! i . read ) {
i . read = true ;
markLocal ( [ i . id ] , true ) ;
}
api ( '/feeds/items/' + i . id + '/progress/' , { progress : p } ) . catch ( ( ) => { } ) ;
}
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
async function openItem ( id , story ) {
ensureReader ( ) ;
2026-10-04 21:54:04 +02:00
if ( readerItem ) saveProgress ( ) ;
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
readerStory = story || null ;
reader . hidden = false ;
document . documentElement . classList . add ( 'fd-reading' ) ;
reader . innerHTML = readerShell ( '<p class="fd-note">Lädt …</p>' ) ;
reader . scrollTop = 0 ;
try {
const data = await api ( '/feeds/items/' + id + '/' ) ;
readerItem = data . item ;
reader . innerHTML = readerShell ( articleHtml ( readerItem ) ) ;
reader . scrollTop = 0 ;
2026-10-19 17:07:51 +02:00
if ( readerStory ) loadOverview ( readerStory ) ;
2026-10-04 21:54:04 +02:00
progressSent = readerItem . progress || 0 ;
if ( readerItem . kind === 'news' ) {
// News is read once opened; a blog post once read to 90 % (the server keeps where).
if ( ! readerItem . read ) {
readerItem . read = true ;
markLocal ( [ id ] , true ) ;
api ( '/feeds/items/read/' , { ids : [ id ] , read : true } ) . catch ( ( ) => { } ) ;
}
} else {
if ( progressSent > 0.02 && progressSent < 0.9 ) {
// Back where reading stopped (on any device).
requestAnimationFrame ( ( ) => { reader . scrollTop = progressSent * ( reader . scrollHeight - reader . clientHeight ) ; } ) ;
} else {
setTimeout ( saveProgress , 1500 ) ; // short enough to fit: read at once
}
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
}
} catch ( e ) {
reader . innerHTML = readerShell ( '<p class="fd-error">' + esc ( e . message === 'Failed to fetch' ? 'Keine Verbindung.' : e . message ) + '</p>' ) ;
}
}
function readerShell ( inner ) {
const i = readerItem ;
return '<div class="fd-art-bar">' +
'<button type="button" class="fd-icon" data-r="close" aria-label="Zurück" title="Zurück">‹ </button>' +
'<span class="fd-art-src">' + esc ( i ? i . source _title : '' ) + '</span>' +
'<button type="button" class="fd-icon" data-r="font" aria-label="Schriftgröße" title="Schriftgröße">Aa</button>' +
( i ? '<button type="button" class="fd-icon' + ( i . starred ? ' on' : '' ) + '" data-r="star" aria-label="Merken" title="Merken">' + ( i . starred ? '★' : '☆' ) + '</button>' : '' ) +
2026-10-19 20:14:17 +02:00
// Projekte (projects.js): this article as a source of a project.
( i && window . DioraProjects ? '<button type="button" class="fd-icon" data-r="project" aria-label="Mit Projekt verknüpfen" title="Mit Projekt verknüpfen">◇</button>' : '' ) +
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
( i && i . url ? '<a class="fd-icon" href="' + esc ( i . url ) + '" target="_blank" rel="noopener" aria-label="Im Browser öffnen" title="Im Browser öffnen">↗</a>' : '' ) +
'</div><div class="fd-art" style="font-size:' + fontSize ( ) + 'px">' + inner + '</div>' ;
}
function articleHtml ( i ) {
let h = '' ;
const story = readerStory ;
if ( story && story . items . length > 1 ) {
h += '<div class="fd-sources"><span class="fd-note">' + story . items . length + ' Berichte:</span>' +
story . items . map ( x => '<button type="button" class="fd-chip' + ( x . id === i . id ? ' on' : '' ) + '" data-r="item" data-id="' + x . id + '">' +
esc ( x . source _title ) + '</button>' ) . join ( '' ) + '</div>' ;
}
h += '<h1 class="fd-art-title">' + esc ( i . title ) + '</h1>' +
'<p class="fd-art-meta">' + [ i . author , i . source _title , when ( i . published ) ] . filter ( Boolean ) . map ( esc ) . join ( ' · ' ) + '</p>' ;
2026-10-19 17:07:51 +02:00
if ( story ) h += '<div class="fd-ov">' + ( story . overview ? overviewHtml ( story ) : '' ) + '</div>' ;
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
const content = i . content || '' ;
if ( i . image _url && content . indexOf ( i . image _url ) < 0 ) h += '<img class="fd-art-img" alt="" src="' + esc ( i . image _url ) + '">' ;
h += '<div class="fd-art-body">' + ( content || '<p>' + esc ( i . summary ) + '</p>' ) + '</div>' ;
2026-10-04 21:24:59 +02:00
if ( i . paywalled ) {
h += '<p class="fd-note">Nur der freie Teil des Artikels. Mit deiner Abo-Anmeldung (⚙ → ' + esc ( i . source _title ) +
') holt diora den ganzen.</p>' ;
} else if ( ! i . content _full && i . url && ( content . replace ( /<[^>]+>/g , '' ) . length < 700 ) ) {
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
h += '<p class="fd-note">Der Feed bringt nur einen Anriss, der ganze Artikel ließ sich nicht laden.</p>' ;
}
if ( story && story . items . length > 1 ) {
h += '<h3 class="fd-art-more">Weitere Berichte</h3>' + story . items . filter ( x => x . id !== i . id ) . map ( x =>
'<button type="button" class="fd-other" data-r="item" data-id="' + x . id + '">' +
'<span class="fd-other-title">' + esc ( x . title ) + '</span>' +
'<span class="fd-row-meta">' + esc ( x . source _title ) + ' · ' + esc ( when ( x . published ) ) + '</span></button>' ) . join ( '' ) ;
}
h += '<div class="fd-art-foot">' ;
if ( i . url ) h += '<a class="btn" href="' + esc ( i . url ) + '" target="_blank" rel="noopener">Original öffnen ↗</a>' ;
const next = nextItem ( i ) ;
if ( next ) h += '<button type="button" class="btn btn-primary" data-r="item" data-id="' + next . id + '">Nächster Artikel › </button>' ;
h += '</div>' ;
return h ;
}
2026-10-19 17:07:51 +02:00
// What the story's reports say together (GET /feeds/stories/<id>/overview/, feeds/textproc.py):
// a summary from all of them, what only one says, each source's words, the same story in the
// other language.
function overviewHtml ( story ) {
const o = story . overview || { } ;
let h = '' ;
if ( o . summary && o . summary . length ) {
h += '<details class="fd-ov-box" open><summary>Überblick aus ' + story . sources + ' Quellen</summary>' +
o . summary . map ( x => '<p>' + esc ( x . text ) + ' <span class="fd-ov-src">' + esc ( x . source ) + '</span></p>' ) . join ( '' ) ;
const novel = Object . keys ( o . novel || { } ) ;
if ( novel . length ) {
h += '<h4>Nur in einem Bericht</h4>' + novel . map ( src => o . novel [ src ] . map ( t =>
'<p>' + esc ( t ) + ' <span class="fd-ov-src">' + esc ( src ) + '</span></p>' ) . join ( '' ) ) . join ( '' ) ;
}
const words = Object . keys ( o . words || { } ) ;
if ( words . length ) {
h += '<h4>Wortwahl</h4>' + words . map ( src => '<p class="fd-ov-words"><b>' + esc ( src ) + ':</b> ' +
o . words [ src ] . map ( esc ) . join ( ', ' ) + '</p>' ) . join ( '' ) ;
}
h += '</details>' ;
}
if ( o . related && o . related . length ) {
h += '<div class="fd-ov-rel"><h4>In der anderen Sprache</h4>' + o . related . map ( x => {
const lead = x . lead || { } ;
return '<button type="button" class="fd-other" data-r="story" data-id="' + x . id + '">' +
'<span class="fd-other-title">' + esc ( lead . title ) + '</span>' +
'<span class="fd-row-meta">' + esc ( lead . source _title ) + ' · ' + esc ( when ( x . last _at ) ) + '</span></button>' ;
} ) . join ( '' ) + '</div>' ;
}
return h ;
}
async function loadOverview ( story ) {
if ( story . overview ) return ;
try {
story . overview = await api ( '/feeds/stories/' + story . id + '/overview/' ) ;
} catch ( e ) {
return ; // offline and never fetched: the article alone
}
const box = reader && ! reader . hidden && readerStory === story && reader . querySelector ( '.fd-ov' ) ;
if ( box ) box . innerHTML = overviewHtml ( story ) ;
}
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
function nextItem ( i ) {
if ( readerStory ) return null ;
const list = readerList ;
const at = list . findIndex ( x => x . id === i . id ) ;
for ( let n = at + 1 ; n < list . length ; n ++ ) if ( ! list [ n ] . read ) return list [ n ] ;
return null ;
}
function markLocal ( ids , read ) {
st . items . forEach ( i => { if ( ids . indexOf ( i . id ) >= 0 ) i . read = read ; } ) ;
st . stories . forEach ( s => {
s . items . forEach ( i => { if ( ids . indexOf ( i . id ) >= 0 ) i . read = read ; } ) ;
s . unread = s . items . filter ( i => ! i . read ) . length ;
} ) ;
}
function onReaderClick ( e ) {
const b = e . target . closest ( '[data-r]' ) ;
if ( ! b ) {
// Links in the article open outside (the app hands them to the browser).
const a = e . target . closest ( '.fd-art-body a[href]' ) ;
if ( a ) { a . target = '_blank' ; a . rel = 'noopener' ; }
return ;
}
const r = b . dataset . r ;
if ( r === 'close' ) closeReader ( ) ;
else if ( r === 'item' ) openItem ( parseInt ( b . dataset . id , 10 ) , readerStory ) ;
2026-10-19 17:07:51 +02:00
else if ( r === 'story' ) {
const other = readerStory && readerStory . overview && ( readerStory . overview . related || [ ] ) . find ( x => x . id === parseInt ( b . dataset . id , 10 ) ) ;
if ( other ) openItem ( ( other . lead || other . items [ 0 ] ) . id , other ) ;
}
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
else if ( r === 'font' ) {
let v = fontSize ( ) + 2 ;
if ( v > 26 ) v = 14 ;
try { localStorage . setItem ( 'diora_feeds_font' , String ( v ) ) ; } catch ( err ) { }
const art = reader . querySelector ( '.fd-art' ) ;
if ( art ) art . style . fontSize = v + 'px' ;
2026-10-19 20:14:17 +02:00
} else if ( r === 'project' && readerItem && window . DioraProjects ) {
window . DioraProjects . link ( 'article' , readerItem . id , { title : readerItem . title , source : readerItem . source _title ,
author : readerItem . author , date : readerItem . published , url : readerItem . url , news : readerItem . kind === 'news' } ) ;
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
} else if ( r === 'star' && readerItem ) {
readerItem . starred = ! readerItem . starred ;
api ( '/feeds/items/' + readerItem . id + '/star/' , { starred : readerItem . starred } ) . catch ( ( ) => { } ) ;
b . textContent = readerItem . starred ? '★' : '☆' ;
b . classList . toggle ( 'on' , readerItem . starred ) ;
st . items . forEach ( i => { if ( i . id === readerItem . id ) i . starred = readerItem . starred ; } ) ;
}
}
async function openStory ( id ) {
let story = st . stories . find ( x => x . id === id ) ;
if ( ! story && st . briefing ) {
const all = [ ] . concat ( st . briefing . top , ... st . briefing . sections . map ( s => s . stories ) ) ;
story = all . find ( x => x . id === id ) ;
}
if ( ! story ) return ;
readerList = [ ] ;
const lead = story . lead || story . items [ 0 ] ;
// Opening a story reads it: all its reports count as read.
const unread = story . items . filter ( i => ! i . read ) . map ( i => i . id ) ;
if ( unread . length ) {
markLocal ( unread , true ) ;
story . unread = 0 ;
api ( '/feeds/items/read/' , { ids : unread , read : true } ) . catch ( ( ) => { } ) ;
}
openItem ( lead . id , story ) ;
}
2026-10-04 21:54:04 +02:00
// ---- folders: rename, dissolve
async function folderAction ( act , name ) {
if ( act === 'folder-rename' ) {
const neu = typeof customPrompt === 'function' ? await customPrompt ( 'Ordner „' + name + '“ umbenennen in:' , name ) : window . prompt ( 'Neuer Name' , name ) ;
if ( ! neu || ! neu . trim ( ) || neu . trim ( ) === name ) return ;
await api ( '/feeds/folders/rename/' , { name , new : neu . trim ( ) } ) . catch ( ( ) => { } ) ;
if ( st . blogFolder !== null && st . blogFolder . toLowerCase ( ) === name . toLowerCase ( ) ) st . blogFolder = neu . trim ( ) . slice ( 0 , 40 ) ;
} else {
const ok = typeof customConfirm === 'function'
? await customConfirm ( 'Ordner „' + name + '“ auflösen? Die Blogs bleiben abonniert.' )
: window . confirm ( 'Auflösen?' ) ;
if ( ! ok ) return ;
await api ( '/feeds/folders/dissolve/' , { name } ) . catch ( ( ) => { } ) ;
if ( st . blogFolder !== null && st . blogFolder . toLowerCase ( ) === name . toLowerCase ( ) ) st . blogFolder = null ;
}
await loadState ( ) . catch ( ( ) => { } ) ;
if ( st . view === 'blogs' ) return load ( ) ;
render ( ) ;
}
tab . addEventListener ( 'contextmenu' , e => {
const chip = e . target . closest ( '[data-folder]' ) ;
if ( ! chip || chip . dataset . folder === '*' || chip . dataset . folder === '' ) return ;
e . preventDefault ( ) ;
folderSheet ( chip . dataset . folder ) ;
} ) ;
// The long-press menu of a folder: open, rename, dissolve.
function folderSheet ( name ) {
const old = document . getElementById ( 'fd-sheet' ) ;
if ( old ) old . remove ( ) ;
const sheet = document . createElement ( 'div' ) ;
sheet . id = 'fd-sheet' ;
sheet . innerHTML = '<div class="fd-sheet-box"><div class="fd-sheet-title">' + esc ( name ) + '</div>' +
'<button type="button" data-s="open">Ordner öffnen</button>' +
'<button type="button" data-s="rename">Umbenennen …</button>' +
'<button type="button" data-s="dissolve">Auflösen</button>' +
'<button type="button" data-s="cancel">Abbrechen</button></div>' ;
sheet . addEventListener ( 'click' , e => {
const b = e . target . closest ( '[data-s]' ) ;
if ( ! b && e . target !== sheet ) return ;
sheet . remove ( ) ;
const what = b ? b . dataset . s : 'cancel' ;
if ( what === 'open' ) { st . blogFolder = name ; st . blogSource = '' ; load ( ) ; }
if ( what === 'rename' ) folderAction ( 'folder-rename' , name ) ;
if ( what === 'dissolve' ) folderAction ( 'folder-dissolve' , name ) ;
} ) ;
document . body . appendChild ( sheet ) ;
}
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
// ---- events
tab . addEventListener ( 'click' , async e => {
2026-10-19 17:07:51 +02:00
const t = e . target . closest ( '[data-view],[data-act],[data-story],[data-item],[data-cat],[data-topic],[data-edit],[data-folder],[data-flag],[data-edit-open]' ) ;
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
if ( ! t || t . tagName === 'SELECT' || ( t . tagName === 'INPUT' && t . type !== 'button' ) ) return ;
if ( t . dataset . view ) {
st . view = t . dataset . view ;
try { localStorage . setItem ( 'diora_feeds_view' , st . view ) ; } catch ( err ) { }
load ( ) ;
return ;
}
Blog-Feed nach Kino: Abschnitte, Favoriten, Seltene zuerst, Ausblenden, NEU
- Abschnitte Heute/Gestern/Diese Woche/Diesen Monat/Älter; je Abschnitt bis zu
5 Posts je Favoriten-Blog zuerst, dann „Seltene zuerst“ (mittlerer Abstand der
letzten 10 Posts, ein Post = 30 Tage) oder chronologisch
- Blog „im Hauptfeed ausblenden“: nicht unter Alle und nicht gezählt, im Ordner da
- NEU (jünger als 24 h, ungelesen) am Post, als Zahl am Tab, Punkt am Ordner
- Chips Ungelesen/Alle, ★ Favoriten, Seltene zuerst; Schalter je Blog
- Lesezeit (Wörter/230) an jedem Post; words wird beim Speichern gezählt
- sw v93
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:32:15 +02:00
if ( t . dataset . flag ) {
const src = sources ( ) . find ( x => x . id === parseInt ( t . dataset . id , 10 ) ) ;
if ( ! src ) return ;
src [ t . dataset . flag ] = ! src [ t . dataset . flag ] ;
render ( ) ;
await api ( '/feeds/sources/' + src . id + '/' , { [ t . dataset . flag ] : src [ t . dataset . flag ] } ) . catch ( ( ) => { } ) ;
return load ( ) ;
}
if ( t . dataset . editOpen ) {
st . view = 'manage' ;
st . editing = parseInt ( t . dataset . editOpen , 10 ) ;
window . scrollTo ( 0 , 0 ) ;
return render ( ) ;
}
2026-10-04 20:54:29 +02:00
if ( t . dataset . folder !== undefined ) {
st . blogFolder = t . dataset . folder === '*' ? null : t . dataset . folder ;
st . blogSource = '' ;
window . scrollTo ( 0 , 0 ) ;
return load ( ) ;
}
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
if ( t . dataset . story ) return openStory ( parseInt ( t . dataset . story , 10 ) ) ;
if ( t . dataset . item ) {
readerList = st . items ;
return openItem ( parseInt ( t . dataset . item , 10 ) , null ) ;
}
2026-10-19 17:07:51 +02:00
if ( t . dataset . topic !== undefined ) {
const topic = ( st . topicList || [ ] ) [ parseInt ( t . dataset . topic , 10 ) ] ;
st . topic = st . topic && topic && st . topic . label === topic . label ? null : topic || null ;
window . scrollTo ( 0 , 0 ) ;
return load ( ) ;
}
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
if ( t . dataset . cat !== undefined && ! t . dataset . act ) {
2026-10-19 17:07:51 +02:00
st . topic = null ;
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
st . category = t . dataset . cat ;
window . scrollTo ( 0 , 0 ) ;
return load ( ) ;
}
if ( t . dataset . edit ) {
st . editing = parseInt ( t . dataset . edit , 10 ) ;
return render ( ) ;
}
const act = t . dataset . act ;
if ( act === 'refresh' ) {
if ( st . loading ) return ;
st . loading = true ;
render ( ) ;
try { await api ( '/feeds/refresh/' , { } ) ; } catch ( err ) { }
return load ( ) ;
}
2026-10-16 20:56:19 +02:00
// Folders the same way as everywhere (folders.js, docs/library.md).
if ( act === 'folders-manage' && window . DioraFolders ) {
const done = ( ) => loadState ( ) . catch ( ( ) => { } ) . then ( ( ) => ( st . view === 'blogs' ? load ( ) : render ( ) ) ) ;
window . DioraFolders . manage ( {
title : 'Ordner der Blogs' ,
folders : ( st . state && st . state . folders ) || [ ] ,
onRename : ( name , neu ) => api ( '/feeds/folders/rename/' , { name , new : neu } ) . catch ( ( ) => { } ) . then ( ( ) => {
if ( st . blogFolder !== null && st . blogFolder . toLowerCase ( ) === name . toLowerCase ( ) ) st . blogFolder = neu ;
return done ( ) ;
} ) ,
onDelete : name => api ( '/feeds/folders/dissolve/' , { name } ) . catch ( ( ) => { } ) . then ( ( ) => {
if ( st . blogFolder !== null && st . blogFolder . toLowerCase ( ) === name . toLowerCase ( ) ) st . blogFolder = null ;
return done ( ) ;
} ) ,
onOrder : order => api ( '/feeds/folders/order/' , { order } ) . then ( d => {
if ( st . state && d && d . folders ) st . state . folders = d . folders ;
render ( ) ;
} ) . catch ( ( ) => { } ) ,
} ) ;
return ;
}
if ( act === 'blog-folders' && window . DioraFolders ) {
const src = sources ( ) . find ( x => x . id === parseInt ( t . dataset . id , 10 ) ) ;
if ( ! src ) return ;
window . DioraFolders . assign ( {
title : 'Ordner für „' + src . title + '“' ,
folders : ( st . state && st . state . folders ) || [ ] ,
selected : src . folders || [ ] ,
onDone : async names => {
await api ( '/feeds/sources/' + src . id + '/' , { folders : names } ) . catch ( ( ) => { } ) ;
await loadState ( ) . catch ( ( ) => { } ) ;
load ( ) ;
} ,
} ) ;
return ;
}
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
if ( act === 'manage' ) {
Abos & Quellen: alle Abos an einem Ort (Navigation Phase 2)
Ein neuer Abschnitt (docs/sources.md, static/js/sources.js) für Podcasts,
YouTube-Kanäle, Hörbücher, Nachrichten und Blogs:
- ein Feld zum Hinzufügen: Adresse (Feed, Seite, YouTube) oder Name eines
Podcasts; die Art ist wählbar (Podcast, Hörbuch, Nachrichten, Blog);
/feeds/discover/ erkennt dafür Podcasts (Folgen mit Ton oder Video);
- eine Liste aller Abos mit Filtern (auch „Probleme“) und Suche;
- je Abo: Inhalt öffnen, Einstellungen (die vorhandenen Editoren), Art ändern,
abbestellen; OPML-Import und -Export.
In der Seitenleiste unten, auf dem Telefon im App-Menü; ⚙ in den Feeds führt
hierher. Abschnitte, die beim Laden schon gezeigt werden, füllen sich jetzt
auch (nav.js zeigt sie nach dem Laden aller Skripte noch einmal).
Podcast-UI aus diora-android 50c6f9c.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 15:23:12 +02:00
// Managing every subscription has its place (Abos & Quellen, docs/sources.md).
const nav = window . DioraNav ;
if ( nav && nav . sections ( ) . some ( s => s . id === 'sources' ) ) {
try { localStorage . setItem ( 'diora_sources_filter' , st . view === 'blogs' ? 'blog' : 'news' ) ; } catch ( err ) { }
nav . go ( 'sources' ) ;
return ;
}
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
st . view = 'manage' ;
st . found = null ;
window . scrollTo ( 0 , 0 ) ;
if ( ! st . state ) await loadState ( ) . catch ( ( ) => { } ) ;
return render ( ) ;
}
if ( act === 'close-manage' ) {
try { st . view = localStorage . getItem ( 'diora_feeds_view' ) || 'news' ; } catch ( err ) { st . view = 'news' ; }
st . editing = null ;
st . opml = null ;
Abos & Quellen: alle Abos an einem Ort (Navigation Phase 2)
Ein neuer Abschnitt (docs/sources.md, static/js/sources.js) für Podcasts,
YouTube-Kanäle, Hörbücher, Nachrichten und Blogs:
- ein Feld zum Hinzufügen: Adresse (Feed, Seite, YouTube) oder Name eines
Podcasts; die Art ist wählbar (Podcast, Hörbuch, Nachrichten, Blog);
/feeds/discover/ erkennt dafür Podcasts (Folgen mit Ton oder Video);
- eine Liste aller Abos mit Filtern (auch „Probleme“) und Suche;
- je Abo: Inhalt öffnen, Einstellungen (die vorhandenen Editoren), Art ändern,
abbestellen; OPML-Import und -Export.
In der Seitenleiste unten, auf dem Telefon im App-Menü; ⚙ in den Feeds führt
hierher. Abschnitte, die beim Laden schon gezeigt werden, füllen sich jetzt
auch (nav.js zeigt sie nach dem Laden aller Skripte noch einmal).
Podcast-UI aus diora-android 50c6f9c.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 15:23:12 +02:00
if ( st . fromSources && window . DioraNav ) { st . fromSources = false ; window . DioraNav . go ( 'sources' ) ; return ; }
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
return load ( ) ;
}
if ( act === 'seen' ) {
t . disabled = true ;
try { await api ( '/feeds/briefing/seen/' , { } ) ; } catch ( err ) { }
return load ( ) ;
}
if ( act === 'more' ) return load ( true ) ;
Blog-Feed nach Kino: Abschnitte, Favoriten, Seltene zuerst, Ausblenden, NEU
- Abschnitte Heute/Gestern/Diese Woche/Diesen Monat/Älter; je Abschnitt bis zu
5 Posts je Favoriten-Blog zuerst, dann „Seltene zuerst“ (mittlerer Abstand der
letzten 10 Posts, ein Post = 30 Tage) oder chronologisch
- Blog „im Hauptfeed ausblenden“: nicht unter Alle und nicht gezählt, im Ordner da
- NEU (jünger als 24 h, ungelesen) am Post, als Zahl am Tab, Punkt am Ordner
- Chips Ungelesen/Alle, ★ Favoriten, Seltene zuerst; Schalter je Blog
- Lesezeit (Wörter/230) an jedem Post; words wird beim Speichern gezählt
- sw v93
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:32:15 +02:00
if ( act === 'mode-unread' || act === 'mode-all' ) {
st . blogUnread = act === 'mode-unread' ;
try { localStorage . setItem ( 'diora_feeds_unread' , st . blogUnread ? '1' : '0' ) ; } catch ( err ) { }
return load ( ) ;
}
if ( act === 'mode-fav' ) {
st . blogFav = ! st . blogFav ;
return load ( ) ;
}
if ( act === 'mode-rare' ) {
st . rareFirst = ! st . rareFirst ;
try { localStorage . setItem ( 'diora_feeds_rare' , st . rareFirst ? '1' : '0' ) ; } catch ( err ) { }
return render ( ) ;
}
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
if ( act === 'blogs-read' ) {
const ids = st . items . filter ( i => ! i . read ) . map ( i => i . id ) ;
if ( ! ids . length ) return ;
await api ( '/feeds/items/read/' , { ids , read : true } ) . catch ( ( ) => { } ) ;
return load ( ) ;
}
2026-10-04 21:54:04 +02:00
if ( act === 'folder-rename' || act === 'folder-dissolve' ) return folderAction ( act , t . dataset . name ) ;
2026-10-04 21:24:59 +02:00
if ( act === 'login-delete' ) {
await api ( '/feeds/logins/delete/' , { source : parseInt ( t . dataset . id , 10 ) } ) . catch ( ( ) => { } ) ;
await loadState ( ) . catch ( ( ) => { } ) ;
return render ( ) ;
}
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
if ( act === 'cancel-edit' ) {
st . editing = null ;
return render ( ) ;
}
if ( act === 'delete' ) {
const src = sources ( ) . find ( x => x . id === parseInt ( t . dataset . id , 10 ) ) ;
const ok = typeof customConfirm === 'function'
? await customConfirm ( '„' + ( src ? src . title : 'Feed' ) + '“ abbestellen? Seine Artikel verschwinden mit.' , { danger : true } )
: window . confirm ( 'Abbestellen?' ) ;
if ( ! ok ) return ;
await api ( '/feeds/sources/' + t . dataset . id + '/delete/' , { } ) . catch ( ( ) => { } ) ;
st . editing = null ;
await loadState ( ) . catch ( ( ) => { } ) ;
return render ( ) ;
}
if ( act === 'opml-cancel' ) {
st . opml = null ;
return render ( ) ;
}
if ( act === 'opml-import' ) {
const list = [ ] ;
st . opml . forEach ( g => {
if ( g . kind === 'skip' ) return ;
2026-10-04 20:54:29 +02:00
g . feeds . forEach ( f => list . push ( { url : f . url , title : f . title , kind : g . kind , category : g . name } ) ) ;
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
} ) ;
t . disabled = true ;
t . textContent = 'Wird übernommen …' ;
try {
const r = await api ( '/feeds/sources/bulk/' , { sources : list } ) ;
st . opml = null ;
st . error = '' ;
await loadState ( ) ;
render ( ) ;
if ( typeof customAlert === 'function' ) customAlert ( r . added + ' Feeds übernommen.' ) ;
} catch ( err ) {
st . error = err . message ;
render ( ) ;
}
}
} ) ;
tab . addEventListener ( 'change' , async e => {
const t = e . target ;
if ( t . dataset . act === 'blog-source' ) { st . blogSource = t . value ; return load ( ) ; }
if ( t . dataset . act === 'keep' ) {
await api ( '/feeds/settings/' , { news _keep _days : parseInt ( t . value , 10 ) } ) . catch ( ( ) => { } ) ;
return ;
}
if ( t . dataset . opml !== undefined ) {
st . opml [ parseInt ( t . dataset . opml , 10 ) ] . kind = t . value ;
return render ( ) ;
}
if ( t . dataset . act === 'opml' && t . files && t . files [ 0 ] ) {
// FileReader rather than File.text(): the e-reader's Chrome 61 lacks the latter.
const text = await new Promise ( ( resolve , reject ) => {
const fr = new FileReader ( ) ;
fr . onload = ( ) => resolve ( fr . result ) ;
fr . onerror = ( ) => reject ( fr . error ) ;
fr . readAsText ( t . files [ 0 ] ) ;
} ) ;
const groups = parseOpml ( text ) ;
st . opml = groups . length ? groups : null ;
st . error = groups . length ? '' : 'In der Datei sind keine Feeds.' ;
render ( ) ;
}
} ) ;
tab . addEventListener ( 'submit' , async e => {
e . preventDefault ( ) ;
const f = e . target ;
const btn = f . querySelector ( '[type=submit]' ) ;
if ( btn ) btn . disabled = true ;
try {
if ( f . dataset . form === 'discover' ) {
st . found = null ;
st . error = '' ;
if ( btn ) btn . textContent = 'Sucht …' ;
const r = await api ( '/feeds/discover/' , { url : f . url . value } ) ;
st . found = r . feeds ;
} else if ( f . dataset . form === 'subscribe' ) {
const found = st . found [ parseInt ( f . dataset . n , 10 ) ] ;
if ( btn ) btn . textContent = 'Lädt Artikel …' ;
await api ( '/feeds/sources/add/' , { url : found . url , title : found . title , kind : f . kind . value ,
2026-10-04 20:54:29 +02:00
category : f . category . value } ) ;
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
found . subscribed = true ;
await loadState ( ) ;
2026-10-04 21:24:59 +02:00
} else if ( f . dataset . form === 'login' ) {
if ( btn ) btn . textContent = 'Meldet an …' ;
const r = await api ( '/feeds/logins/' , { source : parseInt ( f . dataset . id , 10 ) , username : f . username . value , password : f . password . value } ) ;
f . password . value = '' ;
await loadState ( ) ;
if ( typeof customAlert === 'function' ) {
customAlert ( 'Angemeldet.' + ( r . refetched ? ' ' + r . refetched + ' Artikel jetzt vollständig.' : '' ) ) ;
}
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
} else if ( f . dataset . form === 'edit' ) {
2026-10-04 21:54:04 +02:00
const change = { title : f . title . value , kind : f . kind . value , fulltext : f . fulltext . value } ;
if ( f . category ) change . category = f . category . value ;
if ( f . newfolder ) {
change . folders = Array . prototype . filter . call ( f . querySelectorAll ( 'input[name=folder]' ) , c => c . checked ) . map ( c => c . value ) ;
if ( f . newfolder . value . trim ( ) ) change . folders . push ( f . newfolder . value . trim ( ) ) ;
}
Blog-Feed nach Kino: Abschnitte, Favoriten, Seltene zuerst, Ausblenden, NEU
- Abschnitte Heute/Gestern/Diese Woche/Diesen Monat/Älter; je Abschnitt bis zu
5 Posts je Favoriten-Blog zuerst, dann „Seltene zuerst“ (mittlerer Abstand der
letzten 10 Posts, ein Post = 30 Tage) oder chronologisch
- Blog „im Hauptfeed ausblenden“: nicht unter Alle und nicht gezählt, im Ordner da
- NEU (jünger als 24 h, ungelesen) am Post, als Zahl am Tab, Punkt am Ordner
- Chips Ungelesen/Alle, ★ Favoriten, Seltene zuerst; Schalter je Blog
- Lesezeit (Wörter/230) an jedem Post; words wird beim Speichern gezählt
- sw v93
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:32:15 +02:00
if ( f . fav ) change . fav = f . fav . checked ;
if ( f . hidden ) change . hidden = f . hidden . checked ;
2026-10-04 21:40:27 +02:00
if ( f . notify ) change . notify = f . notify . checked ;
Blog-Feed nach Kino: Abschnitte, Favoriten, Seltene zuerst, Ausblenden, NEU
- Abschnitte Heute/Gestern/Diese Woche/Diesen Monat/Älter; je Abschnitt bis zu
5 Posts je Favoriten-Blog zuerst, dann „Seltene zuerst“ (mittlerer Abstand der
letzten 10 Posts, ein Post = 30 Tage) oder chronologisch
- Blog „im Hauptfeed ausblenden“: nicht unter Alle und nicht gezählt, im Ordner da
- NEU (jünger als 24 h, ungelesen) am Post, als Zahl am Tab, Punkt am Ordner
- Chips Ungelesen/Alle, ★ Favoriten, Seltene zuerst; Schalter je Blog
- Lesezeit (Wörter/230) an jedem Post; words wird beim Speichern gezählt
- sw v93
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:32:15 +02:00
await api ( '/feeds/sources/' + f . dataset . id + '/' , change ) ;
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
st . editing = null ;
await loadState ( ) ;
}
st . error = '' ;
} catch ( err ) {
st . error = err . message ;
}
render ( ) ;
} ) ;
2026-10-04 20:54:29 +02:00
// The same field is the Ressort of news and the folder of a blog.
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
tab . addEventListener ( 'input' , e => {
if ( e . target . name !== 'kind' ) return ;
const form = e . target . form ;
const cat = form && form . querySelector ( '.fd-cat-field' ) ;
2026-10-04 20:54:29 +02:00
if ( cat && cat . firstChild ) cat . firstChild . nodeValue = ( e . target . value === 'news' ? 'Ressort' : 'Ordner' ) + ' ' ;
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
} ) ;
// ---- showing the tab
let shownAt = 0 ;
Abos & Quellen: alle Abos an einem Ort (Navigation Phase 2)
Ein neuer Abschnitt (docs/sources.md, static/js/sources.js) für Podcasts,
YouTube-Kanäle, Hörbücher, Nachrichten und Blogs:
- ein Feld zum Hinzufügen: Adresse (Feed, Seite, YouTube) oder Name eines
Podcasts; die Art ist wählbar (Podcast, Hörbuch, Nachrichten, Blog);
/feeds/discover/ erkennt dafür Podcasts (Folgen mit Ton oder Video);
- eine Liste aller Abos mit Filtern (auch „Probleme“) und Suche;
- je Abo: Inhalt öffnen, Einstellungen (die vorhandenen Editoren), Art ändern,
abbestellen; OPML-Import und -Export.
In der Seitenleiste unten, auf dem Telefon im App-Menü; ⚙ in den Feeds führt
hierher. Abschnitte, die beim Laden schon gezeigt werden, füllen sich jetzt
auch (nav.js zeigt sie nach dem Laden aller Skripte noch einmal).
Podcast-UI aus diora-android 50c6f9c.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 15:23:12 +02:00
// Abos & Quellen (sources.js): a source's editor (null: the management page itself, with
// the OPML import), and "the sources changed, load again".
window . _ _dioraFeedsEdit = async function ( id ) {
if ( ! st . state ) await loadState ( ) . catch ( ( ) => { } ) ;
const src = id == null ? null : sources ( ) . find ( x => x . id === Number ( id ) ) ;
const section = src && src . kind === 'blog' ? 'blogs' : 'news' ;
if ( window . DioraNav ) window . DioraNav . go ( section ) ; else showTab ( 'feeds' ) ;
st . view = 'manage' ;
st . editing = src ? src . id : null ;
st . found = null ;
st . fromSources = true ;
window . scrollTo ( 0 , 0 ) ;
render ( ) ;
} ;
window . _ _dioraFeedsStale = function ( ) {
st . state = null ;
shownAt = 0 ;
} ;
Navigation: Lesen und Hören (Phase 1, noch nicht veröffentlicht)
Die Bereiche nach dem, was man tut (docs/navigation.md): Home; Lesen mit
Bücher, Zeitschriften, Nachrichten, Blogs; Hören mit Hörbücher, Podcasts,
Radio, Ambient. static/js/nav.js ist die eine Definition für alle Plattformen
(DioraNav: Abschnitte, Gruppen, go, Hash, zuletzt gewählter Abschnitt je
Gruppe). Die Webseite zeichnet ihre Leiste daraus: breit alle Abschnitte unter
ihrer Gruppe, schmal Home · Lesen · Hören mit einer Abschnittszeile oben.
- Hörbücher haben ein eigenes Panel (#tab-audiobooks), nicht mehr über den E-Books.
- Zeitschriften: die Bücher im Ordner „Zeitschriften“ (oder „Magazine“).
- Nachrichten/Blogs wählt die Navigation; der eigene Umschalter verschwindet dann.
- Ein Link auf einen Abschnitt (#news …) schlägt die Startseite.
Die Apps zeichnen ihre Leiste (native-radio.js, diora-android) bald ebenfalls
daraus; bis dahin bleibt das hier unveröffentlicht.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 14:31:58 +02:00
// Nachrichten and Blogs are sections of Lesen (static/js/nav.js): it chooses, before the
// tab is shown; its own switch is then hidden.
window . _ _dioraFeedsView = function ( view ) {
if ( view !== 'news' && view !== 'blogs' || view === st . view ) return ;
st . view = view ;
try { localStorage . setItem ( 'diora_feeds_view' , view ) ; } catch ( err ) { }
shownAt = 0 ; // shown next: loaded for this view
} ;
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
window . _ _dioraFeedsShow = function ( ) {
// Back after a while: fresh state (new briefing); otherwise as it was.
if ( ! st . state || Date . now ( ) - shownAt > 5 * 60000 ) load ( ) ;
else render ( ) ;
shownAt = Date . now ( ) ;
} ;
// What other parts (Home) may show: the briefing and the unread blog posts.
window . _ _dioraFeedsSummary = async function ( ) {
const [ state , briefing ] = await Promise . all ( [ api ( '/feeds/state/' ) , api ( '/feeds/briefing/' ) ] ) ;
return { state , briefing } ;
} ;
2026-10-04 21:40:27 +02:00
// A notification about a new post was tapped (the app, MainActivity): open it.
window . _ _dioraFeedsOpenItem = function ( id ) {
showTab ( 'feeds' ) ;
readerList = st . items ;
openItem ( id , null ) ;
} ;
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
window . _ _dioraFeedsOpenStory = function ( story ) {
if ( ! st . stories . some ( x => x . id === story . id ) ) st . stories . push ( story ) ;
openStory ( story . id ) ;
} ;
setInterval ( ( ) => {
if ( tab . style . display !== 'none' && ! readerOpen ( ) && st . view !== 'manage' && ! st . loading ) load ( ) ;
} , 10 * 60000 ) ;
// Back (Android): the article, then the management page, then (diora Reader) to the books.
const back = window . _ _dioraBack ;
window . _ _dioraBack = function ( ) {
if ( readerOpen ( ) ) { closeReader ( ) ; return true ; }
if ( tab . style . display !== 'none' && st . view === 'manage' ) {
tab . querySelector ( '[data-act="close-manage"]' ) . click ( ) ;
return true ;
}
if ( back && back ( ) ) return true ;
if ( READER _APP && typeof _currentTopTab !== 'undefined' && _currentTopTab === 'feeds' ) {
showTab ( 'books' ) ;
return true ;
}
return false ;
} ;
document . addEventListener ( 'keydown' , e => {
if ( e . key === 'Escape' && readerOpen ( ) ) closeReader ( ) ;
} ) ;
2026-10-16 14:53:38 +02:00
// diora Reader has no tab bar of its own: a small one. With the navigation (nav.js) it
// holds the sections of Lesen itself — a row of them to swipe is no good on e-ink.
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
if ( READER _APP && ! document . getElementById ( 'nui-tabbar' ) ) {
2026-10-16 14:53:38 +02:00
const NAV = window . DioraNav ;
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
const dock = document . createElement ( 'div' ) ;
dock . id = 'nui-dock' ;
2026-10-16 14:53:38 +02:00
dock . innerHTML = '<nav id="nui-tabbar">' + ( NAV
Abos & Quellen: alle Abos an einem Ort (Navigation Phase 2)
Ein neuer Abschnitt (docs/sources.md, static/js/sources.js) für Podcasts,
YouTube-Kanäle, Hörbücher, Nachrichten und Blogs:
- ein Feld zum Hinzufügen: Adresse (Feed, Seite, YouTube) oder Name eines
Podcasts; die Art ist wählbar (Podcast, Hörbuch, Nachrichten, Blog);
/feeds/discover/ erkennt dafür Podcasts (Folgen mit Ton oder Video);
- eine Liste aller Abos mit Filtern (auch „Probleme“) und Suche;
- je Abo: Inhalt öffnen, Einstellungen (die vorhandenen Editoren), Art ändern,
abbestellen; OPML-Import und -Export.
In der Seitenleiste unten, auf dem Telefon im App-Menü; ⚙ in den Feeds führt
hierher. Abschnitte, die beim Laden schon gezeigt werden, füllen sich jetzt
auch (nav.js zeigt sie nach dem Laden aller Skripte noch einmal).
Podcast-UI aus diora-android 50c6f9c.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 15:23:12 +02:00
? NAV . sections ( ) . filter ( s => ! s . footer ) . map ( s => '<button type="button" data-nav="' + s . id + '"><span class="nui-tab-icon">' +
Neue Navigations-Icons (Stil B), mehrere Bücher auf einmal, generiertes Cover pro Buch wählbar
- Icons: eigene SVGs, kräftig und geometrisch (2,4 px, eckige Enden), in
nav.js für alle Leisten (DioraNav.icon, sections()/groups() .svg); der
aktive Bereich gefüllt.
- Hochladen: mehrere Dateien auf einmal (Auswahl oder auf die Bibliothek
ziehen) und aus der Cloud per Häkchen („Alle auswählen“, „n importieren“);
nacheinander mit Fortschritt, ein schon vorhandenes Buch (Titel und Autor)
wird übersprungen.
- Pro Buch: „Generiertes Cover verwenden“ statt des eigenen (im verschlüsselten
Meta, cover: 'generated').
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 21:21:10 +02:00
( s . svg || esc ( s . icon ) ) + '</span>' + esc ( s . label ) + '</button>' ) . join ( '' )
2026-10-16 14:53:38 +02:00
: '<button type="button" data-tab="books"><span class="nui-tab-icon">▤</span>Bücher</button>' +
'<button type="button" data-tab="feeds"><span class="nui-tab-icon">☰</span>Feeds</button>' ) + '</nav>' ;
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
document . body . appendChild ( dock ) ;
2026-10-16 14:53:38 +02:00
const mark = ( ) => dock . querySelectorAll ( 'button' ) . forEach ( b => b . classList . toggle ( 'nui-active' , NAV
? b . dataset . nav === NAV . current ( )
: typeof _currentTopTab !== 'undefined' && b . dataset . tab === _currentTopTab ) ) ;
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
dock . addEventListener ( 'click' , e => {
const b = e . target . closest ( 'button' ) ;
if ( ! b ) return ;
2026-10-16 14:53:38 +02:00
if ( NAV ) NAV . go ( b . dataset . nav ) ;
else showTab ( b . dataset . tab ) ;
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
window . scrollTo ( 0 , 0 ) ;
} ) ;
2026-10-16 14:53:38 +02:00
if ( NAV ) {
document . documentElement . classList . add ( 'nav-flat' ) ; // every section in the bar: no row on top
NAV . onChange ( mark ) ;
NAV . barsReady ( ) ;
}
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
const orig = window . showTab ;
window . showTab = function ( name ) {
orig . apply ( this , arguments ) ;
mark ( ) ;
} ;
document . documentElement . style . setProperty ( '--nui-dock-h' , '56px' ) ;
const overlay = document . getElementById ( 'reader-overlay' ) ;
if ( overlay ) {
new MutationObserver ( ( ) => document . documentElement . classList . toggle ( 'nui-reading' , overlay . style . display !== 'none' ) )
. observe ( overlay , { attributes : true , attributeFilter : [ 'style' ] } ) ;
}
mark ( ) ;
}
// ---- styles
const style = document . createElement ( 'style' ) ;
style . textContent = [
'#tab-feeds .fd { max-width: 760px; margin: 0 auto; padding-bottom: 24px; }' ,
'#tab-feeds .fd-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin: 4px 0 14px; }' ,
'#tab-feeds .fd-seg { display: flex; border: 1px solid var(--border, #2a2a2a); border-radius: 6px; overflow: hidden; }' ,
'#tab-feeds .fd-seg button { background: none; border: none; color: inherit; font: inherit; padding: 9px 14px; cursor: pointer; }' ,
2026-10-14 17:51:33 +02:00
'#tab-feeds .fd-seg button.on { background: var(--accent, #e63946); color: var(--on-accent, #fff); }' ,
Navigation: Lesen und Hören (Phase 1, noch nicht veröffentlicht)
Die Bereiche nach dem, was man tut (docs/navigation.md): Home; Lesen mit
Bücher, Zeitschriften, Nachrichten, Blogs; Hören mit Hörbücher, Podcasts,
Radio, Ambient. static/js/nav.js ist die eine Definition für alle Plattformen
(DioraNav: Abschnitte, Gruppen, go, Hash, zuletzt gewählter Abschnitt je
Gruppe). Die Webseite zeichnet ihre Leiste daraus: breit alle Abschnitte unter
ihrer Gruppe, schmal Home · Lesen · Hören mit einer Abschnittszeile oben.
- Hörbücher haben ein eigenes Panel (#tab-audiobooks), nicht mehr über den E-Books.
- Zeitschriften: die Bücher im Ordner „Zeitschriften“ (oder „Magazine“).
- Nachrichten/Blogs wählt die Navigation; der eigene Umschalter verschwindet dann.
- Ein Link auf einen Abschnitt (#news …) schlägt die Startseite.
Die Apps zeichnen ihre Leiste (native-radio.js, diora-android) bald ebenfalls
daraus; bis dahin bleibt das hier unveröffentlicht.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 14:31:58 +02:00
'html.nav-bars #tab-feeds .fd-seg { display: none; }' ,
2026-10-14 17:51:33 +02:00
'#tab-feeds .fd-count { display: inline-block; min-width: 18px; padding: 0 5px; border-radius: 9px; background: var(--raised, rgba(255,255,255,0.18)); font-size: 11px; text-align: center; }' ,
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
'#tab-feeds .fd-actions { display: flex; gap: 4px; }' ,
'.fd-icon { background: none; border: none; color: inherit; font: inherit; font-size: 20px; min-width: 40px; min-height: 40px; cursor: pointer; text-decoration: none; display: inline-flex; align-items: center; justify-content: center; }' ,
'.fd-icon.on { color: var(--accent, #e63946); }' ,
'#tab-feeds .fd-back { background: none; border: none; color: inherit; font: inherit; padding: 8px 0; cursor: pointer; }' ,
'#tab-feeds .fd-h { margin: 0; font-size: 15px; }' ,
'.fd-note { color: var(--nui-muted, #9a9a9a); font-size: 13px; font-weight: 400; }' ,
'.fd-error, .fd-warn { color: var(--accent, #e63946); font-size: 13px; }' ,
'#tab-feeds .fd-empty { text-align: center; padding: 40px 0; }' ,
'#tab-feeds h3 { margin: 18px 0 6px; font-size: 12px; font-weight: 400; text-transform: uppercase; letter-spacing: 0.08em; color: var(--nui-muted, #9a9a9a); }' ,
'#tab-feeds h3 small { font-size: 11px; }' ,
2026-10-14 17:51:33 +02:00
'#tab-feeds .fd-brief { border: 1px solid var(--border, #2a2a2a); border-radius: 6px; padding: 12px 14px 14px; margin-bottom: 18px; background: var(--card, #0b0b0b); box-shadow: var(--shadow, none); }' ,
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
'#tab-feeds .fd-brief-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; flex-wrap: wrap; }' ,
'#tab-feeds .fd-brief-head h2 { margin: 0; font-size: 16px; color: var(--accent, #e63946); }' ,
'#tab-feeds .fd-seen { width: 100%; margin-top: 14px; min-height: 42px; }' ,
'#tab-feeds .fd-row { display: flex; align-items: center; gap: 10px; width: 100%; padding: 9px 0; background: none; border: none; border-bottom: 1px solid var(--border, #2a2a2a); color: inherit; font: inherit; text-align: left; cursor: pointer; }' ,
'#tab-feeds .fd-row-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }' ,
'#tab-feeds .fd-row-title { font-size: 14px; line-height: 1.35; }' ,
'.fd-row-meta { font-size: 12px; font-weight: 400; color: var(--nui-muted, #9a9a9a); }' ,
'#tab-feeds .fd-more-src { color: var(--accent, #e63946); }' ,
2026-10-14 17:51:33 +02:00
'#tab-feeds .fd-badge { flex: none; min-width: 26px; height: 26px; border-radius: 13px; background: var(--accent, #e63946); color: var(--on-accent, #fff); font-size: 12px; display: inline-flex; align-items: center; justify-content: center; }' ,
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
'#tab-feeds .fd-link { background: none; border: none; color: var(--nui-muted, #9a9a9a); font: inherit; font-size: 12px; padding: 8px 0; cursor: pointer; }' ,
'#tab-feeds .fd-chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 4px 0 10px; align-items: center; }' ,
// The reports of a story: one row that scrolls sideways, however many there are.
'.fd-sources { display: flex; gap: 6px; margin: 4px -18px 10px; padding: 0 18px; align-items: center; overflow-x: auto; white-space: nowrap; scrollbar-width: none; }' ,
'.fd-sources::-webkit-scrollbar { display: none; }' ,
'.fd-sources .fd-chip { flex: none; }' ,
'#fd-article .fd-art-more { font-family: var(--font); margin: 30px 0 4px; font-size: 12px; font-weight: 400; text-transform: uppercase; letter-spacing: 0.08em; color: var(--nui-muted, #9a9a9a); }' ,
'#fd-article .fd-other { display: flex; flex-direction: column; gap: 3px; width: 100%; padding: 10px 0; background: none; border: none; border-bottom: 1px solid var(--border, #2a2a2a); color: inherit; font: inherit; font-size: 0.85em; text-align: left; cursor: pointer; }' ,
'#fd-article .fd-other-title { font-weight: 600; line-height: 1.35; }' ,
'.fd-chip { padding: 6px 12px; border: 1px solid var(--border, #2a2a2a); border-radius: 16px; background: none; color: inherit; font: inherit; font-size: 13px; cursor: pointer; }' ,
'.fd-chip.on { border-color: var(--accent, #e63946); color: var(--accent, #e63946); }' ,
2026-10-04 20:54:29 +02:00
'.fd-chip small { color: var(--nui-muted, #9a9a9a); font-size: 11px; }' ,
Blog-Feed nach Kino: Abschnitte, Favoriten, Seltene zuerst, Ausblenden, NEU
- Abschnitte Heute/Gestern/Diese Woche/Diesen Monat/Älter; je Abschnitt bis zu
5 Posts je Favoriten-Blog zuerst, dann „Seltene zuerst“ (mittlerer Abstand der
letzten 10 Posts, ein Post = 30 Tage) oder chronologisch
- Blog „im Hauptfeed ausblenden“: nicht unter Alle und nicht gezählt, im Ordner da
- NEU (jünger als 24 h, ungelesen) am Post, als Zahl am Tab, Punkt am Ordner
- Chips Ungelesen/Alle, ★ Favoriten, Seltene zuerst; Schalter je Blog
- Lesezeit (Wörter/230) an jedem Post; words wird beim Speichern gezählt
- sw v93
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:32:15 +02:00
'.fd-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--accent, #e63946); margin-right: 6px; vertical-align: middle; }' ,
'#tab-feeds .fd-modes .fd-chip { font-size: 12px; padding: 5px 10px; }' ,
'#tab-feeds .fd-blogbar { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 8px; }' ,
2026-10-14 17:51:33 +02:00
'#tab-feeds .fd-new { display: inline-block; padding: 1px 5px; border-radius: 3px; background: var(--accent, #e63946); color: var(--on-accent, #fff); font-size: 10px; letter-spacing: 0.05em; vertical-align: 2px; }' ,
Blog-Feed nach Kino: Abschnitte, Favoriten, Seltene zuerst, Ausblenden, NEU
- Abschnitte Heute/Gestern/Diese Woche/Diesen Monat/Älter; je Abschnitt bis zu
5 Posts je Favoriten-Blog zuerst, dann „Seltene zuerst“ (mittlerer Abstand der
letzten 10 Posts, ein Post = 30 Tage) oder chronologisch
- Blog „im Hauptfeed ausblenden“: nicht unter Alle und nicht gezählt, im Ordner da
- NEU (jünger als 24 h, ungelesen) am Post, als Zahl am Tab, Punkt am Ordner
- Chips Ungelesen/Alle, ★ Favoriten, Seltene zuerst; Schalter je Blog
- Lesezeit (Wörter/230) an jedem Post; words wird beim Speichern gezählt
- sw v93
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:32:15 +02:00
'#tab-feeds .fd-hidden { opacity: 0.6; }' ,
2026-10-14 17:51:33 +02:00
'#tab-feeds .fd-prog { display: block; height: 3px; margin-top: 4px; background: var(--raised, #2a2a2a); border-radius: 2px; overflow: hidden; }' ,
2026-10-04 21:54:04 +02:00
'#tab-feeds .fd-prog > span { display: block; height: 100%; background: var(--accent, #e63946); }' ,
'#tab-feeds .fd-folders { border: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; gap: 6px 14px; align-items: center; }' ,
'#tab-feeds .fd-folders legend { font-size: 12px; color: var(--nui-muted, #9a9a9a); margin-bottom: 4px; }' ,
'#tab-feeds .fd-folders input[name=newfolder] { flex-basis: 100%; }' ,
'#tab-feeds .fd-folder-row { display: flex; justify-content: space-between; align-items: center; gap: 8px; padding: 8px 0; border-bottom: 1px solid var(--border, #2a2a2a); flex-wrap: wrap; }' ,
2026-10-14 17:51:33 +02:00
'#fd-sheet { position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 995; background: var(--scrim, rgba(0,0,0,0.5)); display: flex; align-items: flex-end; justify-content: center; }' ,
'#fd-sheet .fd-sheet-box { width: 100%; max-width: 480px; background: var(--panel, #000); border: 1px solid var(--border, #2a2a2a); border-bottom: none; border-radius: 10px 10px 0 0; padding: 8px 0 calc(8px + env(safe-area-inset-bottom, 0px)); }' ,
2026-10-04 21:54:04 +02:00
'#fd-sheet .fd-sheet-title { padding: 10px 18px; font-size: 13px; color: var(--nui-muted, #9a9a9a); }' ,
'#fd-sheet button { display: block; width: 100%; padding: 14px 18px; background: none; border: none; color: var(--fg, #fff); font: inherit; text-align: left; cursor: pointer; }' ,
'body.eink #tab-feeds .fd-prog > span { background: #000; }' ,
'body.eink #fd-sheet .fd-sheet-box { background: #fff; }' ,
'body.eink #fd-sheet button { color: #000; }' ,
Blog-Feed nach Kino: Abschnitte, Favoriten, Seltene zuerst, Ausblenden, NEU
- Abschnitte Heute/Gestern/Diese Woche/Diesen Monat/Älter; je Abschnitt bis zu
5 Posts je Favoriten-Blog zuerst, dann „Seltene zuerst“ (mittlerer Abstand der
letzten 10 Posts, ein Post = 30 Tage) oder chronologisch
- Blog „im Hauptfeed ausblenden“: nicht unter Alle und nicht gezählt, im Ordner da
- NEU (jünger als 24 h, ungelesen) am Post, als Zahl am Tab, Punkt am Ordner
- Chips Ungelesen/Alle, ★ Favoriten, Seltene zuerst; Schalter je Blog
- Lesezeit (Wörter/230) an jedem Post; words wird beim Speichern gezählt
- sw v93
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:32:15 +02:00
'body.eink #tab-feeds .fd-new, body.eink .fd-dot { background: #000; }' ,
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
'#tab-feeds .fd-day { margin-top: 16px; }' ,
'#tab-feeds .fd-card { display: flex; gap: 12px; width: 100%; padding: 12px 0; background: none; border: none; border-bottom: 1px solid var(--border, #2a2a2a); color: inherit; font: inherit; text-align: left; cursor: pointer; }' ,
2026-10-14 17:51:33 +02:00
'#tab-feeds .fd-thumb { width: 92px; height: 66px; flex: none; object-fit: cover; border-radius: 4px; background: var(--raised, #1a1a1a); order: 2; }' ,
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
'#tab-feeds .fd-card-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }' ,
'#tab-feeds .fd-card-title { font-size: 15px; font-weight: 700; line-height: 1.35; }' ,
'#tab-feeds .fd-card-sum { font-size: 13px; font-weight: 400; line-height: 1.4; color: var(--nui-muted, #bdbdbd); overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }' ,
'#tab-feeds .fd-read .fd-card-title, #tab-feeds .fd-read .fd-row-title { font-weight: 400; color: var(--nui-muted, #9a9a9a); }' ,
'#tab-feeds .fd-post { display: flex; flex-direction: column; gap: 4px; width: 100%; padding: 12px 0; background: none; border: none; border-bottom: 1px solid var(--border, #2a2a2a); color: inherit; font: inherit; text-align: left; cursor: pointer; }' ,
'#tab-feeds .fd-filter { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-bottom: 8px; }' ,
'#tab-feeds .fd-select { max-width: 60%; }' ,
'#tab-feeds .fd-check { font-size: 13px; display: inline-flex; gap: 6px; align-items: center; }' ,
'#tab-feeds .fd-allread { margin-left: auto; }' ,
'#tab-feeds .fd-loadmore { width: 100%; margin-top: 14px; }' ,
'#tab-feeds .fd-box { border: 1px solid var(--border, #2a2a2a); border-radius: 6px; padding: 4px 14px 14px; margin-bottom: 14px; }' ,
'#tab-feeds .fd-add { display: flex; gap: 8px; }' ,
'#tab-feeds .fd-add input { flex: 1; min-width: 0; }' ,
'#tab-feeds .fd-found, #tab-feeds .fd-edit { border-top: 1px solid var(--border, #2a2a2a); margin-top: 12px; padding-top: 10px; display: flex; flex-direction: column; gap: 8px; }' ,
'#tab-feeds .fd-found-title { font-weight: 700; }' ,
'#tab-feeds .fd-url { word-break: break-all; }' ,
'#tab-feeds .fd-samples { margin: 0; padding-left: 18px; font-size: 13px; font-weight: 400; }' ,
'#tab-feeds .fd-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 8px; }' ,
'#tab-feeds .fd-grid label, #tab-feeds .fd-edit > label { display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: var(--nui-muted, #9a9a9a); }' ,
'#tab-feeds .fd-buttons { display: flex; flex-wrap: wrap; gap: 8px; }' ,
2026-10-04 21:24:59 +02:00
'#tab-feeds .fd-login h4 { margin: 0; font-size: 13px; }' ,
'#tab-feeds .fd-login p { margin: 0; font-size: 13px; }' ,
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
'#tab-feeds .fd-src { display: flex; flex-direction: column; gap: 2px; width: 100%; padding: 10px 0; background: none; border: none; border-bottom: 1px solid var(--border, #2a2a2a); color: inherit; font: inherit; text-align: left; cursor: pointer; }' ,
'#tab-feeds .fd-opml { margin-top: 12px; display: flex; flex-direction: column; gap: 8px; }' ,
'#tab-feeds .fd-opml-row { display: flex; justify-content: space-between; align-items: center; gap: 8px; }' ,
'#tab-feeds .fd-keep { display: flex; align-items: center; gap: 8px; margin-top: 14px; }' ,
'#tab-feeds .fd-file { display: inline-block; }' ,
// The article: a page over everything, like the book reader.
'#fd-article { position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 900; background: var(--bg, #000); color: var(--fg, #fff); overflow-y: auto; -webkit-overflow-scrolling: touch; }' ,
'#fd-article[hidden] { display: none; }' ,
'#fd-article .fd-art-bar { position: sticky; top: 0; display: flex; align-items: center; gap: 2px; padding: 4px 6px; background: var(--bg, #000); border-bottom: 1px solid var(--border, #2a2a2a); padding-top: calc(4px + env(safe-area-inset-top, 0px)); }' ,
'#fd-article .fd-art-src { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; color: var(--nui-muted, #9a9a9a); }' ,
'#fd-article .fd-art { max-width: 68ch; margin: 0 auto; padding: 12px 18px 48px; font-family: "Source Serif 4", Georgia, serif; font-weight: 380; line-height: 1.65; }' ,
'#fd-article .fd-art-title { font-size: 1.45em; line-height: 1.25; margin: 10px 0 6px; font-weight: 700; }' ,
'#fd-article .fd-art-meta { font-family: var(--font); font-size: 12px; color: var(--nui-muted, #9a9a9a); margin: 0 0 14px; }' ,
'#fd-article .fd-art-img, #fd-article .fd-art-body img { display: block; max-width: 100%; height: auto; margin: 12px auto; border-radius: 4px; }' ,
'#fd-article .fd-art-body a { color: var(--accent, #e63946); }' ,
'#fd-article .fd-art-body figure { margin: 12px 0; }' ,
'#fd-article .fd-art-body figcaption { font-size: 0.8em; color: var(--nui-muted, #9a9a9a); }' ,
2026-10-14 17:51:33 +02:00
'#fd-article .fd-art-body blockquote { margin: 12px 0; padding-left: 14px; border-left: 3px solid var(--border-strong, #444); }' ,
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
'#fd-article .fd-art-body pre { white-space: pre-wrap; font-size: 0.85em; }' ,
'#fd-article .fd-art-body table { display: block; overflow-x: auto; border-collapse: collapse; font-size: 0.85em; }' ,
'#fd-article .fd-art-body td, #fd-article .fd-art-body th { border: 1px solid var(--border, #333); padding: 4px 6px; }' ,
'#fd-article .fd-sources { font-family: var(--font); }' ,
2026-10-19 17:07:51 +02:00
'#tab-feeds .fd-topics .fd-note { margin-right: 2px; }' ,
'#tab-feeds .fd-topics small { opacity: 0.6; }' ,
'#fd-article .fd-ov { font-size: 0.9em; }' ,
'#fd-article .fd-ov-box { margin: 0 0 18px; padding: 10px 14px; border: 1px solid var(--border, #2a2a2a); border-radius: 6px; }' ,
'#fd-article .fd-ov-box summary { font-family: var(--font); font-size: 12px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--nui-muted, #9a9a9a); cursor: pointer; }' ,
'#fd-article .fd-ov p { margin: 8px 0; line-height: 1.45; }' ,
'#fd-article .fd-ov h4 { font-family: var(--font); font-size: 12px; font-weight: 400; text-transform: uppercase; letter-spacing: 0.08em; color: var(--nui-muted, #9a9a9a); margin: 14px 0 4px; }' ,
'#fd-article .fd-ov-src { font-family: var(--font); font-size: 11px; color: var(--nui-muted, #9a9a9a); white-space: nowrap; }' ,
'#fd-article .fd-ov-words { font-family: var(--font); font-size: 13px; }' ,
'#fd-article .fd-ov-rel { margin: 0 0 18px; }' ,
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
'#fd-article .fd-art-foot { display: flex; flex-wrap: wrap; gap: 10px; justify-content: space-between; margin-top: 28px; font-family: var(--font); }' ,
'html.fd-reading #nui-dock { display: none; }' ,
2026-10-14 17:51:33 +02:00
'@media (hover: hover) { #tab-feeds .fd-card:hover, #tab-feeds .fd-row:hover, #tab-feeds .fd-post:hover, #tab-feeds .fd-src:hover { background: var(--hover, #111); } }' ,
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
// Bright pages and e-ink: black on white, no grey boxes, no thumbnails to flicker.
'body.bright-bg #tab-feeds .fd-brief, body.eink #tab-feeds .fd-brief { background: #fff; }' ,
'body.eink #tab-feeds .fd-thumb { display: none; }' ,
'body.eink #tab-feeds .fd-seg button.on, body.eink #tab-feeds .fd-badge { background: #000; color: #fff; }' ,
'body.eink .fd-row-meta, body.eink .fd-note, body.eink #tab-feeds .fd-card-sum, body.eink #tab-feeds .fd-read .fd-card-title, body.eink #tab-feeds .fd-read .fd-row-title { color: #333; }' ,
'body.eink #fd-article, body.eink #fd-article .fd-art-bar { background: #fff; color: #000; }' ,
2026-10-19 17:07:51 +02:00
'body.eink #fd-article .fd-ov-box { border-color: #000; }' ,
'body.eink #fd-article .fd-ov-src, body.eink #fd-article .fd-ov h4, body.eink #fd-article .fd-ov-box summary { color: #333; }' ,
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
'body.eink #fd-article .fd-art-body a, body.eink .fd-chip.on, body.eink .fd-icon.on, body.eink #tab-feeds .fd-brief-head h2 { color: #000; }' ,
] . join ( '\n' ) ;
document . head . appendChild ( style ) ;
} ) ( ) ;