diff --git a/assets/css/app.css b/assets/css/app.css index 356ad92..b5a40ce 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -163,6 +163,7 @@ input[type="checkbox"] { input[type="email"], input[type="tel"], input[type="password"], +input[type="number"], input[type="text"] { border: 1px solid #ccc; padding: 3px; @@ -176,6 +177,7 @@ input[type="text"] { input[type="email"]:focus, input[type="tel"]:focus, input[type="password"]:focus, +input[type="number"]:focus, input[type="text"]:focus, textarea:focus { color: #000; diff --git a/assets/js/all.min.js b/assets/js/all.min.js index 4025a77..9cd80e0 100644 --- a/assets/js/all.min.js +++ b/assets/js/all.min.js @@ -1,16 +1,19 @@ -var g=function(){var e=[];return{i:function(d,a){var b=d.querySelector("span.items-count");if(b){var c=d.getAttribute("data-feed-id"),e=d.querySelector("h2:first-of-type");e.className="loading-icon";var l=new XMLHttpRequest;l.onload=function(){e.className="";d.removeAttribute("data-feed-error");var c=d.querySelector(".feed-last-checked");c&&(c.innerHTML=c.getAttribute("data-after-update"));c=JSON.parse(this.responseText);c.result?b.innerHTML=c.items_count.items_unread+"/"+c.items_count.items_total: -d.setAttribute("data-feed-error","1");a&&a(c)};l.open("POST","?action=refresh-feed&feed_id="+c,!0);l.send()}},j:function(){var d=Array.prototype.slice.call(document.querySelectorAll("article:not([data-feed-disabled])")),a=setInterval(function(){for(;0e.length;){var b=d.shift();e.push(parseInt(b.getAttribute("data-feed-id"),10));g.i(b,function(b){b=e.indexOf(b.feed_id);0<=b&&e.splice(b,1);0===d.length&&0===e.length&&(clearInterval(a),window.location.href="?action=unread")})}},100)}}}(), -q=function(){function e(a){return item_id=a.getAttribute("data-item-id")}function d(a){if(a&&a.hasAttribute("data-reverse-label")){var b=a.innerHTML;a.innerHTML=a.getAttribute("data-reverse-label");a.setAttribute("data-reverse-label",b)}}function a(a){"mouse"!==k.b&&m.c();a.parentNode.removeChild(a);p--}function b(){0===p&&window.location.reload();var a=document.getElementById("page-counter");a.textContent=p||"";document.getElementById("nav-counter").textContent=l||"";switch(document.querySelector("section.page").getAttribute("data-item-page")){case "unread":document.title= -"Miniflux ("+l+")";break;case "feed-items":document.title="("+p+") "+a.parentNode.firstChild.nodeValue;break;default:document.title=a.parentNode.firstChild.nodeValue+" ("+p+")"}}function c(h){var f=e(h),c=new XMLHttpRequest;c.onload=function(){if(m.a()){if(h.getAttribute("data-hide"))a(h);else{h.setAttribute("data-item-status","read");var c=h.querySelector("a.mark");d(c);(c=h.querySelector("a.mark"))&&c.setAttribute("data-action","mark-unread")}l--;b()}};c.open("POST","?action=mark-item-read&id="+ -f,!0);c.send()}function t(h){var c=e(h),n=new XMLHttpRequest;n.onload=function(){if(m.a()){if(h.getAttribute("data-hide"))a(h);else{h.setAttribute("data-item-status","unread");var c=h.querySelector("a.mark");d(c);(c=h.querySelector("a.mark"))&&c.setAttribute("data-action","mark-read")}l++;b()}};n.open("POST","?action=mark-item-unread&id="+c,!0);n.send()}var l=function(){var a=document.getElementById("nav-counter");if(a)return counter=parseInt(a.textContent,10)||0}(),p=function(){var a=document.getElementById("page-counter"); -if(a)return counter=parseInt(a.textContent,10)||0}();return{m:c,o:t,n:function(c){var f=e(c),d=new XMLHttpRequest;d.onload=function(){m.a()&&(a(c),"unread"===c.getAttribute("data-item-status")&&l--,b())};d.open("POST","?action=mark-item-removed&id="+f,!0);d.send()},h:function(c){var f=e(c),n="1"===c.getAttribute("data-item-bookmark")?"0":"1",r=new XMLHttpRequest;r.onload=function(){var f=document.querySelector("section.page");if(m.a()&&"bookmarks"===f.getAttribute("data-item-page"))a(c),b();else if(c.setAttribute("data-item-bookmark", -n),m.a())f=c.querySelector("a.bookmark"),d(f);else if((f=c.querySelector("a.bookmark-icon"))&&f.hasAttribute("data-reverse-title")){var e=f.getAttribute("title");f.setAttribute("title",f.getAttribute("data-reverse-title"));f.setAttribute("data-reverse-title",e)}};r.open("POST","?action=bookmark&id="+f+"&value="+n,!0);r.send()},t:function(a){var b=a.getAttribute("data-item-status");"read"===b?t(a):"unread"===b&&c(a)},r:function(a){(a=a.querySelector("a.show"))&&a.click()},f:function(a){var b=a.querySelector("a.original"); -b&&("unread"===a.getAttribute("data-item-status")&&c(a),b.removeAttribute("data-action"),"mouse"!==k.b&&b.click())},d:function(a){var b=document.getElementById("download-item");if(b){b.innerHTML=" "+b.getAttribute("data-before-message");b.className="loading-icon";var c=new XMLHttpRequest;c.onload=function(){var a=JSON.parse(c.responseText);b.className="";if(a.result){var d=document.getElementById("item-content");d&&(d.innerHTML=a.content);b.innerHTML=b.getAttribute("data-after-message")}else b.innerHTML= -b.getAttribute("data-failure-message")};a=e(a);c.open("POST","?action=download-item&id="+a,!0);c.send()}},e:function(a){for(var b=document.getElementsByTagName("article"),c=[],d=0,l=b.length;db-(a.offsetTop+a.offsetHeight)||b-a.offsetTop>document.documentElement.clientHeight)&&window.scrollTo(0,a.offsetTop-10)}function d(){return document.getElementById("listing")?!0:!1}return{p:function(){var a=document.getElementById("next-page");a&&a.click()},q:function(){var a=document.getElementById("previous-page"); -a&&a.click()},c:function(){var a=document.getElementById("next-item");if(a)a.click();else if(d())if(a=document.getElementsByTagName("article"),document.getElementById("current-item"))for(var b=0,c=a.length;be.length;){var b=d.shift();e.push(parseInt(b.getAttribute("data-feed-id"),10));g.j(b,function(b){b=e.indexOf(b.feed_id);0<=b&&e.splice(b,1);0===d.length&&0===e.length&&(clearInterval(a),window.location.href="?action=unread")})}},100)}}}(), +t=function(){function e(a){return item_id=a.getAttribute("data-item-id")}function d(a){if(a&&a.hasAttribute("data-reverse-label")){var b=a.innerHTML;a.innerHTML=a.getAttribute("data-reverse-label");a.setAttribute("data-reverse-label",b)}}function a(a){"mouse"!==k.b&&m.d();a.parentNode.removeChild(a);r--}function b(){-1l[e])l[e]=f.time,"unread"===f.status&&(c=!0)}document.hidden||d.nbUnread===n&&!q?document.hidden&&!a&&c&&(q=!0,document.title="\u21bb "+document.title):(q=!1,n=d.nbUnread,b())};a.open("POST","?action=latest-feeds-items",!0);a.send()}}}}(),k=function(){function e(a){if(63!==a.keyCode&&63!==a.which&&(a.ctrlKey||a.shiftKey||a.altKey||a.metaKey))return!0;a=a.target|| +a.srcElement;return"INPUT"===a.tagName||"TEXTAREA"===a.tagName?!0:!1}var d=[];return{b:"",n:function(){document.onclick=function(a){var b=a.target.getAttribute("data-action");b&&"original-link"!==b&&a.preventDefault()};document.onmouseup=function(a){if(2!==a.button)if("INPUT"===a.target.nodeName&&"auto-select"===a.target.className)a.target.select();else{var b=a.target.getAttribute("data-action");if(b){k.b="mouse";var c;a:{for(element=a.target;element&&element.parentNode;)if(element=element.parentNode, +element.tagName&&"article"===element.tagName.toLowerCase()){c=element;break a}c=void 0}switch(b){case "refresh-all":g.k();break;case "refresh-feed":c&&g.j(c);break;case "mark-read":c&&t.p(c);break;case "mark-unread":c&&t.r(c);break;case "mark-removed":c&&t.q(c);break;case "bookmark":c&&t.i(c);break;case "download-item":c&&t.e(c);break;case "original-link":c&&t.g(c);break;case "mark-all-read":t.f("?action=unread");break;case "mark-feed-read":t.f("?action=feed-items&feed_id="+a.target.getAttribute("data-feed-id"))}}}}}, +m:function(){document.onkeypress=function(a){if(!e(a))if(k.b="keyboard",d.push(a.keyCode||a.which),103===d[0])switch(d[1]){case void 0:break;case 117:window.location.href="?action=unread";d=[];break;case 98:window.location.href="?action=bookmarks";d=[];break;case 104:window.location.href="?action=history";d=[];break;case 115:window.location.href="?action=feeds";d=[];break;case 112:window.location.href="?action=config";d=[];break;default:d=[]}else{d=[];var b=document.getElementById("current-item"); +switch(a.keyCode||a.which){case 100:b&&t.e(b);break;case 112:case 107:m.h();break;case 110:case 106:m.d();break;case 118:b&&t.g(b);break;case 111:b&&t.v(b);break;case 109:b&&t.A(b);break;case 102:b&&t.i(b);break;case 104:m.t();break;case 108:m.s();break;case 114:g.k();break;case 63:m.w();break;case 122:t.B()}}};document.onkeydown=function(a){if(!e(a))switch(k.b="keyboard",a.keyCode||a.which){case 37:m.h();break;case 39:m.d()}}},o:function(){document.addEventListener("visibilitychange",function(){!document.hidden&& +t.C()&&t.c()})}}}(),m=function(){function e(a){var b=pageYOffset+document.documentElement.clientHeight;(0>b-(a.offsetTop+a.offsetHeight)||b-a.offsetTop>document.documentElement.clientHeight)&&window.scrollTo(0,a.offsetTop-10)}function d(){return document.getElementById("listing")?!0:!1}return{s:function(){var a=document.getElementById("next-page");a&&a.click()},t:function(){var a=document.getElementById("previous-page");a&&a.click()},d:function(){var a=document.getElementById("next-item");if(a)a.click(); +else if(d())if(a=document.getElementsByTagName("article"),document.getElementById("current-item"))for(var b=0,c=a.length;b 0) { + Miniflux.App.Log('Frontend updatecheck interval in minutes: ' + response['frontend_updatecheck_interval']); + Miniflux.Item.CheckForUpdates(); + setInterval(function(){ Miniflux.Item.CheckForUpdates(); }, response['frontend_updatecheck_interval']*60*1000); + } + else { + Miniflux.App.Log('Frontend updatecheck disabled'); + } + }; + + request.open("POST", "?action=get-config", true); + request.send(JSON.stringify(['frontend_updatecheck_interval'])); } - } + }; })(); diff --git a/assets/js/event.js b/assets/js/event.js index 3876029..fbe7490 100644 --- a/assets/js/event.js +++ b/assets/js/event.js @@ -208,8 +208,17 @@ Miniflux.Event = (function() { Miniflux.Nav.SelectNextItem(); break; } - } + }; + }, + ListenVisibilityEvents: function() { + document.addEventListener('visibilitychange', function() { + Miniflux.App.Log('document.visibilityState: ' + document.visibilityState); + + if (!document.hidden && Miniflux.Item.hasNewUnread()) { + Miniflux.App.Log('Need to update the unread counter with fresh values from the database'); + Miniflux.Item.CheckForUpdates(); + } + }); } }; - })(); diff --git a/assets/js/item.js b/assets/js/item.js index c34fd98..a6dc228 100644 --- a/assets/js/item.js +++ b/assets/js/item.js @@ -1,5 +1,11 @@ Miniflux.Item = (function() { + // timestamp of the latest item per feed ever seen + var latest_feeds_items = []; + + // indicator for new unread items + var unreadItems = false; + var nbUnreadItems = function() { var navCounterElement = document.getElementById("nav-counter"); @@ -93,17 +99,36 @@ Miniflux.Item = (function() { function updateCounters() { - // imitate special handling within miniflux - if (nbPageItems === 0) { + // redirect to unread if we're on a nothing to read page + if (window.location.href.indexOf('nothing_to_read=1') > -1 && nbUnreadItems > 0) { + window.location.href = '?action=unread'; + } // reload to get a nothing to read page + else if (nbPageItems === 0) { window.location.reload(); } var pageCounterElement = document.getElementById("page-counter"); - pageCounterElement.textContent = nbPageItems || ''; + if (pageCounterElement) pageCounterElement.textContent = nbPageItems || ''; var navCounterElement = document.getElementById("nav-counter"); navCounterElement.textContent = nbUnreadItems || ''; + var pageHeadingElement = document.querySelector("div.page-header h2:first-of-type"); + if (pageHeadingElement) { + pageHeading = pageHeadingElement.firstChild.nodeValue; + } + else { + // special handling while viewing an article. + // 1. The article does not have a page-header element + // 2. An article could be opened from any page and has the original + // page as data-item-page value + var itemHeading = document.querySelector("article.item h1:first-of-type"); + if (itemHeading) { + document.title = itemHeading.textContent; + return; + } + } + // pagetitle depends on current page var sectionElement = document.querySelector("section.page"); switch (sectionElement.getAttribute("data-item-page")) { @@ -111,10 +136,15 @@ Miniflux.Item = (function() { document.title = "Miniflux (" + nbUnreadItems + ")"; break; case "feed-items": - document.title = "(" + nbPageItems + ") " + pageCounterElement.parentNode.firstChild.nodeValue; + document.title = "(" + nbPageItems + ") " + pageHeading; break; default: - document.title = pageCounterElement.parentNode.firstChild.nodeValue + " (" + nbPageItems + ")"; + if (pageCounterElement) { + document.title = pageHeading + " (" + nbPageItems + ")"; + } + else { + document.title = pageHeading; + } break; } } @@ -283,6 +313,56 @@ Miniflux.Item = (function() { tag.dir = tag.dir == "" ? "rtl" : ""; } } + }, + hasNewUnread: function() { + return unreadItems; + }, + CheckForUpdates: function() { + if (document.hidden && unreadItems) { + Miniflux.App.Log('We already have updates, no need to check again'); + return; + } + + var request = new XMLHttpRequest(); + request.onload = function() { + var first_run = (latest_feeds_items.length === 0); + var current_unread = false; + var response = JSON.parse(this.responseText); + + for(var feed_id in response['feeds']) { + var current_feed = response['feeds'][feed_id]; + + if (!latest_feeds_items.hasOwnProperty(feed_id) || current_feed.time > latest_feeds_items[feed_id]) { + Miniflux.App.Log('feed ' + feed_id + ': New item(s)'); + latest_feeds_items[feed_id] = current_feed.time; + + if (current_feed.status === 'unread') { + Miniflux.App.Log('feed ' + feed_id + ': New unread item(s)'); + current_unread = true; + } + } + } + + Miniflux.App.Log('first_run: ' + first_run + ', current_unread: ' + current_unread + ', response.nbUnread: ' + response['nbUnread'] + ', nbUnreadItems: ' + nbUnreadItems); + + if (!document.hidden && (response['nbUnread'] !== nbUnreadItems || unreadItems)) { + Miniflux.App.Log('Counter changed! Updating unread counter.'); + unreadItems = false; + nbUnreadItems = response['nbUnread']; + updateCounters(); + } + else if (document.hidden && !first_run && current_unread) { + Miniflux.App.Log('New Unread! Updating pagetitle.'); + unreadItems = true; + document.title = "↻ " + document.title; + } else { + Miniflux.App.Log('No update.'); + } + + Miniflux.App.Log('unreadItems: ' + unreadItems); + }; + request.open("POST", "?action=latest-feeds-items", true); + request.send(); } }; diff --git a/controllers/config.php b/controllers/config.php index 73453fd..8477fe2 100644 --- a/controllers/config.php +++ b/controllers/config.php @@ -172,6 +172,22 @@ Router\post_action('config', function() { ))); }); +Router\post_action('get-config', function() { + $return = array(); + $options = Request\values(); + + if (empty($options)) { + $return = Model\Config\get_all(); + } + else { + foreach ($options as $name) { + $return[$name] = Model\Config\get($name); + } + } + + Response\json($return); +}); + // Display help page Router\get_action('help', function() { diff --git a/controllers/item.php b/controllers/item.php index 625ace6..4722eeb 100644 --- a/controllers/item.php +++ b/controllers/item.php @@ -200,3 +200,21 @@ Router\get_action('mark-item-removed', function() { Response\Redirect('?action='.$redirect.'&offset='.$offset.'&feed_id='.$feed_id); }); + +Router\post_action('latest-feeds-items', function() { + $items = Model\Item\get_latest_feeds_items(); + $nb_unread_items = Model\Item\count_by_status('unread'); + + $feeds = array_reduce($items, function ($result, $item) { + $result[$item['id']] = array( + 'time' => $item['updated'] ?: 0, + 'status' => $item['status'] + ); + return $result; + }, array()); + + Response\json(array( + 'feeds' => $feeds, + 'nbUnread' => $nb_unread_items + )); +}); diff --git a/locales/cs_CZ/translations.php b/locales/cs_CZ/translations.php index 16c4704..123c83e 100644 --- a/locales/cs_CZ/translations.php +++ b/locales/cs_CZ/translations.php @@ -231,4 +231,5 @@ return array( // 'Download favicons' => '', // 'general' => '', // 'An error occurred during the last check. Refresh the feed manually and check the %sconsole%s for errors afterwards!' => '', + // 'Frontend updatecheck interval in minutes' => '', ); diff --git a/locales/de_DE/translations.php b/locales/de_DE/translations.php index 18dba30..9c27769 100644 --- a/locales/de_DE/translations.php +++ b/locales/de_DE/translations.php @@ -231,4 +231,5 @@ return array( 'Download favicons' => 'Favicons herunterladen', 'general' => 'allgemein', 'An error occurred during the last check. Refresh the feed manually and check the %sconsole%s for errors afterwards!' => 'Fehler bei der letzten Aktualisierung. Aktualisiere den Feed manuell und prüfe die %sKonsole%s anschließend auf Fehler!', + // 'Frontend updatecheck interval in minutes' => '', ); diff --git a/locales/es_ES/translations.php b/locales/es_ES/translations.php index 8a56b53..56dde05 100644 --- a/locales/es_ES/translations.php +++ b/locales/es_ES/translations.php @@ -231,4 +231,5 @@ return array( // 'Download favicons' => '', // 'general' => '', // 'An error occurred during the last check. Refresh the feed manually and check the %sconsole%s for errors afterwards!' => '', + // 'Frontend updatecheck interval in minutes' => '', ); diff --git a/locales/fr_FR/translations.php b/locales/fr_FR/translations.php index 64414d3..7d5e1b6 100644 --- a/locales/fr_FR/translations.php +++ b/locales/fr_FR/translations.php @@ -231,4 +231,5 @@ return array( 'Download favicons' => 'Télécharger les icônes des sites web', 'general' => 'général', 'An error occurred during the last check. Refresh the feed manually and check the %sconsole%s for errors afterwards!' => 'Une erreur est survenue pendant la dernière vérification. Actualisez, le flux manuellement and vérifiez les erreurs dans la %sconsole%s !', + 'Frontend updatecheck interval in minutes' => 'Frontend updatecheck interval in minutes', ); diff --git a/locales/it_IT/translations.php b/locales/it_IT/translations.php index 93baf71..ed03c7f 100644 --- a/locales/it_IT/translations.php +++ b/locales/it_IT/translations.php @@ -231,4 +231,5 @@ return array( // 'Download favicons' => '', // 'general' => '', // 'An error occurred during the last check. Refresh the feed manually and check the %sconsole%s for errors afterwards!' => '', + // 'Frontend updatecheck interval in minutes' => '', ); diff --git a/locales/pt_BR/translations.php b/locales/pt_BR/translations.php index df17330..2cb0c64 100644 --- a/locales/pt_BR/translations.php +++ b/locales/pt_BR/translations.php @@ -231,4 +231,5 @@ return array( 'Download favicons' => 'Download favicon', // 'general' => '', // 'An error occurred during the last check. Refresh the feed manually and check the %sconsole%s for errors afterwards!' => '', + // 'Frontend updatecheck interval in minutes' => '', ); diff --git a/locales/zh_CN/translations.php b/locales/zh_CN/translations.php index 69d15a1..37c9b4b 100644 --- a/locales/zh_CN/translations.php +++ b/locales/zh_CN/translations.php @@ -231,4 +231,5 @@ return array( // 'Download favicons' => '', // 'general' => '', // 'An error occurred during the last check. Refresh the feed manually and check the %sconsole%s for errors afterwards!' => '', + // 'Frontend updatecheck interval in minutes' => '', ); diff --git a/models/config.php b/models/config.php index 46a1f28..4108e75 100644 --- a/models/config.php +++ b/models/config.php @@ -282,6 +282,7 @@ function validate_modification(array $values) new Validators\Required('items_per_page', t('Value required')), new Validators\Integer('items_per_page', t('Must be an integer')), new Validators\Required('theme', t('Value required')), + new Validators\Integer('frontend_updatecheck_interval', t('Must be an integer')), ); if (ENABLE_AUTO_UPDATE) { diff --git a/models/item.php b/models/item.php index 6aeb081..cef8ba9 100644 --- a/models/item.php +++ b/models/item.php @@ -64,6 +64,21 @@ function get_everything_since($timestamp) ->findAll(); } +function get_latest_feeds_items() +{ + return Database::get('db') + ->table('feeds') + ->columns( + 'feeds.id', + 'MAX(items.updated) as updated', + 'items.status' + ) + ->join('items', 'feed_id', 'id') + ->groupBy('feeds.id') + ->orderBy('feeds.id') + ->findAll(); +} + // Get a list of [item_id => status,...] function get_all_status() { diff --git a/models/schema.php b/models/schema.php index ea2d372..00d4351 100644 --- a/models/schema.php +++ b/models/schema.php @@ -5,7 +5,12 @@ namespace Schema; use PDO; use Model\Config; -const VERSION = 35; +const VERSION = 36; + +function version_36($pdo) +{ + $pdo->exec('INSERT INTO settings ("key", "value") VALUES ("frontend_updatecheck_interval", 10)'); +} function version_35($pdo) { diff --git a/templates/config.php b/templates/config.php index fb59d7b..25fa1a4 100644 --- a/templates/config.php +++ b/templates/config.php @@ -35,6 +35,9 @@
+ +
+