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 : '' ,
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 = [ ] ;
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 ) ) ;
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 ;
}
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>' ;
}
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
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 || [ ] ) ;
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 inFolder = f => blogs . filter ( b => ( b . category || '' ) === f ) ;
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 ( '' ) +
( blogs . some ( b => ! b . category ) ? chip ( '' , 'Ohne Ordner' , inFolder ( '' ) ) : '' ) + '</div>' ;
}
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>' +
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>' ;
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>' : '' ) +
'</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 20:54:29 +02:00
'<span class="fd-row-meta">' + ( x . kind === 'news' ? esc ( x . category || 'Allgemein' ) + ' · ' : ( x . category ? esc ( x . 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
( 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 20:54:29 +02:00
h += group ( 'Blogs' , blogs . sort ( ( a , b ) => ( a . category || '~' ) . localeCompare ( b . category || '~' ) || a . title . localeCompare ( b . title ) ) ) ;
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 20:54:29 +02:00
'<label class="fd-cat-field">' + ( x . kind === 'news' ? 'Ressort' : 'Ordner' ) + ' ' + categoryInput ( x . category ) + '</label>' +
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 ) ;
}
// 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 ) ;
document . body . appendChild ( reader ) ;
return reader ;
}
function readerOpen ( ) {
return ! ! ( reader && ! reader . hidden ) ;
}
function closeReader ( ) {
if ( ! reader ) return ;
reader . hidden = true ;
reader . innerHTML = '' ;
document . documentElement . classList . remove ( 'fd-reading' ) ;
readerItem = readerStory = null ;
render ( ) ;
}
async function openItem ( id , story ) {
ensureReader ( ) ;
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 ;
if ( ! readerItem . read ) {
readerItem . read = true ;
markLocal ( [ id ] , true ) ;
api ( '/feeds/items/read/' , { ids : [ id ] , read : true } ) . catch ( ( ) => { } ) ;
}
} 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>' : '' ) +
( 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>' ;
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 ;
}
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 ) ;
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' ;
} 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 ) ;
}
// ---- events
tab . addEventListener ( 'click' , async e => {
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 t = e . target . closest ( '[data-view],[data-act],[data-story],[data-item],[data-cat],[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 ) ;
}
if ( t . dataset . cat !== undefined && ! t . dataset . act ) {
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 ( ) ;
}
if ( act === 'manage' ) {
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 ;
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: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' ) {
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 change = { title : f . title . value , kind : f . kind . value , category : f . category . value , fulltext : f . fulltext . value } ;
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 ;
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 ( ) ;
} ) ;
// diora Reader has no tab bar of its own: a small one for books and feeds.
if ( READER _APP && ! document . getElementById ( 'nui-tabbar' ) ) {
const dock = document . createElement ( 'div' ) ;
dock . id = 'nui-dock' ;
dock . innerHTML = '<nav id="nui-tabbar">' +
'<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>' ;
document . body . appendChild ( dock ) ;
const mark = ( ) => dock . querySelectorAll ( 'button' ) . forEach ( b =>
b . classList . toggle ( 'nui-active' , typeof _currentTopTab !== 'undefined' && b . dataset . tab === _currentTopTab ) ) ;
dock . addEventListener ( 'click' , e => {
const b = e . target . closest ( 'button' ) ;
if ( ! b ) return ;
showTab ( b . dataset . tab ) ;
window . scrollTo ( 0 , 0 ) ;
} ) ;
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; }' ,
'#tab-feeds .fd-seg button.on { background: var(--accent, #e63946); color: #fff; }' ,
'#tab-feeds .fd-count { display: inline-block; min-width: 18px; padding: 0 5px; border-radius: 9px; background: rgba(255,255,255,0.18); font-size: 11px; text-align: center; }' ,
'#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; }' ,
'#tab-feeds .fd-brief { border: 1px solid var(--border, #2a2a2a); border-radius: 6px; padding: 12px 14px 14px; margin-bottom: 18px; background: #0b0b0b; }' ,
'#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); }' ,
'#tab-feeds .fd-badge { flex: none; min-width: 26px; height: 26px; border-radius: 13px; background: var(--accent, #e63946); color: #fff; font-size: 12px; display: inline-flex; align-items: center; justify-content: center; }' ,
'#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; }' ,
'#tab-feeds .fd-new { display: inline-block; padding: 1px 5px; border-radius: 3px; background: var(--accent, #e63946); color: #fff; font-size: 10px; letter-spacing: 0.05em; vertical-align: 2px; }' ,
'#tab-feeds .fd-hidden { opacity: 0.6; }' ,
'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; }' ,
'#tab-feeds .fd-thumb { width: 92px; height: 66px; flex: none; object-fit: cover; border-radius: 4px; background: #1a1a1a; order: 2; }' ,
'#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); }' ,
'#fd-article .fd-art-body blockquote { margin: 12px 0; padding-left: 14px; border-left: 3px solid var(--border, #444); }' ,
'#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); }' ,
'#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; }' ,
'@media (hover: hover) { #tab-feeds .fd-card:hover, #tab-feeds .fd-row:hover, #tab-feeds .fd-post:hover, #tab-feeds .fd-src:hover { background: #111; } }' ,
// 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; }' ,
'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 ) ;
} ) ( ) ;