Browse Source

UI: Add optional thumbnail and summary on feed items (#3805)

* UI: Add optional thumbnail and summary on feed items

Implements #561

* UI: Thumbnail: Own column, Custom size, Lazy load

* UI: Thumbnail: Remove unnecessary CSS rule

Remove rule already defined in base theme, no override needed

* CSS lint + RTL

* Improve thumbail and summary generation

* Support img alt

* Missing htmlspecialchars

Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
ORelio 4 years ago
parent
commit
50ba6bbe07

+ 2 - 0
app/Controllers/configureController.php

@@ -48,6 +48,8 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 			FreshRSS_Context::$user_conf->topline_favorite = Minz_Request::param('topline_favorite', false);
 			FreshRSS_Context::$user_conf->topline_date = Minz_Request::param('topline_date', false);
 			FreshRSS_Context::$user_conf->topline_link = Minz_Request::param('topline_link', false);
+			FreshRSS_Context::$user_conf->topline_thumbnail = Minz_Request::param('topline_thumbnail', false);
+			FreshRSS_Context::$user_conf->topline_summary = Minz_Request::param('topline_summary', false);
 			FreshRSS_Context::$user_conf->topline_display_authors = Minz_Request::param('topline_display_authors', false);
 			FreshRSS_Context::$user_conf->bottomline_read = Minz_Request::param('bottomline_read', false);
 			FreshRSS_Context::$user_conf->bottomline_favorite = Minz_Request::param('bottomline_favorite', false);

+ 10 - 0
app/Models/ConfigurationSetter.php

@@ -254,6 +254,16 @@ class FreshRSS_ConfigurationSetter {
 	private function _topline_read(&$data, $value) {
 		$data['topline_read'] = $this->handleBool($value);
 	}
+	private function _topline_thumbnail(&$data, $value) {
+		$value = strtolower($value);
+		if (!in_array($value, array('none', 'portrait', 'square', 'landscape'))) {
+			$value = 'none';
+		}
+		$data['topline_thumbnail'] = $value;
+	}
+	private function _topline_summary(&$data, $value) {
+		$data['topline_summary'] = $this->handleBool($value);
+	}
 	private function _topline_display_authors(&$data, $value) {
 		$data['topline_display_authors'] = $this->handleBool($value);
 	}

+ 33 - 3
app/Models/Entry.php

@@ -59,13 +59,19 @@ class FreshRSS_Entry extends Minz_Model {
 	public function content() {
 		return $this->content;
 	}
-	public function enclosures() {
+
+	public function enclosures($searchBodyImages = false) {
 		$results = [];
 		try {
-			if (strpos($this->content, '<p class="enclosure-content') !== false) {
+			$searchEnclosures = strpos($this->content, '<p class="enclosure-content') !== false;
+			$searchBodyImages &= (stripos($this->content, '<img') !== false);
+			$xpath = null;
+			if ($searchEnclosures || $searchBodyImages) {
 				$dom = new DOMDocument();
-				$dom->loadHTML($this->content, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING);
+				$dom->loadHTML('<?xml version="1.0" encoding="UTF-8" ?>' . $this->content, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING);
 				$xpath = new DOMXpath($dom);
+			}
+			if ($searchEnclosures) {
 				$enclosures = $xpath->query('//div[@class="enclosure"]/p[@class="enclosure-content"]/*[@src]');
 				foreach ($enclosures as $enclosure) {
 					$results[] = [
@@ -75,12 +81,36 @@ class FreshRSS_Entry extends Minz_Model {
 					];
 				}
 			}
+			if ($searchBodyImages) {
+				$images = $xpath->query('//img');
+				foreach ($images as $img) {
+					$src = $img->getAttribute('src');
+					if ($src == null) {
+						$src = $img->getAttribute('data-src');
+					}
+					if ($src != null) {
+						$results[] = [
+							'url' => $src,
+							'alt' => $img->getAttribute('alt'),
+						];
+					}
+				}
+			}
 			return $results;
 		} catch (Exception $ex) {
 			return $results;
 		}
 	}
 
+	public function thumbnail() {
+		foreach ($this->enclosures(true) as $enclosure) {
+			if (!empty($enclosure['url']) && empty($enclosure['type'])) {
+				return $enclosure;
+			}
+		}
+		return null;
+	}
+
 	public function link() {
 		return $this->link;
 	}

+ 8 - 0
app/i18n/cz/conf.php

@@ -25,6 +25,7 @@ return array(
 		'_' => 'Zobrazení',
 		'icon' => array(
 			'bottom_line' => 'Spodní řádek',
+			'summary' => 'Summary', // TODO - Translation
 			'display_authors' => 'Authors',	// TODO - Translation
 			'entry' => 'Ikony článků',
 			'publication_date' => 'Datum vydání',
@@ -47,6 +48,13 @@ return array(
 			'no_limit' => 'Bez limitu',
 			'thin' => 'Tenká',
 		),
+		'thumbnail' => array(
+			'label' => 'Thumbnail', // TODO - Translation
+			'none' => 'None', // TODO - Translation
+			'portrait' => 'Portrait', // TODO - Translation
+			'square' => 'Square', // TODO - Translation
+			'landscape' => 'Landscape', // TODO - Translation
+		),
 	),
 	'profile' => array(
 		'_' => 'Správa profilu',

+ 8 - 0
app/i18n/de/conf.php

@@ -25,6 +25,7 @@ return array(
 		'_' => 'Anzeige',
 		'icon' => array(
 			'bottom_line' => 'Fußzeile',
+			'summary' => 'Summary', // TODO - Translation
 			'display_authors' => 'Autoren',
 			'entry' => 'Artikel-Symbole',
 			'publication_date' => 'Datum der Veröffentlichung',
@@ -47,6 +48,13 @@ return array(
 			'no_limit' => 'Keine Begrenzung',
 			'thin' => 'Klein',
 		),
+		'thumbnail' => array(
+			'label' => 'Thumbnail', // TODO - Translation
+			'none' => 'None', // TODO - Translation
+			'portrait' => 'Portrait', // TODO - Translation
+			'square' => 'Square', // TODO - Translation
+			'landscape' => 'Landscape', // TODO - Translation
+		),
 	),
 	'profile' => array(
 		'_' => 'Profil-Verwaltung',

+ 8 - 0
app/i18n/en-us/conf.php

@@ -25,6 +25,7 @@ return array(
 		'_' => 'Display',
 		'icon' => array(
 			'bottom_line' => 'Bottom line',
+			'summary' => 'Summary',
 			'display_authors' => 'Authors',
 			'entry' => 'Article icons',
 			'publication_date' => 'Date of publication',
@@ -47,6 +48,13 @@ return array(
 			'no_limit' => 'Full Width',
 			'thin' => 'Narrow',
 		),
+		'thumbnail' => array(
+			'label' => 'Thumbnail',
+			'none' => 'None',
+			'portrait' => 'Portrait',
+			'square' => 'Square',
+			'landscape' => 'Landscape',
+		),
 	),
 	'profile' => array(
 		'_' => 'Profile management',

+ 8 - 0
app/i18n/en/conf.php

@@ -25,6 +25,7 @@ return array(
 		'_' => 'Display',
 		'icon' => array(
 			'bottom_line' => 'Bottom line',
+			'summary' => 'Summary',
 			'display_authors' => 'Authors',
 			'entry' => 'Article icons',
 			'publication_date' => 'Date of publication',
@@ -47,6 +48,13 @@ return array(
 			'no_limit' => 'Full Width',
 			'thin' => 'Narrow',
 		),
+		'thumbnail' => array(
+			'label' => 'Thumbnail',
+			'none' => 'None',
+			'portrait' => 'Portrait',
+			'square' => 'Square',
+			'landscape' => 'Landscape',
+		),
 	),
 	'profile' => array(
 		'_' => 'Profile management',

+ 8 - 0
app/i18n/es/conf.php

@@ -25,6 +25,7 @@ return array(
 		'_' => 'Visualización',
 		'icon' => array(
 			'bottom_line' => 'Línea inferior',
+			'summary' => 'Summary', // TODO - Translation
 			'display_authors' => 'Authors',	// TODO - Translation
 			'entry' => 'Iconos de artículos',
 			'publication_date' => 'Fecha de publicación',
@@ -47,6 +48,13 @@ return array(
 			'no_limit' => 'Sin límite',
 			'thin' => 'Estrecho',
 		),
+		'thumbnail' => array(
+			'label' => 'Thumbnail', // TODO - Translation
+			'none' => 'None', // TODO - Translation
+			'portrait' => 'Portrait', // TODO - Translation
+			'square' => 'Square', // TODO - Translation
+			'landscape' => 'Landscape', // TODO - Translation
+		),
 	),
 	'profile' => array(
 		'_' => 'Administración de perfiles',

+ 8 - 0
app/i18n/fr/conf.php

@@ -25,6 +25,7 @@ return array(
 		'_' => 'Affichage',
 		'icon' => array(
 			'bottom_line' => 'Ligne du bas',
+			'summary' => 'Résumé',
 			'display_authors' => 'Auteurs',
 			'entry' => 'Icônes d’article',
 			'publication_date' => 'Date de publication',
@@ -47,6 +48,13 @@ return array(
 			'no_limit' => 'Pas de limite',
 			'thin' => 'Fine',
 		),
+		'thumbnail' => array(
+			'label' => 'Miniature',
+			'none' => 'Sans',
+			'portrait' => 'Portrait',
+			'square' => 'Carrée',
+			'landscape' => 'Paysage',
+		),
 	),
 	'profile' => array(
 		'_' => 'Gestion du profil',

+ 8 - 0
app/i18n/he/conf.php

@@ -25,6 +25,7 @@ return array(
 		'_' => 'תצוגה',
 		'icon' => array(
 			'bottom_line' => 'שורה תחתונה',
+			'summary' => 'Summary', // TODO - Translation
 			'display_authors' => 'Authors',	// TODO - Translation
 			'entry' => 'סמלילי מאמרים',
 			'publication_date' => 'תאריך הפרסום',
@@ -47,6 +48,13 @@ return array(
 			'no_limit' => 'ללא הגבלה',
 			'thin' => 'צר',
 		),
+		'thumbnail' => array(
+			'label' => 'Thumbnail', // TODO - Translation
+			'none' => 'None', // TODO - Translation
+			'portrait' => 'Portrait', // TODO - Translation
+			'square' => 'Square', // TODO - Translation
+			'landscape' => 'Landscape', // TODO - Translation
+		),
 	),
 	'profile' => array(
 		'_' => 'Profile management',	// TODO - Translation

+ 8 - 0
app/i18n/it/conf.php

@@ -25,6 +25,7 @@ return array(
 		'_' => 'Visualizzazione',
 		'icon' => array(
 			'bottom_line' => 'Barra in fondo',
+			'summary' => 'Summary', // TODO - Translation
 			'display_authors' => 'Authors',	// TODO - Translation
 			'entry' => 'Icone degli articoli',
 			'publication_date' => 'Data di pubblicazione',
@@ -47,6 +48,13 @@ return array(
 			'no_limit' => 'Nessun limite',
 			'thin' => 'Stretto',
 		),
+		'thumbnail' => array(
+			'label' => 'Thumbnail', // TODO - Translation
+			'none' => 'None', // TODO - Translation
+			'portrait' => 'Portrait', // TODO - Translation
+			'square' => 'Square', // TODO - Translation
+			'landscape' => 'Landscape', // TODO - Translation
+		),
 	),
 	'profile' => array(
 		'_' => 'Gestione profili',

+ 8 - 0
app/i18n/kr/conf.php

@@ -25,6 +25,7 @@ return array(
 		'_' => '표시',
 		'icon' => array(
 			'bottom_line' => '하단',
+			'summary' => 'Summary', // TODO - Translation
 			'display_authors' => 'Authors',	// TODO - Translation
 			'entry' => '문서 아이콘',
 			'publication_date' => '발행일',
@@ -47,6 +48,13 @@ return array(
 			'no_limit' => '제한 없음',
 			'thin' => '얇음',
 		),
+		'thumbnail' => array(
+			'label' => 'Thumbnail', // TODO - Translation
+			'none' => 'None', // TODO - Translation
+			'portrait' => 'Portrait', // TODO - Translation
+			'square' => 'Square', // TODO - Translation
+			'landscape' => 'Landscape', // TODO - Translation
+		),
 	),
 	'profile' => array(
 		'_' => '프로필 관리',

+ 8 - 0
app/i18n/nl/conf.php

@@ -25,6 +25,7 @@ return array(
 		'_' => 'Opmaak',
 		'icon' => array(
 			'bottom_line' => 'Onderaan',
+			'summary' => 'Summary', // TODO - Translation
 			'display_authors' => 'Auteurs',
 			'entry' => 'Artikel pictogrammen',
 			'publication_date' => 'Publicatie datum',
@@ -47,6 +48,13 @@ return array(
 			'no_limit' => 'Geen limiet',
 			'thin' => 'Smal',
 		),
+		'thumbnail' => array(
+			'label' => 'Thumbnail', // TODO - Translation
+			'none' => 'None', // TODO - Translation
+			'portrait' => 'Portrait', // TODO - Translation
+			'square' => 'Square', // TODO - Translation
+			'landscape' => 'Landscape', // TODO - Translation
+		),
 	),
 	'profile' => array(
 		'_' => 'Profielbeheer',

+ 8 - 0
app/i18n/oc/conf.php

@@ -25,6 +25,7 @@ return array(
 		'_' => 'Afichatge',
 		'icon' => array(
 			'bottom_line' => 'Linha enbàs',
+			'summary' => 'Summary', // TODO - Translation
 			'display_authors' => 'Autors',
 			'entry' => 'Icònas d’article',
 			'publication_date' => 'Data de publicacion',
@@ -47,6 +48,13 @@ return array(
 			'no_limit' => 'Cap de limit',
 			'thin' => 'Fina',
 		),
+		'thumbnail' => array(
+			'label' => 'Thumbnail', // TODO - Translation
+			'none' => 'None', // TODO - Translation
+			'portrait' => 'Portrait', // TODO - Translation
+			'square' => 'Square', // TODO - Translation
+			'landscape' => 'Landscape', // TODO - Translation
+		),
 	),
 	'profile' => array(
 		'_' => 'Gestion del perfil',

+ 8 - 0
app/i18n/pl/conf.php

@@ -25,6 +25,7 @@ return array(
 		'_' => 'Wyświetlanie',
 		'icon' => array(
 			'bottom_line' => 'Dolny margines',
+			'summary' => 'Summary', // TODO - Translation
 			'display_authors' => 'Autorzy',
 			'entry' => 'Ikony wiadomości',
 			'publication_date' => 'Data publikacji',
@@ -47,6 +48,13 @@ return array(
 			'no_limit' => 'Pełna szerokość',
 			'thin' => 'Wąska',
 		),
+		'thumbnail' => array(
+			'label' => 'Thumbnail', // TODO - Translation
+			'none' => 'None', // TODO - Translation
+			'portrait' => 'Portrait', // TODO - Translation
+			'square' => 'Square', // TODO - Translation
+			'landscape' => 'Landscape', // TODO - Translation
+		),
 	),
 	'profile' => array(
 		'_' => 'Zarządzanie profilem',

+ 8 - 0
app/i18n/pt-br/conf.php

@@ -25,6 +25,7 @@ return array(
 		'_' => 'Exibição',
 		'icon' => array(
 			'bottom_line' => 'Linha inferior',
+			'summary' => 'Summary', // TODO - Translation
 			'display_authors' => 'Autores',
 			'entry' => 'Ícones de artigos',
 			'publication_date' => 'Data da publicação',
@@ -47,6 +48,13 @@ return array(
 			'no_limit' => 'Sem limite',
 			'thin' => 'Fino',
 		),
+		'thumbnail' => array(
+			'label' => 'Thumbnail', // TODO - Translation
+			'none' => 'None', // TODO - Translation
+			'portrait' => 'Portrait', // TODO - Translation
+			'square' => 'Square', // TODO - Translation
+			'landscape' => 'Landscape', // TODO - Translation
+		),
 	),
 	'profile' => array(
 		'_' => 'Gerenciamento de perfil',

+ 8 - 0
app/i18n/ru/conf.php

@@ -25,6 +25,7 @@ return array(
 		'_' => 'Отображение',
 		'icon' => array(
 			'bottom_line' => 'Нижняя линия',
+			'summary' => 'Summary', // TODO - Translation
 			'display_authors' => 'Авторы',
 			'entry' => 'Иконки статей',
 			'publication_date' => 'Дата публикации',
@@ -47,6 +48,13 @@ return array(
 			'no_limit' => 'Во всю ширину',
 			'thin' => 'Узкое',
 		),
+		'thumbnail' => array(
+			'label' => 'Thumbnail', // TODO - Translation
+			'none' => 'None', // TODO - Translation
+			'portrait' => 'Portrait', // TODO - Translation
+			'square' => 'Square', // TODO - Translation
+			'landscape' => 'Landscape', // TODO - Translation
+		),
 	),
 	'profile' => array(
 		'_' => 'Настройки профиля',

+ 8 - 0
app/i18n/sk/conf.php

@@ -25,6 +25,7 @@ return array(
 		'_' => 'Zobrazenie',
 		'icon' => array(
 			'bottom_line' => 'Spodný riadok',
+			'summary' => 'Summary', // TODO - Translation
 			'display_authors' => 'Autori',
 			'entry' => 'Ikony článku',
 			'publication_date' => 'Dátum zverejnenia',
@@ -47,6 +48,13 @@ return array(
 			'no_limit' => 'Bez obmedzenia',
 			'thin' => 'Úzka',
 		),
+		'thumbnail' => array(
+			'label' => 'Thumbnail', // TODO - Translation
+			'none' => 'None', // TODO - Translation
+			'portrait' => 'Portrait', // TODO - Translation
+			'square' => 'Square', // TODO - Translation
+			'landscape' => 'Landscape', // TODO - Translation
+		),
 	),
 	'profile' => array(
 		'_' => 'Správca profilu',

+ 8 - 0
app/i18n/tr/conf.php

@@ -25,6 +25,7 @@ return array(
 		'_' => 'Görünüm',
 		'icon' => array(
 			'bottom_line' => 'Alt çizgi',
+			'summary' => 'Summary', // TODO - Translation
 			'display_authors' => 'Yazarlar',
 			'entry' => 'Makale ikonları',
 			'publication_date' => 'Yayınlama Tarihi',
@@ -47,6 +48,13 @@ return array(
 			'no_limit' => 'Sınırsız',
 			'thin' => 'Zayıf',
 		),
+		'thumbnail' => array(
+			'label' => 'Thumbnail', // TODO - Translation
+			'none' => 'None', // TODO - Translation
+			'portrait' => 'Portrait', // TODO - Translation
+			'square' => 'Square', // TODO - Translation
+			'landscape' => 'Landscape', // TODO - Translation
+		),
 	),
 	'profile' => array(
 		'_' => 'Profil yönetimi',

+ 8 - 0
app/i18n/zh-cn/conf.php

@@ -25,6 +25,7 @@ return array(
 		'_' => '显示',
 		'icon' => array(
 			'bottom_line' => '底栏',
+			'summary' => 'Summary', // TODO - Translation
 			'display_authors' => '作者',
 			'entry' => '文章图标',
 			'publication_date' => '更新日期',
@@ -47,6 +48,13 @@ return array(
 			'no_limit' => '无限制',
 			'thin' => '窄',
 		),
+		'thumbnail' => array(
+			'label' => 'Thumbnail', // TODO - Translation
+			'none' => 'None', // TODO - Translation
+			'portrait' => 'Portrait', // TODO - Translation
+			'square' => 'Square', // TODO - Translation
+			'landscape' => 'Landscape', // TODO - Translation
+		),
 	),
 	'profile' => array(
 		'_' => '用户管理',

+ 26 - 0
app/views/configure/display.phtml

@@ -75,6 +75,27 @@
 			</div>
 		</div>
 
+		<?php $topline_thumbnail = FreshRSS_Context::$user_conf->topline_thumbnail; ?>
+		<div class="form-group">
+			<label class="group-name" for="topline_thumbnail"><?= _t('conf.display.thumbnail.label') ?></label>
+			<div class="group-controls">
+				<select name="topline_thumbnail" id="topline_thumbnail" required="" data-leave-validation="<?= $topline_thumbnail ?>">
+					<option value="none" <?= $topline_thumbnail === 'none' ? 'selected="selected"' : '' ?>>
+						<?= _t('conf.display.thumbnail.none') ?>
+					</option>
+					<option value="portrait" <?= $topline_thumbnail === 'portrait' ? 'selected="selected"' : '' ?>>
+						<?= _t('conf.display.thumbnail.portrait') ?>
+					</option>
+					<option value="square" <?= $topline_thumbnail === 'square' ? 'selected="selected"' : '' ?>>
+						<?= _t('conf.display.thumbnail.square') ?>
+					</option>
+					<option value="landscape" <?= $topline_thumbnail === 'landscape' ? 'selected="selected"' : '' ?>>
+						<?= _t('conf.display.thumbnail.landscape') ?>
+					</option>
+				</select>
+			</div>
+		</div>
+
 		<div class="form-group">
 			<label class="group-name"><?= _t('conf.display.icon.entry') ?></label>
 			<div class="group-controls">
@@ -86,6 +107,7 @@
 							<th title="<?= _t('gen.action.mark_favorite') ?>"><?= _i('bookmark') ?></th>
 							<th><?= _t('conf.display.icon.related_tags') ?></th>
 							<th><?= _t('conf.display.icon.sharing') ?></th>
+							<th><?= _t('conf.display.icon.summary') ?></th>
 							<th><?= _t('conf.display.icon.display_authors') ?></th>
 							<th><?= _t('conf.display.icon.publication_date') ?></th>
 							<th><?= _i('link') ?></th>
@@ -102,6 +124,9 @@
 								data-leave-validation="<?= FreshRSS_Context::$user_conf->topline_favorite ?>"/></td>
 							<td><input type="checkbox" disabled="disabled" /></td>
 							<td><input type="checkbox" disabled="disabled" /></td>
+							<td><input type="checkbox" name="topline_summary" value="1"<?=
+								FreshRSS_Context::$user_conf->topline_summary ? 'checked="checked"' : '' ?>
+								data-leave-validation="<?= FreshRSS_Context::$user_conf->topline_summary ?>"/></td>
 							<td><input type="checkbox" name="topline_display_authors" value="1"<?=
 								FreshRSS_Context::$user_conf->topline_display_authors ? ' checked="checked"' : '' ?>
 								data-leave-validation="<?= FreshRSS_Context::$user_conf->topline_display_authors ?>"/></td>
@@ -125,6 +150,7 @@
 								FreshRSS_Context::$user_conf->bottomline_sharing ? ' checked="checked"' : '' ?>
 								data-leave-validation="<?= FreshRSS_Context::$user_conf->bottomline_sharing ?>"/></td>
 							<td><input type="checkbox" disabled="disabled" /></td>
+							<td><input type="checkbox" disabled="disabled" /></td>
 							<td><input type="checkbox" name="bottomline_date" value="1"<?=
 								FreshRSS_Context::$user_conf->bottomline_date ? ' checked="checked"' : '' ?>
 								data-leave-validation="<?= FreshRSS_Context::$user_conf->bottomline_date ?>"/></td>

+ 23 - 2
app/views/helpers/index/normal/entry_header.phtml

@@ -1,9 +1,12 @@
 <?php
 	$topline_read = FreshRSS_Context::$user_conf->topline_read;
 	$topline_favorite = FreshRSS_Context::$user_conf->topline_favorite;
+	$topline_thumbnail = FreshRSS_Context::$user_conf->topline_thumbnail;
+	$topline_summary = FreshRSS_Context::$user_conf->topline_summary;
 	$topline_display_authors = FreshRSS_Context::$user_conf->topline_display_authors;
 	$topline_date = FreshRSS_Context::$user_conf->topline_date;
 	$topline_link = FreshRSS_Context::$user_conf->topline_link;
+	$lazyload = FreshRSS_Context::$user_conf->lazyload;
 ?><ul class="horizontal-list flux_header"><?php
 	if (FreshRSS_Auth::hasAccess()) {
 		if ($topline_read) {
@@ -29,8 +32,21 @@
 	}
 	?><li class="item website"><a href="<?= _url('index', 'index', 'get', 'f_' . $this->feed->id()) ?>" title="<?= _t('gen.action.filter') ?>">
 		<?php if (FreshRSS_Context::$user_conf->show_favicons): ?><img class="favicon" src="<?= $this->feed->favicon() ?>" alt="✇" loading="lazy" /><?php endif; ?>
-		<span><?= $this->feed->name() ?></span></a></li>
-	<li class="item title" dir="auto"><a target="_blank" rel="noreferrer" href="<?= $this->entry->link() ?>"><?= $this->entry->title() ?><?php
+		<span><?= $this->feed->name() ?></span></a>
+	</li>
+
+	<?php
+	if ($topline_thumbnail !== 'none'):
+	?><li class="item thumbnail <?= $topline_thumbnail ?> <?= $topline_summary ? '' : 'small' ?>"><?php
+		$thumbnail = $this->entry->thumbnail();
+		if ($thumbnail != null):
+			?><img src="<?= htmlspecialchars($thumbnail['url'], ENT_COMPAT, 'UTF-8') ?>"<?= $lazyload ? ' loading="lazy"' : '' ?><?=
+				empty($thumbnail['alt']) ? '' : ' alt="' . htmlspecialchars(strip_tags($thumbnail['alt']), ENT_COMPAT, 'UTF-8') . '"' ?> /><?php
+		endif;
+	?></li><?php
+	endif; ?>
+
+	<li class="item title<?= (($topline_thumbnail !== 'none') || $topline_summary) ? ' multiline' : '' ?>" dir="auto"><a target="_blank" rel="noreferrer" href="<?= $this->entry->link() ?>"><?= $this->entry->title() ?><?php
 		if ($topline_display_authors):
 			?><span class="author"><?php
 			$authors = $this->entry->authors();
@@ -43,6 +59,11 @@
 			}
 			?></span><?php
 		endif;
+		if ($topline_summary):
+			?><div class="summary">
+				<?= mb_substr(strip_tags($this->entry->content()), 0, 500, 'UTF-8') ?>
+			</div><?php
+		endif;
 	?></a></li>
 	<?php if ($topline_date) { ?><li class="item date"><time datetime="<?= $this->entry->machineReadableDate() ?>"><?= $this->entry->date() ?></time>&nbsp;</li><?php } ?>
 	<?php if ($topline_link) { ?><li class="item link"><a target="_blank" rel="noreferrer" href="<?= $this->entry->link() ?>" title="<?=

+ 2 - 0
cli/i18n/ignore/en-us.php

@@ -159,6 +159,8 @@ return array(
 	'conf.archiving.ttl',
 	'conf.display._',
 	'conf.display.icon.bottom_line',
+	'conf.display.icon.thumbnail',
+	'conf.display.icon.summary',
 	'conf.display.icon.display_authors',
 	'conf.display.icon.entry',
 	'conf.display.icon.publication_date',

+ 2 - 0
config-user.default.php

@@ -81,6 +81,8 @@ return array (
 	'show_favicons' => true,
 	'topline_read' => true,
 	'topline_favorite' => true,
+	'topline_thumbnail' => 'none',
+	'topline_summary' => false,
 	'topline_display_authors' => false,
 	'topline_date' => true,
 	'topline_link' => true,

+ 37 - 0
p/themes/Origine-compact/origine-compact.css

@@ -878,6 +878,43 @@ a.btn,
 	font-size: 0.8rem;
 }
 
+.flux .item.thumbnail {
+	padding: 5px;
+	height: 50px;
+}
+
+.flux .item.thumbnail.small {
+	height: 30px;
+}
+
+.flux .item.thumbnail.portrait {
+	width: 38px;
+}
+
+.flux .item.thumbnail.square {
+	width: 50px;
+}
+
+.flux .item.thumbnail.landscape {
+	width: 80px;
+}
+
+.flux .item.thumbnail.portrait.small {
+	width: 20px;
+}
+
+.flux .item.thumbnail.square.small {
+	width: 30px;
+}
+
+.flux .item.thumbnail.landscape.small {
+	width: 40px;
+}
+
+.flux .item.title .summary {
+	max-height: 1.5em;
+}
+
 .flux .website .favicon {
 	padding: 5px;
 }

+ 37 - 0
p/themes/Origine-compact/origine-compact.rtl.css

@@ -878,6 +878,43 @@ a.btn,
 	font-size: 0.8rem;
 }
 
+.flux .item.thumbnail {
+	padding: 5px;
+	height: 50px;
+}
+
+.flux .item.thumbnail.small {
+	height: 30px;
+}
+
+.flux .item.thumbnail.portrait {
+	width: 38px;
+}
+
+.flux .item.thumbnail.square {
+	width: 50px;
+}
+
+.flux .item.thumbnail.landscape {
+	width: 80px;
+}
+
+.flux .item.thumbnail.portrait.small {
+	width: 20px;
+}
+
+.flux .item.thumbnail.square.small {
+	width: 30px;
+}
+
+.flux .item.thumbnail.landscape.small {
+	width: 40px;
+}
+
+.flux .item.title .summary {
+	max-height: 1.5em;
+}
+
 .flux .website .favicon {
 	padding: 5px;
 }

+ 55 - 0
p/themes/base-theme/template.css

@@ -723,11 +723,66 @@ input[type="search"] {
 	position: absolute;
 }
 
+.flux:not(.current):hover .item.title.multiline {
+	position: initial;
+}
+
 .flux .item.title a {
 	color: #000;
 	text-decoration: none;
 }
 
+.flux .item.thumbnail {
+	line-height: 0;
+	padding: 10px;
+	height: 80px;
+}
+
+.flux .item.thumbnail.small {
+	height: 40px;
+}
+
+.flux .item.thumbnail.portrait {
+	width: 60px;
+}
+
+.flux .item.thumbnail.square {
+	width: 80px;
+}
+
+.flux .item.thumbnail.landscape {
+	width: 128px;
+}
+
+.flux .item.thumbnail.portrait.small {
+	width: 30px;
+}
+
+.flux .item.thumbnail.square.small {
+	width: 40px;
+}
+
+.flux .item.thumbnail.landscape.small {
+	width: 64px;
+}
+
+.flux .item.thumbnail img {
+	width: 100%;
+	height: 100%;
+	object-fit: cover;
+}
+
+.flux .item.title .summary {
+	max-height: 3em;
+	color: #666;
+	font-size: 0.9em;
+	line-height: 1.5em;
+	font-weight: normal;
+	white-space: initial;
+	overflow: hidden;
+	text-overflow: ellipsis;
+}
+
 .flux .item.title .author {
 	padding-left: 1rem;
 	color: #555;

+ 55 - 0
p/themes/base-theme/template.rtl.css

@@ -723,11 +723,66 @@ input[type="search"] {
 	position: absolute;
 }
 
+.flux:not(.current):hover .item.title.multiline {
+	position: initial;
+}
+
 .flux .item.title a {
 	color: #000;
 	text-decoration: none;
 }
 
+.flux .item.thumbnail {
+	line-height: 0;
+	padding: 10px;
+	height: 80px;
+}
+
+.flux .item.thumbnail.small {
+	height: 40px;
+}
+
+.flux .item.thumbnail.portrait {
+	width: 60px;
+}
+
+.flux .item.thumbnail.square {
+	width: 80px;
+}
+
+.flux .item.thumbnail.landscape {
+	width: 128px;
+}
+
+.flux .item.thumbnail.portrait.small {
+	width: 30px;
+}
+
+.flux .item.thumbnail.square.small {
+	width: 40px;
+}
+
+.flux .item.thumbnail.landscape.small {
+	width: 64px;
+}
+
+.flux .item.thumbnail img {
+	width: 100%;
+	height: 100%;
+	object-fit: cover;
+}
+
+.flux .item.title .summary {
+	max-height: 3em;
+	color: #666;
+	font-size: 0.9em;
+	line-height: 1.5em;
+	font-weight: normal;
+	white-space: initial;
+	overflow: hidden;
+	text-overflow: ellipsis;
+}
+
 .flux .item.title .author {
 	padding-right: 1rem;
 	color: #555;