Browse Source

konflitki

Schunka Calcifer 1 year ago
parent
commit
c5167a8113
50 changed files with 1313 additions and 1272 deletions
  1. 6 275
      classes/article.php
  2. 1 1
      classes/ccache.php
  3. 3 3
      classes/counters.php
  4. 3 3
      classes/dlg.php
  5. 114 384
      classes/feeds.php
  6. 218 12
      classes/handler/public.php
  7. 1 1
      classes/pluginhost.php
  8. 2 2
      classes/pref/prefs.php
  9. 1 0
      classes/rssutils.php
  10. 9 8
      css/cdm.less
  11. 108 67
      css/default.css
  12. 0 0
      css/default.css.map
  13. 4 1
      css/defines.less
  14. 43 5
      css/dijit_basic.less
  15. 0 8
      css/dijit_light.less
  16. 1 5
      css/prefs.less
  17. 78 45
      css/tt-rss.less
  18. 3 9
      css/utility.less
  19. 10 4
      include/controls.php
  20. 5 22
      include/functions.php
  21. 5 4
      index.php
  22. 32 2
      js/AppBase.js
  23. 59 49
      js/Article.js
  24. 0 29
      js/ArticleCache.js
  25. 6 34
      js/Feeds.js
  26. 168 14
      js/Headlines.js
  27. 5 2
      js/PluginHost.js
  28. 5 1
      js/PrefFeedTree.js
  29. 5 8
      js/PrefFilterTree.js
  30. 1 1
      js/PrefLabelTree.js
  31. 17 2
      js/common.js
  32. 2 0
      js/prefs.js
  33. 10 8
      js/tt-rss.js
  34. 0 0
      lib/flat-ttrss/flat_combined_dark.css
  35. 2 2
      plugins/af_psql_trgm/init.php
  36. 6 9
      plugins/bookmarklets/init.php
  37. BIN
      plugins/close_button/button.png
  38. 7 4
      plugins/close_button/init.php
  39. 1 1
      plugins/mailto/init.php
  40. 5 3
      plugins/no_title_counters/init.js
  41. 0 2
      plugins/note/note.js
  42. 1 1
      plugins/share/init.php
  43. 10 6
      plugins/share/share.js
  44. 3 3
      plugins/shorten_expanded/init.css
  45. 26 16
      plugins/shorten_expanded/init.js
  46. BIN
      plugins/vf_shared/share.png
  47. 4 4
      prefs.php
  48. 185 139
      themes/night.css
  49. 0 0
      themes/night.css.map
  50. 138 73
      themes/night.less

+ 6 - 275
classes/article.php

@@ -27,6 +27,7 @@ class Article extends Handler_Protected {
 		}
 	}
 
+	/*
 	function view() {
 		$id = clean($_REQUEST["id"]);
 		$cids = explode(",", clean($_REQUEST["cids"]));
@@ -63,8 +64,9 @@ class Article extends Handler_Protected {
 		}
 
 		print json_encode($articles);
-	}
+	} */
 
+	/*
 	private function catchupArticleById($id, $cmode) {
 
 		if ($cmode == 0) {
@@ -86,6 +88,7 @@ class Article extends Handler_Protected {
 		$feed_id = $this->getArticleFeed($id);
 		CCache::update($feed_id, $_SESSION["uid"]);
 	}
+	*/
 
 	static function create_published_article($title, $url, $content, $labels_str,
 			$owner_uid) {
@@ -516,9 +519,9 @@ class Article extends Handler_Protected {
 			}
 
 			if (count($entries_inline) > 0) {
-				$rv .= "<hr clear='both'/>";
+				//$rv .= "<hr clear='both'/>";
 				foreach ($entries_inline as $entry) { $rv .= $entry; };
-				$rv .= "<hr clear='both'/>";
+				$rv .= "<br clear='both'/>";
 			}
 
 			$rv .= "<div class=\"attachments\" dojoType=\"dijit.form.DropDownButton\">".
@@ -549,278 +552,6 @@ class Article extends Handler_Protected {
 		return $rv;
 	}
 
-	static function format_article($id, $mark_as_read = true, $zoom_mode = false, $owner_uid = false) {
-		if (!$owner_uid) $owner_uid = $_SESSION["uid"];
-
-		$rv = array();
-
-		$rv['id'] = $id;
-
-		/* we can figure out feed_id from article id anyway, why do we
-		 * pass feed_id here? let's ignore the argument :(*/
-
-		$pdo = Db::pdo();
-
-		$sth = $pdo->prepare("SELECT feed_id FROM ttrss_user_entries
-			WHERE ref_id = ?");
-		$sth->execute([$id]);
-		$row = $sth->fetch();
-
-		$feed_id = (int) $row["feed_id"];
-
-		$rv['feed_id'] = $feed_id;
-
-		//if (!$zoom_mode) { print "<article id='$id'><![CDATA["; };
-
-		if ($mark_as_read) {
-			$sth = $pdo->prepare("UPDATE ttrss_user_entries
-				SET unread = false,last_read = NOW()
-				WHERE ref_id = ? AND owner_uid = ?");
-			$sth->execute([$id, $owner_uid]);
-
-			CCache::update($feed_id, $owner_uid);
-		}
-
-		$sth = $pdo->prepare("SELECT id,title,link,content,feed_id,comments,int_id,lang,
-			".SUBSTRING_FOR_DATE."(updated,1,16) as updated,
-			(SELECT site_url FROM ttrss_feeds WHERE id = feed_id) as site_url,
-			(SELECT title FROM ttrss_feeds WHERE id = feed_id) as feed_title,
-			(SELECT hide_images FROM ttrss_feeds WHERE id = feed_id) as hide_images,
-			(SELECT always_display_enclosures FROM ttrss_feeds WHERE id = feed_id) as always_display_enclosures,
-			num_comments,
-			tag_cache,
-			author,
-			guid,
-			orig_feed_id,
-			note
-			FROM ttrss_entries,ttrss_user_entries
-			WHERE	id = ? AND ref_id = id AND owner_uid = ?");
-		$sth->execute([$id, $owner_uid]);
-
-		if ($line = $sth->fetch()) {
-
-			$line["tags"] = Article::get_article_tags($id, $owner_uid, $line["tag_cache"]);
-			unset($line["tag_cache"]);
-
-			$line["content"] = sanitize($line["content"],
-				$line['hide_images'],
-				$owner_uid, $line["site_url"], false, $line["id"]);
-
-			foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_RENDER_ARTICLE) as $p) {
-				$line = $p->hook_render_article($line);
-			}
-
-			$line['content'] = rewrite_cached_urls($line['content']);
-
-			$num_comments = (int) $line["num_comments"];
-			$entry_comments = "";
-
-			if ($num_comments > 0) {
-				if ($line["comments"]) {
-					$comments_url = htmlspecialchars($line["comments"]);
-				} else {
-					$comments_url = htmlspecialchars($line["link"]);
-				}
-				$entry_comments = "<a class=\"comments\"
-					target='_blank' rel=\"noopener noreferrer\" href=\"$comments_url\">$num_comments ".
-					_ngettext("comment", "comments", $num_comments)."</a>";
-
-			} else {
-				if ($line["comments"] && $line["link"] != $line["comments"]) {
-					$entry_comments = "<a class=\"comments\" target='_blank' rel=\"noopener noreferrer\" href=\"".
-						htmlspecialchars($line["comments"])."\">".__("comments")."</a>";
-				}
-			}
-
-			$enclosures = self::get_article_enclosures($line["id"]);
-
-			if ($zoom_mode) {
-				header("Content-Type: text/html");
-				$rv['content'] .= "<!DOCTYPE html>
-						<html><head>
-						<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/>
-						<title>".$line["title"]."</title>".
-						stylesheet_tag("css/default.css")."
-						<link rel=\"shortcut icon\" type=\"image/png\" href=\"images/favicon.png\">
-						<link rel=\"icon\" type=\"image/png\" sizes=\"72x72\" href=\"images/favicon-72px.png\">";
-
-				$rv['content'] .= "<meta property=\"og:title\" content=\"".htmlspecialchars($line["title"])."\"/>\n";
-				$rv['content'] .= "<meta property=\"og:site_name\" content=\"".htmlspecialchars($line["feed_title"])."\"/>\n";
-				$rv['content'] .= "<meta property=\"og:description\" content=\"".
-					htmlspecialchars(truncate_string(strip_tags($line["content"]), 500, "..."))."\"/>\n";
-
-				$rv['content'] .= "</head>";
-
-				$og_image = false;
-
-				foreach ($enclosures as $enc) {
-					if (strpos($enc["content_type"], "image/") !== FALSE) {
-						$og_image = $enc["content_url"];
-						break;
-					}
-				}
-
-				if (!$og_image) {
-					$tmpdoc = new DOMDocument();
-
-					if (@$tmpdoc->loadHTML(mb_substr($line["content"], 0, 131070))) {
-						$tmpxpath = new DOMXPath($tmpdoc);
-						$first_img = $tmpxpath->query("//img")->item(0);
-
-						if ($first_img) {
-							$og_image = $first_img->getAttribute("src");
-						}
-					}
-				}
-
-				if ($og_image) {
-					$rv['content'] .= "<meta property=\"og:image\" content=\"" . htmlspecialchars($og_image) . "\"/>";
-				}
-
-				$rv['content'] .= "<body class='flat ttrss_utility ttrss_zoom'>";
-			}
-
-			$rv['content'] .= "<div class='post post-$id'>";
-
-			/* header */
-
-			$rv['content'] .= "<div class='header'>";
-			$rv['content'] .= "<div class='row'>"; # row
-
-			//$entry_author = $line["author"] ? " - " . $line["author"] : "";
-			$parsed_updated = make_local_datetime($line["updated"], true,
-				$owner_uid, true);
-
-			if ($line["link"]) {
-				$rv['content'] .= "<div class='title'><a target='_blank' rel='noopener noreferrer'
-					title=\"".htmlspecialchars($line['title'])."\"
-					href=\"" .htmlspecialchars($line["link"]) . "\">" .	$line["title"] . "</a></div>";
-			} else {
-				$rv['content'] .= "<div class='title'>" . $line["title"] . "</div>";
-			}
-
-			if (!$zoom_mode)
-				$rv['content'] .= "<div class='date'>$parsed_updated<br/></div>";
-
-			$rv['content'] .= "</div>"; # row
-
-			$rv['content'] .= "<div class='row'>"; # row
-
-			/* left buttons */
-
-			$rv['content'] .= "<div class='buttons left'>";
-			foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_ARTICLE_LEFT_BUTTON) as $p) {
-				$rv['content'] .= $p->hook_article_left_button($line);
-			}
-			$rv['content'] .= "</div>";
-
-			/* comments */
-
-			$rv['content'] .= "<div class='comments'>$entry_comments</div>";
-			$rv['content'] .= "<div class='author'>".$line['author']."</div>";
-
-			/* tags */
-
-			$tags_str = Article::format_tags_string($line["tags"], $id);
-			$tags_str_full = join(", ", $line["tags"]);
-
-			if (!$tags_str_full) $tags_str_full = __("no tags");
-
-			$rv['content'] .= "<i class='material-icons'>label_outline</i><div>";
-
-			if (!$zoom_mode) {
-				$rv['content'] .= "<span id=\"ATSTR-$id\">$tags_str</span>
-					<a title=\"".__('Edit tags for this article')."\"
-					href=\"#\" onclick=\"Article.editTags($id)\">(+)</a>";
-
-				$rv['content'] .= "<div dojoType=\"dijit.Tooltip\"
-					id=\"ATSTRTIP-$id\" connectId=\"ATSTR-$id\"
-					position=\"below\">$tags_str_full</div>";
-
-			} else {
-				$tags_str = strip_tags($tags_str);
-				$rv['content'] .= "<span id=\"ATSTR-$id\">$tags_str</span>";
-			}
-
-			$rv['content'] .= "</div>";
-
-			/* buttons */
-
-			$rv['content'] .= "<div class='buttons right'>";
-			foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_ARTICLE_BUTTON) as $p) {
-				$rv['content'] .= $p->hook_article_button($line);
-			}
-			$rv['content'] .= "</div>";
-
-			$rv['content'] .= "</div>"; # row
-
-			$rv['content'] .= "</div>"; # header
-
-			/* note */
-
-			$rv['content'] .= "<div id=\"POSTNOTE-$id\">";
-			if ($line['note']) {
-				$rv['content'] .= Article::format_article_note($id, $line['note'], !$zoom_mode);
-			}
-			$rv['content'] .= "</div>";
-
-			/* content */
-
-			$lang = $line['lang'] ? $line['lang'] : "en";
-			$rv['content'] .= "<div class=\"content\" lang=\"$lang\">";
-
-			/* originally from */
-
-			if (!$zoom_mode && $line["orig_feed_id"]) {
-
-				$of_sth = $pdo->prepare("SELECT * FROM ttrss_archived_feeds
-					WHERE id = ? AND owner_uid = ?");
-				$of_sth->execute([$line["orig_feed_id"], $owner_uid]);
-
-				if ($tmp_line = $of_sth->fetch()) {
-
-					$rv['content'] .= "<div clear='both'>";
-					$rv['content'] .= __("Originally from:");
-
-					$rv['content'] .= "&nbsp;";
-
-					$rv['content'] .= "<a target='_blank' rel='noopener noreferrer'
-						href=' " . htmlspecialchars($tmp_line['site_url']) . "'>" .
-						$tmp_line['title'] . "</a>";
-
-					$rv['content'] .= "&nbsp;";
-
-					$rv['content'] .= "<a target='_blank' rel='noopener noreferrer' href='" . htmlspecialchars($tmp_line['feed_url']) . "'>";
-
-					$rv['content'] .= "</div>";
-				}
-			}
-
-			/* content body */
-
-			$rv['content'] .= $line["content"];
-
-			if (!$zoom_mode) {
-				$rv['content'] .= Article::format_article_enclosures($id,
-					$line["always_display_enclosures"],
-					$line["content"],
-					$line["hide_images"]);
-			}
-
-			$rv['content'] .= "</div>"; # content
-
-			$rv['content'] .= "</div>"; # post
-
-		}
-
-		foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_FORMAT_ARTICLE) as $p) {
-			$rv['content'] = $p->hook_format_article($rv['content'], $line, $zoom_mode);
-		}
-
-		return $rv;
-
-	}
-
 	static function get_article_tags($id, $owner_uid = 0, $tag_cache = false) {
 
 		$a_id = $id;

+ 1 - 1
classes/ccache.php

@@ -141,7 +141,7 @@ class CCache {
 
 			$sth = $pdo->prepare("SELECT SUM(value) AS sv
 				FROM ttrss_counters_cache, ttrss_feeds
-				WHERE id = feed_id AND
+				WHERE ttrss_feeds.id = feed_id AND
 				(cat_id = :cat OR (:cat = 0 AND cat_id IS NULL)) AND
 				ttrss_counters_cache.owner_uid = :uid AND
 				ttrss_feeds.owner_uid = :uid");

+ 3 - 3
classes/counters.php

@@ -24,11 +24,11 @@ class Counters {
 
 		$pdo = DB::pdo();
 
-		$sth = $pdo->prepare("SELECT id AS cat_id, value AS unread,
+		$sth = $pdo->prepare("SELECT ttrss_feed_categories.id AS cat_id, value AS unread,
 			(SELECT COUNT(id) FROM ttrss_feed_categories AS c2
 				WHERE c2.parent_cat = ttrss_feed_categories.id) AS num_children
 			FROM ttrss_feed_categories, ttrss_cat_counters_cache
-			WHERE ttrss_cat_counters_cache.feed_id = id AND
+			WHERE ttrss_cat_counters_cache.feed_id = ttrss_feed_categories.id AND
 			ttrss_cat_counters_cache.owner_uid = ttrss_feed_categories.owner_uid AND
 			ttrss_feed_categories.owner_uid = ?");
 		$sth->execute([$_SESSION['uid']]);
@@ -172,7 +172,7 @@ class Counters {
 			FROM ttrss_feeds, ttrss_counters_cache
 			WHERE ttrss_feeds.owner_uid = ?
 				AND ttrss_counters_cache.owner_uid = ttrss_feeds.owner_uid
-				AND ttrss_counters_cache.feed_id = id");
+				AND ttrss_counters_cache.feed_id = ttrss_feeds.id");
 		$sth->execute([$_SESSION['uid']]);
 
 		while ($line = $sth->fetch()) {

+ 3 - 3
classes/dlg.php

@@ -40,7 +40,7 @@ class Dlg extends Handler_Protected {
 
 		print __("Your Public OPML URL is:");
 
-		print "<div class='panel'>";
+		print "<div class='panel text-center'>";
 		print "<a id='pub_opml_url' href='$url_path' target='_blank'>$url_path</a>";
 		print "</div>";
 
@@ -91,7 +91,7 @@ class Dlg extends Handler_Protected {
 	}
 
 	function printTagCloud() {
-		print "<div class='panel'>";
+		print "<div class='panel text-center'>";
 
 		// from here: http://www.roscripts.com/Create_tag_cloud-71.html
 
@@ -167,7 +167,7 @@ class Dlg extends Handler_Protected {
 
 		print "<div>".T_sprintf("%s can be accessed via the following secret URL:", $feed_title)."</div>";
 
-		print "<div class='panel'>";
+		print "<div class='panel text-center'>";
 		print "<a id='gen_feed_url' href='$url_path' target='_blank'>$url_path</a>";
 		print "</div>";
 

+ 114 - 384
classes/feeds.php

@@ -113,18 +113,14 @@ class Feeds extends Handler_Protected {
 	}
 
 	private function format_headlines_list($feed, $method, $view_mode, $limit, $cat_view,
-					$offset, $vgr_last_feed = false,
-					$override_order = false, $include_children = false, $check_first_id = false,
-					$skip_first_id_check = false) {
+					$offset, $override_order = false, $include_children = false, $check_first_id = false,
+					$skip_first_id_check = false, $order_by = false) {
 
 		$disable_cache = false;
 
 		$reply = array();
 
 		$rgba_cache = array();
-
-		$timing_info = microtime(true);
-
 		$topmost_article_ids = array();
 
 		if (!$offset) $offset = 0;
@@ -161,8 +157,6 @@ class Feeds extends Handler_Protected {
 			$disable_cache = true;
 		}
 
-		if ($_REQUEST["debug"]) $timing_info = print_checkpoint("H0", $timing_info);
-
 		if (!$cat_view && is_numeric($feed) && $feed < PLUGIN_FEED_BASE_INDEX && $feed > LABEL_BASE_INDEX) {
 			$handler = PluginHost::getInstance()->get_feed_handler(
 				PluginHost::feed_to_pfeed_id($feed));
@@ -178,7 +172,8 @@ class Feeds extends Handler_Protected {
 					"owner_uid" => $_SESSION["uid"],
 					"filter" => false,
 					"since_id" => 0,
-					"include_children" => $include_children);
+					"include_children" => $include_children,
+					"order_by" => $order_by);
 
 				$qfh_ret = $handler->get_headlines(PluginHost::feed_to_pfeed_id($feed),
 					$options);
@@ -197,7 +192,8 @@ class Feeds extends Handler_Protected {
 				"offset" => $offset,
 				"include_children" => $include_children,
 				"check_first_id" => $check_first_id,
-				"skip_first_id_check" => $skip_first_id_check
+				"skip_first_id_check" => $skip_first_id_check,
+                "order_by" => $order_by
 			);
 
 			$qfh_ret = $this->queryFeedHeadlines($params);
@@ -205,8 +201,6 @@ class Feeds extends Handler_Protected {
 
 		$vfeed_group_enabled = get_pref("VFEED_GROUP_BY_FEED") && $feed != -6;
 
-		if ($_REQUEST["debug"]) $timing_info = print_checkpoint("H1", $timing_info);
-
 		$result = $qfh_ret[0]; // this could be either a PDO query result or a -1 if first id changed
 		$feed_title = $qfh_ret[1];
 		$feed_site_url = $qfh_ret[2];
@@ -216,8 +210,7 @@ class Feeds extends Handler_Protected {
 		$highlight_words = $qfh_ret[5];
 		$reply['first_id'] = $qfh_ret[6];
 		$reply['search_query'] = [$search, $search_language];
-
-		$vgroup_last_feed = $vgr_last_feed;
+		$reply['vfeed_group_enabled'] = $vfeed_group_enabled;
 
 		$reply['toolbar'] = $this->format_headline_subtoolbar($feed_site_url,
 			$feed_title,
@@ -230,42 +223,44 @@ class Feeds extends Handler_Protected {
 			}
 		}
 
-		$reply['content'] = '';
+		$reply['content'] = [];
 
 		$headlines_count = 0;
 
-        $lnum = $offset;
-        $num_unread = 0;
-        if ($_REQUEST["debug"]) $timing_info = print_checkpoint("PS", $timing_info);
-
         if (is_object($result)) {
-
-			while ($line = $result->fetch()) {
+			while ($line = $result->fetch(PDO::FETCH_ASSOC)) {
 
 				++$headlines_count;
 
-				$line["content_preview"] =  "&mdash; " . truncate_string(strip_tags($line["content"]), 250);
-
-				foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_QUERY_HEADLINES) as $p) {
-					$line = $p->hook_query_headlines($line, 250, false);
-				}
+				if (!get_pref('SHOW_CONTENT_PREVIEW')) {
+					$line["content_preview"] = "";
+				} else {
+					$line["content_preview"] =  "&mdash; " . truncate_string(strip_tags($line["content"]), 250);
 
-				if (get_pref('SHOW_CONTENT_PREVIEW')) {
-					$content_preview =  $line["content_preview"];
-				}
+					foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_QUERY_HEADLINES) as $p) {
+						$line = $p->hook_query_headlines($line, 250, false);
+					}
+                }
 
 				$id = $line["id"];
-				$feed_id = $line["feed_id"];
-				$label_cache = $line["label_cache"];
-				$labels = false;
+
+				// frontend doesn't expect pdo returning booleans as strings on mysql
+				if (DB_TYPE == "mysql") {
+					foreach (["unread", "marked", "published"] as $k) {
+						$line[$k] = $line[$k] === "1";
+					}
+				}
 
 				// normalize archived feed
-				if ($feed_id === null) {
-					$feed_id = 0;
+				if ($line['feed_id'] === null) {
+					$line['feed_id'] = 0;
 					$line["feed_title"] = __("Archived articles");
 				}
 
-				$mouseover_attrs = "onmouseover='Article.mouseIn($id)' onmouseout='Article.mouseOut($id)'";
+				$feed_id = $line["feed_id"];
+
+				$label_cache = $line["label_cache"];
+				$labels = false;
 
 				if ($label_cache) {
 					$label_cache = json_decode($label_cache, true);
@@ -284,361 +279,112 @@ class Feeds extends Handler_Protected {
 				$labels_str .= Article::format_article_labels($labels);
 				$labels_str .= "</span>";
 
+				$line["labels"] = $labels_str;
+
 				if (count($topmost_article_ids) < 3) {
 					array_push($topmost_article_ids, $id);
 				}
 
-				$class = "";
-
-				if ($line["unread"]) {
-					$class .= " Unread";
-					++$num_unread;
-				}
-
-				$class .= $line["marked"] ? " marked" : "";
-				$marked_pic = "<i class=\"marked-pic marked-$id material-icons\" onclick='Headlines.toggleMark($id)'>star</i>";
-
-				$class .= $line["published"] ? " published" : "";
-                $published_pic = "<i class=\"pub-pic pub-$id material-icons\" onclick='Headlines.togglePub($id)'>rss_feed</i>";
-
-				$updated_fmt = make_local_datetime($line["updated"], false, false, false, true);
-				$date_entered_fmt = T_sprintf("Imported at %s",
-					make_local_datetime($line["date_entered"], false));
-
-				$score = $line["score"];
-
-				$score_pic = "<i class='material-icons icon-score' title='$score' 
-                       data-score='$score' onclick='Article.setScore($id, this)'>" .
-                        get_score_pic($score) . "</i>";
-                $score_class = get_score_class($score);
-
-				$entry_author = $line["author"];
-
-				if ($entry_author) {
-					$entry_author = " &mdash; $entry_author";
-				}
-
-				if (feeds::feedHasIcon($feed_id)) {
-					$feed_icon_img = "<img class=\"icon\" src=\"".ICONS_URL."/$feed_id.ico\" alt=\"\">";
-				} else {
-					$feed_icon_img = "<i class='icon-no-feed material-icons'>rss_feed</i>";
-				}
-
-				$entry_site_url = $line["site_url"];
-
-				//setting feed headline background color, needs to change text color based on dark/light
-				$fav_color = $line['favicon_avg_color'];
-
-				require_once "colors.php";
-
-				if ($fav_color && $fav_color != 'fail') {
-					if (!isset($rgba_cache[$feed_id])) {
-						$rgba_cache[$feed_id] = join(",", _color_unpack($fav_color));
-					}
-				}
-
-				if (!get_pref('COMBINED_DISPLAY_MODE')) {
-
-					if ($vfeed_group_enabled) {
-						if ($feed_id != $vgroup_last_feed) {
-
-							$vgroup_last_feed = $feed_id;
-
-							$vf_catchup_link = "<a class='catchup' onclick='Feeds.catchupFeedInGroup($feed_id);' href='#'>".__('mark feed as read')."</a>";
-
-							$reply['content'] .= "<div data-feed-id='$feed_id' class='feed-title'>".
-								"<div style='float : right'>$feed_icon_img</div>".
-								"<a class='title' href=\"#\" onclick=\"Feeds.open({feed:$feed_id})\">".
-								$line["feed_title"]."</a>
-                            $vf_catchup_link</div>";
-						}
-					}
-
-					$reply['content'] .= "<div class='hl $class $score_class' data-orig-feed-id='$feed_id' data-article-id='$id' id='RROW-$id' $mouseover_attrs>";
-
-					$reply['content'] .= "<div class='left'>";
-
-					$reply['content'] .= "<input dojoType=\"dijit.form.CheckBox\"
-                        type=\"checkbox\" onclick=\"Headlines.onRowChecked(this)\"
-                        class='rchk'>";
-
-					$reply['content'] .= "$marked_pic";
-					$reply['content'] .= "$published_pic";
-
-					$reply['content'] .= "</div>";
-
-					$reply['content'] .= "<div onclick='return Headlines.click(event, $id)'
-                    class=\"title\"><span data-article-id=\"$id\" class='hl-content hlMenuAttach'>";
-					$reply['content'] .= "<a class=\"title\"
-                    href=\"" . htmlspecialchars($line["link"]) . "\"
-                    onclick=\"\">" .
-						truncate_string($line["title"], 200);
-
-					if (get_pref('SHOW_CONTENT_PREVIEW')) {
-						$reply['content'] .= "<span class=\"preview\">" . $line["content_preview"] . "</span>";
-					}
-
-					$reply['content'] .= "</a></span>";
-
-					$reply['content'] .= $labels_str;
-
-					$reply['content'] .= "</div>";
-
-					if (!$vfeed_group_enabled) {
-						if (@$line["feed_title"]) {
-							$rgba = @$rgba_cache[$feed_id];
-
-							$reply['content'] .= "<span class=\"feed\"><a style=\"background : rgba($rgba, 0.3)\" href=\"#\" onclick=\"Feeds.open({feed:$feed_id})\">".
-								truncate_string($line["feed_title"],30)."</a></span>";
-						}
-					}
-
-
-					$reply['content'] .= "<span class=\"updated\">";
-
-					$reply['content'] .= "<div title='$date_entered_fmt'>$updated_fmt</div>
-                    </span>";
-
-					$reply['content'] .= "<div class=\"right\">";
-
-					$reply['content'] .= $score_pic;
-
-					if (!$vfeed_group_enabled) {
-						$reply['content'] .= "<span onclick=\"Feeds.open({feed:$feed_id})\"
-                        style=\"cursor : pointer\"
-                        title=\"".htmlspecialchars($line['feed_title'])."\">
-                        $feed_icon_img</span>";
-					}
-
-					$reply['content'] .= "</div>";
-					$reply['content'] .= "</div>";
-
-				} else {
-
-					if ($line["tag_cache"])
-						$tags = explode(",", $line["tag_cache"]);
-					else
-						$tags = false;
-
-					$line["content"] = sanitize($line["content"],
-						$line['hide_images'], false, $entry_site_url, $highlight_words, $line["id"]);
-
-					foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_RENDER_ARTICLE_CDM) as $p) {
-						$line = $p->hook_render_article_cdm($line);
-					}
-
-					$line['content'] = rewrite_cached_urls($line['content']);
-
-					if ($vfeed_group_enabled && $line["feed_title"]) {
-						if ($feed_id != $vgroup_last_feed) {
+				if (!$line["feed_title"]) $line["feed_title"] = "";
 
-							$vgroup_last_feed = $feed_id;
+                $line["buttons_left"] = "";
+                foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_ARTICLE_LEFT_BUTTON) as $p) {
+                    $line["buttons_left"] .= $p->hook_article_left_button($line);
+                }
 
-							$vf_catchup_link = "<a class='catchup' onclick='Feeds.catchupFeedInGroup($feed_id);' href='#'>".__('mark feed as read')."</a>";
+                $line["buttons"] = "";
+                foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_ARTICLE_BUTTON) as $p) {
+                    $line["buttons"] .= $p->hook_article_button($line);
+                }
 
-							$feed_icon_src = Feeds::getFeedIcon($feed_id);
-							$feed_icon_img = "<img class=\"icon\" src=\"$feed_icon_src\">";
+                $line["content"] = sanitize($line["content"],
+                    $line['hide_images'], false, $line["site_url"], $highlight_words, $line["id"]);
 
-							$reply['content'] .= "<div data-feed-id='$feed_id' class='feed-title'>".
-								"<div style=\"float : right\">$feed_icon_img</div>".
-								"<a href=\"#\" class='title' onclick=\"Feeds.open({feed:$feed_id})\">".
-								$line["feed_title"]."</a> $vf_catchup_link</div>";
+                foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_RENDER_ARTICLE_CDM) as $p) {
+                    $line = $p->hook_render_article_cdm($line);
+                }
 
-						}
-					}
-
-                    $content_encoded = htmlspecialchars($line["content"]);
-
-					$expanded_class = get_pref("CDM_EXPANDED") ? "expanded" : "expandable";
-                    $tmp_content = "<div class=\"cdm $expanded_class $score_class $class\"
-                        id=\"RROW-$id\" data-content=\"$content_encoded\" data-article-id='$id' data-orig-feed-id='$feed_id' $mouseover_attrs>";
-
-					$tmp_content .= "<div class=\"header\">";
-					$tmp_content .= "<div class=\"left\">";
+                $line['content'] = rewrite_cached_urls($line['content']);
 
-					$tmp_content .= "<input dojoType=\"dijit.form.CheckBox\"
-                        type=\"checkbox\" onclick=\"Headlines.onRowChecked(this)\"
-                        class='rchk'>";
+                if ($line['note'])
+                    $line['note'] = Article::format_article_note($id, $line['note']);
+                else
+                    $line['note'] = "";
 
-					$tmp_content .= "$marked_pic";
-					$tmp_content .= "$published_pic";
-
-					$tmp_content .= "</div>";
-
-					if ($highlight_words && count($highlight_words) > 0) {
-						foreach ($highlight_words as $word) {
-						    $word = preg_quote($word, "/");
-
-							$line["title"] = preg_replace("/($word)/i",
-								"<span class=\"highlight\">$1</span>", $line["title"]);
-						}
-					}
-
-					// data-article-id included for context menu
-					$tmp_content .= "<span
-                    onclick=\"return Headlines.click(event, $id);\"
-                    data-article-id=\"$id\"
-                    class=\"titleWrap hlMenuAttach\">
-                    <a class=\"title\"
-                    title=\"".htmlspecialchars($line["title"])."\"
-                    target=\"_blank\" rel=\"noopener noreferrer\" href=\"".
-						htmlspecialchars($line["link"])."\">".
-						$line["title"] .
-						"</a> <span class=\"author\">$entry_author</span>";
-
-					$tmp_content .= $labels_str;
-
-					if (!get_pref("CDM_EXPANDED")) {
-						$tmp_content .= "<span class='collapse'>
-                            <i class=\"material-icons\" onclick=\"return Article.cdmUnsetActive(event)\"
+                if (!get_pref("CDM_EXPANDED")) {
+                    $line["cdm_excerpt"] = "<span class='collapse'>
+                        <i class='material-icons' onclick='return Article.cdmUnsetActive(event)'
                             title=\"" . __("Collapse article") . "\">remove_circle</i></span>";
 
-						if (get_pref('SHOW_CONTENT_PREVIEW')) {
-							$tmp_content .= "<span class='excerpt'>" . $line["content_preview"] . "</span>";
-						}
-					}
-
-					$tmp_content .= "</span>";
-
-					if (!$vfeed_group_enabled) {
-						if (@$line["feed_title"]) {
-							$rgba = @$rgba_cache[$feed_id];
-
-							$tmp_content .= "<div class=\"feed\">
-                            <a href=\"#\" style=\"background-color: rgba($rgba,0.3)\"
-                            onclick=\"Feeds.open({feed:$feed_id})\">".
-								truncate_string($line["feed_title"],30)."</a>
-                        </div>";
-						}
-					}
-
-					$tmp_content .= "<span class='updated' title='$date_entered_fmt'>$updated_fmt</span>";
-
-					$tmp_content .= "<div class='right'>";
-					$tmp_content .= "$score_pic";
-
-					if (!get_pref("VFEED_GROUP_BY_FEED")) {
-						$tmp_content .= "<span style=\"cursor : pointer\"
-                        title=\"".htmlspecialchars($line["feed_title"])."\"
-                        onclick=\"Feeds.open({feed:$feed_id})\">$feed_icon_img</span>";
-					}
-					$tmp_content .= "</div>"; //score wrapper2
-
-					$tmp_content .= "</div>"; //header
-
-					$tmp_content .= "<div class=\"content\" onclick=\"return Headlines.click(event, $id, true);\">";
-
-					$tmp_content .= "<div id=\"POSTNOTE-$id\">";
-					if ($line['note']) {
-						$tmp_content .= Article::format_article_note($id, $line['note']);
-					}
-					$tmp_content .= "</div>"; //POSTNOTE
-
-					if (!$line['lang']) $line['lang'] = 'en';
-
-					// this is filled from RROW data-content
-					$tmp_content .= "<div class=\"content-inner\" lang=\"".$line['lang']."\">
-                        <img src='images/indicator_white.gif'>
-                    </div>";
-					$tmp_content .= "<div class=\"intermediate\">";
-
-					if ($line["orig_feed_id"]) {
+                    if (get_pref('SHOW_CONTENT_PREVIEW')) {
+                        $line["cdm_excerpt"] .= "<span class='excerpt'>" . $line["content_preview"] . "</span>";
+                    }
+                }
 
-						$ofgh = $this->pdo->prepare("SELECT * FROM ttrss_archived_feeds
-                        WHERE id = ? AND owner_uid = ?");
-						$ofgh->execute([$line["orig_feed_id"], $_SESSION['uid']]);
+                $line["enclosures"] = Article::format_article_enclosures($id, $line["always_display_enclosures"],
+                    $line["content"], $line["hide_images"]);
 
-						if ($tmp_line = $ofgh->fetch()) {
+                if ($line["orig_feed_id"]) {
 
-							$tmp_content .= "<div clear='both'>";
-							$tmp_content .= __("Originally from:");
+                    $ofgh = $this->pdo->prepare("SELECT * FROM ttrss_archived_feeds
+                    WHERE id = ? AND owner_uid = ?");
+                    $ofgh->execute([$line["orig_feed_id"], $_SESSION['uid']]);
 
-							$tmp_content .= "&nbsp;";
+                    if ($tmp_line = $ofgh->fetch()) {
+                        $line["orig_feed"] = [ $tmp_line["title"], $tmp_line["site_url"], $tmp_line["feed_url"] ];
+                    }
+                }
 
-							$tmp_content .= "<a target='_blank' rel='noopener noreferrer'
-                            href=' " . htmlspecialchars($tmp_line['site_url']) . "'>" .
-								$tmp_line['title'] . "</a>";
+				$line["updated_long"] = make_local_datetime($line["updated"],true);
+				$line["updated"] = make_local_datetime($line["updated"], false, false, false, true);
 
-							$tmp_content .= "&nbsp;";
 
-							$tmp_content .= "<a target='_blank' rel='noopener noreferrer' href='" . htmlspecialchars($tmp_line['feed_url']) . "'>";
-
-							$tmp_content .= "</div>";
-						}
-					}
-
-
-					$always_display_enclosures = $line["always_display_enclosures"];
-					$tmp_content .= Article::format_article_enclosures($id, $always_display_enclosures,
-						$line["content"], $line["hide_images"]);
-
-					$tmp_content .= "</div>"; // cdmIntermediate
-
-					$tmp_content .= "<div class=\"footer\" onclick=\"event.stopPropagation()\">";
-
-					foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_ARTICLE_LEFT_BUTTON) as $p) {
-						$tmp_content .= $p->hook_article_left_button($line);
-					}
-
-					$tags_str = Article::format_tags_string($tags, $id);
+				$line['imported'] = T_sprintf("Imported at %s",
+					make_local_datetime($line["date_entered"], false));
 
-					$tmp_content .= "<div class='left'>";
+				$score = $line["score"];
 
-					$tmp_content .= "<i class='material-icons'>label_outline</i>
-                        <span id=\"ATSTR-$id\">$tags_str</span>
-                        <a title=\"".__('Edit tags for this article')."\"
-                        href=\"#\" onclick=\"Article.editTags($id)\">(+)</a>";
+				$line["score_pic"] = get_score_pic($score);
+                $line["score_class"] = get_score_class($score);
 
-					$num_comments = (int) $line["num_comments"];
-					$entry_comments = "";
+				if ($line["tag_cache"])
+					$tags = explode(",", $line["tag_cache"]);
+				else
+					$tags = false;
 
-					if ($num_comments > 0) {
-						if ($line["comments"]) {
-							$comments_url = htmlspecialchars($line["comments"]);
-						} else {
-							$comments_url = htmlspecialchars($line["link"]);
-						}
-						$entry_comments = "<a class=\"comments\"
-                        target='_blank' rel='noopener noreferrer' href=\"$comments_url\">$num_comments ".
-							_ngettext("comment", "comments", $num_comments)."</a>";
+				$line["tags_str"] = Article::format_tags_string($tags, $id);
 
-					} else {
-						if ($line["comments"] && $line["link"] != $line["comments"]) {
-							$entry_comments = "<a class=\"comments\" target='_blank' rel='noopener noreferrer' href=\"".htmlspecialchars($line["comments"])."\">".__("comments")."</a>";
-						}
-					}
+				if (feeds::feedHasIcon($feed_id)) {
+					$line['feed_icon'] = "<img class=\"icon\" src=\"".ICONS_URL."/$feed_id.ico\" alt=\"\">";
+				} else {
+					$line['feed_icon'] = "<i class='icon-no-feed material-icons'>rss_feed</i>";
+				}
 
-					if ($entry_comments) $tmp_content .= "&nbsp;($entry_comments)";
+			    //setting feed headline background color, needs to change text color based on dark/light
+				$fav_color = $line['favicon_avg_color'];
 
-					$tmp_content .= "</div>";
-					$tmp_content .= "<div class='right'>";
+				require_once "colors.php";
 
-					foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_ARTICLE_BUTTON) as $p) {
-						$tmp_content .= $p->hook_article_button($line);
+				if ($fav_color && $fav_color != 'fail') {
+					if (!isset($rgba_cache[$feed_id])) {
+						$rgba_cache[$feed_id] = join(",", _color_unpack($fav_color)) . ",0.3";
 					}
 
-					$tmp_content .= "</div>"; // buttons
+					$line['favicon_avg_color_rgba'] = $rgba_cache[$feed_id];
+				}
 
-					$tmp_content .= "</div>"; // cdm footer
-					$tmp_content .= "</div>"; // cdmContent
-					$tmp_content .= "</div>"; // RROW.cdm
+				/* we don't need those */
 
-					foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_FORMAT_ARTICLE_CDM) as $p) {
-						$tmp_content = $p->hook_format_article_cdm($tmp_content, $line);
-					}
+                foreach (["date_entered", "guid", "last_published", "last_marked", "tag_cache", "favicon_avg_color", "uuid", "label_cache"] as $k)
+                    unset($line[$k]);
 
-					$reply['content'] .= $tmp_content;
-				}
-
-				++$lnum;
+				array_push($reply['content'], $line);
 			}
         }
 
-        if ($_REQUEST["debug"]) $timing_info = print_checkpoint("PE", $timing_info);
-
 		if (!$headlines_count) {
 
-			if (!is_numeric($result)) {
+			if (is_object($result)) {
 
 				switch ($view_mode) {
 					case "unread":
@@ -692,10 +438,7 @@ class Feeds extends Handler_Protected {
 			}
 		}
 
-		if ($_REQUEST["debug"]) $timing_info = print_checkpoint("H2", $timing_info);
-
-		return array($topmost_article_ids, $headlines_count, $feed, $disable_cache,
-			$vgroup_last_feed, $reply);
+		return array($topmost_article_ids, $headlines_count, $feed, $disable_cache, $reply);
 	}
 
 	function catchupAll() {
@@ -707,12 +450,8 @@ class Feeds extends Handler_Protected {
 	}
 
 	function view() {
-		$timing_info = microtime(true);
-
 		$reply = array();
 
-		if ($_REQUEST["debug"]) $timing_info = print_checkpoint("0", $timing_info);
-
 		$feed = $_REQUEST["feed"];
 		$method = $_REQUEST["m"];
 		$view_mode = $_REQUEST["view_mode"];
@@ -720,7 +459,6 @@ class Feeds extends Handler_Protected {
 		@$cat_view = $_REQUEST["cat"] == "true";
 		@$next_unread_feed = $_REQUEST["nuf"];
 		@$offset = $_REQUEST["skip"];
-		@$vgroup_last_feed = $_REQUEST["vgrlf"];
 		$order_by = $_REQUEST["order_by"];
 		$check_first_id = $_REQUEST["fid"];
 
@@ -786,7 +524,7 @@ class Feeds extends Handler_Protected {
 			$sth->execute([$feed, $_SESSION['uid']]);
 		}
 
-		$reply['headlines'] = array();
+		$reply['headlines'] = [];
 
 		$override_order = false;
 		$skip_first_id_check = false;
@@ -804,22 +542,13 @@ class Feeds extends Handler_Protected {
 			break;
 		}
 
-		if ($_REQUEST["debug"]) $timing_info = print_checkpoint("04", $timing_info);
-
 		$ret = $this->format_headlines_list($feed, $method,
 			$view_mode, $limit, $cat_view, $offset,
-			$vgroup_last_feed, $override_order, true, $check_first_id, $skip_first_id_check);
+			$override_order, true, $check_first_id, $skip_first_id_check, $order_by);
 
-		//$topmost_article_ids = $ret[0];
 		$headlines_count = $ret[1];
-		/* $returned_feed = $ret[2]; */
 		$disable_cache = $ret[3];
-		$vgroup_last_feed = $ret[4];
-
-		//$reply['headlines']['content'] =& $ret[5]['content'];
-		//$reply['headlines']['toolbar'] =& $ret[5]['toolbar'];
-
-		$reply['headlines'] = $ret[5];
+		$reply['headlines'] = $ret[4];
 
 		if (!$next_unread_feed)
 			$reply['headlines']['id'] = $feed;
@@ -828,14 +557,10 @@ class Feeds extends Handler_Protected {
 
 		$reply['headlines']['is_cat'] = (bool) $cat_view;
 
-		if ($_REQUEST["debug"]) $timing_info = print_checkpoint("05", $timing_info);
-
-		$reply['headlines-info'] = array("count" => (int) $headlines_count,
-						"vgroup_last_feed" => $vgroup_last_feed,
-						"disable_cache" => (bool) $disable_cache);
-
-		if ($_REQUEST["debug"]) $timing_info = print_checkpoint("30", $timing_info);
+		$reply['headlines-info'] = ["count" => (int) $headlines_count,
+            						"disable_cache" => (bool) $disable_cache];
 
+		// this is parsed by handleRpcJson() on first viewfeed() to set cdm expanded, etc
 		$reply['runtime-info'] = make_runtime_info();
 
 		print json_encode($reply);
@@ -878,7 +603,6 @@ class Feeds extends Handler_Protected {
 		$reply['headlines']['content'] .= "</span></p>";
 
 		$reply['headlines-info'] = array("count" => 0,
-			"vgroup_last_feed" => '',
 			"unread" => 0,
 			"disable_cache" => true);
 
@@ -895,7 +619,6 @@ class Feeds extends Handler_Protected {
 		$reply['headlines']['content'] = "<div class='whiteBox'>". $error . "</div>";
 
 		$reply['headlines-info'] = array("count" => 0,
-			"vgroup_last_feed" => '',
 			"unread" => 0,
 			"disable_cache" => true);
 
@@ -1710,6 +1433,7 @@ class Feeds extends Handler_Protected {
 		$start_ts = isset($params["start_ts"]) ? $params["start_ts"] : false;
 		$check_first_id = isset($params["check_first_id"]) ? $params["check_first_id"] : false;
 		$skip_first_id_check = isset($params["skip_first_id_check"]) ? $params["skip_first_id_check"] : false;
+		$order_by = isset($params["order_by"]) ? $params["order_by"] : false;
 
 		$ext_tables_part = "";
 		$limit_query_part = "";
@@ -1939,10 +1663,12 @@ class Feeds extends Handler_Protected {
 		if (is_numeric($feed)) {
 			// proper override_order applied above
 			if ($vfeed_query_part && !$ignore_vfeed_group && get_pref('VFEED_GROUP_BY_FEED', $owner_uid)) {
+                $yyiw_desc = $order_by == "date_reverse" ? "" : "desc";
+
 				if (!$override_order) {
-					$order_by = "ttrss_feeds.title, ".$order_by;
+					$order_by = "yyiw $yyiw_desc, ttrss_feeds.title, ".$order_by;
 				} else {
-					$order_by = "ttrss_feeds.title, ".$override_order;
+					$order_by = "yyiw $yyiw_desc, ttrss_feeds.title, ".$override_order;
 				}
 			}
 
@@ -1972,8 +1698,10 @@ class Feeds extends Handler_Protected {
 
 			if (DB_TYPE == "pgsql") {
 				$sanity_interval_qpart = "date_entered >= NOW() - INTERVAL '1 hour' AND";
+				$yyiw_qpart = "to_char(date_entered, 'IYYY-IW') AS yyiw";
 			} else {
 				$sanity_interval_qpart = "date_entered >= DATE_SUB(NOW(), INTERVAL 1 hour) AND";
+				$yyiw_qpart = "date_format(date_entered, '%Y-%u') AS yyiw";
 			}
 
 			if (!$search && !$skip_first_id_check) {
@@ -1981,6 +1709,7 @@ class Feeds extends Handler_Protected {
 				$query = "SELECT DISTINCT
 							ttrss_feeds.title,
 							date_entered,
+                            $yyiw_qpart,
 							guid,
 							ttrss_entries.id,
 							ttrss_entries.title,
@@ -2019,6 +1748,7 @@ class Feeds extends Handler_Protected {
 
 			$query = "SELECT DISTINCT
 						date_entered,
+                        $yyiw_qpart,
 						guid,
 						ttrss_entries.id,ttrss_entries.title,
 						updated,

+ 218 - 12
classes/handler/public.php

@@ -139,18 +139,27 @@ class Handler_Public extends Handler {
 
 				$enclosures = Article::get_article_enclosures($line["id"]);
 
-				foreach ($enclosures as $e) {
-					$type = htmlspecialchars($e['content_type']);
-					$url = htmlspecialchars($e['content_url']);
-					$length = $e['duration'] ? $e['duration'] : 1;
+				if (count($enclosures) > 0) {
+					foreach ($enclosures as $e) {
+						$type = htmlspecialchars($e['content_type']);
+						$url = htmlspecialchars($e['content_url']);
+						$length = $e['duration'] ? $e['duration'] : 1;
 
-					$tpl->setVariable('ARTICLE_ENCLOSURE_URL', $url, true);
-					$tpl->setVariable('ARTICLE_ENCLOSURE_TYPE', $type, true);
-					$tpl->setVariable('ARTICLE_ENCLOSURE_LENGTH', $length, true);
+						$tpl->setVariable('ARTICLE_ENCLOSURE_URL', $url, true);
+						$tpl->setVariable('ARTICLE_ENCLOSURE_TYPE', $type, true);
+						$tpl->setVariable('ARTICLE_ENCLOSURE_LENGTH', $length, true);
 
-					$tpl->addBlock('enclosure');
+						$tpl->addBlock('enclosure');
+					}
+				} else {
+					$tpl->setVariable('ARTICLE_ENCLOSURE_URL', null, true);
+					$tpl->setVariable('ARTICLE_ENCLOSURE_TYPE', null, true);
+					$tpl->setVariable('ARTICLE_ENCLOSURE_LENGTH', null, true);
 				}
 
+				$tpl->setVariable('ARTICLE_OG_IMAGE',
+                        $this->get_article_image($enclosures, $line['content'], $feed_site_url), true);
+
 				$tpl->addBlock('entry');
 			}
 
@@ -300,16 +309,213 @@ class Handler_Public extends Handler {
 			$id = $row["ref_id"];
 			$owner_uid = $row["owner_uid"];
 
-			$article = Article::format_article($id, false, true, $owner_uid);
-
-			print_r($article['content']);
+			print $this->format_article($id, $owner_uid);
 
 		} else {
+			header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found");
 			print "Article not found.";
 		}
 
 	}
 
+	private function get_article_image($enclosures, $content, $site_url) {
+	    $og_image = false;
+
+		foreach ($enclosures as $enc) {
+			if (strpos($enc["content_type"], "image/") !== FALSE) {
+				$og_image = $enc["content_url"];
+				break;
+			}
+		}
+
+		if (!$og_image) {
+			$tmpdoc = new DOMDocument();
+
+			if (@$tmpdoc->loadHTML(mb_substr($content, 0, 131070))) {
+				$tmpxpath = new DOMXPath($tmpdoc);
+				$first_img = $tmpxpath->query("//img")->item(0);
+
+				if ($first_img) {
+					$og_image = $first_img->getAttribute("src");
+				}
+			}
+		}
+
+		return rewrite_relative_url($site_url, $og_image);
+    }
+
+	private function format_article($id, $owner_uid) {
+
+		$pdo = Db::pdo();
+
+		$sth = $pdo->prepare("SELECT id,title,link,content,feed_id,comments,int_id,lang,
+			".SUBSTRING_FOR_DATE."(updated,1,16) as updated,
+			(SELECT site_url FROM ttrss_feeds WHERE id = feed_id) as site_url,
+			(SELECT title FROM ttrss_feeds WHERE id = feed_id) as feed_title,
+			(SELECT hide_images FROM ttrss_feeds WHERE id = feed_id) as hide_images,
+			(SELECT always_display_enclosures FROM ttrss_feeds WHERE id = feed_id) as always_display_enclosures,
+			num_comments,
+			tag_cache,
+			author,
+			guid,
+			orig_feed_id,
+			note
+			FROM ttrss_entries,ttrss_user_entries
+			WHERE	id = ? AND ref_id = id AND owner_uid = ?");
+		$sth->execute([$id, $owner_uid]);
+
+		$rv = '';
+
+		if ($line = $sth->fetch()) {
+
+			$line["tags"] = Article::get_article_tags($id, $owner_uid, $line["tag_cache"]);
+			unset($line["tag_cache"]);
+
+			$line["content"] = sanitize($line["content"],
+				$line['hide_images'],
+				$owner_uid, $line["site_url"], false, $line["id"]);
+
+			foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_RENDER_ARTICLE) as $p) {
+				$line = $p->hook_render_article($line);
+			}
+
+			$line['content'] = rewrite_cached_urls($line['content']);
+
+			$num_comments = (int) $line["num_comments"];
+			$entry_comments = "";
+
+			if ($num_comments > 0) {
+				if ($line["comments"]) {
+					$comments_url = htmlspecialchars($line["comments"]);
+				} else {
+					$comments_url = htmlspecialchars($line["link"]);
+				}
+				$entry_comments = "<a class=\"comments\"
+					target='_blank' rel=\"noopener noreferrer\" href=\"$comments_url\">$num_comments ".
+					_ngettext("comment", "comments", $num_comments)."</a>";
+
+			} else {
+				if ($line["comments"] && $line["link"] != $line["comments"]) {
+					$entry_comments = "<a class=\"comments\" target='_blank' rel=\"noopener noreferrer\" href=\"".
+						htmlspecialchars($line["comments"])."\">".__("comments")."</a>";
+				}
+			}
+
+			$enclosures = Article::get_article_enclosures($line["id"]);
+
+            header("Content-Type: text/html");
+
+            $rv .= "<!DOCTYPE html>
+                    <html><head>
+                    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/>
+                    <title>".$line["title"]."</title>".
+                    stylesheet_tag("css/default.css")."
+                    <link rel=\"shortcut icon\" type=\"image/png\" href=\"images/favicon.png\">
+                    <link rel=\"icon\" type=\"image/png\" sizes=\"72x72\" href=\"images/favicon-72px.png\">";
+
+            $rv .= "<meta property=\"og:title\" content=\"".htmlspecialchars($line["title"])."\"/>\n";
+            $rv .= "<meta property=\"og:site_name\" content=\"".htmlspecialchars($line["feed_title"])."\"/>\n";
+            $rv .= "<meta property=\"og:description\" content=\"".
+                htmlspecialchars(truncate_string(strip_tags($line["content"]), 500, "..."))."\"/>\n";
+
+            $rv .= "</head>";
+
+            $og_image = $this->get_article_image($enclosures, $line['content'], $line["site_url"]);
+
+            if ($og_image) {
+                $rv .= "<meta property=\"og:image\" content=\"" . htmlspecialchars($og_image) . "\"/>";
+            }
+
+            $rv .= "<body class='flat ttrss_utility ttrss_zoom'>";
+			$rv .= "<div class='post post-$id'>";
+
+			/* header */
+
+			$rv .= "<div class='header'>";
+			$rv .= "<div class='row'>"; # row
+
+			//$entry_author = $line["author"] ? " - " . $line["author"] : "";
+			$parsed_updated = make_local_datetime($line["updated"], true,
+				$owner_uid, true);
+
+			if ($line["link"]) {
+				$rv .= "<div class='title'><a target='_blank' rel='noopener noreferrer'
+					title=\"".htmlspecialchars($line['title'])."\"
+					href=\"" .htmlspecialchars($line["link"]) . "\">" .	$line["title"] . "</a></div>";
+			} else {
+				$rv .= "<div class='title'>" . $line["title"] . "</div>";
+			}
+
+            $rv .= "<div class='date'>$parsed_updated<br/></div>";
+
+			$rv .= "</div>"; # row
+
+			$rv .= "<div class='row'>"; # row
+
+			/* left buttons */
+
+			$rv .= "<div class='buttons left'>";
+			foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_ARTICLE_LEFT_BUTTON) as $p) {
+				$rv .= $p->hook_article_left_button($line);
+			}
+			$rv .= "</div>";
+
+			/* comments */
+
+			$rv .= "<div class='comments'>$entry_comments</div>";
+			$rv .= "<div class='author'>".$line['author']."</div>";
+
+			/* tags */
+
+			$tags_str = Article::format_tags_string($line["tags"], $id);
+
+			$rv .= "<i class='material-icons'>label_outline</i><div>";
+
+            $tags_str = strip_tags($tags_str);
+			$rv .= "<span id=\"ATSTR-$id\">$tags_str</span>";
+
+			$rv .= "</div>";
+
+			/* buttons */
+
+			$rv .= "<div class='buttons right'>";
+			foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_ARTICLE_BUTTON) as $p) {
+				$rv .= $p->hook_article_button($line);
+			}
+			$rv .= "</div>";
+
+			$rv .= "</div>"; # row
+
+			$rv .= "</div>"; # header
+
+			/* content */
+
+			$lang = $line['lang'] ? $line['lang'] : "en";
+			$rv .= "<div class=\"content\" lang=\"$lang\">";
+
+			/* content body */
+
+			$rv .= $line["content"];
+
+            /* $rv .= Article::format_article_enclosures($id,
+                $line["always_display_enclosures"],
+                $line["content"],
+                $line["hide_images"]); */
+
+			$rv .= "</div>"; # content
+
+			$rv .= "</div>"; # post
+
+		}
+
+		foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_FORMAT_ARTICLE) as $p) {
+			$rv = $p->hook_format_article($rv, $line, true);
+		}
+
+		return $rv;
+
+	}
+
 	function rss() {
 		$feed = clean($_REQUEST["id"]);
 		$key = clean($_REQUEST["key"]);
@@ -404,7 +610,7 @@ class Handler_Public extends Handler {
 
 				?>
 
-				<table height='100%' width='100%'><tr><td colspan='2'>
+				<table height='100%' width='100%' class="panel"><tr><td colspan='2'>
 				<h1><?php echo __("Share with Tiny Tiny RSS") ?></h1>
 				</td></tr>
 

+ 1 - 1
classes/pluginhost.php

@@ -52,7 +52,7 @@ class PluginHost {
 	const HOOK_MAIN_TOOLBAR_BUTTON = 32;
 	const HOOK_ENCLOSURE_ENTRY = 33;
 	const HOOK_FORMAT_ARTICLE = 34;
-	const HOOK_FORMAT_ARTICLE_CDM = 35;
+	const HOOK_FORMAT_ARTICLE_CDM = 35; /* RIP */
 	const HOOK_FEED_BASIC_INFO = 36;
 	const HOOK_SEND_LOCAL_FILE = 37;
 	const HOOK_UNSUBSCRIBE_FEED = 38;

+ 2 - 2
classes/pref/prefs.php

@@ -552,10 +552,10 @@ class Pref_Prefs extends Handler_Protected {
 
 				$themes = array_merge(glob("themes/*.php"), glob("themes/*.css"), glob("themes.local/*.css"));
 				$themes = array_map("basename", $themes);
-				$themes = array_filter($themes, "theme_valid");
+				$themes = array_filter($themes, "theme_exists");
 				asort($themes);
 
-				if (!theme_valid($value)) $value = "default.php";
+				if (!theme_exists($value)) $value = "default.php";
 
 				print "<select name='$pref_name' id='$pref_name' dojoType='dijit.form.Select'>";
 

+ 1 - 0
classes/rssutils.php

@@ -923,6 +923,7 @@ class RSSUtils {
 					$entry_ref_id = $ref_id;
 
 					if (RSSUtils::find_article_filter($article_filters, "filter")) {
+						Debug::log("article is filtered out, nothing to do.");
 						$pdo->commit();
 						continue;
 					}

+ 9 - 8
css/cdm.less

@@ -67,7 +67,8 @@
 	}
 
 	.intermediate {
-		margin : 10px;
+		margin-top : 10px;
+		margin-left : 10px;
 	}
 
 	.content-inner {
@@ -99,7 +100,7 @@
 	}
 
 	.footer {
-		border: 0px solid #ddd;
+		border: 0px solid @border-default;
 		border-bottom-width: 1px;
 	}
 
@@ -147,6 +148,10 @@ div.cdm.active div.content {
 	color : black;
 }
 
+div.cdm.vgrlf .feed {
+	display : none;
+}
+
 .cdm {
 	div.feed-title {
 		border: 0px solid @color-link;
@@ -213,7 +218,7 @@ div#floatingTitle {
 	top : 0px;
 	right : 0px;
 	left : 0px;
-	border: 0px solid #ddd;
+	border: 0px solid @border-default;
 	border-bottom-width: 1px;
 	background : white;
 	color : @default-text;
@@ -301,10 +306,6 @@ div#floatingTitle {
 		white-space : normal;
 	}
 
-	img.score-pic {
-		display : none;
-	}
-
 	.feed-title {
 		> * {
 			display : table-cell;
@@ -336,7 +337,7 @@ div#floatingTitle.Unread a.title {
 
 .cdm.expandable {
 	background-color : @color-panel-bg;
-	border: 0px solid #ddd;
+	border: 0px solid @border-default;
 	border-bottom-width: 1px;
 
 	> hr {

+ 108 - 67
css/default.css

@@ -10,8 +10,8 @@ body.ttrss_prefs,
   margin: 0;
 }
 body.ttrss_main {
-  background: white;
-  color: black;
+  background: #ffffff;
+  color: #000000;
   font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
   font-size: 14px;
   overflow: hidden;
@@ -26,9 +26,9 @@ body.ttrss_main div.post {
 body.ttrss_main div.post div.header {
   padding: 5px;
   color: #909090;
-  border: 0px solid #ddd;
+  border: 0px solid #dddddd;
   border-bottom-width: 1px;
-  background: #f0f0f0;
+  background: #f5f5f5;
 }
 body.ttrss_main div.post div.header .left,
 body.ttrss_main div.post div.header .right {
@@ -80,6 +80,15 @@ body.ttrss_main div.post div.content iframe {
   min-width: 50%;
   max-width: 98%;
 }
+body.ttrss_main .inline-player {
+  display: flex;
+}
+body.ttrss_main .inline-player > * {
+  align-self: center;
+}
+body.ttrss_main .inline-player audio {
+  margin-right: 8px;
+}
 body.ttrss_main .article-note {
   background-color: #fff7d5;
   margin: 5px;
@@ -180,7 +189,7 @@ body.ttrss_main .notify.notify_error i.icon-close {
   color: white;
 }
 body.ttrss_main .hl {
-  border: 0px solid #ddd;
+  border: 0px solid #dddddd;
   border-bottom-width: 1px;
   transition: color 0.2s, background 0.2s;
   display: flex;
@@ -218,6 +227,12 @@ body.ttrss_main .hl div.title {
   overflow: hidden;
   text-overflow: ellipsis;
 }
+body.ttrss_main .hl span.author {
+  white-space: nowrap;
+  color: #555555;
+  font-size: 11px;
+  font-weight: normal;
+}
 body.ttrss_main .hl div.right {
   text-align: right;
 }
@@ -259,6 +274,9 @@ body.ttrss_main .hl a.title.high,
 body.ttrss_main .hl span.hl-content.high .preview {
   color: #00aa00;
 }
+body.ttrss_main .hl.vgrlf .feed {
+  display: none;
+}
 body.ttrss_main .hl.Unread {
   background: white;
 }
@@ -322,7 +340,7 @@ body.ttrss_main .dijitContentPane pre {
 body.ttrss_main .alert {
   padding: 8px 35px 8px 14px;
   margin-bottom: 10px;
-  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
+  /* text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); */
   background-color: #fcf8e3;
   border: 1px solid #fbeed5;
   border-radius: 4px;
@@ -385,12 +403,12 @@ body.ttrss_main span.preview {
 }
 body.ttrss_main .hl.Selected .hlLabelRef,
 body.ttrss_main .hl.active .hlLabelRef {
-  color: #063064;
+  color: #000000;
 }
 body.ttrss_main span.hlLabelRef {
   background-color: #fff7d5;
   font-size: 8px;
-  color: #063064;
+  color: #000000;
   font-weight: normal;
   margin-left: 2px;
   padding: 1px 4px 1px 4px;
@@ -405,7 +423,7 @@ body.ttrss_main i.pub-pic {
   color: #ccc;
 }
 body.ttrss_main div.errorExplained {
-  border: 1px solid #ddd;
+  border: 1px solid #dddddd;
   margin: 5px 0px 5px 0px;
   padding: 5px;
 }
@@ -414,7 +432,7 @@ body.ttrss_main ul.browseFeedList {
   width: 100%;
   overflow: auto;
   border-width: 0px 1px 1px 1px;
-  border-color: #ddd;
+  border-color: #dddddd;
   border-style: solid;
   margin: 0px 0px 5px 0px;
   background-color: white;
@@ -444,7 +462,7 @@ body.ttrss_main .noborder {
   border-width: 0px;
 }
 body.ttrss_main #overlay {
-  background: white;
+  background: #ffffff;
   left: 0;
   top: 0;
   height: 100%;
@@ -466,14 +484,14 @@ body.ttrss_main div.whiteBox {
   text-align: center;
   padding: 1em 1em 0px 1em;
   font-size: 11px;
-  border: 0px solid #ddd;
+  border: 0px solid #dddddd;
   border-bottom-width: 1px;
 }
 body.ttrss_main div.autocomplete {
   position: absolute;
   width: 250px;
-  background-color: white;
-  border: 1px solid #778899;
+  background-color: #ffffff;
+  border: 1px solid #dddddd;
   margin: 0px;
   padding: 0px;
 }
@@ -483,7 +501,7 @@ body.ttrss_main div.autocomplete ul {
   padding: 0px;
 }
 body.ttrss_main div.autocomplete ul li.selected {
-  background-color: #fff7d5;
+  background-color: #e6e6e6;
 }
 body.ttrss_main div.autocomplete ul li {
   list-style-type: none;
@@ -548,7 +566,7 @@ body.ttrss_main div#cmdline {
   font-size: 11px;
   color: #555555;
   font-weight: bold;
-  background-color: white;
+  background-color: #ffffff;
   border: 1px solid #257aa7;
   padding: 3px 5px 3px 5px;
   z-index: 5;
@@ -575,7 +593,7 @@ body.ttrss_main #content-wrap {
 }
 body.ttrss_main #feeds-holder {
   padding: 0px;
-  border: 0px solid #ddd;
+  border: 0px solid #dddddd;
   overflow: hidden;
   background: #f5f5f5;
   box-shadow: inset -1px 0px 2px -1px rgba(0, 0, 0, 0.1);
@@ -588,9 +606,9 @@ body.ttrss_main #feeds-holder #feedTree {
   font-family: "Segoe UI", Ubuntu, "Helvetica Neue", Helvetica, Arial, sans-serif;
 }
 body.ttrss_main #feeds-holder #feedTree .counterNode.aux {
-  background: #f0f0f0;
-  color: #999;
-  border-color: #f0f0f0;
+  background: #f5f5f5;
+  color: #6f6f6f;
+  border-color: #dcdcdc;
 }
 body.ttrss_main #feeds-holder #feedTree .counterNode {
   font-weight: bold;
@@ -622,16 +640,13 @@ body.ttrss_main #feeds-holder #feedTree .dijitTreeRow .dijitTreeLabel.Unread {
 body.ttrss_main #feeds-holder #feedTree .dijitTreeRow.Error .dijitTreeLabel {
   color: red;
 }
-body.ttrss_main #feeds-holder #feedTree .dijitTreeRow.UpdatesDisabled .dijitTreeLabel {
-  color: #909090;
-}
 body.ttrss_main #feeds-holder #feedTree .dijitTreeNode .dijitTreeRow {
   border: 1px solid transparent;
 }
 body.ttrss_main #feeds-holder #feedTree .dijitTreeNode .dijitTreeRowSelected {
   box-shadow: -1px 0px 2px -1px rgba(0, 0, 0, 0.1);
-  border-color: #ddd transparent;
-  background: white;
+  border-color: #dddddd transparent;
+  background: #ffffff;
   color: #333;
 }
 body.ttrss_main #feeds-holder #feedTree .icon {
@@ -671,7 +686,7 @@ body.ttrss_main #headlines-wrap-inner {
 }
 body.ttrss_main #headlines-frame {
   padding: 0px;
-  border: 0px #ddd;
+  border: 0px #dddddd;
   margin-top: 0px;
   -webkit-overflow-scrolling: touch;
   -webkit-transform: translateZ(0);
@@ -680,7 +695,7 @@ body.ttrss_main #headlines-frame {
 body.ttrss_main #headlines-frame div.feed-title {
   border: 0px solid #257aa7;
   border-bottom-width: 1px;
-  padding: 5px 3px 5px 5px;
+  padding: 5px 8px;
 }
 body.ttrss_main #headlines-frame div.feed-title a.title {
   color: #555555;
@@ -704,7 +719,7 @@ body.ttrss_main #toolbar-frame {
 }
 body.ttrss_main #toolbar-frame #toolbar {
   background: white;
-  border: 0px solid #ddd;
+  border: 0px solid #dddddd;
   border-bottom-width: 1px;
   padding-left: 4px;
   height: 32px;
@@ -756,6 +771,7 @@ body.ttrss_main #toolbar-frame #toolbar #selected_prompt {
   font-style: italic;
   text-align: right;
   margin-right: 4px;
+  color: #257aa7;
 }
 @media (max-width: 992px) {
   body.ttrss_main #toolbar-frame #toolbar #selected_prompt {
@@ -775,7 +791,7 @@ body.ttrss_main #header {
 }
 body.ttrss_main #content-insert {
   padding: 0px;
-  border-color: #ddd;
+  border-color: #dddddd;
   border-width: 0px;
   line-height: 1.5;
   overflow: auto;
@@ -799,18 +815,20 @@ body.ttrss_main .player {
   margin: 0px 2px 0px 2px;
   width: 50px;
   text-align: center;
-  background: white;
+  background: #ffffff;
 }
 body.ttrss_main .player.playing {
   color: #00c000;
   border-color: #00c000;
 }
 body.ttrss_main .player:hover {
-  background: #f0f0f0;
+  background: #f5f5f5;
   cursor: pointer;
 }
-body.ttrss_main #headlines-spacer {
+body.ttrss_main #headlines-frame.auto_catchup #headlines-spacer {
   height: 100%;
+}
+body.ttrss_main #headlines-spacer {
   margin-left: 1px;
   text-align: center;
   color: #555555;
@@ -832,9 +850,9 @@ body.ttrss_main ul#filterDlg_Actions {
   overflow: auto;
   list-style-type: none;
   border-style: solid;
-  border-color: #ddd;
+  border-color: #dddddd;
   border-width: 1px 1px 1px 1px;
-  background-color: white;
+  background-color: #ffffff;
   margin: 0px 0px 5px 0px;
   padding: 4px;
   min-height: 16px;
@@ -851,7 +869,7 @@ body.ttrss_main ul.helpKbList {
   max-height: 300px;
   overflow: auto;
   list-style-type: none;
-  border: 1px solid #ddd;
+  border: 1px solid #dddddd;
   margin: 0px 0px 5px 0px;
   padding: 5px;
 }
@@ -969,6 +987,9 @@ body.ttrss_main i.icon-syndicate {
 body.ttrss_main i.icon-no-feed {
   opacity: 0.2;
 }
+body.ttrss_main .dijitTreeRow.UpdatesDisabled .dijitTreeLabel {
+  opacity: 0.5;
+}
 body.ttrss_main #floatingTitle.marked i.marked-pic,
 body.ttrss_main .cdm.marked .left i.marked-pic,
 body.ttrss_main .hl.marked .left i.marked-pic {
@@ -988,13 +1009,16 @@ body.ttrss_main .score-low i.icon-score {
 body.ttrss_main .score-neutral i.icon-score {
   opacity: 0.5;
 }
+body.ttrss_main i.icon-score {
+  cursor: pointer;
+}
 body.ttrss_main .panel {
-  border: 1px solid #ddd;
+  border: 1px solid #dddddd;
   background: #f5f5f5;
   padding: 4px;
 }
 body.ttrss_main .dijitDialog .panel {
-  background: white;
+  background: #ffffff;
 }
 body.ttrss_main .panel-scrollable {
   overflow: auto;
@@ -1009,9 +1033,12 @@ body.ttrss_main ul.list {
 body.ttrss_main ul.list-unstyled {
   list-style-type: none;
 }
+body.ttrss_main .text-center {
+  text-align: center;
+}
 ::selection {
   background: #257aa7;
-  color: white;
+  color: #ffffff;
 }
 ::-webkit-scrollbar {
   width: 4px;
@@ -1084,7 +1111,8 @@ video::-webkit-media-controls-overlay-play-button {
   flex-grow: 2;
 }
 .cdm .intermediate {
-  margin: 10px;
+  margin-top: 10px;
+  margin-left: 10px;
 }
 .cdm .content-inner {
   margin: 10px;
@@ -1111,7 +1139,7 @@ video::-webkit-media-controls-overlay-play-button {
   white-space: normal;
 }
 .cdm.expanded .footer {
-  border: 0px solid #ddd;
+  border: 0px solid #dddddd;
   border-bottom-width: 1px;
 }
 .cdm.expanded > hr {
@@ -1147,6 +1175,9 @@ div.cdm.expanded.Unread div.content {
 div.cdm.active div.content {
   color: black;
 }
+div.cdm.vgrlf .feed {
+  display: none;
+}
 .cdm div.feed-title {
   border: 0px solid #257aa7;
   border-bottom-width: 1px;
@@ -1202,7 +1233,7 @@ div#floatingTitle {
   top: 0px;
   right: 0px;
   left: 0px;
-  border: 0px solid #ddd;
+  border: 0px solid #dddddd;
   border-bottom-width: 1px;
   background: white;
   color: #555555;
@@ -1282,9 +1313,6 @@ div#floatingTitle span.titleWrap {
   width: 100%;
   white-space: normal;
 }
-div#floatingTitle img.score-pic {
-  display: none;
-}
 div#floatingTitle .feed-title > * {
   display: table-cell;
   vertical-align: middle;
@@ -1307,7 +1335,7 @@ div#floatingTitle.Unread a.title {
 }
 .cdm.expandable {
   background-color: #f5f5f5;
-  border: 0px solid #ddd;
+  border: 0px solid #dddddd;
   border-bottom-width: 1px;
 }
 .cdm.expandable > hr {
@@ -1396,9 +1424,6 @@ body.ttrss_prefs #footer {
   border: 0px;
   text-align: center;
 }
-body.ttrss_prefs #header a:hover {
-  color: black;
-}
 body.ttrss_prefs #header img {
   vertical-align: middle;
   cursor: pointer;
@@ -1449,7 +1474,7 @@ body.ttrss_prefs table.prefPluginsList label {
   white-space: nowrap;
 }
 body.ttrss_prefs table.prefPluginsList i.plugin-enabled {
-  color: #69C671;
+  color: #69c671;
 }
 body.ttrss_prefs table.prefPluginsList label img {
   vertical-align: middle;
@@ -1476,7 +1501,7 @@ body.ttrss_utility.sanity_failed {
 }
 body.ttrss_utility {
   background: #f5f5f5;
-  color: black;
+  color: #000000;
   padding: 0px;
   font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
   font-size: 14px;
@@ -1692,20 +1717,15 @@ body.small_margins {
   max-width: none;
 }
 body#sharepopup {
-  background: #ffffff url("../images/toolbar.png") repeat-x bottom;
+  color: #000000;
+  background: #ffffff;
   margin: 10px;
-  padding: 0px;
 }
 body#sharepopup h1 {
   font-size: 14px;
   margin: 0px;
   color: #257aa7;
 }
-body#sharepopup table {
-  background: white;
-  border: 1px solid #257aa7;
-  padding: 5px;
-}
 body#sharepopup form {
   height: 100%;
 }
@@ -1734,23 +1754,31 @@ body#sharepopup input {
   box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.1);
 }
 .flat .dijitCheckBox.dijitCheckBoxChecked {
-  background-color: #69C671;
+  background-color: #69c671;
+  border-color: #45b74f;
 }
 .flat .dijitMenu .dijitMenuItem .dijitMenuItemLabel {
   padding: 4px 8px;
   font-size: 13px;
 }
+.flat .dijitMenu .dijitMenuItem.dijitDisabled:not(.dijitMenuItemSelected) .dijitMenuItemLabel {
+  color: #2e99d1;
+}
 .flat .dijitMenu .dijitMenuItem td {
   padding: 0px;
 }
 .flat .dijitCheckBox {
-  border: 0px;
+  margin: 1px;
 }
 .flat .dijitCheckBox:before {
   font-family: "flat-icon";
   content: "\f00c";
   color: white;
 }
+.flat .dijitTab i.material-icons,
+.flat .dijitAccordionInnerContainer:not(.dijitSelected) i.material-icons {
+  color: #257aa7;
+}
 .flat .dijitTree .dijitFolderClosed,
 .flat .dijitTree .dijitFolderOpened {
   display: none;
@@ -1772,9 +1800,11 @@ body#sharepopup input {
   float: right;
   margin-right: 16px;
 }
-.flat .dijitTree .labelParam.filterDisabled,
-.flat .dijitTree .dijitTreeLabel.filterDisabled {
-  text-decoration: line-through;
+.flat .dijitTree .dijitTreeRow.filterDisabled {
+  opacity: 0.5;
+}
+.flat .dijitTree .dijitTreeRow.filterDisabled .filterRules {
+  filter: saturate(0%);
 }
 .flat .dijitTree .feedParam {
   float: right;
@@ -1793,6 +1823,22 @@ body#sharepopup input {
   overflow: hidden;
   text-overflow: ellipsis;
 }
+.flat label.dijitButton {
+  border: 1px solid #ccc;
+  padding: 6px;
+  border-radius: 4px;
+  cursor: pointer;
+  position: relative;
+  top: 1px;
+}
+.flat label.dijitButton:hover {
+  background-color: #f5f5f5;
+}
+.flat .dijitTree .dijitTreeNode .dijitTreeRow {
+  padding: 4px 0px 4px;
+  border-width: 1px;
+  color: #555555;
+}
 @font-face {
   font-family: 'Material Icons';
   font-style: normal;
@@ -1835,11 +1881,6 @@ body#sharepopup input {
 .flat .dijitCheckBox {
   background: #ccc;
 }
-.flat .dijitTree .dijitTreeNode .dijitTreeRow {
-  padding: 4px 0px 4px;
-  border-width: 1px;
-  color: #333;
-}
 body.ttrss_zoom {
   margin-left: auto;
   margin-right: auto;

File diff suppressed because it is too large
+ 0 - 0
css/default.css.map


+ 4 - 1
css/defines.less

@@ -1,13 +1,16 @@
 @fonts-ui-bold: "Segoe WP Semibold", "Segoe UI Semibold", "Segoe UI Web Semibold", "Segoe UI", Ubuntu, "Helvetica Neue", Helvetica, Arial, sans-serif;
 @fonts-ui:  "Segoe UI", Ubuntu, "Helvetica Neue", Helvetica, Arial, sans-serif;
 
[email protected]: black;
[email protected]: white;
 @color-accent: #257aa7;
 @color-accent-light: lighten(@color-accent, 50%);
 @color-link: @color-accent;
 @color-published: lighten(#ff5718, 10%);
 @color-marked: #ffc069;
 @color-panel-bg: #f5f5f5;
[email protected]: #69C671;
[email protected]: #69C671;
[email protected] : #ddd;
 @default-text: #555;
 
 body.ttrss_main,

+ 43 - 5
css/dijit_basic.less

@@ -28,7 +28,8 @@
 	}
 
 	.dijitCheckBox.dijitCheckBoxChecked {
-		background-color : #69C671;
+		background-color : @color-checked;
+		border-color : darken(@color-checked, 10%);
 	}
 
 	.dijitMenu .dijitMenuItem .dijitMenuItemLabel {
@@ -36,12 +37,16 @@
 		font-size: 13px;
 	}
 
+	.dijitMenu .dijitMenuItem.dijitDisabled:not(.dijitMenuItemSelected) .dijitMenuItemLabel {
+		color : lighten(@color-accent, 10%);
+	}
+
 	.dijitMenu .dijitMenuItem td {
 		padding: 0px;
 	}
 
 	.dijitCheckBox {
-		border : 0px;
+		margin : 1px;
 	}
 
 	.dijitCheckBox:before {
@@ -50,6 +55,13 @@
 		color: white;
 	}
 
+	.dijitTab,
+	.dijitAccordionInnerContainer:not(.dijitSelected) {
+		i.material-icons {
+			color: @color-accent;
+		}
+	}
+
 	.dijitTree {
 		.dijitFolderClosed,
 		.dijitFolderOpened {
@@ -74,14 +86,18 @@
 			position : relative;
 			top : -2px;
 		}
+
 		.labelParam {
 			float: right;
 			margin-right: 16px;
 		}
 
-		.labelParam.filterDisabled,
-		.dijitTreeLabel.filterDisabled {
-			text-decoration : line-through;
+		.dijitTreeRow.filterDisabled {
+			opacity : 0.5;
+
+			.filterRules {
+				filter : saturate(0%);
+			}
 		}
 
 		.feedParam {
@@ -107,4 +123,26 @@
 		}
 
 	}
+
+	label.dijitButton {
+		border : 1px solid #ccc;
+		padding : 6px;
+		border-radius : 4px;
+		cursor : pointer;
+		position: relative;
+		top : 1px;
+	}
+
+	label.dijitButton:hover {
+		background-color : @color-panel-bg;
+	}
+
+	.dijitTree {
+		.dijitTreeNode .dijitTreeRow {
+			padding : 4px 0px 4px;
+			border-width : 1px;
+			color : @default-text;
+		}
+	}
+
 }

+ 0 - 8
css/dijit_light.less

@@ -11,12 +11,4 @@
 	.dijitCheckBox {
 		background : #ccc;
 	}
-
-	.dijitTree {
-		.dijitTreeNode .dijitTreeRow {
-			padding : 4px 0px 4px;
-			border-width : 1px;
-			color : #333;
-		}
-	}
 }

+ 1 - 5
css/prefs.less

@@ -23,10 +23,6 @@ body.ttrss_prefs {
 		text-align : center;
 	}
 
-	#header a:hover {
-		color : black;
-	}
-
 	#header img {
 		vertical-align : middle;
 		cursor : pointer;
@@ -85,7 +81,7 @@ body.ttrss_prefs {
 		}
 
 		i.plugin-enabled {
-			color : #69C671;
+			color : @color-checked;
 		}
 	}
 

+ 78 - 45
css/tt-rss.less

@@ -1,6 +1,6 @@
 body.ttrss_main {
-	background : white;
-	color : black;
+	background : @default-bg;
+	color : @default-fg;
 	font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
 	font-size: 14px;
 	overflow : hidden;
@@ -16,9 +16,9 @@ body.ttrss_main {
 		div.header {
 			padding : 5px;
 			color : #909090;
-			border: 0px solid #ddd;
+			border: 0px solid @border-default;
 			border-bottom-width: 1px;
-			background: #f0f0f0;
+			background: @color-panel-bg;
 
 			.left, .right {
 				display : flex;
@@ -81,6 +81,18 @@ body.ttrss_main {
 		}
 	}
 
+	.inline-player {
+		display : flex;
+
+		> * {
+			align-self : center;
+		}
+
+		audio {
+			margin-right : 8px;
+		}
+	}
+
 	.article-note {
 		background-color : #fff7d5;
 		margin : 5px;
@@ -207,7 +219,7 @@ body.ttrss_main {
 	}
 
 	.hl {
-		border: 0px solid #ddd;
+		border: 0px solid @border-default;
 		border-bottom-width: 1px;
 		transition : color 0.2s, background 0.2s;
 		display : flex;
@@ -224,6 +236,7 @@ body.ttrss_main {
 		img {
 			vertical-align : middle;
 		}
+
 		.left, .right {
 			display : flex;
 
@@ -247,6 +260,13 @@ body.ttrss_main {
 			text-overflow : ellipsis;
 		}
 
+		span.author {
+			white-space : nowrap;
+			color : @default-text;
+			font-size : 11px;
+			font-weight : normal;
+		}
+
 		div.right {
 			text-align : right;
 		}
@@ -296,6 +316,10 @@ body.ttrss_main {
 		}
 	}
 
+	.hl.vgrlf .feed {
+		display : none;
+	}
+
 	.hl.Unread {
 		background : white;
 	}
@@ -365,7 +389,7 @@ body.ttrss_main {
 	.alert {
 		padding: 8px 35px 8px 14px;
 		margin-bottom: 10px;
-		text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
+		/* text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); */
 		background-color: #fcf8e3;
 		border: 1px solid #fbeed5;
 		border-radius: 4px;
@@ -442,13 +466,13 @@ body.ttrss_main {
 
 	.hl.Selected .hlLabelRef,
 	.hl.active .hlLabelRef {
-		color : #063064;
+		color : @default-fg;
 	}
 
 	span.hlLabelRef {
 		background-color : #fff7d5;
 		font-size : 8px;
-		color : #063064;
+		color : @default-fg;
 		font-weight : normal;
 		margin-left : 2px;
 		padding : 1px 4px 1px 4px;
@@ -464,7 +488,7 @@ body.ttrss_main {
 	}
 
 	div.errorExplained {
-		border : 1px solid #ddd;
+		border : 1px solid @border-default;
 		margin : 5px 0px 5px 0px;
 		padding : 5px;
 	}
@@ -474,7 +498,7 @@ body.ttrss_main {
 		width : 100%;
 		overflow : auto;
 		border-width : 0px 1px 1px 1px;
-		border-color : #ddd;
+		border-color : @border-default;
 		border-style : solid;
 		margin : 0px 0px 5px 0px;
 		background-color : white;
@@ -512,7 +536,7 @@ body.ttrss_main {
 	}
 
 	#overlay {
-		background : white;
+		background : @default-bg;
 		left : 0;
 		top : 0;
 		height : 100%;
@@ -537,15 +561,15 @@ body.ttrss_main {
 		text-align : center;
 		padding : 1em 1em 0px 1em;
 		font-size : 11px;
-		border: 0px solid #ddd;
+		border: 0px solid @border-default;
 		border-bottom-width: 1px;
 	}
 
 	div.autocomplete {
 		position : absolute;
 		width : 250px;
-		background-color : white;
-		border :1px solid #778899;
+		background-color : @default-bg;
+		border :1px solid @border-default;
 		margin : 0px;
 		padding : 0px;
 
@@ -556,7 +580,7 @@ body.ttrss_main {
 		}
 
 		ul li.selected {
-			background-color : #fff7d5;
+			background-color : darken(@default-bg, 10%);
 		}
 
 		ul li {
@@ -581,9 +605,6 @@ body.ttrss_main {
 		}
 	}
 
-	img.score-pic {
-
-	}
 
 	div.dlgSec {
 		font-size : 14px;
@@ -641,7 +662,7 @@ body.ttrss_main {
 		font-size : 11px;
 		color : @default-text;
 		font-weight : bold;
-		background-color : white;
+		background-color : @default-bg;
 		border : 1px solid @color-accent;
 		padding : 3px 5px 3px 5px;
 		z-index : 5;
@@ -674,7 +695,7 @@ body.ttrss_main {
 
 	#feeds-holder {
 		padding : 0px;
-		border: 0px solid #ddd;
+		border: 0px solid @border-default;
 		overflow : hidden;
 		background : @color-panel-bg;
 		box-shadow : inset -1px 0px 2px -1px rgba(0,0,0,0.1);
@@ -687,9 +708,9 @@ body.ttrss_main {
 			font-family : @fonts-ui;
 
 			.counterNode.aux {
-				background : #f0f0f0;
-				color : #999;
-				border-color : #f0f0f0;
+				background : @color-panel-bg;
+				color : lighten(@default-text, 10%);
+				border-color : darken(@color-panel-bg, 10%);
 			}
 
 			.counterNode {
@@ -726,18 +747,14 @@ body.ttrss_main {
 				color : red;
 			}
 
-			.dijitTreeRow.UpdatesDisabled .dijitTreeLabel {
-				color : #909090;
-			}
-
 			.dijitTreeNode .dijitTreeRow {
 				border : 1px solid transparent;
 			}
 
 			.dijitTreeNode .dijitTreeRowSelected {
 				box-shadow : -1px 0px 2px -1px rgba(0,0,0,0.1);
-				border-color : #ddd transparent;
-				background : white;
+				border-color : @border-default transparent;
+				background : @default-bg;
 				color : #333;
 			}
 
@@ -770,7 +787,7 @@ body.ttrss_main {
 			}
 
 			i.icon.icon-whatshot {
-				color : @color-enabled;
+				color : @color-checked;
 			}
 
 			i.icon.icon-restore {
@@ -790,7 +807,7 @@ body.ttrss_main {
 
 	#headlines-frame {
 		padding : 0px;
-		border: 0px #ddd;
+		border: 0px @border-default;
 		margin-top : 0px;
 		-webkit-overflow-scrolling : touch;
 		-webkit-transform: translateZ(0);
@@ -799,7 +816,7 @@ body.ttrss_main {
 		div.feed-title {
 			border: 0px solid @color-link;
 			border-bottom-width: 1px;
-			padding: 5px 3px 5px 5px;
+			padding: 5px 8px;
 		}
 
 		div.feed-title a.title {
@@ -829,7 +846,7 @@ body.ttrss_main {
 
 		#toolbar {
 			background : white;
-			border: 0px solid #ddd;
+			border: 0px solid @border-default;
 			border-bottom-width: 1px;
 			padding-left : 4px;
 			height : 32px;
@@ -882,7 +899,7 @@ body.ttrss_main {
 			}
 
 			#updates-available {
-				color : @color-enabled;
+				color : @color-checked;
 				padding-right : 4px;
 			}
 
@@ -890,6 +907,7 @@ body.ttrss_main {
 				font-style : italic;
 				text-align : right;
 				margin-right : 4px;
+				color : @color-accent;
 			}
 
 			@media (max-width: 992px) {
@@ -914,7 +932,7 @@ body.ttrss_main {
 
 	#content-insert {
 		padding : 0px;
-		border-color : #ddd;
+		border-color : @border-default;
 		border-width : 0px;
 		line-height: 1.5;
 		overflow : auto;
@@ -939,7 +957,7 @@ body.ttrss_main {
 		margin : 0px 2px 0px 2px;
 		width : 50px;
 		text-align : center;
-		background : white;
+		background : @default-bg;
 	}
 
 	.player.playing {
@@ -948,12 +966,15 @@ body.ttrss_main {
 	}
 
 	.player:hover {
-		background : #f0f0f0;
+		background : @color-panel-bg;
 		cursor : pointer;
 	}
 
-	#headlines-spacer {
+	#headlines-frame.auto_catchup #headlines-spacer {
 		height : 100%;
+	}
+
+	#headlines-spacer {
 		margin-left : 1px;
 		text-align : center;
 		color : @default-text;
@@ -976,9 +997,9 @@ body.ttrss_main {
 		overflow : auto;
 		list-style-type : none;
 		border-style : solid;
-		border-color : #ddd;
+		border-color : @border-default;
 		border-width : 1px 1px 1px 1px;
-		background-color : white;
+		background-color : @default-bg;
 		margin : 0px 0px 5px 0px;
 		padding : 4px;
 		min-height : 16px;
@@ -996,7 +1017,7 @@ body.ttrss_main {
 		max-height : 300px;
 		overflow : auto;
 		list-style-type : none;
-		border : 1px solid #ddd;
+		border : 1px solid @border-default;
 		margin : 0px 0px 5px 0px;
 		padding : 5px;
 
@@ -1146,6 +1167,10 @@ body.ttrss_main {
 		opacity : 0.2;
 	}
 
+	.dijitTreeRow.UpdatesDisabled .dijitTreeLabel {
+		opacity : 0.5;
+	}
+
 	#floatingTitle.marked i.marked-pic,
 	.cdm.marked .left i.marked-pic,
 	.hl.marked .left i.marked-pic {
@@ -1159,7 +1184,7 @@ body.ttrss_main {
 	}
 
 	.score-high i.icon-score {
-		color : @color-enabled;
+		color : @color-checked;
 	}
 
 	.score-low i.icon-score {
@@ -1170,14 +1195,18 @@ body.ttrss_main {
 		opacity : 0.5;
 	}
 
+	i.icon-score {
+		cursor : pointer;
+	}
+
 	.panel {
-		border : 1px solid #ddd;
+		border : 1px solid @border-default;
 		background : @color-panel-bg;
 		padding : 4px;
 	}
 
 	.dijitDialog .panel {
-		background : white;
+		background : @default-bg;
 	}
 
 	.panel-scrollable {
@@ -1197,11 +1226,15 @@ body.ttrss_main {
 		list-style-type : none;
 	}
 
+	.text-center {
+		text-align : center;
+	}
+
 }
 
 ::selection {
 	background : @color-accent;
-	color : white;
+	color : @default-bg;
 }
 
 ::-webkit-scrollbar {

+ 3 - 9
css/utility.less

@@ -4,7 +4,7 @@ body.ttrss_utility.sanity_failed {
 
 body.ttrss_utility {
 	background : @color-panel-bg;
-	color : black;
+	color : @default-fg;
 	padding : 0px;
 	font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
 	font-size: 14px;
@@ -263,9 +263,9 @@ body.small_margins {
 }
 
 body#sharepopup {
-	background: white url("../images/toolbar.png") repeat-x bottom;
+	color : @default-fg;
+	background: @default-bg;
 	margin : 10px;
-	padding : 0px;
 
 	h1 {
 		font-size : 14px;
@@ -273,12 +273,6 @@ body#sharepopup {
 		color : @color-accent;
 	}
 
-	table {
-		background : white;
-		border : 1px solid @color-accent;
-		padding : 5px;
-	}
-
 	form {
 		height : 100%;
 	}

+ 10 - 4
include/controls.php

@@ -233,10 +233,12 @@ function print_feed_cat_select($id, $default_id,
 	}
 }
 
-function stylesheet_tag($filename) {
+function stylesheet_tag($filename, $id = false) {
 	$timestamp = filemtime($filename);
 
-	return "<link rel=\"stylesheet\" type=\"text/css\" href=\"$filename?$timestamp\"/>\n";
+	$id_part = $id ? "id=\"$id\"" : "";
+
+	return "<link rel=\"stylesheet\" $id_part type=\"text/css\" href=\"$filename?$timestamp\"/>\n";
 }
 
 function javascript_tag($filename) {
@@ -286,18 +288,22 @@ function format_inline_player($url, $ctype) {
 
 	if (strpos($ctype, "audio/") === 0) {
 
+		$entry .= "<div class='inline-player'>";
+
 		if ($_SESSION["hasAudio"] && (strpos($ctype, "ogg") !== false ||
 				$_SESSION["hasMp3"])) {
 
 			$entry .= "<audio preload=\"none\" controls>
 					<source type=\"$ctype\" src=\"$url\"/>
-					</audio>";
+					</audio> ";
 
 		}
 
-		if ($entry) $entry .= "&nbsp; <a target=\"_blank\" rel=\"noopener noreferrer\"
+		if ($entry) $entry .= "<a target=\"_blank\" rel=\"noopener noreferrer\"
 				href=\"$url\">" . basename($url) . "</a>";
 
+		$entry .= "</div>";
+
 		return $entry;
 
 	}

+ 5 - 22
include/functions.php

@@ -1078,7 +1078,7 @@
 		$params["label_base_index"] = (int) LABEL_BASE_INDEX;
 
 		$theme = get_pref( "USER_CSS_THEME", false, false);
-		$params["theme"] = theme_valid("$theme") ? $theme : "";
+		$params["theme"] = theme_exists($theme) ? $theme : "";
 
 		$params["plugins"] = implode(", ", PluginHost::getInstance()->get_plugin_names());
 
@@ -1174,6 +1174,7 @@
 				"create_label" => __("Create label"),
 				"create_filter" => __("Create filter"),
 				"collapse_sidebar" => __("Un/collapse sidebar"),
+				"toggle_night_mode" => __("Toggle night mode"),
 				"help_dialog" => __("Show help dialog"))
 		);
 
@@ -1245,6 +1246,7 @@
 			"c l" => "create_label",
 			"c f" => "create_filter",
 			"c s" => "collapse_sidebar",
+			"a *n" => "toggle_night_mode",
 			"^(191)|Ctrl+/" => "help_dialog",
 		);
 
@@ -2442,27 +2444,8 @@
 		if (file_exists($check)) return $check;
 	}
 
-	function theme_valid($theme) {
-		$bundled_themes = [ "night.css", "compact.css" ];
-
-		if (in_array($theme, $bundled_themes)) return true;
-
-		$file = "themes/" . basename($theme);
-
-		if (!file_exists($file)) $file = "themes.local/" . basename($theme);
-
-		if (file_exists($file) && is_readable($file)) {
-			$fh = fopen($file, "r");
-
-			if ($fh) {
-				$header = fgets($fh);
-				fclose($fh);
-
-				return strpos($header, "supports-version:" . VERSION_STATIC) !== FALSE;
-			}
-		}
-
-		return false;
+	function theme_exists($theme) {
+		return file_exists("themes/$theme") || file_exists("themes.local/$theme");
 	}
 
 	/**

+ 5 - 4
index.php

@@ -63,10 +63,10 @@
 
 	<?php if ($_SESSION["uid"]) {
 		$theme = get_pref("USER_CSS_THEME", false, false);
-		if ($theme && theme_valid("$theme")) {
-			echo stylesheet_tag(get_theme_path($theme));
+		if ($theme && theme_exists("$theme")) {
+			echo stylesheet_tag(get_theme_path($theme), 'theme_css');
 		} else {
-			echo stylesheet_tag("css/default.css");
+			echo stylesheet_tag("css/default.css", 'theme_css');
 		}
 	}
 	?>
@@ -138,7 +138,7 @@
 
 <div id="overlay" style="display : block">
 	<div id="overlay_inner">
-		<div class="insensitive"><?php echo __("Loading, please wait...") ?></div>
+		<?php echo __("Loading, please wait...") ?>
 		<div dojoType="dijit.ProgressBar" places="0" style="width : 300px" id="loading_bar"
 	     progress="0" maximum="100">
 		</div>
@@ -238,6 +238,7 @@
                         <div dojoType="dijit.MenuItem" onclick="App.onActionSelected('qmcShowOnlyUnread')"><?php echo __('(Un)hide read feeds') ?></div>
                         <div dojoType="dijit.MenuItem" disabled="1"><?php echo __('Other actions:') ?></div>
                         <div dojoType="dijit.MenuItem" onclick="App.onActionSelected('qmcToggleWidescreen')"><?php echo __('Toggle widescreen mode') ?></div>
+                        <div dojoType="dijit.MenuItem" onclick="App.onActionSelected('qmcToggleNightMode')"><?php echo __('Toggle night mode') ?></div>
                         <div dojoType="dijit.MenuItem" onclick="App.onActionSelected('qmcHKhelp')"><?php echo __('Keyboard shortcuts help') ?></div>