Browse Source

Merge branch 'dev' into beta

Marien Fressinaud 12 năm trước cách đây
mục cha
commit
8d7ac978f9
76 tập tin đã thay đổi với 6929 bổ sung4022 xóa
  1. 14 0
      CHANGELOG
  2. 1 1
      README.md
  3. 185 105
      app/Controllers/configureController.php
  4. 6 6
      app/Controllers/entryController.php
  5. 57 49
      app/Controllers/feedController.php
  6. 3 2
      app/Controllers/importExportController.php
  7. 13 29
      app/Controllers/indexController.php
  8. 2 2
      app/Controllers/javascriptController.php
  9. 67 0
      app/Controllers/statsController.php
  10. 8 6
      app/Controllers/usersController.php
  11. 1 1
      app/Models/Category.php
  12. 8 8
      app/Models/CategoryDAO.php
  13. 22 0
      app/Models/Configuration.php
  14. 2 2
      app/Models/Entry.php
  15. 241 329
      app/Models/EntryDAO.php
  16. 129 0
      app/Models/EntryDAOSQLite.php
  17. 32 0
      app/Models/Factory.php
  18. 106 88
      app/Models/Feed.php
  19. 140 152
      app/Models/FeedDAO.php
  20. 19 0
      app/Models/FeedDAOSQLite.php
  21. 45 43
      app/Models/StatsDAO.php
  22. 37 0
      app/Models/StatsDAOSQLite.php
  23. 10 1
      app/Models/Themes.php
  24. 21 10
      app/Models/UserDAO.php
  25. 4 1
      app/SQL/install.sql.mysql.php
  26. 58 0
      app/SQL/install.sql.sqlite.php
  27. 40 1
      app/i18n/en.php
  28. 77 38
      app/i18n/fr.php
  29. 1 1
      app/i18n/install.en.php
  30. 1 1
      app/i18n/install.fr.php
  31. 90 50
      app/install.php
  32. 3 0
      app/layout/aside_configure.phtml
  33. 9 0
      app/layout/aside_stats.phtml
  34. 51 50
      app/layout/header.phtml
  35. 45 12
      app/layout/nav_menu.phtml
  36. 21 0
      app/views/configure/archiving.phtml
  37. 21 0
      app/views/configure/feed.phtml
  38. 90 0
      app/views/configure/queries.phtml
  39. 11 1
      app/views/configure/reading.phtml
  40. 7 7
      app/views/configure/sharing.phtml
  41. 1 1
      app/views/helpers/pagination.phtml
  42. 4 2
      app/views/javascript/actualize.phtml
  43. 19 0
      app/views/stats/idle.phtml
  44. 127 0
      app/views/stats/index.phtml
  45. 7 5
      app/views/stats/main.phtml
  46. 0 0
      data/do-install.txt
  47. 50 30
      lib/Minz/Configuration.php
  48. 28 30
      lib/Minz/ModelPdo.php
  49. 3 2
      lib/Minz/Request.php
  50. 21 13
      lib/Minz/Translate.php
  51. 30 8
      lib/SimplePie/SimplePie.php
  52. 10 2
      lib/SimplePie/SimplePie/File.php
  53. 1 7
      lib/lib_rss.php
  54. 8 6
      p/api/greader.php
  55. 8 8
      p/i/index.php
  56. 45 15
      p/scripts/main.js
  57. 985 0
      p/themes/Dark/dark.css
  58. 0 926
      p/themes/Dark/freshrss.css
  59. 0 525
      p/themes/Dark/global.css
  60. 12 0
      p/themes/Dark/icons/icon.svg
  61. 2 2
      p/themes/Dark/metadata.json
  62. 695 0
      p/themes/Dark/template.css
  63. 950 0
      p/themes/Flat/flat.css
  64. 0 900
      p/themes/Flat/freshrss.css
  65. 0 528
      p/themes/Flat/global.css
  66. 12 0
      p/themes/Flat/icons/icon.svg
  67. 7 0
      p/themes/Flat/icons/key.svg
  68. 2 2
      p/themes/Flat/metadata.json
  69. 695 0
      p/themes/Flat/template.css
  70. 16 12
      p/themes/Origine/origine.css
  71. 11 2
      p/themes/Origine/template.css
  72. 12 0
      p/themes/base-theme/README.md
  73. 762 0
      p/themes/base-theme/base.css
  74. 7 0
      p/themes/base-theme/metadata.json
  75. 695 0
      p/themes/base-theme/template.css
  76. 6 0
      p/themes/icons/bookmark-add.svg

+ 14 - 0
CHANGELOG

@@ -1,5 +1,19 @@
 # Journal des modifications
 
+## 2014-xx-xx FreshRSS 0.7.x
+
+* New options
+	* Add system of user queries which are shortcuts to filter the view
+	* New TTL option to limit the frequency at which feeds are refreshed (by cron or manual refresh button).
+		It is still possible to manually refresh an individual feed at a higher frequency.
+* SQL
+	* Add support for SQLite (beta) in addition to MySQL
+* SimplePie
+	* Complies with HTTP "301 Moved Permanently" responses by automatically updating the URL of feeds that have changed address.
+* Themes
+	* Flat and Dark designs are based on same template file as Origine
+
+
 ## 2014-06-13 FreshRSS 0.7.2
 
 * API compatible with Google Reader API level 2

+ 1 - 1
README.md

@@ -35,7 +35,7 @@ Privilégiez pour cela des demandes sur GitHub
 * PHP 5.2.1+ (PHP 5.3.7+ recommandé)
 	* Requis : [PDO_MySQL](http://php.net/pdo-mysql), [cURL](http://php.net/curl), [LibXML](http://php.net/xml), [PCRE](http://php.net/pcre), [ctype](http://php.net/ctype)
 	* Recommandés : [JSON](http://php.net/json), [zlib](http://php.net/zlib), [mbstring](http://php.net/mbstring), [iconv](http://php.net/iconv), [Zip](http://php.net/zip)
-* MySQL 5.0.3+ (ou SQLite 3.7.4+ à venir)
+* MySQL 5.0.3+ (recommandé) ou SQLite 3.7.4+ (en bêta)
 * Un navigateur Web récent tel Firefox 4+, Chrome, Opera, Safari, Internet Explorer 9+
 	* Fonctionne aussi sur mobile
 

+ 185 - 105
app/Controllers/configureController.php

@@ -1,81 +1,81 @@
 <?php
 
 class FreshRSS_configure_Controller extends Minz_ActionController {
-	public function firstAction () {
+	public function firstAction() {
 		if (!$this->view->loginOk) {
-			Minz_Error::error (
+			Minz_Error::error(
 				403,
-				array ('error' => array (Minz_Translate::t ('access_denied')))
+				array('error' => array(Minz_Translate::t('access_denied')))
 			);
 		}
 
-		$catDAO = new FreshRSS_CategoryDAO ();
-		$catDAO->checkDefault ();
+		$catDAO = new FreshRSS_CategoryDAO();
+		$catDAO->checkDefault();
 	}
 
-	public function categorizeAction () {
-		$feedDAO = new FreshRSS_FeedDAO ();
-		$catDAO = new FreshRSS_CategoryDAO ();
-		$defaultCategory = $catDAO->getDefault ();
-		$defaultId = $defaultCategory->id ();
+	public function categorizeAction() {
+		$feedDAO = FreshRSS_Factory::createFeedDao();
+		$catDAO = new FreshRSS_CategoryDAO();
+		$defaultCategory = $catDAO->getDefault();
+		$defaultId = $defaultCategory->id();
 
-		if (Minz_Request::isPost ()) {
-			$cats = Minz_Request::param ('categories', array ());
-			$ids = Minz_Request::param ('ids', array ());
-			$newCat = trim (Minz_Request::param ('new_category', ''));
+		if (Minz_Request::isPost()) {
+			$cats = Minz_Request::param('categories', array());
+			$ids = Minz_Request::param('ids', array());
+			$newCat = trim(Minz_Request::param('new_category', ''));
 
 			foreach ($cats as $key => $name) {
-				if (strlen ($name) > 0) {
-					$cat = new FreshRSS_Category ($name);
-					$values = array (
-						'name' => $cat->name (),
+				if (strlen($name) > 0) {
+					$cat = new FreshRSS_Category($name);
+					$values = array(
+						'name' => $cat->name(),
 					);
-					$catDAO->updateCategory ($ids[$key], $values);
+					$catDAO->updateCategory($ids[$key], $values);
 				} elseif ($ids[$key] != $defaultId) {
-					$feedDAO->changeCategory ($ids[$key], $defaultId);
-					$catDAO->deleteCategory ($ids[$key]);
+					$feedDAO->changeCategory($ids[$key], $defaultId);
+					$catDAO->deleteCategory($ids[$key]);
 				}
 			}
 
 			if ($newCat != '') {
-				$cat = new FreshRSS_Category ($newCat);
-				$values = array (
-					'id' => $cat->id (),
-					'name' => $cat->name (),
+				$cat = new FreshRSS_Category($newCat);
+				$values = array(
+					'id' => $cat->id(),
+					'name' => $cat->name(),
 				);
 
-				if ($catDAO->searchByName ($newCat) == null) {
-					$catDAO->addCategory ($values);
+				if ($catDAO->searchByName($newCat) == null) {
+					$catDAO->addCategory($values);
 				}
 			}
 			invalidateHttpCache();
 
-			$notif = array (
+			$notif = array(
 				'type' => 'good',
-				'content' => Minz_Translate::t ('categories_updated')
+				'content' => Minz_Translate::t('categories_updated')
 			);
-			Minz_Session::_param ('notification', $notif);
+			Minz_Session::_param('notification', $notif);
 
-			Minz_Request::forward (array ('c' => 'configure', 'a' => 'categorize'), true);
+			Minz_Request::forward(array('c' => 'configure', 'a' => 'categorize'), true);
 		}
 
-		$this->view->categories = $catDAO->listCategories (false);
-		$this->view->defaultCategory = $catDAO->getDefault ();
-		$this->view->feeds = $feedDAO->listFeeds ();
+		$this->view->categories = $catDAO->listCategories(false);
+		$this->view->defaultCategory = $catDAO->getDefault();
+		$this->view->feeds = $feedDAO->listFeeds();
 
-		Minz_View::prependTitle (Minz_Translate::t ('categories_management') . ' · ');
+		Minz_View::prependTitle(Minz_Translate::t('categories_management') . ' · ');
 	}
 
-	public function feedAction () {
-		$catDAO = new FreshRSS_CategoryDAO ();
-		$this->view->categories = $catDAO->listCategories (false);
+	public function feedAction() {
+		$catDAO = new FreshRSS_CategoryDAO();
+		$this->view->categories = $catDAO->listCategories(false);
 
-		$feedDAO = new FreshRSS_FeedDAO ();
-		$this->view->feeds = $feedDAO->listFeeds ();
+		$feedDAO = FreshRSS_Factory::createFeedDao();
+		$this->view->feeds = $feedDAO->listFeeds();
 
-		$id = Minz_Request::param ('id');
-		if ($id == false && !empty ($this->view->feeds)) {
-			$id = current ($this->view->feeds)->id ();
+		$id = Minz_Request::param('id');
+		if ($id == false && !empty($this->view->feeds)) {
+			$id = current($this->view->feeds)->id();
 		}
 
 		$this->view->flux = false;
@@ -83,14 +83,14 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 			$this->view->flux = $this->view->feeds[$id];
 
 			if (!$this->view->flux) {
-				Minz_Error::error (
+				Minz_Error::error(
 					404,
-					array ('error' => array (Minz_Translate::t ('page_not_found')))
+					array('error' => array(Minz_Translate::t('page_not_found')))
 				);
 			} else {
-				if (Minz_Request::isPost () && $this->view->flux) {
-					$user = Minz_Request::param ('http_user', '');
-					$pass = Minz_Request::param ('http_pass', '');
+				if (Minz_Request::isPost() && $this->view->flux) {
+					$user = Minz_Request::param('http_user', '');
+					$pass = Minz_Request::param('http_pass', '');
 
 					$httpAuth = '';
 					if ($user != '' || $pass != '') {
@@ -99,45 +99,46 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 
 					$cat = intval(Minz_Request::param('category', 0));
 
-					$values = array (
-						'name' => Minz_Request::param ('name', ''),
+					$values = array(
+						'name' => Minz_Request::param('name', ''),
 						'description' => sanitizeHTML(Minz_Request::param('description', '', true)),
 						'website' => Minz_Request::param('website', ''),
 						'url' => Minz_Request::param('url', ''),
 						'category' => $cat,
-						'pathEntries' => Minz_Request::param ('path_entries', ''),
-						'priority' => intval(Minz_Request::param ('priority', 0)),
+						'pathEntries' => Minz_Request::param('path_entries', ''),
+						'priority' => intval(Minz_Request::param('priority', 0)),
 						'httpAuth' => $httpAuth,
-						'keep_history' => intval(Minz_Request::param ('keep_history', -2)),
+						'keep_history' => intval(Minz_Request::param('keep_history', -2)),
+						'ttl' => intval(Minz_Request::param('ttl', -2)),
 					);
 
-					if ($feedDAO->updateFeed ($id, $values)) {
-						$this->view->flux->_category ($cat);
+					if ($feedDAO->updateFeed($id, $values)) {
+						$this->view->flux->_category($cat);
 						$this->view->flux->faviconPrepare();
-						$notif = array (
+						$notif = array(
 							'type' => 'good',
-							'content' => Minz_Translate::t ('feed_updated')
+							'content' => Minz_Translate::t('feed_updated')
 						);
 					} else {
-						$notif = array (
+						$notif = array(
 							'type' => 'bad',
-							'content' => Minz_Translate::t ('error_occurred_update')
+							'content' => Minz_Translate::t('error_occurred_update')
 						);
 					}
 					invalidateHttpCache();
 
-					Minz_Session::_param ('notification', $notif);
-					Minz_Request::forward (array ('c' => 'configure', 'a' => 'feed', 'params' => array ('id' => $id)), true);
+					Minz_Session::_param('notification', $notif);
+					Minz_Request::forward(array('c' => 'configure', 'a' => 'feed', 'params' => array('id' => $id)), true);
 				}
 
-				Minz_View::prependTitle (Minz_Translate::t ('rss_feed_management') . ' — ' . $this->view->flux->name () . ' · ');
+				Minz_View::prependTitle(Minz_Translate::t('rss_feed_management') . ' — ' . $this->view->flux->name() . ' · ');
 			}
 		} else {
-			Minz_View::prependTitle (Minz_Translate::t ('rss_feed_management') . ' · ');
+			Minz_View::prependTitle(Minz_Translate::t('rss_feed_management') . ' · ');
 		}
 	}
 
-	public function displayAction () {
+	public function displayAction() {
 		if (Minz_Request::isPost()) {
 			$this->view->conf->_language(Minz_Request::param('language', 'en'));
 			$themeId = Minz_Request::param('theme', '');
@@ -158,36 +159,37 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 			$this->view->conf->_bottomline_link(Minz_Request::param('bottomline_link', false));
 			$this->view->conf->save();
 
-			Minz_Session::_param ('language', $this->view->conf->language);
-			Minz_Translate::reset ();
+			Minz_Session::_param('language', $this->view->conf->language);
+			Minz_Translate::reset();
 			invalidateHttpCache();
 
-			$notif = array (
+			$notif = array(
 				'type' => 'good',
-				'content' => Minz_Translate::t ('configuration_updated')
+				'content' => Minz_Translate::t('configuration_updated')
 			);
-			Minz_Session::_param ('notification', $notif);
+			Minz_Session::_param('notification', $notif);
 
-			Minz_Request::forward (array ('c' => 'configure', 'a' => 'display'), true);
+			Minz_Request::forward(array('c' => 'configure', 'a' => 'display'), true);
 		}
 
 		$this->view->themes = FreshRSS_Themes::get();
 
-		Minz_View::prependTitle (Minz_Translate::t ('display_configuration') . ' · ');
+		Minz_View::prependTitle(Minz_Translate::t('display_configuration') . ' · ');
 	}
 
-	public function readingAction () {
+	public function readingAction() {
 		if (Minz_Request::isPost()) {
 			$this->view->conf->_posts_per_page(Minz_Request::param('posts_per_page', 10));
 			$this->view->conf->_view_mode(Minz_Request::param('view_mode', 'normal'));
-			$this->view->conf->_default_view (Minz_Request::param('default_view', 'a'));
+			$this->view->conf->_default_view((int)Minz_Request::param('default_view', FreshRSS_Entry::STATE_ALL));
 			$this->view->conf->_auto_load_more(Minz_Request::param('auto_load_more', false));
 			$this->view->conf->_display_posts(Minz_Request::param('display_posts', false));
 			$this->view->conf->_onread_jump_next(Minz_Request::param('onread_jump_next', false));
-			$this->view->conf->_lazyload (Minz_Request::param('lazyload', false));
-			$this->view->conf->_sticky_post (Minz_Request::param('sticky_post', false));
+			$this->view->conf->_lazyload(Minz_Request::param('lazyload', false));
+			$this->view->conf->_sticky_post(Minz_Request::param('sticky_post', false));
+			$this->view->conf->_reading_confirm(Minz_Request::param('reading_confirm', false));
 			$this->view->conf->_sort_order(Minz_Request::param('sort_order', 'DESC'));
-			$this->view->conf->_mark_when (array(
+			$this->view->conf->_mark_when(array(
 				'article' => Minz_Request::param('mark_open_article', false),
 				'site' => Minz_Request::param('mark_open_site', false),
 				'scroll' => Minz_Request::param('mark_scroll', false),
@@ -195,43 +197,43 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 			));
 			$this->view->conf->save();
 
-			Minz_Session::_param ('language', $this->view->conf->language);
-			Minz_Translate::reset ();
+			Minz_Session::_param('language', $this->view->conf->language);
+			Minz_Translate::reset();
 			invalidateHttpCache();
 
-			$notif = array (
+			$notif = array(
 				'type' => 'good',
-				'content' => Minz_Translate::t ('configuration_updated')
+				'content' => Minz_Translate::t('configuration_updated')
 			);
-			Minz_Session::_param ('notification', $notif);
+			Minz_Session::_param('notification', $notif);
 
-			Minz_Request::forward (array ('c' => 'configure', 'a' => 'reading'), true);
+			Minz_Request::forward(array('c' => 'configure', 'a' => 'reading'), true);
 		}
 
-		Minz_View::prependTitle (Minz_Translate::t ('reading_configuration') . ' · ');
+		Minz_View::prependTitle(Minz_Translate::t('reading_configuration') . ' · ');
 	}
 
-	public function sharingAction () {
-		if (Minz_Request::isPost ()) {
+	public function sharingAction() {
+		if (Minz_Request::isPost()) {
 			$params = Minz_Request::params();
-			$this->view->conf->_sharing ($params['share']);
+			$this->view->conf->_sharing($params['share']);
 			$this->view->conf->save();
 			invalidateHttpCache();
 
-			$notif = array (
+			$notif = array(
 				'type' => 'good',
-				'content' => Minz_Translate::t ('configuration_updated')
+				'content' => Minz_Translate::t('configuration_updated')
 			);
-			Minz_Session::_param ('notification', $notif);
+			Minz_Session::_param('notification', $notif);
 
-			Minz_Request::forward (array ('c' => 'configure', 'a' => 'sharing'), true);
+			Minz_Request::forward(array('c' => 'configure', 'a' => 'sharing'), true);
 		}
 
-		Minz_View::prependTitle (Minz_Translate::t ('sharing') . ' · ');
+		Minz_View::prependTitle(Minz_Translate::t('sharing') . ' · ');
 	}
 
-	public function shortcutAction () {
-		$list_keys = array ('a', 'b', 'backspace', 'c', 'd', 'delete', 'down', 'e', 'end', 'enter',
+	public function shortcutAction() {
+		$list_keys = array('a', 'b', 'backspace', 'c', 'd', 'delete', 'down', 'e', 'end', 'enter',
 		                    'escape', 'f', 'g', 'h', 'home', 'i', 'insert', 'j', 'k', 'l', 'left',
 		                    'm', 'n', 'o', 'p', 'page_down', 'page_up', 'q', 'r', 'return', 'right',
 		                    's', 'space', 't', 'tab', 'u', 'up', 'v', 'w', 'x', 'y',
@@ -240,9 +242,9 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 		                    'f10', 'f11', 'f12');
 		$this->view->list_keys = $list_keys;
 
-		if (Minz_Request::isPost ()) {
-			$shortcuts = Minz_Request::param ('shortcuts');
-			$shortcuts_ok = array ();
+		if (Minz_Request::isPost()) {
+			$shortcuts = Minz_Request::param('shortcuts');
+			$shortcuts_ok = array();
 
 			foreach ($shortcuts as $key => $value) {
 				if (in_array($value, $list_keys)) {
@@ -250,33 +252,35 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 				}
 			}
 
-			$this->view->conf->_shortcuts ($shortcuts_ok);
+			$this->view->conf->_shortcuts($shortcuts_ok);
 			$this->view->conf->save();
 			invalidateHttpCache();
 
-			$notif = array (
+			$notif = array(
 				'type' => 'good',
-				'content' => Minz_Translate::t ('shortcuts_updated')
+				'content' => Minz_Translate::t('shortcuts_updated')
 			);
-			Minz_Session::_param ('notification', $notif);
+			Minz_Session::_param('notification', $notif);
 
-			Minz_Request::forward (array ('c' => 'configure', 'a' => 'shortcut'), true);
+			Minz_Request::forward(array('c' => 'configure', 'a' => 'shortcut'), true);
 		}
 
-		Minz_View::prependTitle (Minz_Translate::t ('shortcuts') . ' · ');
+		Minz_View::prependTitle(Minz_Translate::t('shortcuts') . ' · ');
 	}
 
 	public function usersAction() {
-		Minz_View::prependTitle(Minz_Translate::t ('users') . ' · ');
+		Minz_View::prependTitle(Minz_Translate::t('users') . ' · ');
 	}
 
-	public function archivingAction () {
+	public function archivingAction() {
 		if (Minz_Request::isPost()) {
 			$old = Minz_Request::param('old_entries', 3);
 			$keepHistoryDefault = Minz_Request::param('keep_history_default', 0);
+			$ttlDefault = Minz_Request::param('ttl_default', -2);
 
 			$this->view->conf->_old_entries($old);
 			$this->view->conf->_keep_history_default($keepHistoryDefault);
+			$this->view->conf->_ttl_default($ttlDefault);
 			$this->view->conf->save();
 			invalidateHttpCache();
 
@@ -291,7 +295,7 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 
 		Minz_View::prependTitle(Minz_Translate::t('archiving_configuration') . ' · ');
 
-		$entryDAO = new FreshRSS_EntryDAO();
+		$entryDAO = FreshRSS_Factory::createEntryDao();
 		$this->view->nb_total = $entryDAO->count();
 		$this->view->size_user = $entryDAO->size();
 
@@ -299,4 +303,80 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 			$this->view->size_total = $entryDAO->size(true);
 		}
 	}
+	
+	public function queriesAction() {
+		if (Minz_Request::isPost()) {
+			$queries = Minz_Request::param('queries', array());
+
+			foreach ($queries as $key => $query) {
+				if (!$query['name']) {
+					$query['name'] = Minz_Translate::t('query_number', $key + 1);
+				}
+			}
+			$this->view->conf->_queries($queries);
+			$this->view->conf->save();
+
+			$notif = array(
+				'type' => 'good',
+				'content' => Minz_Translate::t('configuration_updated')
+			);
+			Minz_Session::_param('notification', $notif);
+
+			Minz_Request::forward(array('c' => 'configure', 'a' => 'queries'), true);
+		} else {
+			$this->view->query_get = array();
+			foreach ($this->view->conf->queries as $key => $query) {
+				if (!isset($query['get'])) {
+					continue;
+				}
+
+				switch ($query['get'][0]) {
+				case 'c':
+					$dao = new FreshRSS_CategoryDAO();
+					$category = $dao->searchById(substr($query['get'], 2));
+					$this->view->query_get[$key] = array(
+						'type' => 'category',
+						'name' => $category->name(),
+					);
+					break;
+				case 'f':
+					$dao = FreshRSS_Factory::createFeedDao();
+					$feed = $dao->searchById(substr($query['get'], 2));
+					$this->view->query_get[$key] = array(
+						'type' => 'feed',
+						'name' => $feed->name(),
+					);
+					break;
+				case 's':
+					$this->view->query_get[$key] = array(
+						'type' => 'favorite',
+						'name' => 'favorite',
+					);
+					break;
+				case 'a':
+					$this->view->query_get[$key] = array(
+						'type' => 'all',
+						'name' => 'all',
+					);
+					break;
+				}
+			}
+		}
+
+		Minz_View::prependTitle(Minz_Translate::t('queries') . ' · ');
+	}
+	
+	public function addQueryAction() {
+		$queries = $this->view->conf->queries;
+		$query = Minz_Request::params();
+		$query['name'] = Minz_Translate::t('query_number', count($queries) + 1);
+		unset($query['output']);
+		unset($query['token']);
+		$queries[] = $query;
+		$this->view->conf->_queries($queries);
+		$this->view->conf->save();
+
+		// Minz_Request::forward(array('params' => $query), true);
+		Minz_Request::forward(array('c' => 'configure', 'a' => 'queries'), true);
+	}
 }

+ 6 - 6
app/Controllers/entryController.php

@@ -43,7 +43,7 @@ class FreshRSS_entry_Controller extends Minz_ActionController {
 		$nextGet = Minz_Request::param ('nextGet', $get); 
 		$idMax = Minz_Request::param ('idMax', 0);
 
-		$entryDAO = new FreshRSS_EntryDAO ();
+		$entryDAO = FreshRSS_Factory::createEntryDao();
 		if ($id == false) {
 			if (!$get) {
 				$entryDAO->markReadEntries ($idMax);
@@ -85,7 +85,7 @@ class FreshRSS_entry_Controller extends Minz_ActionController {
 
 		$id = Minz_Request::param ('id');
 		if ($id) {
-			$entryDAO = new FreshRSS_EntryDAO ();
+			$entryDAO = FreshRSS_Factory::createEntryDao();
 			$entryDAO->markFavorite ($id, (bool)(Minz_Request::param ('is_favorite', true)));
 		}
 	}
@@ -97,10 +97,10 @@ class FreshRSS_entry_Controller extends Minz_ActionController {
 			// La table des entrées a tendance à grossir énormément
 			// Cette action permet d'optimiser cette table permettant de grapiller un peu de place
 			// Cette fonctionnalité n'est à appeler qu'occasionnellement
-			$entryDAO = new FreshRSS_EntryDAO();
+			$entryDAO = FreshRSS_Factory::createEntryDao();
 			$entryDAO->optimizeTable();
 
-			$feedDAO = new FreshRSS_FeedDAO();
+			$feedDAO = FreshRSS_Factory::createFeedDao();
 			$feedDAO->updateCachedValues();
 
 			invalidateHttpCache();
@@ -124,8 +124,8 @@ class FreshRSS_entry_Controller extends Minz_ActionController {
 		$nb_month_old = max($this->view->conf->old_entries, 1);
 		$date_min = time() - (3600 * 24 * 30 * $nb_month_old);
 
-		$feedDAO = new FreshRSS_FeedDAO();
-		$feeds = $feedDAO->listFeedsOrderUpdate();
+		$feedDAO = FreshRSS_Factory::createFeedDao();
+		$feeds = $feedDAO->listFeeds();
 		$nbTotal = 0;
 
 		invalidateHttpCache();

+ 57 - 49
app/Controllers/feedController.php

@@ -31,7 +31,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 			), true);
 		}
 
-		$feedDAO = new FreshRSS_FeedDAO ();
+		$feedDAO = FreshRSS_Factory::createFeedDao();
 		$this->catDAO = new FreshRSS_CategoryDAO ();
 		$this->catDAO->checkDefault ();
 
@@ -102,25 +102,30 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 
 						$is_read = $this->view->conf->mark_when['reception'] ? 1 : 0;
 
-						$entryDAO = new FreshRSS_EntryDAO ();
+						$entryDAO = FreshRSS_Factory::createEntryDao();
 						$entries = array_reverse($feed->entries());	//We want chronological order and SimplePie uses reverse order
 
 						// on calcule la date des articles les plus anciens qu'on accepte
 						$nb_month_old = $this->view->conf->old_entries;
 						$date_min = time () - (3600 * 24 * 30 * $nb_month_old);
 
+						//MySQL: http://docs.oracle.com/cd/E17952_01/refman-5.5-en/optimizing-innodb-transaction-management.html
+						//SQLite: http://stackoverflow.com/questions/1711631/how-do-i-improve-the-performance-of-sqlite
+						$preparedStatement = $entryDAO->addEntryPrepare();
 						$transactionStarted = true;
-						$feedDAO->beginTransaction ();
+						$feedDAO->beginTransaction();
 						// on ajoute les articles en masse sans vérification
 						foreach ($entries as $entry) {
-							$values = $entry->toArray ();
-							$values['id_feed'] = $feed->id ();
-							$values['id'] = min(time(), $entry->date (true)) . uSecString();
+							$values = $entry->toArray();
+							$values['id_feed'] = $feed->id();
+							$values['id'] = min(time(), $entry->date(true)) . uSecString();
 							$values['is_read'] = $is_read;
-							$entryDAO->addEntry ($values);
+							$entryDAO->addEntry($values, $preparedStatement);
+						}
+						$feedDAO->updateLastUpdate($feed->id());
+						if ($transactionStarted) {
+							$feedDAO->commit();
 						}
-						$feedDAO->updateLastUpdate ($feed->id ());
-						$feedDAO->commit ();
 						$transactionStarted = false;
 
 						// ok, ajout terminé
@@ -162,45 +167,46 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 			}
 
 			Minz_Request::forward (array ('c' => 'configure', 'a' => 'feed', 'params' => $params), true);
-		}
-
-		// GET request so we must ask confirmation to user
-		Minz_View::prependTitle(Minz_Translate::t('add_rss_feed') . ' · ');
-		$this->view->categories = $this->catDAO->listCategories();
-		$this->view->feed = new FreshRSS_Feed($url);
-		try {
-			// We try to get some more information about the feed
-			$this->view->feed->load(true);
-			$this->view->load_ok = true;
-		} catch (Exception $e) {
-			$this->view->load_ok = false;
-		}
+		} else {
 
-		$feed = $feedDAO->searchByUrl($this->view->feed->url());
-		if ($feed) {
-			// Already subscribe so we redirect to the feed configuration page
-			$notif = array(
-				'type' => 'bad',
-				'content' => Minz_Translate::t(
-					'already_subscribed', $feed->name()
-				)
-			);
-			Minz_Session::_param('notification', $notif);
+			// GET request so we must ask confirmation to user
+			Minz_View::prependTitle(Minz_Translate::t('add_rss_feed') . ' · ');
+			$this->view->categories = $this->catDAO->listCategories();
+			$this->view->feed = new FreshRSS_Feed($url);
+			try {
+				// We try to get some more information about the feed
+				$this->view->feed->load(true);
+				$this->view->load_ok = true;
+			} catch (Exception $e) {
+				$this->view->load_ok = false;
+			}
 
-			Minz_Request::forward(array(
-				'c' => 'configure',
-				'a' => 'feed',
-				'params' => array(
-					'id' => $feed->id()
-				)
-			), true);
+			$feed = $feedDAO->searchByUrl($this->view->feed->url());
+			if ($feed) {
+				// Already subscribe so we redirect to the feed configuration page
+				$notif = array(
+					'type' => 'bad',
+					'content' => Minz_Translate::t(
+						'already_subscribed', $feed->name()
+					)
+				);
+				Minz_Session::_param('notification', $notif);
+
+				Minz_Request::forward(array(
+					'c' => 'configure',
+					'a' => 'feed',
+					'params' => array(
+						'id' => $feed->id()
+					)
+				), true);
+			}
 		}
 	}
 
 	public function truncateAction () {
 		if (Minz_Request::isPost ()) {
 			$id = Minz_Request::param ('id');
-			$feedDAO = new FreshRSS_FeedDAO ();
+			$feedDAO = FreshRSS_Factory::createFeedDao();
 			$n = $feedDAO->truncate($id);
 			$notif = array(
 				'type' => $n === false ? 'bad' : 'good',
@@ -215,8 +221,8 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 	public function actualizeAction () {
 		@set_time_limit(300);
 
-		$feedDAO = new FreshRSS_FeedDAO ();
-		$entryDAO = new FreshRSS_EntryDAO ();
+		$feedDAO = FreshRSS_Factory::createFeedDao();
+		$entryDAO = FreshRSS_Factory::createEntryDao();
 
 		Minz_Session::_param('actualize_feeds', false);
 		$id = Minz_Request::param ('id');
@@ -232,7 +238,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 				$feeds = array ($feed);
 			}
 		} else {
-			$feeds = $feedDAO->listFeedsOrderUpdate ();
+			$feeds = $feedDAO->listFeedsOrderUpdate($this->view->conf->ttl_default);
 		}
 
 		// on calcule la date des articles les plus anciens qu'on accepte
@@ -264,22 +270,23 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 						$feedHistory = $this->view->conf->keep_history_default;
 					}
 
+					$preparedStatement = $entryDAO->addEntryPrepare();
 					$hasTransaction = true;
 					$feedDAO->beginTransaction();
 
 					// On ne vérifie pas strictement que l'article n'est pas déjà en BDD
 					// La BDD refusera l'ajout car (id_feed, guid) doit être unique
 					foreach ($entries as $entry) {
-						$eDate = $entry->date (true);
-						if ((!isset ($existingGuids[$entry->guid ()])) &&
+						$eDate = $entry->date(true);
+						if ((!isset($existingGuids[$entry->guid()])) &&
 							(($feedHistory != 0) || ($eDate  >= $date_min))) {
-							$values = $entry->toArray ();
+							$values = $entry->toArray();
 							//Use declared date at first import, otherwise use discovery date
 							$values['id'] = ($useDeclaredDate || $eDate < $date_min) ?
 								min(time(), $eDate) . uSecString() :
 								uTimeString();
 							$values['is_read'] = $is_read;
-							$entryDAO->addEntry ($values);
+							$entryDAO->addEntry($values, $preparedStatement);
 						}
 					}
 				}
@@ -300,7 +307,8 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 					$feedDAO->commit();
 				}
 				$flux_update++;
-				if ($feed->url() !== $url) {	//URL has changed (auto-discovery)
+				if (($feed->url() !== $url)) {	//HTTP 301 Moved Permanently
+					Minz_Log::record('Feed ' . $url . ' moved permanently to ' . $feed->url(), Minz_Log::NOTICE);
 					$feedDAO->updateFeed($feed->id(), array('url' => $feed->url()));
 				}
 			} catch (FreshRSS_Feed_Exception $e) {
@@ -373,7 +381,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 			$type = Minz_Request::param ('type', 'feed');
 			$id = Minz_Request::param ('id');
 
-			$feedDAO = new FreshRSS_FeedDAO ();
+			$feedDAO = FreshRSS_Factory::createFeedDao();
 			if ($type == 'category') {
 				if ($feedDAO->deleteFeedByCategory ($id)) {
 					$notif = array (

+ 3 - 2
app/Controllers/importExportController.php

@@ -12,8 +12,8 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 		require_once(LIB_PATH . '/lib_opml.php');
 
 		$this->catDAO = new FreshRSS_CategoryDAO();
-		$this->entryDAO = new FreshRSS_EntryDAO();
-		$this->feedDAO = new FreshRSS_FeedDAO();
+		$this->entryDAO = FreshRSS_Factory::createEntryDao();
+		$this->feedDAO = FreshRSS_Factory::createFeedDao();
 	}
 
 	public function indexAction() {
@@ -266,6 +266,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 			);
 			$entry->_tags($tags);
 
+			//FIME: Use entryDAO->addEntryPrepare(). Do not call entryDAO->listLastGuidsByFeed() for each entry. Consider using a transaction.
 			$id = $this->entryDAO->addEntryObject(
 				$entry, $this->view->conf, $feed->keepHistory()
 			);

+ 13 - 29
app/Controllers/indexController.php

@@ -45,7 +45,7 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 		}
 
 		$catDAO = new FreshRSS_CategoryDAO();
-		$entryDAO = new FreshRSS_EntryDAO();
+		$entryDAO = FreshRSS_Factory::createEntryDao();
 
 		$this->view->cat_aside = $catDAO->listCategories ();
 		$this->view->nb_favorites = $entryDAO->countUnreadReadFavorites ();
@@ -70,11 +70,11 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 		// mise à jour des titres
 		$this->view->rss_title = $this->view->currentName . ' | ' . Minz_View::title();
 		if ($this->view->nb_not_read > 0) {
-			Minz_View::appendTitle (' (' . formatNumber($this->view->nb_not_read) . ')');
+			Minz_View::prependTitle('(' . formatNumber($this->view->nb_not_read) . ') ');
 		}
-		Minz_View::prependTitle (
+		Minz_View::prependTitle(
+			($this->nb_not_read_cat > 0 ? '(' . formatNumber($this->nb_not_read_cat) . ') ' : '') .
 			$this->view->currentName .
-			($this->nb_not_read_cat > 0 ? ' (' . formatNumber($this->nb_not_read_cat) . ')' : '') .
 			' · '
 		);
 
@@ -82,9 +82,6 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 		$this->view->state = $state = Minz_Request::param ('state', $this->view->conf->default_view);
 		$state_param = Minz_Request::param ('state', null);
 		$filter = Minz_Request::param ('search', '');
-		if (!empty($filter)) {
-			$state = FreshRSS_Entry::STATE_ALL;	//Search always in read and unread articles
-		}
 		$this->view->order = $order = Minz_Request::param ('order', $this->view->conf->sort_order);
 		$nb = Minz_Request::param ('nb', $this->view->conf->posts_per_page);
 		$first = Minz_Request::param ('next', '');
@@ -127,8 +124,14 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 
 			// Si on a récupéré aucun article "non lus"
 			// on essaye de récupérer tous les articles
-			if ($state === FreshRSS_Entry::STATE_NOT_READ && empty($entries) && ($state_param === null)) {
-				Minz_Log::record ('Conflicting information about nbNotRead!', Minz_Log::DEBUG);
+			if ($state === FreshRSS_Entry::STATE_NOT_READ && empty($entries) && ($state_param === null) && ($filter == '')) {
+				Minz_Log::record('Conflicting information about nbNotRead!', Minz_Log::DEBUG);
+				$feedDAO = FreshRSS_Factory::createFeedDao();
+				try {
+					$feedDAO->updateCachedValues();
+				} catch (Exception $ex) {
+					Minz_Log::record('Failed to automatically correct nbNotRead! ' + $ex->getMessage(), Minz_Log::NOTICE);
+				}
 				$this->view->state = FreshRSS_Entry::STATE_ALL;
 				$entries = $entryDAO->listWhere($getType, $getId, $this->view->state, $order, $nb, $first, $filter, $date_min, true, $keepHistoryDefault);
 			}
@@ -184,7 +187,7 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 			case 'f':
 				$feed = FreshRSS_CategoryDAO::findFeed($this->view->cat_aside, $getId);
 				if (empty($feed)) {
-					$feedDAO = new FreshRSS_FeedDAO();
+					$feedDAO = FreshRSS_Factory::createFeedDao();
 					$feed = $feedDAO->searchById($getId);
 				}
 				if ($feed) {
@@ -201,25 +204,6 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 		}
 	}
 	
-	public function statsAction () {
-		if (!$this->view->loginOk) {
-			Minz_Error::error (
-				403,
-				array ('error' => array (Minz_Translate::t ('access_denied')))
-			);
-		}
-
-		Minz_View::prependTitle (Minz_Translate::t ('stats') . ' · ');
-
-		$statsDAO = new FreshRSS_StatsDAO ();
-		Minz_View::appendScript (Minz_Url::display ('/scripts/flotr2.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/flotr2.min.js')));
-		$this->view->repartition = $statsDAO->calculateEntryRepartition();
-		$this->view->count = ($statsDAO->calculateEntryCount());
-		$this->view->feedByCategory = $statsDAO->calculateFeedByCategory();
-		$this->view->entryByCategory = $statsDAO->calculateEntryByCategory();
-		$this->view->topFeed = $statsDAO->calculateTopFeed();
-	}
-
 	public function aboutAction () {
 		Minz_View::prependTitle (Minz_Translate::t ('about') . ' · ');
 	}

+ 2 - 2
app/Controllers/javascriptController.php

@@ -7,8 +7,8 @@ class FreshRSS_javascript_Controller extends Minz_ActionController {
 
 	public function actualizeAction () {
 		header('Content-Type: text/javascript; charset=UTF-8');
-		$feedDAO = new FreshRSS_FeedDAO ();
-		$this->view->feeds = $feedDAO->listFeedsOrderUpdate();
+		$feedDAO = FreshRSS_Factory::createFeedDao();
+		$this->view->feeds = $feedDAO->listFeedsOrderUpdate($this->view->conf->ttl_default);
 	}
 
 	public function nbUnreadsPerFeedAction() {

+ 67 - 0
app/Controllers/statsController.php

@@ -0,0 +1,67 @@
+<?php
+
+class FreshRSS_stats_Controller extends Minz_ActionController {
+
+	public function indexAction() {
+		$statsDAO = FreshRSS_Factory::createStatsDAO();
+		Minz_View::appendScript (Minz_Url::display ('/scripts/flotr2.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/flotr2.min.js')));
+		$this->view->repartition = $statsDAO->calculateEntryRepartition();
+		$this->view->count = ($statsDAO->calculateEntryCount());
+		$this->view->feedByCategory = $statsDAO->calculateFeedByCategory();
+		$this->view->entryByCategory = $statsDAO->calculateEntryByCategory();
+		$this->view->topFeed = $statsDAO->calculateTopFeed();
+	}
+
+	public function idleAction() {
+		$statsDAO = FreshRSS_Factory::createStatsDAO();
+		$feeds = $statsDAO->calculateFeedLastDate();
+		$idleFeeds = array();
+		$now = new \DateTime();
+		$feedDate = clone $now;
+		$lastWeek = clone $now;
+		$lastWeek->modify('-1 week');
+		$lastMonth = clone $now;
+		$lastMonth->modify('-1 month');
+		$last3Month = clone $now;
+		$last3Month->modify('-3 month');
+		$last6Month = clone $now;
+		$last6Month->modify('-6 month');
+		$lastYear = clone $now;
+		$lastYear->modify('-1 year');
+
+		foreach ($feeds as $feed) {
+			$feedDate->setTimestamp($feed['last_date']);
+			if ($feedDate >= $lastWeek) {
+				continue;
+			}
+			if ($feedDate < $lastWeek) {
+				$idleFeeds['last_week'][] = $feed['name'];
+			}
+			if ($feedDate < $lastMonth) {
+				$idleFeeds['last_month'][] = $feed['name'];
+			}
+			if ($feedDate < $last3Month) {
+				$idleFeeds['last_3_month'][] = $feed['name'];
+			}
+			if ($feedDate < $last6Month) {
+				$idleFeeds['last_6_month'][] = $feed['name'];
+			}
+			if ($feedDate < $lastYear) {
+				$idleFeeds['last_year'][] = $feed['name'];
+			}
+		}
+
+		$this->view->idleFeeds = array_reverse($idleFeeds);
+	}
+	
+	public function firstAction() {
+		if (!$this->view->loginOk) {
+			Minz_Error::error(
+			    403, array('error' => array(Minz_Translate::t('access_denied')))
+			);
+		}
+
+		Minz_View::prependTitle(Minz_Translate::t('stats') . ' · ');
+	}
+
+}

+ 8 - 6
app/Controllers/usersController.php

@@ -17,7 +17,7 @@ class FreshRSS_users_Controller extends Minz_ActionController {
 		if (Minz_Request::isPost()) {
 			$ok = true;
 
-			$passwordPlain = Minz_Request::param('passwordPlain', false);
+			$passwordPlain = Minz_Request::param('passwordPlain', '', true);
 			if ($passwordPlain != '') {
 				Minz_Request::_param('passwordPlain');	//Discard plain-text password ASAP
 				$_POST['passwordPlain'] = '';
@@ -32,7 +32,7 @@ class FreshRSS_users_Controller extends Minz_ActionController {
 			}
 			Minz_Session::_param('passwordHash', $this->view->conf->passwordHash);
 
-			$passwordPlain = Minz_Request::param('apiPasswordPlain', false);
+			$passwordPlain = Minz_Request::param('apiPasswordPlain', '', true);
 			if ($passwordPlain != '') {
 				if (!function_exists('password_hash')) {
 					include_once(LIB_PATH . '/password_compat.php');
@@ -45,7 +45,7 @@ class FreshRSS_users_Controller extends Minz_ActionController {
 			}
 
 			if (Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) {
-				$this->view->conf->_mail_login(Minz_Request::param('mail_login', false));
+				$this->view->conf->_mail_login(Minz_Request::param('mail_login', '', true));
 			}
 			$email = $this->view->conf->mail_login;
 			Minz_Session::_param('mail', $email);
@@ -99,7 +99,8 @@ class FreshRSS_users_Controller extends Minz_ActionController {
 
 	public function createAction() {
 		if (Minz_Request::isPost() && Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) {
-			require_once(APP_PATH . '/sql.php');
+			$db = Minz_Configuration::dataBase();
+			require_once(APP_PATH . '/SQL/sql.' . $db['type'] . '.php');
 
 			$new_user_language = Minz_Request::param('new_user_language', $this->view->conf->language);
 			if (!in_array($new_user_language, $this->view->conf->availableLanguages())) {
@@ -119,7 +120,7 @@ class FreshRSS_users_Controller extends Minz_ActionController {
 			}
 			if ($ok) {
 			
-				$passwordPlain = Minz_Request::param('new_user_passwordPlain', false);
+				$passwordPlain = Minz_Request::param('new_user_passwordPlain', '', true);
 				$passwordHash = '';
 				if ($passwordPlain != '') {
 					Minz_Request::_param('new_user_passwordPlain');	//Discard plain-text password ASAP
@@ -170,7 +171,8 @@ class FreshRSS_users_Controller extends Minz_ActionController {
 
 	public function deleteAction() {
 		if (Minz_Request::isPost() && Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) {
-			require_once(APP_PATH . '/sql.php');
+			$db = Minz_Configuration::dataBase();
+			require_once(APP_PATH . '/SQL/sql.' . $db['type'] . '.php');
 
 			$username = Minz_Request::param('username');
 			$ok = ctype_alnum($username);

+ 1 - 1
app/Models/Category.php

@@ -44,7 +44,7 @@ class FreshRSS_Category extends Minz_Model {
 	}
 	public function feeds () {
 		if ($this->feeds === null) {
-			$feedDAO = new FreshRSS_FeedDAO ();
+			$feedDAO = FreshRSS_Factory::createFeedDao();
 			$this->feeds = $feedDAO->listByCategory ($this->id ());
 			$this->nbFeed = 0;
 			$this->nbNotRead = 0;

+ 8 - 8
app/Models/CategoryDAO.php

@@ -12,8 +12,8 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo {
 		if ($stm && $stm->execute ($values)) {
 			return $this->bd->lastInsertId();
 		} else {
-			$info = $stm->errorInfo();
-			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error addCategory: ' . $info[2], Minz_Log::ERROR);
 			return false;
 		}
 	}
@@ -43,8 +43,8 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo {
 		if ($stm && $stm->execute ($values)) {
 			return $stm->rowCount();
 		} else {
-			$info = $stm->errorInfo();
-			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error updateCategory: ' . $info[2], Minz_Log::ERROR);
 			return false;
 		}
 	}
@@ -58,8 +58,8 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo {
 		if ($stm && $stm->execute ($values)) {
 			return $stm->rowCount();
 		} else {
-			$info = $stm->errorInfo();
-			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error deleteCategory: ' . $info[2], Minz_Log::ERROR);
 			return false;
 		}
 	}
@@ -102,7 +102,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo {
 			$sql = 'SELECT c.id AS c_id, c.name AS c_name, '
 			     . ($details ? 'f.* ' : 'f.id, f.name, f.url, f.website, f.priority, f.error, f.cache_nbEntries, f.cache_nbUnreads ')
 			     . 'FROM `' . $this->prefix . 'category` c '
-			     . 'LEFT OUTER JOIN `' . $this->prefix . 'feed` f ON f.category = c.id '
+			     . 'LEFT OUTER JOIN `' . $this->prefix . 'feed` f ON f.category=c.id '
 			     . 'GROUP BY f.id '
 			     . 'ORDER BY c.name, f.name';
 			$stm = $this->bd->prepare ($sql);
@@ -166,7 +166,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo {
 	}
 
 	public function countNotRead ($id) {
-		$sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id WHERE category=? AND e.is_read=0';
+		$sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id WHERE category=? AND e.is_read=0';
 		$stm = $this->bd->prepare ($sql);
 		$values = array ($id);
 		$stm->execute ($values);

+ 22 - 0
app/Models/Configuration.php

@@ -7,6 +7,7 @@ class FreshRSS_Configuration {
 		'language' => 'en',
 		'old_entries' => 3,
 		'keep_history_default' => 0,
+		'ttl_default' => 3600,
 		'mail_login' => '',
 		'token' => '',
 		'passwordHash' => '',	//CRYPT_BLOWFISH
@@ -19,6 +20,7 @@ class FreshRSS_Configuration {
 		'onread_jump_next' => true,
 		'lazyload' => true,
 		'sticky_post' => true,
+		'reading_confirm' => false,
 		'sort_order' => 'DESC',
 		'anon_access' => false,
 		'mark_when' => array(
@@ -53,6 +55,7 @@ class FreshRSS_Configuration {
 		'bottomline_date' => true,
 		'bottomline_link' => true,
 		'sharing' => array(),
+		'queries' => array(),
 	);
 
 	private $available_languages = array(
@@ -147,6 +150,9 @@ class FreshRSS_Configuration {
 	public function _sticky_post($value) {
 		$this->data['sticky_post'] = ((bool)$value) && $value !== 'no';
 	}
+	public function _reading_confirm($value) {
+		$this->data['reading_confirm'] = ((bool)$value) && $value !== 'no';
+	}
 	public function _sort_order ($value) {
 		$this->data['sort_order'] = $value === 'ASC' ? 'ASC' : 'DESC';
 	}
@@ -158,6 +164,10 @@ class FreshRSS_Configuration {
 		$value = intval($value);
 		$this->data['keep_history_default'] = $value >= -1 ? $value : 0;
 	}
+	public function _ttl_default($value) {
+		$value = intval($value);
+		$this->data['ttl_default'] = $value >= -1 ? $value : 3600;
+	}
 	public function _shortcuts ($values) {
 		foreach ($values as $key => $value) {
 			if (isset($this->data['shortcuts'][$key])) {
@@ -219,6 +229,18 @@ class FreshRSS_Configuration {
 			$this->data['sharing'][] = $value;
 		}
 	}
+	public function _queries ($values) {
+		$this->data['queries'] = array();
+		foreach ($values as $value) {
+			$value = array_filter($value);
+			$params = $value;
+			unset($params['name']);
+			unset($params['url']);
+			$value['url'] = Minz_Url::display(array('params' => $params));
+
+			$this->data['queries'][] = $value;
+		}
+	}
 	public function _theme($value) {
 		$this->data['theme'] = $value;
 	}

+ 2 - 2
app/Models/Entry.php

@@ -74,7 +74,7 @@ class FreshRSS_Entry extends Minz_Model {
 	}
 	public function feed ($object = false) {
 		if ($object) {
-			$feedDAO = new FreshRSS_FeedDAO ();
+			$feedDAO = FreshRSS_Factory::createFeedDao();
 			return $feedDAO->searchById ($this->feed);
 		} else {
 			return $this->feed;
@@ -154,7 +154,7 @@ class FreshRSS_Entry extends Minz_Model {
 		// Gestion du contenu
 		// On cherche à récupérer les articles en entier... même si le flux ne le propose pas
 		if ($pathEntries) {
-			$entryDAO = new FreshRSS_EntryDAO();
+			$entryDAO = FreshRSS_Factory::createEntryDao();
 			$entry = $entryDAO->searchByGuid($this->feed, $this->guid);
 
 			if($entry) {

+ 241 - 329
app/Models/EntryDAO.php

@@ -1,12 +1,25 @@
 <?php
 
 class FreshRSS_EntryDAO extends Minz_ModelPdo {
-	public function addEntry ($valuesTmp) {
-		$sql = 'INSERT INTO `' . $this->prefix . 'entry`(id, guid, title, author, content_bin, link, date, is_read, is_favorite, id_feed, tags) '
-		     . 'VALUES(?, ?, ?, ?, COMPRESS(?), ?, ?, ?, ?, ?, ?)';
-		$stm = $this->bd->prepare ($sql);
 
-		$values = array (
+	public function isCompressed() {
+		return parent::$sharedDbType !== 'sqlite';
+	}
+
+	public function addEntryPrepare() {
+		$sql = 'INSERT INTO `' . $this->prefix . 'entry`(id, guid, title, author, '
+		     . ($this->isCompressed() ? 'content_bin' : 'content')
+		     . ', link, date, is_read, is_favorite, id_feed, tags) '
+		     . 'VALUES(?, ?, ?, ?, '
+		     . ($this->isCompressed() ? 'COMPRESS(?)' : '?')
+		     . ', ?, ?, ?, ?, ?, ?)';
+		return $this->bd->prepare($sql);
+	}
+
+	public function addEntry($valuesTmp, $preparedStatement = null) {
+		$stm = $preparedStatement === null ? addEntryPrepare() : $preparedStatement;
+
+		$values = array(
 			$valuesTmp['id'],
 			substr($valuesTmp['guid'], 0, 760),
 			substr($valuesTmp['title'], 0, 255),
@@ -20,12 +33,12 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
 			substr($valuesTmp['tags'], 0, 1023),
 		);
 
-		if ($stm && $stm->execute ($values)) {
+		if ($stm && $stm->execute($values)) {
 			return $this->bd->lastInsertId();
 		} else {
-			$info = $stm->errorInfo();
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
 			if ((int)($info[0] / 1000) !== 23) {	//Filter out "SQLSTATE Class code 23: Constraint Violation" because of expected duplicate entries
-				Minz_Log::record ('SQL error ' . $info[0] . ': ' . $info[1] . ' ' . $info[2]
+				Minz_Log::record('SQL error addEntry: ' . $info[0] . ': ' . $info[1] . ' ' . $info[2]
 				. ' while adding entry in feed ' . $valuesTmp['id_feed'] . ' with title: ' . $valuesTmp['title'], Minz_Log::ERROR);
 			} /*else {
 				Minz_Log::record ('SQL error ' . $info[0] . ': ' . $info[1] . ' ' . $info[2]
@@ -69,23 +82,53 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
 		if (!is_array($ids)) {
 			$ids = array($ids);
 		}
-		$sql = 'UPDATE `' . $this->prefix . 'entry` e '
-		     . 'SET e.is_favorite = ? '
-		     . 'WHERE e.id IN (' . str_repeat('?,', count($ids) - 1). '?)';
-		$values = array ($is_favorite ? 1 : 0);
+		$sql = 'UPDATE `' . $this->prefix . 'entry` '
+		     . 'SET is_favorite=? '
+		     . 'WHERE id IN (' . str_repeat('?,', count($ids) - 1). '?)';
+		$values = array($is_favorite ? 1 : 0);
 		$values = array_merge($values, $ids);
-		$stm = $this->bd->prepare ($sql);
-		if ($stm && $stm->execute ($values)) {
+		$stm = $this->bd->prepare($sql);
+		if ($stm && $stm->execute($values)) {
 			return $stm->rowCount();
 		} else {
-			$info = $stm->errorInfo();
-			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error markFavorite: ' . $info[2], Minz_Log::ERROR);
+			return false;
+		}
+	}
+
+	protected function updateCacheUnreads($catId = false, $feedId = false) {
+		$sql = 'UPDATE `' . $this->prefix . 'feed` f '
+		 . 'LEFT OUTER JOIN ('
+		 .	'SELECT e.id_feed, '
+		 .	'COUNT(*) AS nbUnreads '
+		 .	'FROM `' . $this->prefix . 'entry` e '
+		 .	'WHERE e.is_read=0 '
+		 .	'GROUP BY e.id_feed'
+		 . ') x ON x.id_feed=f.id '
+		 . 'SET f.cache_nbUnreads=COALESCE(x.nbUnreads, 0) '
+		 . 'WHERE 1';
+		$values = array();
+		if ($feedId !== false) {
+			$sql .= ' AND f.id=?';
+			$values[] = $id;
+		}
+		if ($catId !== false) {
+			$sql .= ' AND f.category=?';
+			$values[] = $catId;
+		}
+		$stm = $this->bd->prepare($sql);
+		if ($stm && $stm->execute($values)) {
+			return true;
+		} else {
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error updateCacheUnreads: ' . $info[2], Minz_Log::ERROR);
 			return false;
 		}
 	}
 
 	public function markRead($ids, $is_read = true) {
-		if (is_array($ids)) {
+		if (is_array($ids)) {	//Many IDs at once (used by API)
 			if (count($ids) < 6) {	//Speed heuristics
 				$affected = 0;
 				foreach ($ids as $id) {
@@ -94,316 +137,164 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
 				return $affected;
 			}
 
-			$this->bd->beginTransaction();
-			$sql = 'UPDATE `' . $this->prefix . 'entry` e '
-				 . 'SET e.is_read = ? '
-				 . 'WHERE e.id IN (' . str_repeat('?,', count($ids) - 1). '?)';
+			$sql = 'UPDATE `' . $this->prefix . 'entry` '
+				 . 'SET is_read=? '
+				 . 'WHERE id IN (' . str_repeat('?,', count($ids) - 1). '?)';
 			$values = array($is_read ? 1 : 0);
 			$values = array_merge($values, $ids);
 			$stm = $this->bd->prepare($sql);
 			if (!($stm && $stm->execute($values))) {
-				$info = $stm->errorInfo();
-				Minz_Log::record('SQL error : ' . $info[2], Minz_Log::ERROR);
-				$this->bd->rollBack();
+				$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+				Minz_Log::record('SQL error markRead: ' . $info[2], Minz_Log::ERROR);
 				return false;
 			}
 			$affected = $stm->rowCount();
-
-			if ($affected > 0) {
-				$sql = 'UPDATE `' . $this->prefix . 'feed` f '
-				     . 'INNER JOIN ('
-				     .	'SELECT e.id_feed, '
-				     .	'COUNT(CASE WHEN e.is_read = 0 THEN 1 END) AS nbUnreads, '
-				     .	'COUNT(e.id) AS nbEntries '
-				     .	'FROM `' . $this->prefix . 'entry` e '
-				     .	'GROUP BY e.id_feed'
-				     . ') x ON x.id_feed=f.id '
-				     . 'SET f.cache_nbEntries=x.nbEntries, f.cache_nbUnreads=x.nbUnreads';
-				$stm = $this->bd->prepare($sql);
-				if (!($stm && $stm->execute())) {
-					$info = $stm->errorInfo();
-					Minz_Log::record('SQL error : ' . $info[2], Minz_Log::ERROR);
-					$this->bd->rollBack();
-					return false;
-				}
+			if (($affected > 0) && (!$this->updateCacheUnreads(false, false))) {
+				return false;
 			}
-
-			$this->bd->commit();
 			return $affected;
 		} else {
-			$sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id '
-				 . 'SET e.is_read = ?,'
+			$sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id '
+				 . 'SET e.is_read=?,'
 				 . 'f.cache_nbUnreads=f.cache_nbUnreads' . ($is_read ? '-' : '+') . '1 '
-				 . 'WHERE e.id=?';
-			$values = array($is_read ? 1 : 0, $ids);
+				 . 'WHERE e.id=? AND e.is_read=?';
+			$values = array($is_read ? 1 : 0, $ids, $is_read ? 0 : 1);
 			$stm = $this->bd->prepare($sql);
 			if ($stm && $stm->execute($values)) {
 				return $stm->rowCount();
 			} else {
-				$info = $stm->errorInfo();
-				Minz_Log::record('SQL error : ' . $info[2], Minz_Log::ERROR);
+				$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+				Minz_Log::record('SQL error markRead: ' . $info[2], Minz_Log::ERROR);
 				return false;
 			}
 		}
 	}
 
-	public function markReadEntries ($idMax = 0, $onlyFavorites = false, $priorityMin = 0) {
+	public function markReadEntries($idMax = 0, $onlyFavorites = false, $priorityMin = 0) {
 		if ($idMax == 0) {
-			$sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id '
-			     . 'SET e.is_read = 1, f.cache_nbUnreads=0 '
-			     . 'WHERE e.is_read = 0';
-			if ($onlyFavorites) {
-				$sql .= ' AND e.is_favorite = 1';
-			} elseif ($priorityMin >= 0) {
-				$sql .= ' AND f.priority > ' . intval($priorityMin);
-			}
-			$stm = $this->bd->prepare ($sql);
-			if ($stm && $stm->execute ()) {
-				return $stm->rowCount();
-			} else {
-				$info = $stm->errorInfo();
-				Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
-				return false;
-			}
-		} else {
-			$this->bd->beginTransaction ();
-
-			$sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id '
-			     . 'SET e.is_read = 1 '
-			     . 'WHERE e.is_read = 0 AND e.id <= ?';
-			if ($onlyFavorites) {
-				$sql .= ' AND e.is_favorite = 1';
-			} elseif ($priorityMin >= 0) {
-				$sql .= ' AND f.priority > ' . intval($priorityMin);
-			}
-			$values = array ($idMax);
-			$stm = $this->bd->prepare ($sql);
-			if (!($stm && $stm->execute ($values))) {
-				$info = $stm->errorInfo();
-				Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
-				$this->bd->rollBack ();
-				return false;
-			}
-			$affected = $stm->rowCount();
-
-			if ($affected > 0) {
-				$sql = 'UPDATE `' . $this->prefix . 'feed` f '
-			     . 'LEFT OUTER JOIN ('
-			     .	'SELECT e.id_feed, '
-			     .	'COUNT(*) AS nbUnreads '
-			     .	'FROM `' . $this->prefix . 'entry` e '
-			     .	'WHERE e.is_read = 0 '
-			     .	'GROUP BY e.id_feed'
-			     . ') x ON x.id_feed=f.id '
-			     . 'SET f.cache_nbUnreads=COALESCE(x.nbUnreads, 0)';
-				$stm = $this->bd->prepare ($sql);
-				if (!($stm && $stm->execute ())) {
-					$info = $stm->errorInfo();
-					Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
-					$this->bd->rollBack ();
-					return false;
-				}
-			}
+			$idMax = time() . '000000';
+			Minz_Log::record($nb . 'Calling markReadEntries(0) is deprecated!', Minz_Log::DEBUG);
+		}
 
-			$this->bd->commit ();
-			return $affected;
+		$sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id '
+			 . 'SET e.is_read=1 '
+			 . 'WHERE e.is_read=0 AND e.id <= ?';
+		if ($onlyFavorites) {
+			$sql .= ' AND e.is_favorite=1';
+		} elseif ($priorityMin >= 0) {
+			$sql .= ' AND f.priority > ' . intval($priorityMin);
 		}
+		$values = array($idMax);
+		$stm = $this->bd->prepare($sql);
+		if (!($stm && $stm->execute($values))) {
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error markReadEntries: ' . $info[2], Minz_Log::ERROR);
+			return false;
+		}
+		$affected = $stm->rowCount();
+		if (($affected > 0) && (!$this->updateCacheUnreads(false, false))) {
+			return false;
+		}
+		return $affected;
 	}
 
-	public function markReadCat ($id, $idMax = 0) {
+	public function markReadCat($id, $idMax = 0) {
 		if ($idMax == 0) {
-			$sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id '
-			     . 'SET e.is_read = 1, f.cache_nbUnreads=0 '
-			     . 'WHERE f.category = ? AND e.is_read = 0';
-			$values = array ($id);
-			$stm = $this->bd->prepare ($sql);
-			if ($stm && $stm->execute ($values)) {
-				return $stm->rowCount();
-			} else {
-				$info = $stm->errorInfo();
-				Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
-				return false;
-			}
-		} else {
-			$this->bd->beginTransaction ();
-
-			$sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id '
-			     . 'SET e.is_read = 1 '
-			     . 'WHERE f.category = ? AND e.is_read = 0 AND e.id <= ?';
-			$values = array ($id, $idMax);
-			$stm = $this->bd->prepare ($sql);
-			if (!($stm && $stm->execute ($values))) {
-				$info = $stm->errorInfo();
-				Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
-				$this->bd->rollBack ();
-				return false;
-			}
-			$affected = $stm->rowCount();
-
-			if ($affected > 0) {
-				$sql = 'UPDATE `' . $this->prefix . 'feed` f '
-			     . 'LEFT OUTER JOIN ('
-			     .	'SELECT e.id_feed, '
-			     .	'COUNT(*) AS nbUnreads '
-			     .	'FROM `' . $this->prefix . 'entry` e '
-			     .	'WHERE e.is_read = 0 '
-			     .	'GROUP BY e.id_feed'
-			     . ') x ON x.id_feed=f.id '
-			     . 'SET f.cache_nbUnreads=COALESCE(x.nbUnreads, 0) '
-			     . 'WHERE f.category = ?';
-				$values = array ($id);
-				$stm = $this->bd->prepare ($sql);
-				if (!($stm && $stm->execute ($values))) {
-					$info = $stm->errorInfo();
-					Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
-					$this->bd->rollBack ();
-					return false;
-				}
-			}
+			$idMax = time() . '000000';
+			Minz_Log::record($nb . 'Calling markReadCat(0) is deprecated!', Minz_Log::DEBUG);
+		}
 
-			$this->bd->commit ();
-			return $affected;
+		$sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id '
+			 . 'SET e.is_read=1 '
+			 . 'WHERE f.category=? AND e.is_read=0 AND e.id <= ?';
+		$values = array($id, $idMax);
+		$stm = $this->bd->prepare($sql);
+		if (!($stm && $stm->execute($values))) {
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error markReadCat: ' . $info[2], Minz_Log::ERROR);
+			return false;
+		}
+		$affected = $stm->rowCount();
+		if (($affected > 0) && (!$this->updateCacheUnreads($id, false))) {
+			return false;
 		}
+		return $affected;
 	}
 
-	public function markReadCatName($name, $idMax = 0) {
+	public function markReadFeed($id, $idMax = 0) {
 		if ($idMax == 0) {
-			$sql = 'UPDATE `' . $this->prefix . 'entry` e '
-			     . 'INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id '
-			     . 'INNER JOIN `' . $this->prefix . 'category` c ON c.id = f.category '
-			     . 'SET e.is_read = 1, f.cache_nbUnreads=0 '
-			     . 'WHERE c.name = ?';
-			$values = array($name);
-			$stm = $this->bd->prepare($sql);
-			if ($stm && $stm->execute($values)) {
-				return $stm->rowCount();
-			} else {
-				$info = $stm->errorInfo();
-				Minz_Log::record('SQL error : ' . $info[2], Minz_Log::ERROR);
-				return false;
-			}
-		} else {
-			$this->bd->beginTransaction();
-
-			$sql = 'UPDATE `' . $this->prefix . 'entry` e '
-			     . 'INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id '
-			     . 'INNER JOIN `' . $this->prefix . 'category` c ON c.id = f.category '
-			     . 'SET e.is_read = 1 '
-			     . 'WHERE c.name = ? AND e.id <= ?';
-			$values = array($name, $idMax);
+			$idMax = time() . '000000';
+			Minz_Log::record($nb . 'Calling markReadFeed(0) is deprecated!', Minz_Log::DEBUG);
+		}
+		$this->bd->beginTransaction();
+
+		$sql = 'UPDATE `' . $this->prefix . 'entry`  '
+			 . 'SET is_read=1 '
+			 . 'WHERE id_feed=? AND is_read=0 AND id <= ?';
+		$values = array($id, $idMax);
+		$stm = $this->bd->prepare($sql);
+		if (!($stm && $stm->execute($values))) {
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error markReadFeed: ' . $info[2], Minz_Log::ERROR);
+			$this->bd->rollBack();
+			return false;
+		}
+		$affected = $stm->rowCount();
+
+		if ($affected > 0) {
+			$sql = 'UPDATE `' . $this->prefix . 'feed` '
+				 . 'SET cache_nbUnreads=cache_nbUnreads-' . $affected
+				 . ' WHERE id=?';
+			$values = array($id);
 			$stm = $this->bd->prepare($sql);
 			if (!($stm && $stm->execute($values))) {
-				$info = $stm->errorInfo();
-				Minz_Log::record('SQL error : ' . $info[2], Minz_Log::ERROR);
+				$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+				Minz_Log::record('SQL error markReadFeed: ' . $info[2], Minz_Log::ERROR);
 				$this->bd->rollBack();
 				return false;
 			}
-			$affected = $stm->rowCount();
-
-			if ($affected > 0) {
-				$sql = 'UPDATE `' . $this->prefix . 'feed` f '
-			     . 'LEFT OUTER JOIN ('
-			     .	'SELECT e.id_feed, '
-			     .	'COUNT(*) AS nbUnreads '
-			     .	'FROM `' . $this->prefix . 'entry` e '
-			     .	'WHERE e.is_read = 0 '
-			     .	'GROUP BY e.id_feed'
-			     . ') x ON x.id_feed=f.id '
-			     . 'INNER JOIN `' . $this->prefix . 'category` c ON c.id = f.category '
-			     . 'SET f.cache_nbUnreads=COALESCE(x.nbUnreads, 0) '
-			     . 'WHERE c.name = ?';
-				$values = array($name);
-				$stm = $this->bd->prepare($sql);
-				if (!($stm && $stm->execute($values))) {
-					$info = $stm->errorInfo();
-					Minz_Log::record('SQL error : ' . $info[2], Minz_Log::ERROR);
-					$this->bd->rollBack();
-					return false;
-				}
-			}
-
-			$this->bd->commit();
-			return $affected;
 		}
-	}
-
-	public function markReadFeed ($id, $idMax = 0) {
-		if ($idMax == 0) {
-			$sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id '
-			     . 'SET e.is_read = 1, f.cache_nbUnreads=0 '
-			     . 'WHERE f.id=? AND e.is_read = 0';
-			$values = array ($id);
-			$stm = $this->bd->prepare ($sql);
-			if ($stm && $stm->execute ($values)) {
-				return $stm->rowCount();
-			} else {
-				$info = $stm->errorInfo();
-				Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
-				return false;
-			}
-		} else {
-			$this->bd->beginTransaction ();
-
-			$sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id '
-			     . 'SET e.is_read = 1 '
-			     . 'WHERE f.id=? AND e.is_read = 0 AND e.id <= ?';
-			$values = array ($id, $idMax);
-			$stm = $this->bd->prepare ($sql);
-			if (!($stm && $stm->execute ($values))) {
-				$info = $stm->errorInfo();
-				Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
-				$this->bd->rollBack ();
-				return false;
-			}
-			$affected = $stm->rowCount();
-
-			if ($affected > 0) {
-				$sql = 'UPDATE `' . $this->prefix . 'feed` f '
-				     . 'SET f.cache_nbUnreads=f.cache_nbUnreads-' . $affected
-				     . ' WHERE f.id=?';
-				$values = array ($id);
-				$stm = $this->bd->prepare ($sql);
-				if (!($stm && $stm->execute ($values))) {
-					$info = $stm->errorInfo();
-					Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
-					$this->bd->rollBack ();
-					return false;
-				}
-			}
 
-			$this->bd->commit ();
-			return $affected;
-		}
+		$this->bd->commit();
+		return $affected;
 	}
 
-	public function searchByGuid ($feed_id, $id) {
+	public function searchByGuid($feed_id, $id) {
 		// un guid est unique pour un flux donné
-		$sql = 'SELECT id, guid, title, author, UNCOMPRESS(content_bin) AS content, link, date, is_read, is_favorite, id_feed, tags '
+		$sql = 'SELECT id, guid, title, author, '
+		     . ($this->isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content')
+		     . ', link, date, is_read, is_favorite, id_feed, tags '
 		     . 'FROM `' . $this->prefix . 'entry` WHERE id_feed=? AND guid=?';
-		$stm = $this->bd->prepare ($sql);
+		$stm = $this->bd->prepare($sql);
 
-		$values = array (
+		$values = array(
 			$feed_id,
 			$id
 		);
 
-		$stm->execute ($values);
-		$res = $stm->fetchAll (PDO::FETCH_ASSOC);
-		$entries = self::daoToEntry ($res);
-		return isset ($entries[0]) ? $entries[0] : null;
+		$stm->execute($values);
+		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
+		$entries = self::daoToEntry($res);
+		return isset($entries[0]) ? $entries[0] : null;
 	}
 
-	public function searchById ($id) {
-		$sql = 'SELECT id, guid, title, author, UNCOMPRESS(content_bin) AS content, link, date, is_read, is_favorite, id_feed, tags '
+	public function searchById($id) {
+		$sql = 'SELECT id, guid, title, author, '
+		     . ($this->isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content')
+		     . ', link, date, is_read, is_favorite, id_feed, tags '
 		     . 'FROM `' . $this->prefix . 'entry` WHERE id=?';
-		$stm = $this->bd->prepare ($sql);
+		$stm = $this->bd->prepare($sql);
+
+		$values = array($id);
 
-		$values = array ($id);
+		$stm->execute($values);
+		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
+		$entries = self::daoToEntry($res);
+		return isset($entries[0]) ? $entries[0] : null;
+	}
 
-		$stm->execute ($values);
-		$res = $stm->fetchAll (PDO::FETCH_ASSOC);
-		$entries = self::daoToEntry ($res);
-		return isset ($entries[0]) ? $entries[0] : null;
+	protected function sqlConcat($s1, $s2) {
+		return 'CONCAT(' . $s1 . ',' . $s2 . ')';	//MySQL
 	}
 
 	private function sqlListWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0, $showOlderUnreadsorFavorites = false, $keepHistoryDefault = 0) {
@@ -419,39 +310,39 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
 				$joinFeed = true;
 				break;
 			case 's':	//Deprecated: use $state instead
-				$where .= 'e1.is_favorite = 1 ';
+				$where .= 'e1.is_favorite=1 ';
 				break;
 			case 'c':
-				$where .= 'f.category = ? ';
+				$where .= 'f.category=? ';
 				$values[] = intval($id);
 				$joinFeed = true;
 				break;
 			case 'f':
-				$where .= 'e1.id_feed = ? ';
+				$where .= 'e1.id_feed=? ';
 				$values[] = intval($id);
 				break;
 			case 'A':
 				$where .= '1 ';
 				break;
 			default:
-				throw new FreshRSS_EntriesGetter_Exception ('Bad type in Entry->listByType: [' . $type . ']!');
+				throw new FreshRSS_EntriesGetter_Exception('Bad type in Entry->listByType: [' . $type . ']!');
 		}
 
 		if ($state & FreshRSS_Entry::STATE_NOT_READ) {
 			if (!($state & FreshRSS_Entry::STATE_READ)) {
-				$where .= 'AND e1.is_read = 0 ';
+				$where .= 'AND e1.is_read=0 ';
 			}
 		}
 		elseif ($state & FreshRSS_Entry::STATE_READ) {
-			$where .= 'AND e1.is_read = 1 ';
+			$where .= 'AND e1.is_read=1 ';
 		}
 		if ($state & FreshRSS_Entry::STATE_FAVORITE) {
 			if (!($state & FreshRSS_Entry::STATE_NOT_FAVORITE)) {
-				$where .= 'AND e1.is_favorite = 1 ';
+				$where .= 'AND e1.is_favorite=1 ';
 			}
 		}
 		elseif ($state & FreshRSS_Entry::STATE_NOT_FAVORITE) {
-			$where .= 'AND e1.is_favorite = 0 ';
+			$where .= 'AND e1.is_favorite=0 ';
 		}
 
 		switch ($order) {
@@ -459,7 +350,10 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
 			case 'ASC':
 				break;
 			default:
-				throw new FreshRSS_EntriesGetter_Exception ('Bad order in Entry->listByType: [' . $order . ']!');
+				throw new FreshRSS_EntriesGetter_Exception('Bad order in Entry->listByType: [' . $order . ']!');
+		}
+		if ($firstId === '' && parent::$sharedDbType === 'mysql') {
+			$firstId = $order === 'DESC' ? '9000000000'. '000000' : '0';	//MySQL optimization. Tested on MySQL 5.5 with 150k articles
 		}
 		if ($firstId !== '') {
 			$where .= 'AND e1.id ' . ($order === 'DESC' ? '<=' : '>=') . $firstId . ' ';
@@ -467,7 +361,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
 		if (($date_min > 0) && ($type !== 's')) {
 			$where .= 'AND (e1.id >= ' . $date_min . '000000';
 			if ($showOlderUnreadsorFavorites) {	//Lax date constraint
-				$where .= ' OR e1.is_read = 0 OR e1.is_favorite = 1 OR (f.keep_history <> 0';
+				$where .= ' OR e1.is_read=0 OR e1.is_favorite=1 OR (f.keep_history <> 0';
 				if (intval($keepHistoryDefault) === 0) {
 					$where .= ' AND f.keep_history <> -2';	//default
 				}
@@ -520,7 +414,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
 						$search .= 'AND e1.tags LIKE ? ';
 						$values[] = '%' . $word .'%';
 					} else {
-						$search .= 'AND CONCAT(e1.title, UNCOMPRESS(e1.content_bin)) LIKE ? ';
+						$search .= 'AND ' . $this->sqlconcat('e1.title', $this->isCompressed() ? 'UNCOMPRESS(content_bin)' : 'content') . ' LIKE ? ';
 						$values[] = '%' . $word .'%';
 					}
 				}
@@ -529,7 +423,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
 
 		return array($values,
 			'SELECT e1.id FROM `' . $this->prefix . 'entry` e1 '
-			. ($joinFeed ? 'INNER JOIN `' . $this->prefix . 'feed` f ON e1.id_feed = f.id ' : '')
+			. ($joinFeed ? 'INNER JOIN `' . $this->prefix . 'feed` f ON e1.id_feed=f.id ' : '')
 			. 'WHERE ' . $where
 			. $search
 			. 'ORDER BY e1.id ' . $order
@@ -539,17 +433,19 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
 	public function listWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0, $showOlderUnreadsorFavorites = false, $keepHistoryDefault = 0) {
 		list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filter, $date_min, $showOlderUnreadsorFavorites, $keepHistoryDefault);
 
-		$sql = 'SELECT e.id, e.guid, e.title, e.author, UNCOMPRESS(e.content_bin) AS content, e.link, e.date, e.is_read, e.is_favorite, e.id_feed, e.tags '
+		$sql = 'SELECT e.id, e.guid, e.title, e.author, '
+		     . ($this->isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content')
+		     . ', e.link, e.date, e.is_read, e.is_favorite, e.id_feed, e.tags '
 		     . 'FROM `' . $this->prefix . 'entry` e '
 		     . 'INNER JOIN ('
 		     . $sql
-		     . ') e2 ON e2.id = e.id '
+		     . ') e2 ON e2.id=e.id '
 		     . 'ORDER BY e.id ' . $order;
 
-		$stm = $this->bd->prepare ($sql);
-		$stm->execute ($values);
+		$stm = $this->bd->prepare($sql);
+		$stm->execute($values);
 
-		return self::daoToEntry ($stm->fetchAll (PDO::FETCH_ASSOC));
+		return self::daoToEntry($stm->fetchAll(PDO::FETCH_ASSOC));
 	}
 
 	public function listIdsWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0, $showOlderUnreadsorFavorites = false, $keepHistoryDefault = 0) {	//For API
@@ -563,69 +459,85 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
 
 	public function listLastGuidsByFeed($id, $n) {
 		$sql = 'SELECT guid FROM `' . $this->prefix . 'entry` WHERE id_feed=? ORDER BY id DESC LIMIT ' . intval($n);
-		$stm = $this->bd->prepare ($sql);
-		$values = array ($id);
-		$stm->execute ($values);
-		return $stm->fetchAll (PDO::FETCH_COLUMN, 0);
+		$stm = $this->bd->prepare($sql);
+		$values = array($id);
+		$stm->execute($values);
+		return $stm->fetchAll(PDO::FETCH_COLUMN, 0);
 	}
 
-	public function countUnreadRead () {
-		$sql = 'SELECT COUNT(e.id) AS count FROM `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id WHERE priority > 0'
-		     . ' UNION SELECT COUNT(e.id) AS count FROM `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id WHERE priority > 0 AND is_read = 0';
-		$stm = $this->bd->prepare ($sql);
-		$stm->execute ();
-		$res = $stm->fetchAll (PDO::FETCH_COLUMN, 0);
+	public function countUnreadRead() {
+		$sql = 'SELECT COUNT(e.id) AS count FROM `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id WHERE priority > 0'
+		     . ' UNION SELECT COUNT(e.id) AS count FROM `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id WHERE priority > 0 AND is_read=0';
+		$stm = $this->bd->prepare($sql);
+		$stm->execute();
+		$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
 		$all = empty($res[0]) ? 0 : $res[0];
 		$unread = empty($res[1]) ? 0 : $res[1];
 		return array('all' => $all, 'unread' => $unread, 'read' => $all - $unread);
 	}
-	public function count ($minPriority = null) {
-		$sql = 'SELECT COUNT(e.id) AS count FROM `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id';
+	public function count($minPriority = null) {
+		$sql = 'SELECT COUNT(e.id) AS count FROM `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id';
 		if ($minPriority !== null) {
 			$sql = ' WHERE priority > ' . intval($minPriority);
 		}
-		$stm = $this->bd->prepare ($sql);
-		$stm->execute ();
-		$res = $stm->fetchAll (PDO::FETCH_COLUMN, 0);
+		$stm = $this->bd->prepare($sql);
+		$stm->execute();
+		$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
 		return $res[0];
 	}
-	public function countNotRead ($minPriority = null) {
-		$sql = 'SELECT COUNT(e.id) AS count FROM `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id WHERE is_read = 0';
+	public function countNotRead($minPriority = null) {
+		$sql = 'SELECT COUNT(e.id) AS count FROM `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id WHERE is_read=0';
 		if ($minPriority !== null) {
 			$sql = ' AND priority > ' . intval($minPriority);
 		}
-		$stm = $this->bd->prepare ($sql);
-		$stm->execute ();
-		$res = $stm->fetchAll (PDO::FETCH_COLUMN, 0);
+		$stm = $this->bd->prepare($sql);
+		$stm->execute();
+		$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
 		return $res[0];
 	}
 
-	public function countUnreadReadFavorites () {
-		$sql = 'SELECT COUNT(id) FROM `' . $this->prefix . 'entry` WHERE is_favorite=1'
-		     . ' UNION SELECT COUNT(id) FROM `' . $this->prefix . 'entry` WHERE is_favorite=1 AND is_read = 0';
-		$stm = $this->bd->prepare ($sql);
-		$stm->execute ();
-		$res = $stm->fetchAll (PDO::FETCH_COLUMN, 0);
+	public function countUnreadReadFavorites() {
+		$sql = 'SELECT c FROM ('
+		     .	'SELECT COUNT(id) AS c, 1 as o FROM `' . $this->prefix . 'entry` WHERE is_favorite=1 '
+		     .	'UNION SELECT COUNT(id) AS c, 2 AS o FROM `' . $this->prefix . 'entry` WHERE is_favorite=1 AND is_read=0'
+		     .	') u ORDER BY o';
+		$stm = $this->bd->prepare($sql);
+		$stm->execute();
+		$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
 		$all = empty($res[0]) ? 0 : $res[0];
 		$unread = empty($res[1]) ? 0 : $res[1];
 		return array('all' => $all, 'unread' => $unread, 'read' => $all - $unread);
 	}
 
 	public function optimizeTable() {
-		$sql = 'OPTIMIZE TABLE `' . $this->prefix . 'entry`';
-		$stm = $this->bd->prepare ($sql);
-		$stm->execute ();
+		$sql = 'OPTIMIZE TABLE `' . $this->prefix . 'entry`';	//MySQL
+		$stm = $this->bd->prepare($sql);
+		$stm->execute();
+	}
+
+	public function size($all = false) {
+		$db = Minz_Configuration::dataBase();
+		$sql = 'SELECT SUM(data_length + index_length) FROM information_schema.TABLES WHERE table_schema=?';	//MySQL
+		$values = array($db['base']);
+		if (!$all) {
+			$sql .= ' AND table_name LIKE ?';
+			$values[] = $this->prefix . '%';
+		}
+		$stm = $this->bd->prepare($sql);
+		$stm->execute($values);
+		$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
+		return $res[0];
 	}
 
-	public static function daoToEntry ($listDAO) {
-		$list = array ();
+	public static function daoToEntry($listDAO) {
+		$list = array();
 
-		if (!is_array ($listDAO)) {
-			$listDAO = array ($listDAO);
+		if (!is_array($listDAO)) {
+			$listDAO = array($listDAO);
 		}
 
 		foreach ($listDAO as $key => $dao) {
-			$entry = new FreshRSS_Entry (
+			$entry = new FreshRSS_Entry(
 				$dao['id_feed'],
 				$dao['guid'],
 				$dao['title'],
@@ -637,13 +549,13 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
 				$dao['is_favorite'],
 				$dao['tags']
 			);
-			if (isset ($dao['id'])) {
-				$entry->_id ($dao['id']);
+			if (isset($dao['id'])) {
+				$entry->_id($dao['id']);
 			}
 			$list[] = $entry;
 		}
 
-		unset ($listDAO);
+		unset($listDAO);
 
 		return $list;
 	}

+ 129 - 0
app/Models/EntryDAOSQLite.php

@@ -0,0 +1,129 @@
+<?php
+
+class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
+
+	protected function sqlConcat($s1, $s2) {
+		return $s1 . '||' . $s2;
+	}
+
+	protected function updateCacheUnreads($catId = false, $feedId = false) {
+		$sql = 'UPDATE `' . $this->prefix . 'feed` '
+		 . 'SET cache_nbUnreads=('
+		 .	'SELECT COUNT(*) AS nbUnreads FROM `' . $this->prefix . 'entry` e '
+		 .	'WHERE e.id_feed=`' . $this->prefix . 'feed`.id AND e.is_read=0) '
+		 . 'WHERE 1';
+		$values = array();
+		if ($feedId !== false) {
+			$sql .= ' AND id=?';
+			$values[] = $feedId;
+		}
+		if ($catId !== false) {
+			$sql .= ' AND category=?';
+			$values[] = $catId;
+		}
+		$stm = $this->bd->prepare($sql);
+		if ($stm && $stm->execute($values)) {
+			return true;
+		} else {
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error updateCacheUnreads: ' . $info[2], Minz_Log::ERROR);
+			return false;
+		}
+	}
+
+	public function markRead($ids, $is_read = true) {
+		if (is_array($ids)) {	//Many IDs at once (used by API)
+			if (true) {	//Speed heuristics	//TODO: Not implemented yet for SQLite (so always call IDs one by one)
+				$affected = 0;
+				foreach ($ids as $id) {
+					$affected += $this->markRead($id, $is_read);
+				}
+				return $affected;
+			}
+		} else {
+			$this->bd->beginTransaction();
+			$sql = 'UPDATE `' . $this->prefix . 'entry` SET is_read=? WHERE id=? AND is_read=?';
+			$values = array($is_read ? 1 : 0, $ids, $is_read ? 0 : 1);
+			$stm = $this->bd->prepare($sql);
+			if (!($stm && $stm->execute($values))) {
+				$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+				Minz_Log::record('SQL error markRead 1: ' . $info[2], Minz_Log::ERROR);
+				$this->bd->rollBack();
+				return false;
+			}
+			$affected = $stm->rowCount();
+			if ($affected > 0) {
+				$sql = 'UPDATE `' . $this->prefix . 'feed` SET cache_nbUnreads=cache_nbUnreads' . ($is_read ? '-' : '+') . '1 '
+				 . 'WHERE id=(SELECT e.id_feed FROM `' . $this->prefix . 'entry` e WHERE e.id=?)';
+				$values = array($ids);
+				$stm = $this->bd->prepare($sql);
+				if (!($stm && $stm->execute($values))) {
+					$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+					Minz_Log::record('SQL error markRead 2: ' . $info[2], Minz_Log::ERROR);
+					$this->bd->rollBack();
+					return false;
+				}
+			}
+			$this->bd->commit();
+			return $affected;
+		}
+	}
+
+	public function markReadEntries($idMax = 0, $onlyFavorites = false, $priorityMin = 0) {
+		if ($idMax == 0) {
+			$idMax = time() . '000000';
+			Minz_Log::record($nb . 'Calling markReadEntries(0) is deprecated!', Minz_Log::DEBUG);
+		}
+
+		$sql = 'UPDATE `' . $this->prefix . 'entry` SET is_read=1 WHERE is_read=0 AND id <= ?';
+		if ($onlyFavorites) {
+			$sql .= ' AND is_favorite=1';
+		} elseif ($priorityMin >= 0) {
+			$sql .= ' AND id_feed IN (SELECT f.id FROM `' . $this->prefix . 'feed` f WHERE f.priority > ' . intval($priorityMin) . ')';
+		}
+		$values = array($idMax);
+		$stm = $this->bd->prepare($sql);
+		if (!($stm && $stm->execute($values))) {
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error markReadEntries: ' . $info[2], Minz_Log::ERROR);
+			return false;
+		}
+		$affected = $stm->rowCount();
+		if (($affected > 0) && (!$this->updateCacheUnreads(false, false))) {
+			return false;
+		}
+		return $affected;
+	}
+
+	public function markReadCat($id, $idMax = 0) {
+		if ($idMax == 0) {
+			$idMax = time() . '000000';
+			Minz_Log::record($nb . 'Calling markReadCat(0) is deprecated!', Minz_Log::DEBUG);
+		}
+
+		$sql = 'UPDATE `' . $this->prefix . 'entry` '
+			 . 'SET is_read=1 '
+			 . 'WHERE is_read=0 AND id <= ? AND '
+			 . 'id_feed IN (SELECT f.id FROM `' . $this->prefix . 'feed` f WHERE f.category=?)';
+		$values = array($idMax, $id);
+		$stm = $this->bd->prepare($sql);
+		if (!($stm && $stm->execute($values))) {
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error markReadCat: ' . $info[2], Minz_Log::ERROR);
+			return false;
+		}
+		$affected = $stm->rowCount();
+		if (($affected > 0) && (!$this->updateCacheUnreads($id, false))) {
+			return false;
+		}
+		return $affected;
+	}
+
+	public function optimizeTable() {
+		//TODO: Search for an equivalent in SQLite
+	}
+
+	public function size($all = false) {
+		return @filesize(DATA_PATH . '/' . Minz_Session::param('currentUser', '_') . '.sqlite');
+	}
+}

+ 32 - 0
app/Models/Factory.php

@@ -0,0 +1,32 @@
+<?php
+
+class FreshRSS_Factory {
+
+	public static function createFeedDao() {
+		$db = Minz_Configuration::dataBase();
+		if ($db['type'] === 'sqlite') {
+			return new FreshRSS_FeedDAOSQLite();
+		} else {
+			return new FreshRSS_FeedDAO();
+		}
+	}
+
+	public static function createEntryDao() {
+		$db = Minz_Configuration::dataBase();
+		if ($db['type'] === 'sqlite') {
+			return new FreshRSS_EntryDAOSQLite();
+		} else {
+			return new FreshRSS_EntryDAO();
+		}
+	}
+
+	public static function createStatsDAO() {
+		$db = Minz_Configuration::dataBase();
+		if ($db['type'] === 'sqlite') {
+			return new FreshRSS_StatsDAOSQLite();
+		} else {
+			return new FreshRSS_StatsDAO();
+		}
+	}
+
+}

+ 106 - 88
app/Models/Feed.php

@@ -16,17 +16,19 @@ class FreshRSS_Feed extends Minz_Model {
 	private $httpAuth = '';
 	private $error = false;
 	private $keep_history = -2;
+	private $ttl = -2;
 	private $hash = null;
+	private $lockPath = '';
 
-	public function __construct ($url, $validate=true) {
+	public function __construct($url, $validate=true) {
 		if ($validate) {
-			$this->_url ($url);
+			$this->_url($url);
 		} else {
 			$this->url = $url;
 		}
 	}
 
-	public function id () {
+	public function id() {
 		return $this->id;
 	}
 
@@ -37,74 +39,77 @@ class FreshRSS_Feed extends Minz_Model {
 		return $this->hash;
 	}
 
-	public function url () {
+	public function url() {
 		return $this->url;
 	}
-	public function category () {
+	public function category() {
 		return $this->category;
 	}
-	public function entries () {
+	public function entries() {
 		return $this->entries === null ? array() : $this->entries;
 	}
-	public function name () {
+	public function name() {
 		return $this->name;
 	}
-	public function website () {
+	public function website() {
 		return $this->website;
 	}
-	public function description () {
+	public function description() {
 		return $this->description;
 	}
-	public function lastUpdate () {
+	public function lastUpdate() {
 		return $this->lastUpdate;
 	}
-	public function priority () {
+	public function priority() {
 		return $this->priority;
 	}
-	public function pathEntries () {
+	public function pathEntries() {
 		return $this->pathEntries;
 	}
-	public function httpAuth ($raw = true) {
+	public function httpAuth($raw = true) {
 		if ($raw) {
 			return $this->httpAuth;
 		} else {
-			$pos_colon = strpos ($this->httpAuth, ':');
-			$user = substr ($this->httpAuth, 0, $pos_colon);
-			$pass = substr ($this->httpAuth, $pos_colon + 1);
+			$pos_colon = strpos($this->httpAuth, ':');
+			$user = substr($this->httpAuth, 0, $pos_colon);
+			$pass = substr($this->httpAuth, $pos_colon + 1);
 
-			return array (
+			return array(
 				'username' => $user,
 				'password' => $pass
 			);
 		}
 	}
-	public function inError () {
+	public function inError() {
 		return $this->error;
 	}
-	public function keepHistory () {
+	public function keepHistory() {
 		return $this->keep_history;
 	}
-	public function nbEntries () {
+	public function ttl() {
+		return $this->ttl;
+	}
+	public function nbEntries() {
 		if ($this->nbEntries < 0) {
-			$feedDAO = new FreshRSS_FeedDAO ();
-			$this->nbEntries = $feedDAO->countEntries ($this->id ());
+			$feedDAO = FreshRSS_Factory::createFeedDao();
+			$this->nbEntries = $feedDAO->countEntries($this->id());
 		}
 
 		return $this->nbEntries;
 	}
-	public function nbNotRead () {
+	public function nbNotRead() {
 		if ($this->nbNotRead < 0) {
-			$feedDAO = new FreshRSS_FeedDAO ();
-			$this->nbNotRead = $feedDAO->countNotRead ($this->id ());
+			$feedDAO = FreshRSS_Factory::createFeedDao();
+			$this->nbNotRead = $feedDAO->countNotRead($this->id());
 		}
 
 		return $this->nbNotRead;
 	}
 	public function faviconPrepare() {
 		$file = DATA_PATH . '/favicons/' . $this->hash() . '.txt';
-		if (!file_exists ($file)) {
+		if (!file_exists($file)) {
 			$t = $this->website;
-			if (empty($t)) {
+			if ($t == '') {
 				$t = $this->url;
 			}
 			file_put_contents($file, $t);
@@ -115,109 +120,123 @@ class FreshRSS_Feed extends Minz_Model {
 		@unlink($path . '.ico');
 		@unlink($path . '.txt');
 	}
-	public function favicon () {
-		return Minz_Url::display ('/f.php?' . $this->hash());
+	public function favicon() {
+		return Minz_Url::display('/f.php?' . $this->hash());
 	}
 
-	public function _id ($value) {
+	public function _id($value) {
 		$this->id = $value;
 	}
-	public function _url ($value, $validate=true) {
+	public function _url($value, $validate=true) {
+		$this->hash = null;
 		if ($validate) {
 			$value = checkUrl($value);
 		}
-		if (empty ($value)) {
-			throw new FreshRSS_BadUrl_Exception ($value);
+		if (empty($value)) {
+			throw new FreshRSS_BadUrl_Exception($value);
 		}
 		$this->url = $value;
 	}
-	public function _category ($value) {
+	public function _category($value) {
 		$value = intval($value);
 		$this->category = $value >= 0 ? $value : 0;
 	}
-	public function _name ($value) {
+	public function _name($value) {
 		$this->name = $value === null ? '' : $value;
 	}
-	public function _website ($value, $validate=true) {
+	public function _website($value, $validate=true) {
 		if ($validate) {
 			$value = checkUrl($value);
 		}
-		if (empty ($value)) {
+		if (empty($value)) {
 			$value = '';
 		}
 		$this->website = $value;
 	}
-	public function _description ($value) {
+	public function _description($value) {
 		$this->description = $value === null ? '' : $value;
 	}
-	public function _lastUpdate ($value) {
+	public function _lastUpdate($value) {
 		$this->lastUpdate = $value;
 	}
-	public function _priority ($value) {
+	public function _priority($value) {
 		$value = intval($value);
 		$this->priority = $value >= 0 ? $value : 10;
 	}
-	public function _pathEntries ($value) {
+	public function _pathEntries($value) {
 		$this->pathEntries = $value;
 	}
-	public function _httpAuth ($value) {
+	public function _httpAuth($value) {
 		$this->httpAuth = $value;
 	}
-	public function _error ($value) {
+	public function _error($value) {
 		$this->error = (bool)$value;
 	}
-	public function _keepHistory ($value) {
+	public function _keepHistory($value) {
 		$value = intval($value);
 		$value = min($value, 1000000);
 		$value = max($value, -2);
 		$this->keep_history = $value;
 	}
-	public function _nbNotRead ($value) {
+	public function _ttl($value) {
+		$value = intval($value);
+		$value = min($value, 100000000);
+		$value = max($value, -2);
+		$this->ttl = $value;
+	}
+	public function _nbNotRead($value) {
 		$this->nbNotRead = intval($value);
 	}
-	public function _nbEntries ($value) {
+	public function _nbEntries($value) {
 		$this->nbEntries = intval($value);
 	}
 
-	public function load ($loadDetails = false) {
+	public function load($loadDetails = false) {
 		if ($this->url !== null) {
 			if (CACHE_PATH === false) {
-				throw new Minz_FileNotExistException (
+				throw new Minz_FileNotExistException(
 					'CACHE_PATH',
 					Minz_Exception::ERROR
 				);
 			} else {
-				$url = htmlspecialchars_decode ($this->url, ENT_QUOTES);
+				$url = htmlspecialchars_decode($this->url, ENT_QUOTES);
 				if ($this->httpAuth != '') {
-					$url = preg_replace ('#((.+)://)(.+)#', '${1}' . $this->httpAuth . '@${3}', $url);
+					$url = preg_replace('#((.+)://)(.+)#', '${1}' . $this->httpAuth . '@${3}', $url);
 				}
 				$feed = customSimplePie();
-				$feed->set_feed_url ($url);
+				$feed->set_feed_url($url);
+				if (!$loadDetails) {	//Only activates auto-discovery when adding a new feed
+					$feed->set_autodiscovery_level(SIMPLEPIE_LOCATOR_NONE);
+				}
 				$mtime = $feed->init();
 
 				if ((!$mtime) || $feed->error()) {
-					throw new FreshRSS_Feed_Exception ($feed->error() . ' [' . $url . ']');
-				}
-
-				// si on a utilisé l'auto-discover, notre url va avoir changé
-				$subscribe_url = $feed->subscribe_url ();
-				if ($subscribe_url !== null && $subscribe_url !== $this->url) {
-					if ($this->httpAuth != '') {
-						// on enlève les id si authentification HTTP
-						$subscribe_url = preg_replace ('#((.+)://)((.+)@)(.+)#', '${1}${5}', $subscribe_url);
-					}
-					$this->_url ($subscribe_url);
+					throw new FreshRSS_Feed_Exception($feed->error() . ' [' . $url . ']');
 				}
 
 				if ($loadDetails) {
+					// si on a utilisé l'auto-discover, notre url va avoir changé
+					$subscribe_url = $feed->subscribe_url(false);
+
 					$title = strtr(html_only_entity_decode($feed->get_title()), array('<' => '&lt;', '>' => '&gt;', '"' => '&quot;'));	//HTML to HTML-PRE	//ENT_COMPAT except &
-					$this->_name ($title == '' ? $this->url : $title);
+					$this->_name($title == '' ? $this->url : $title);
 
 					$this->_website(html_only_entity_decode($feed->get_link()));
 					$this->_description(html_only_entity_decode($feed->get_description()));
+				} else {
+					//The case of HTTP 301 Moved Permanently
+					$subscribe_url = $feed->subscribe_url(true);
+				}
+
+				if ($subscribe_url !== null && $subscribe_url !== $this->url) {
+					if ($this->httpAuth != '') {
+						// on enlève les id si authentification HTTP
+						$subscribe_url = preg_replace('#((.+)://)((.+)@)(.+)#', '${1}${5}', $subscribe_url);
+					}
+					$this->_url($subscribe_url);
 				}
 
-				if (($mtime === true) || ($mtime > $this->lastUpdate)) {
+				if (($mtime === true) ||($mtime > $this->lastUpdate)) {
 					syslog(LOG_DEBUG, 'FreshRSS no cache ' . $mtime . ' > ' . $this->lastUpdate . ' for ' . $subscribe_url);
 					$this->loadEntries($feed);	// et on charge les articles du flux
 				} else {
@@ -231,25 +250,25 @@ class FreshRSS_Feed extends Minz_Model {
 		}
 	}
 
-	private function loadEntries ($feed) {
-		$entries = array ();
+	private function loadEntries($feed) {
+		$entries = array();
 
-		foreach ($feed->get_items () as $item) {
-			$title = html_only_entity_decode (strip_tags ($item->get_title ()));
-			$author = $item->get_author ();
-			$link = $item->get_permalink ();
-			$date = @strtotime ($item->get_date ());
+		foreach ($feed->get_items() as $item) {
+			$title = html_only_entity_decode(strip_tags($item->get_title()));
+			$author = $item->get_author();
+			$link = $item->get_permalink();
+			$date = @strtotime($item->get_date());
 
 			// gestion des tags (catégorie == tag)
-			$tags_tmp = $item->get_categories ();
-			$tags = array ();
+			$tags_tmp = $item->get_categories();
+			$tags = array();
 			if ($tags_tmp !== null) {
 				foreach ($tags_tmp as $tag) {
-					$tags[] = html_only_entity_decode ($tag->get_label ());
+					$tags[] = html_only_entity_decode($tag->get_label());
 				}
 			}
 
-			$content = html_only_entity_decode ($item->get_content ());
+			$content = html_only_entity_decode($item->get_content());
 
 			$elinks = array();
 			foreach ($item->get_enclosures() as $enclosure) {
@@ -267,16 +286,16 @@ class FreshRSS_Feed extends Minz_Model {
 				}
 			}
 
-			$entry = new FreshRSS_Entry (
-				$this->id (),
-				$item->get_id (),
+			$entry = new FreshRSS_Entry(
+				$this->id(),
+				$item->get_id(),
 				$title === null ? '' : $title,
-				$author === null ? '' : html_only_entity_decode ($author->name),
+				$author === null ? '' : html_only_entity_decode($author->name),
 				$content === null ? '' : $content,
 				$link === null ? '' : $link,
-				$date ? $date : time ()
+				$date ? $date : time()
 			);
-			$entry->_tags ($tags);
+			$entry->_tags($tags);
 			// permet de récupérer le contenu des flux tronqués
 			$entry->loadCompleteContent($this->pathEntries());
 
@@ -288,20 +307,19 @@ class FreshRSS_Feed extends Minz_Model {
 	}
 
 	function lock() {
-		$lock = TMP_PATH . '/' . md5(Minz_Configuration::salt() . $this->url) . '.freshrss.lock';
-		if (file_exists($lock) && ((time() - @filemtime($lock)) > 3600)) {
-			@unlink($lock);
+		$this->lockPath = TMP_PATH . '/' . $this->hash() . '.freshrss.lock';
+		if (file_exists($this->lockPath) && ((time() - @filemtime($this->lockPath)) > 3600)) {
+			@unlink($this->lockPath);
 		}
-		if (($handle = @fopen($lock, 'x')) === false) {
+		if (($handle = @fopen($this->lockPath, 'x')) === false) {
 			return false;
 		}
-		//register_shutdown_function('unlink', $lock);
+		//register_shutdown_function('unlink', $this->lockPath);
 		@fclose($handle);
 		return true;
 	}
 
 	function unlock() {
-		$lock = TMP_PATH . '/' . md5(Minz_Configuration::salt() . $this->url) . '.freshrss.lock';
-		@unlink($lock);
+		@unlink($this->lockPath);
 	}
 }

+ 140 - 152
app/Models/FeedDAO.php

@@ -1,25 +1,25 @@
 <?php
 
 class FreshRSS_FeedDAO extends Minz_ModelPdo {
-	public function addFeed ($valuesTmp) {
-		$sql = 'INSERT INTO `' . $this->prefix . 'feed` (url, category, name, website, description, lastUpdate, priority, httpAuth, error, keep_history) VALUES(?, ?, ?, ?, ?, ?, 10, ?, 0, -2)';
-		$stm = $this->bd->prepare ($sql);
+	public function addFeed($valuesTmp) {
+		$sql = 'INSERT INTO `' . $this->prefix . 'feed` (url, category, name, website, description, lastUpdate, priority, httpAuth, error, keep_history, ttl) VALUES(?, ?, ?, ?, ?, ?, 10, ?, 0, -2, -2)';
+		$stm = $this->bd->prepare($sql);
 
-		$values = array (
+		$values = array(
 			substr($valuesTmp['url'], 0, 511),
 			$valuesTmp['category'],
 			substr($valuesTmp['name'], 0, 255),
 			substr($valuesTmp['website'], 0, 255),
 			substr($valuesTmp['description'], 0, 1023),
 			$valuesTmp['lastUpdate'],
-			base64_encode ($valuesTmp['httpAuth']),
+			base64_encode($valuesTmp['httpAuth']),
 		);
 
-		if ($stm && $stm->execute ($values)) {
+		if ($stm && $stm->execute($values)) {
 			return $this->bd->lastInsertId();
 		} else {
-			$info = $stm->errorInfo();
-			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error addFeed: ' . $info[2], Minz_Log::ERROR);
 			return false;
 		}
 	}
@@ -54,185 +54,163 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo {
 		return $feed_search->id();
 	}
 
-	public function updateFeed ($id, $valuesTmp) {
+	public function updateFeed($id, $valuesTmp) {
 		$set = '';
 		foreach ($valuesTmp as $key => $v) {
 			$set .= $key . '=?, ';
 
 			if ($key == 'httpAuth') {
-				$valuesTmp[$key] = base64_encode ($v);
+				$valuesTmp[$key] = base64_encode($v);
 			}
 		}
-		$set = substr ($set, 0, -2);
+		$set = substr($set, 0, -2);
 
 		$sql = 'UPDATE `' . $this->prefix . 'feed` SET ' . $set . ' WHERE id=?';
-		$stm = $this->bd->prepare ($sql);
+		$stm = $this->bd->prepare($sql);
 
 		foreach ($valuesTmp as $v) {
 			$values[] = $v;
 		}
 		$values[] = $id;
 
-		if ($stm && $stm->execute ($values)) {
+		if ($stm && $stm->execute($values)) {
 			return $stm->rowCount();
 		} else {
-			$info = $stm->errorInfo();
-			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error updateFeed: ' . $info[2], Minz_Log::ERROR);
 			return false;
 		}
 	}
 
-	public function updateLastUpdate ($id, $inError = 0, $updateCache = true) {
+	public function updateLastUpdate($id, $inError = 0, $updateCache = true) {
 		if ($updateCache) {
-			$sql = 'UPDATE `' . $this->prefix . 'feed` f '	//2 sub-requests with FOREIGN KEY(e.id_feed), INDEX(e.is_read) faster than 1 request with GROUP BY or CASE
-			     . 'SET f.cache_nbEntries=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=f.id),'
-			     . 'f.cache_nbUnreads=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=f.id AND e2.is_read=0),'
+			$sql = 'UPDATE `' . $this->prefix . 'feed` '	//2 sub-requests with FOREIGN KEY(e.id_feed), INDEX(e.is_read) faster than 1 request with GROUP BY or CASE
+			     . 'SET cache_nbEntries=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=`' . $this->prefix . 'feed`.id),'
+			     . 'cache_nbUnreads=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=`' . $this->prefix . 'feed`.id AND e2.is_read=0),'
 			     . 'lastUpdate=?, error=? '
-			     . 'WHERE f.id=?';
+			     . 'WHERE id=?';
 		} else {
-			$sql = 'UPDATE `' . $this->prefix . 'feed` f '
+			$sql = 'UPDATE `' . $this->prefix . 'feed` '
 			     . 'SET lastUpdate=?, error=? '
-			     . 'WHERE f.id=?';
+			     . 'WHERE id=?';
 		}
 
-		$values = array (
+		$values = array(
 			time(),
 			$inError,
 			$id,
 		);
 
-		$stm = $this->bd->prepare ($sql);
+		$stm = $this->bd->prepare($sql);
 
-		if ($stm && $stm->execute ($values)) {
+		if ($stm && $stm->execute($values)) {
 			return $stm->rowCount();
 		} else {
-			$info = $stm->errorInfo();
-			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error updateLastUpdate: ' . $info[2], Minz_Log::ERROR);
 			return false;
 		}
 	}
 
-	public function changeCategory ($idOldCat, $idNewCat) {
-		$catDAO = new FreshRSS_CategoryDAO ();
-		$newCat = $catDAO->searchById ($idNewCat);
+	public function changeCategory($idOldCat, $idNewCat) {
+		$catDAO = new FreshRSS_CategoryDAO();
+		$newCat = $catDAO->searchById($idNewCat);
 		if (!$newCat) {
-			$newCat = $catDAO->getDefault ();
+			$newCat = $catDAO->getDefault();
 		}
 
 		$sql = 'UPDATE `' . $this->prefix . 'feed` SET category=? WHERE category=?';
-		$stm = $this->bd->prepare ($sql);
+		$stm = $this->bd->prepare($sql);
 
-		$values = array (
-			$newCat->id (),
+		$values = array(
+			$newCat->id(),
 			$idOldCat
 		);
 
-		if ($stm && $stm->execute ($values)) {
+		if ($stm && $stm->execute($values)) {
 			return $stm->rowCount();
 		} else {
-			$info = $stm->errorInfo();
-			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error changeCategory: ' . $info[2], Minz_Log::ERROR);
 			return false;
 		}
 	}
 
-	public function deleteFeed ($id) {
-		/*//For MYISAM (MySQL 5.5-) without FOREIGN KEY
-		$sql = 'DELETE FROM `' . $this->prefix . 'entry` WHERE id_feed=?';
-		$stm = $this->bd->prepare ($sql);
-		$values = array ($id);
-		if (!($stm && $stm->execute ($values))) {
-			$info = $stm->errorInfo();
-			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
-			return false;
-		}*/
-
+	public function deleteFeed($id) {
 		$sql = 'DELETE FROM `' . $this->prefix . 'feed` WHERE id=?';
-		$stm = $this->bd->prepare ($sql);
+		$stm = $this->bd->prepare($sql);
 
-		$values = array ($id);
+		$values = array($id);
 
-		if ($stm && $stm->execute ($values)) {
+		if ($stm && $stm->execute($values)) {
 			return $stm->rowCount();
 		} else {
-			$info = $stm->errorInfo();
-			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error deleteFeed: ' . $info[2], Minz_Log::ERROR);
 			return false;
 		}
 	}
-	public function deleteFeedByCategory ($id) {
-		/*//For MYISAM (MySQL 5.5-) without FOREIGN KEY
-		$sql = 'DELETE FROM `' . $this->prefix . 'entry` e '
-		     . 'INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id '
-		     . 'WHERE f.category=?';
-		$stm = $this->bd->prepare ($sql);
-		$values = array ($id);
-		if (!($stm && $stm->execute ($values))) {
-			$info = $stm->errorInfo();
-			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
-			return false;
-		}*/
-
+	public function deleteFeedByCategory($id) {
 		$sql = 'DELETE FROM `' . $this->prefix . 'feed` WHERE category=?';
-		$stm = $this->bd->prepare ($sql);
+		$stm = $this->bd->prepare($sql);
 
-		$values = array ($id);
+		$values = array($id);
 
-		if ($stm && $stm->execute ($values)) {
+		if ($stm && $stm->execute($values)) {
 			return $stm->rowCount();
 		} else {
-			$info = $stm->errorInfo();
-			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error deleteFeedByCategory: ' . $info[2], Minz_Log::ERROR);
 			return false;
 		}
 	}
 
-	public function searchById ($id) {
+	public function searchById($id) {
 		$sql = 'SELECT * FROM `' . $this->prefix . 'feed` WHERE id=?';
-		$stm = $this->bd->prepare ($sql);
+		$stm = $this->bd->prepare($sql);
 
-		$values = array ($id);
+		$values = array($id);
 
-		$stm->execute ($values);
-		$res = $stm->fetchAll (PDO::FETCH_ASSOC);
-		$feed = self::daoToFeed ($res);
+		$stm->execute($values);
+		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
+		$feed = self::daoToFeed($res);
 
-		if (isset ($feed[$id])) {
+		if (isset($feed[$id])) {
 			return $feed[$id];
 		} else {
 			return null;
 		}
 	}
-	public function searchByUrl ($url) {
+	public function searchByUrl($url) {
 		$sql = 'SELECT * FROM `' . $this->prefix . 'feed` WHERE url=?';
-		$stm = $this->bd->prepare ($sql);
+		$stm = $this->bd->prepare($sql);
 
-		$values = array ($url);
+		$values = array($url);
 
-		$stm->execute ($values);
-		$res = $stm->fetchAll (PDO::FETCH_ASSOC);
-		$feed = current (self::daoToFeed ($res));
+		$stm->execute($values);
+		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
+		$feed = current(self::daoToFeed($res));
 
-		if (isset ($feed)) {
+		if (isset($feed)) {
 			return $feed;
 		} else {
 			return null;
 		}
 	}
 
-	public function listFeeds () {
+	public function listFeeds() {
 		$sql = 'SELECT * FROM `' . $this->prefix . 'feed` ORDER BY name';
-		$stm = $this->bd->prepare ($sql);
-		$stm->execute ();
+		$stm = $this->bd->prepare($sql);
+		$stm->execute();
 
-		return self::daoToFeed ($stm->fetchAll (PDO::FETCH_ASSOC));
+		return self::daoToFeed($stm->fetchAll(PDO::FETCH_ASSOC));
 	}
 
 	public function arrayFeedCategoryNames() {	//For API
 		$sql = 'SELECT f.id, f.name, c.name as c_name FROM `' . $this->prefix . 'feed` f '
 		     . 'INNER JOIN `' . $this->prefix . 'category` c ON c.id = f.category';
-		$stm = $this->bd->prepare ($sql);
-		$stm->execute ();
+		$stm = $this->bd->prepare($sql);
+		$stm->execute();
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 		$feedCategoryNames = array();
 		foreach ($res as $line) {
@@ -244,49 +222,58 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo {
 		return $feedCategoryNames;
 	}
 
-	public function listFeedsOrderUpdate ($cacheDuration = 1500) {
-		$sql = 'SELECT id, name, url, lastUpdate, pathEntries, httpAuth, keep_history '
+	public function listFeedsOrderUpdate($defaultCacheDuration = 3600) {
+		if ($defaultCacheDuration < 0) {
+			$defaultCacheDuration = 2147483647;
+		}
+		$sql = 'SELECT id, url, name, website, lastUpdate, pathEntries, httpAuth, keep_history, ttl '
 		     . 'FROM `' . $this->prefix . 'feed` '
-		     . 'WHERE lastUpdate < ' . (time() - intval($cacheDuration))
-		     . ' ORDER BY lastUpdate';
-		$stm = $this->bd->prepare ($sql);
-		$stm->execute ();
+		     . 'WHERE ttl <> -1 AND lastUpdate < (' . (time() + 60) . '-(CASE WHEN ttl=-2 THEN ' . intval($defaultCacheDuration) . ' ELSE ttl END)) '
+		     . 'ORDER BY lastUpdate';
+		$stm = $this->bd->prepare($sql);
+		if (!($stm && $stm->execute())) {
+			$sql2 = 'ALTER TABLE `' . $this->prefix . 'feed` ADD COLUMN ttl INT NOT NULL DEFAULT -2';	//v0.7.3
+			$stm = $this->bd->prepare($sql2);
+			$stm->execute();
+			$stm = $this->bd->prepare($sql);
+			$stm->execute();
+		}
 
-		return self::daoToFeed ($stm->fetchAll (PDO::FETCH_ASSOC));
+		return self::daoToFeed($stm->fetchAll(PDO::FETCH_ASSOC));
 	}
 
-	public function listByCategory ($cat) {
+	public function listByCategory($cat) {
 		$sql = 'SELECT * FROM `' . $this->prefix . 'feed` WHERE category=? ORDER BY name';
-		$stm = $this->bd->prepare ($sql);
+		$stm = $this->bd->prepare($sql);
 
-		$values = array ($cat);
+		$values = array($cat);
 
-		$stm->execute ($values);
+		$stm->execute($values);
 
-		return self::daoToFeed ($stm->fetchAll (PDO::FETCH_ASSOC));
+		return self::daoToFeed($stm->fetchAll(PDO::FETCH_ASSOC));
 	}
 
-	public function countEntries ($id) {
+	public function countEntries($id) {
 		$sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'entry` WHERE id_feed=?';
-		$stm = $this->bd->prepare ($sql);
-		$values = array ($id);
-		$stm->execute ($values);
-		$res = $stm->fetchAll (PDO::FETCH_ASSOC);
+		$stm = $this->bd->prepare($sql);
+		$values = array($id);
+		$stm->execute($values);
+		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 
 		return $res[0]['count'];
 	}
 
-	public function countNotRead ($id) {
+	public function countNotRead($id) {
 		$sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'entry` WHERE id_feed=? AND is_read=0';
-		$stm = $this->bd->prepare ($sql);
-		$values = array ($id);
-		$stm->execute ($values);
-		$res = $stm->fetchAll (PDO::FETCH_ASSOC);
+		$stm = $this->bd->prepare($sql);
+		$values = array($id);
+		$stm->execute($values);
+		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 
 		return $res[0]['count'];
 	}
 
-	public function updateCachedValues () {	//For one single feed, call updateLastUpdate($id)
+	public function updateCachedValues() {	//For one single feed, call updateLastUpdate($id)
 		$sql = 'UPDATE `' . $this->prefix . 'feed` f '
 		     . 'INNER JOIN ('
 		     .	'SELECT e.id_feed, '
@@ -296,50 +283,50 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo {
 		     .	'GROUP BY e.id_feed'
 		     . ') x ON x.id_feed=f.id '
 		     . 'SET f.cache_nbEntries=x.nbEntries, f.cache_nbUnreads=x.nbUnreads';
-		$stm = $this->bd->prepare ($sql);
+		$stm = $this->bd->prepare($sql);
 
 		if ($stm && $stm->execute()) {
 			return $stm->rowCount();
 		} else {
-			$info = $stm->errorInfo();
-			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error updateCachedValues: ' . $info[2], Minz_Log::ERROR);
 			return false;
 		}
 	}
 
-	public function truncate ($id) {
-		$sql = 'DELETE e.* FROM `' . $this->prefix . 'entry` e WHERE e.id_feed=?';
+	public function truncate($id) {
+		$sql = 'DELETE FROM `' . $this->prefix . 'entry` WHERE id_feed=?';
 		$stm = $this->bd->prepare($sql);
 		$values = array($id);
-		$this->bd->beginTransaction ();
-		if (!($stm && $stm->execute ($values))) {
-				$info = $stm->errorInfo();
-				Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
-				$this->bd->rollBack ();
-				return false;
-			}
+		$this->bd->beginTransaction();
+		if (!($stm && $stm->execute($values))) {
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error truncate: ' . $info[2], Minz_Log::ERROR);
+			$this->bd->rollBack();
+			return false;
+		}
 		$affected = $stm->rowCount();
 
-		$sql = 'UPDATE `' . $this->prefix . 'feed` f '
-			 . 'SET f.cache_nbEntries=0, f.cache_nbUnreads=0 WHERE f.id=?';
-		$values = array ($id);
-		$stm = $this->bd->prepare ($sql);
-		if (!($stm && $stm->execute ($values))) {
-			$info = $stm->errorInfo();
-			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
-			$this->bd->rollBack ();
+		$sql = 'UPDATE `' . $this->prefix . 'feed` '
+			 . 'SET cache_nbEntries=0, cache_nbUnreads=0 WHERE id=?';
+		$values = array($id);
+		$stm = $this->bd->prepare($sql);
+		if (!($stm && $stm->execute($values))) {
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error truncate: ' . $info[2], Minz_Log::ERROR);
+			$this->bd->rollBack();
 			return false;
 		}
 
-		$this->bd->commit ();
+		$this->bd->commit();
 		return $affected;
 	}
 
-	public function cleanOldEntries ($id, $date_min, $keep = 15) {	//Remember to call updateLastUpdate($id) just after
-		$sql = 'DELETE e.* FROM `' . $this->prefix . 'entry` e '
-		     . 'WHERE e.id_feed = :id_feed AND e.id <= :id_max AND e.is_favorite = 0 AND e.id NOT IN '
-		     . '(SELECT id FROM (SELECT e2.id FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed = :id_feed ORDER BY id DESC LIMIT :keep) keep)';	//Double select because of: MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery'
-		$stm = $this->bd->prepare ($sql);
+	public function cleanOldEntries($id, $date_min, $keep = 15) {	//Remember to call updateLastUpdate($id) just after
+		$sql = 'DELETE FROM `' . $this->prefix . 'entry` '
+		     . 'WHERE id_feed = :id_feed AND id <= :id_max AND is_favorite=0 AND id NOT IN '
+		     . '(SELECT id FROM (SELECT e2.id FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed = :id_feed ORDER BY id DESC LIMIT :keep) keep)';	//Double select MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery'
+		$stm = $this->bd->prepare($sql);
 
 		$id_max = intval($date_min) . '000000';
 
@@ -347,27 +334,27 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo {
 		$stm->bindParam(':id_max', $id_max, PDO::PARAM_INT);
 		$stm->bindParam(':keep', $keep, PDO::PARAM_INT);
 
-		if ($stm && $stm->execute ()) {
+		if ($stm && $stm->execute()) {
 			return $stm->rowCount();
 		} else {
-			$info = $stm->errorInfo();
-			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error cleanOldEntries: ' . $info[2], Minz_Log::ERROR);
 			return false;
 		}
 	}
 
-	public static function daoToFeed ($listDAO, $catID = null) {
-		$list = array ();
+	public static function daoToFeed($listDAO, $catID = null) {
+		$list = array();
 
-		if (!is_array ($listDAO)) {
-			$listDAO = array ($listDAO);
+		if (!is_array($listDAO)) {
+			$listDAO = array($listDAO);
 		}
 
 		foreach ($listDAO as $key => $dao) {
-			if (!isset ($dao['name'])) {
+			if (!isset($dao['name'])) {
 				continue;
 			}
-			if (isset ($dao['id'])) {
+			if (isset($dao['id'])) {
 				$key = $dao['id'];
 			}
 			if ($catID === null) {
@@ -384,13 +371,14 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo {
 			$myFeed->_lastUpdate(isset($dao['lastUpdate']) ? $dao['lastUpdate'] : 0);
 			$myFeed->_priority(isset($dao['priority']) ? $dao['priority'] : 10);
 			$myFeed->_pathEntries(isset($dao['pathEntries']) ? $dao['pathEntries'] : '');
-			$myFeed->_httpAuth(isset($dao['httpAuth']) ? base64_decode ($dao['httpAuth']) : '');
+			$myFeed->_httpAuth(isset($dao['httpAuth']) ? base64_decode($dao['httpAuth']) : '');
 			$myFeed->_error(isset($dao['error']) ? $dao['error'] : 0);
 			$myFeed->_keepHistory(isset($dao['keep_history']) ? $dao['keep_history'] : -2);
+			$myFeed->_ttl(isset($dao['ttl']) ? $dao['ttl'] : -2);
 			$myFeed->_nbNotRead(isset($dao['cache_nbUnreads']) ? $dao['cache_nbUnreads'] : 0);
 			$myFeed->_nbEntries(isset($dao['cache_nbEntries']) ? $dao['cache_nbEntries'] : 0);
-			if (isset ($dao['id'])) {
-				$myFeed->_id ($dao['id']);
+			if (isset($dao['id'])) {
+				$myFeed->_id($dao['id']);
 			}
 			$list[$key] = $myFeed;
 		}

+ 19 - 0
app/Models/FeedDAOSQLite.php

@@ -0,0 +1,19 @@
+<?php
+
+class FreshRSS_FeedDAOSQLite extends FreshRSS_FeedDAO {
+
+	public function updateCachedValues() {	//For one single feed, call updateLastUpdate($id)
+		$sql = 'UPDATE `' . $this->prefix . 'feed` '
+		     . 'SET cache_nbEntries=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=`' . $this->prefix . 'feed`.id),'
+		     . 'cache_nbUnreads=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=`' . $this->prefix . 'feed`.id AND e2.is_read=0)';
+		$stm = $this->bd->prepare($sql);
+		if ($stm && $stm->execute()) {
+			return $stm->rowCount();
+		} else {
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error updateCachedValues: ' . $info[2], Minz_Log::ERROR);
+			return false;
+		}
+	}
+
+}

+ 45 - 43
app/Models/StatsDAO.php

@@ -2,6 +2,8 @@
 
 class FreshRSS_StatsDAO extends Minz_ModelPdo {
 
+	const ENTRY_COUNT_PERIOD = 30;
+
 	/**
 	 * Calculates entry repartition for all feeds and for main stream.
 	 * The repartition includes:
@@ -9,7 +11,7 @@ class FreshRSS_StatsDAO extends Minz_ModelPdo {
 	 *   - read entries
 	 *   - unread entries
 	 *   - favorite entries
-	 * 
+	 *
 	 * @return type
 	 */
 	public function calculateEntryRepartition() {
@@ -50,50 +52,19 @@ SQL;
 	/**
 	 * Calculates entry count per day on a 30 days period.
 	 * Returns the result as a JSON string.
-	 * 
+	 *
 	 * @return string
 	 */
 	public function calculateEntryCount() {
-		$count = array();
+		$count = $this->initEntryCountArray();
+		$period = self::ENTRY_COUNT_PERIOD;
 
-		// Generates a list of 30 last day to be sure we always have 30 days.
-		// If we do not do that kind of thing, we'll end up with holes in the
-		// days if the user do not have a lot of feeds.
-		$sql = <<<SQL
-SELECT - (tens.val + units.val + 1) AS day
-FROM (
-    SELECT 0 AS val
-    UNION ALL SELECT 1
-    UNION ALL SELECT 2
-    UNION ALL SELECT 3
-    UNION ALL SELECT 4
-    UNION ALL SELECT 5
-    UNION ALL SELECT 6
-    UNION ALL SELECT 7
-    UNION ALL SELECT 8
-    UNION ALL SELECT 9
-) AS units
-CROSS JOIN (
-    SELECT 0 AS val
-    UNION ALL SELECT 10
-    UNION ALL SELECT 20
-) AS tens
-ORDER BY day ASC
-SQL;
-		$stm = $this->bd->prepare($sql);
-		$stm->execute();
-		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
-		foreach ($res as $value) {
-			$count[$value['day']] = 0;
-		}
-
-		// Get stats per day for the last 30 days and applies the result on 
-		// the array created with the last query.
+		// Get stats per day for the last 30 days
 		$sql = <<<SQL
 SELECT DATEDIFF(FROM_UNIXTIME(e.date), NOW()) AS day,
 COUNT(1) AS count
 FROM {$this->prefix}entry AS e
-WHERE FROM_UNIXTIME(e.date, '%Y%m%d') BETWEEN DATE_FORMAT(DATE_ADD(NOW(), INTERVAL -30 DAY), '%Y%m%d') AND DATE_FORMAT(DATE_ADD(NOW(), INTERVAL -1 DAY), '%Y%m%d')
+WHERE FROM_UNIXTIME(e.date, '%Y%m%d') BETWEEN DATE_FORMAT(DATE_ADD(NOW(), INTERVAL -{$period} DAY), '%Y%m%d') AND DATE_FORMAT(DATE_ADD(NOW(), INTERVAL -1 DAY), '%Y%m%d')
 GROUP BY day
 ORDER BY day ASC
 SQL;
@@ -108,10 +79,21 @@ SQL;
 		return $this->convertToSerie($count);
 	}
 
+	/**
+	 * Initialize an array for the entry count.
+	 *
+	 * @return array
+	 */
+	protected function initEntryCountArray() {
+		return array_map(function () {
+			return 0;
+		}, array_flip(range(-self::ENTRY_COUNT_PERIOD, -1)));
+	}
+
 	/**
 	 * Calculates feed count per category.
 	 * Returns the result as a JSON string.
-	 * 
+	 *
 	 * @return string
 	 */
 	public function calculateFeedByCategory() {
@@ -134,7 +116,7 @@ SQL;
 	/**
 	 * Calculates entry count per category.
 	 * Returns the result as a JSON string.
-	 * 
+	 *
 	 * @return string
 	 */
 	public function calculateEntryByCategory() {
@@ -158,7 +140,7 @@ SQL;
 
 	/**
 	 * Calculates the 10 top feeds based on their number of entries
-	 * 
+	 *
 	 * @return array
 	 */
 	public function calculateTopFeed() {
@@ -172,7 +154,7 @@ FROM {$this->prefix}category AS c,
 {$this->prefix}entry AS e
 WHERE c.id = f.category
 AND f.id = e.id_feed
-GROUP BY id
+GROUP BY f.id
 ORDER BY count DESC
 LIMIT 10
 SQL;
@@ -181,7 +163,27 @@ SQL;
 		return $stm->fetchAll(PDO::FETCH_ASSOC);
 	}
 
-	private function convertToSerie($data) {
+	/**
+	 * Calculates the last publication date for each feed
+	 *
+	 * @return array
+	 */
+	public function calculateFeedLastDate() {
+		$sql = <<<SQL
+SELECT MAX(f.name) AS name
+, MAX(date) AS last_date
+FROM {$this->prefix}feed AS f,
+{$this->prefix}entry AS e
+WHERE f.id = e.id_feed
+GROUP BY f.id
+ORDER BY name
+SQL;
+		$stm = $this->bd->prepare($sql);
+		$stm->execute();
+		return $stm->fetchAll(PDO::FETCH_ASSOC);
+	}
+
+	protected function convertToSerie($data) {
 		$serie = array();
 
 		foreach ($data as $key => $value) {
@@ -191,7 +193,7 @@ SQL;
 		return json_encode($serie);
 	}
 
-	private function convertToPieSerie($data) {
+	protected function convertToPieSerie($data) {
 		$serie = array();
 
 		foreach ($data as $value) {

+ 37 - 0
app/Models/StatsDAOSQLite.php

@@ -0,0 +1,37 @@
+<?php
+
+class FreshRSS_StatsDAOSQLite extends FreshRSS_StatsDAO {
+
+	/**
+	 * Calculates entry count per day on a 30 days period.
+	 * Returns the result as a JSON string.
+	 *
+	 * @return string
+	 */
+	public function calculateEntryCount() {
+		$count = $this->initEntryCountArray();
+		$period = parent::ENTRY_COUNT_PERIOD;
+
+		// Get stats per day for the last 30 days
+		$sql = <<<SQL
+SELECT round(julianday(e.date, 'unixepoch') - julianday('now')) AS day,
+COUNT(1) AS count
+FROM {$this->prefix}entry AS e
+WHERE strftime('%Y%m%d', e.date, 'unixepoch')
+	BETWEEN strftime('%Y%m%d', 'now', '-{$period} days')
+	AND strftime('%Y%m%d', 'now', '-1 day')
+GROUP BY day
+ORDER BY day ASC
+SQL;
+		$stm = $this->bd->prepare($sql);
+		$stm->execute();
+		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
+
+		foreach ($res as $value) {
+			$count[(int)$value['day']] = (int) $value['count'];
+		}
+
+		return $this->convertToSerie($count);
+	}
+
+}

+ 10 - 1
app/Models/Themes.php

@@ -31,7 +31,10 @@ class FreshRSS_Themes extends Minz_Model {
 			if (file_exists($json_filename)) {
 				$content = file_get_contents($json_filename);
 				$res = json_decode($content, true);
-				if ($res && isset($res['files']) && is_array($res['files'])) {
+				if ($res &&
+						!empty($res['name']) &&
+						isset($res['files']) &&
+						is_array($res['files'])) {
 					$res['id'] = $theme_id;
 					return $res;
 				}
@@ -70,6 +73,7 @@ class FreshRSS_Themes extends Minz_Model {
 			'add' => '✚',
 			'all' => '☰',
 			'bookmark' => '★',
+			'bookmark-add' => '✚',
 			'category' => '☷',
 			'category-white' => '☷',
 			'close' => '❌',
@@ -77,6 +81,7 @@ class FreshRSS_Themes extends Minz_Model {
 			'down' => '▽',
 			'favorite' => '★',
 			'help' => 'ⓘ',
+			'icon' => '⊚',
 			'key' => '⚿',
 			'link' => '↗',
 			'login' => '🔒',
@@ -109,3 +114,7 @@ class FreshRSS_Themes extends Minz_Model {
 			'<img class="icon" src="' . Minz_Url::display($url) . '" alt="' . $alts[$name] . '" />';
 	}
 }
+
+function _i($icon, $url_only = false) {
+	return FreshRSS_Themes::icon($icon, $url_only);
+}

+ 21 - 10
app/Models/UserDAO.php

@@ -2,33 +2,44 @@
 
 class FreshRSS_UserDAO extends Minz_ModelPdo {
 	public function createUser($username) {
-		require_once(APP_PATH . '/sql.php');
 		$db = Minz_Configuration::dataBase();
+		require_once(APP_PATH . '/SQL/sql.' . $db['type'] . '.php');
+		
+		if (defined('SQL_CREATE_TABLES')) {
+			$sql = sprintf(SQL_CREATE_TABLES, $db['prefix'] . $username . '_', Minz_Translate::t('default_category'));
+			$stm = $c->prepare($sql);
+			$ok = $stm && $stm->execute();
+		} else {
+			global $SQL_CREATE_TABLES;
+			if (is_array($SQL_CREATE_TABLES)) {
+				$ok = true;
+				foreach ($SQL_CREATE_TABLES as $instruction) {
+					$sql = sprintf($instruction, '', Minz_Translate::t('default_category'));
+					$stm = $c->prepare($sql);
+					$ok &= ($stm && $stm->execute());
+				}
+			}
+		}
 
-		$sql = sprintf(SQL_CREATE_TABLES, $db['prefix'] . $username . '_');
-		$stm = $this->bd->prepare($sql, array(PDO::ATTR_EMULATE_PREPARES => true));
-		$values = array(
-			'catName' => Minz_Translate::t('default_category'),
-		);
-		if ($stm && $stm->execute($values)) {
+		if ($ok) {
 			return true;
 		} else {
-			$info = $stm->errorInfo();
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
 			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
 			return false;
 		}
 	}
 
 	public function deleteUser($username) {
-		require_once(APP_PATH . '/sql.php');
 		$db = Minz_Configuration::dataBase();
+		require_once(APP_PATH . '/SQL/sql.' . $db['type'] . '.php');
 
 		$sql = sprintf(SQL_DROP_TABLES, $db['prefix'] . $username . '_');
 		$stm = $this->bd->prepare($sql);
 		if ($stm && $stm->execute()) {
 			return true;
 		} else {
-			$info = $stm->errorInfo();
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
 			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
 			return false;
 		}

+ 4 - 1
app/sql.php → app/SQL/install.sql.mysql.php

@@ -21,6 +21,7 @@ CREATE TABLE IF NOT EXISTS `%1$sfeed` (
 	`httpAuth` varchar(511) DEFAULT NULL,
 	`error` boolean DEFAULT 0,
 	`keep_history` MEDIUMINT NOT NULL DEFAULT -2,	-- v0.7
+	`ttl` INT NOT NULL DEFAULT -2,	-- v0.7.3
 	`cache_nbEntries` int DEFAULT 0,	-- v0.7
 	`cache_nbUnreads` int DEFAULT 0,	-- v0.7
 	PRIMARY KEY (`id`),
@@ -52,7 +53,9 @@ CREATE TABLE IF NOT EXISTS `%1$sentry` (
 ) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci
 ENGINE = INNODB;
 
-INSERT IGNORE INTO `%1$scategory` (id, name) VALUES(1, :catName);
+INSERT IGNORE INTO `%1$scategory` (id, name) VALUES(1, "%2$s");
 ');
 
 define('SQL_DROP_TABLES', 'DROP TABLES %1$sentry, %1$sfeed, %1$scategory');
+
+define('SQL_SHOW_TABLES', 'SHOW tables;');

+ 58 - 0
app/SQL/install.sql.sqlite.php

@@ -0,0 +1,58 @@
+<?php
+$SQL_CREATE_TABLES = array(
+'CREATE TABLE IF NOT EXISTS `%1$scategory` (
+	`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+	`name` varchar(255) NOT NULL,
+	UNIQUE (`name`)
+);',
+
+'CREATE TABLE IF NOT EXISTS `%1$sfeed` (
+	`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+	`url` varchar(511) NOT NULL,
+	`%1$scategory` SMALLINT DEFAULT 0,
+	`name` varchar(255) NOT NULL,
+	`website` varchar(255),
+	`description` text,
+	`lastUpdate` int(11) DEFAULT 0,
+	`priority` tinyint(2) NOT NULL DEFAULT 10,
+	`pathEntries` varchar(511) DEFAULT NULL,
+	`httpAuth` varchar(511) DEFAULT NULL,
+	`error` boolean DEFAULT 0,
+	`keep_history` MEDIUMINT NOT NULL DEFAULT -2,
+	`ttl` INT NOT NULL DEFAULT -2,
+	`cache_nbEntries` int DEFAULT 0,
+	`cache_nbUnreads` int DEFAULT 0,
+	FOREIGN KEY (`%1$scategory`) REFERENCES `%1$scategory`(`id`) ON DELETE SET NULL ON UPDATE CASCADE,
+	UNIQUE (`url`)
+);',
+
+'CREATE INDEX IF NOT EXISTS feed_name_index ON `%1$sfeed`(`name`);',
+'CREATE INDEX IF NOT EXISTS feed_priority_index ON `%1$sfeed`(`priority`);',
+'CREATE INDEX IF NOT EXISTS feed_keep_history_index ON `%1$sfeed`(`keep_history`);',
+
+'CREATE TABLE IF NOT EXISTS `%1$sentry` (
+	`id` bigint NOT NULL,
+	`guid` varchar(760) NOT NULL,
+	`title` varchar(255) NOT NULL,
+	`author` varchar(255),
+	`content` text,
+	`link` varchar(1023) NOT NULL,
+	`date` int(11),
+	`is_read` boolean NOT NULL DEFAULT 0,
+	`is_favorite` boolean NOT NULL DEFAULT 0,
+	`id_feed` SMALLINT,
+	`tags` varchar(1023),
+	PRIMARY KEY (`id`),
+	FOREIGN KEY (`id_feed`) REFERENCES `%1$sfeed`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
+	UNIQUE (`id_feed`,`guid`)
+);',
+
+'CREATE INDEX IF NOT EXISTS entry_is_favorite_index ON `%1$sentry`(`is_favorite`);',
+'CREATE INDEX IF NOT EXISTS entry_is_read_index ON `%1$sentry`(`is_read`);',
+
+'INSERT OR IGNORE INTO `%1$scategory` (id, name) VALUES(1, "%2$s");',
+);
+
+define('SQL_DROP_TABLES', 'DROP TABLES %1$sentry, %1$sfeed, %1$scategory');
+
+define('SQL_SHOW_TABLES', 'SELECT name FROM sqlite_master WHERE type="table"');

+ 40 - 1
app/i18n/en.php

@@ -15,8 +15,45 @@ return array (
 	'feed'				=> 'Feed',
 	'feeds'				=> 'Feeds',
 	'shortcuts'			=> 'Shortcuts',
+	'queries'			=> 'User queries',
+	'query_search'			=> 'Search for "%s"',
+	'query_order_asc'		=> 'Display oldest articles first',
+	'query_order_desc'		=> 'Display newest articles first',
+	'query_get_category'		=> 'Display "%s" category',
+	'query_get_feed'		=> 'Display "%s" feed',
+	'query_get_all'			=> 'Display all articles',
+	'query_get_favorite'		=> 'Display favorite articles',
+	'query_state_0'			=> 'Display all articles',
+	'query_state_1'			=> 'Display read articles',
+	'query_state_2'			=> 'Display unread articles',
+	'query_state_3'			=> 'Display all articles',
+	'query_state_4'			=> 'Display favorite articles',
+	'query_state_5'			=> 'Display read favorite articles',
+	'query_state_6'			=> 'Display unread favorite articles',
+	'query_state_7'			=> 'Display favorite articles',
+	'query_state_8'			=> 'Display not favorite articles',
+	'query_state_9'			=> 'Display read not favorite articles',
+	'query_state_10'		=> 'Display unread not favorite articles',
+	'query_state_11'		=> 'Display not favorite articles',
+	'query_state_12'		=> 'Display all articles',
+	'query_state_13'		=> 'Display read articles',
+	'query_state_14'		=> 'Display unread articles',
+	'query_state_15'		=> 'Display all articles',
+	'query_number'			=> 'Query n°%d',
+	'add_query'			=> 'Add a query',
+	'no_query'			=> 'You haven’t created any user query yet.',
+	'query_filter'			=> 'Filter applied:',
+	'no_query_filter'		=> 'No filter',
 	'about'				=> 'About',
 	'stats'				=> 'Statistics',
+	'stats_idle'			=> 'Idle feeds',
+	'stats_main'			=> 'Main statistics',
+    
+	'last_week'			=> 'Last week',
+	'last_month'			=> 'Last month',
+	'last_3_month'			=> 'Last three months',
+	'last_6_month'			=> 'Last six months',
+	'last_year'			=> 'Last year',
 
 	'your_rss_feeds'		=> 'Your RSS feeds',
 	'add_rss_feed'			=> 'Add a RSS feed',
@@ -160,6 +197,7 @@ return array (
 	'by_feed'			=> 'by feed',
 	'by_default'			=> 'By default',
 	'keep_history'			=> 'Minimum number of articles to keep',
+	'ttl'				=> 'Do not automatically refresh more often than',
 	'categorize'			=> 'Store in a category',
 	'truncate'			=> 'Delete all articles',
 	'advanced'			=> 'Advanced',
@@ -226,8 +264,9 @@ return array (
 	'bottom_line'			=> 'Bottom line',
 	'img_with_lazyload'		=> 'Use "lazy load" mode to load pictures',
 	'sticky_post'			=> 'Stick the article to the top when opened',
+	'reading_confirm'		=> 'Display a confirmation dialog on “mark all as read” actions',
 	'auto_read_when'		=> 'Mark article as read…',
-	'article_selected'		=> 'when article is selected',
+	'article_viewed'		=> 'when article is viewed',
 	'article_open_on_website'	=> 'when article is opened on its original website',
 	'scroll'			=> 'during page scrolls',
 	'upon_reception'		=> 'upon reception of the article',

+ 77 - 38
app/i18n/fr.php

@@ -15,8 +15,45 @@ return array (
 	'feed'				=> 'Flux',
 	'feeds'				=> 'Flux',
 	'shortcuts'			=> 'Raccourcis',
+	'queries'			=> 'Filtres utilisateurs',
+	'query_search'			=> 'Recherche de "%s"',
+	'query_order_asc'		=> 'Afficher les articles les plus anciens en premier',
+	'query_order_desc'		=> 'Afficher les articles les plus récents en premier',
+	'query_get_category'		=> 'Afficher la catégorie "%s"',
+	'query_get_feed'		=> 'Afficher le flux "%s"',
+	'query_get_all'			=> 'Afficher tous les articles',
+	'query_get_favorite'		=> 'Afficher les articles favoris',
+	'query_state_0'			=> 'Afficher tous les articles',
+	'query_state_1'			=> 'Afficher les articles lus',
+	'query_state_2'			=> 'Afficher les articles non lus',
+	'query_state_3'			=> 'Afficher tous les articles',
+	'query_state_4'			=> 'Afficher les articles favoris',
+	'query_state_5'			=> 'Afficher les articles lus et favoris',
+	'query_state_6'			=> 'Afficher les articles non lus et favoris',
+	'query_state_7'			=> 'Afficher les articles favoris',
+	'query_state_8'			=> 'Afficher les articles non favoris',
+	'query_state_9'			=> 'Afficher les articles lus et non favoris',
+	'query_state_10'		=> 'Afficher les articles non lus et non favoris',
+	'query_state_11'		=> 'Afficher les articles non favoris',
+	'query_state_12'		=> 'Afficher tous les articles',
+	'query_state_13'		=> 'Afficher les articles lus',
+	'query_state_14'		=> 'Afficher les articles non lus',
+	'query_state_15'		=> 'Afficher tous les articles',
+	'query_number'			=> 'Filtre n°%d',
+	'add_query'			=> 'Créer un filtre',
+	'no_query'			=> 'Vous n’avez pas encore créé de filtre.',
+	'query_filter'			=> 'Filtres appliqués :',
+	'no_query_filter'		=> 'Aucun filtre appliqué',
 	'about'				=> 'À propos',
 	'stats'				=> 'Statistiques',
+	'stats_idle'			=> 'Flux inactifs',
+	'stats_main'			=> 'Statistiques principales',
+
+	'last_week'			=> 'La dernière semaine',
+	'last_month'			=> 'Le dernier mois',
+	'last_3_month'			=> 'Les derniers trois mois',
+	'last_6_month'			=> 'Les derniers six mois',
+	'last_year'			=> 'La dernière année',
 
 	'your_rss_feeds'		=> 'Vos flux RSS',
 	'add_rss_feed'			=> 'Ajouter un flux RSS',
@@ -33,7 +70,7 @@ return array (
 
 	'filter'			=> 'Filtrer',
 	'see_website'			=> 'Voir le site',
-	'administration'		=> 'Gestion',
+	'administration'		=> 'Gérer',
 	'actualize'			=> 'Actualiser',
 
 	'mark_read'			=> 'Marquer comme lu',
@@ -66,49 +103,49 @@ return array (
 	'article_published_on'		=> 'Article publié initialement sur <a href="%s">%s</a>',
 	'article_published_on_author'	=> 'Article publié initialement sur <a href="%s">%s</a> par %s',
 
-	'access_denied'			=> 'Vous n’avez pas le droit d’accéder à cette page',
-	'page_not_found'		=> 'La page que vous cherchez n’existe pas',
-	'error_occurred'		=> 'Une erreur est survenue',
-	'error_occurred_update'	=> 'Rien n’a été modifié',
+	'access_denied'			=> 'Vous n’avez pas le droit d’accéder à cette page !',
+	'page_not_found'		=> 'La page que vous cherchez n’existe pas !',
+	'error_occurred'		=> 'Une erreur est survenue !',
+	'error_occurred_update'		=> 'Rien n’a été modifié !',
 
 	'default_category'		=> 'Sans catégorie',
-	'categories_updated'		=> 'Les catégories ont été mises à jour',
+	'categories_updated'		=> 'Les catégories ont été mises à jour.',
 	'categories_management'		=> 'Gestion des catégories',
-	'feed_updated'			=> 'Le flux a été mis à jour',
+	'feed_updated'			=> 'Le flux a été mis à jour.',
 	'rss_feed_management'		=> 'Gestion des flux RSS',
-	'configuration_updated'		=> 'La configuration a été mise à jour',
+	'configuration_updated'		=> 'La configuration a été mise à jour.',
 	'sharing_management'		=> 'Gestion des options de partage',
-	'bad_opml_file'			=> 'Votre fichier OPML n’est pas valide',
-	'shortcuts_updated'		=> 'Les raccourcis ont été mis à jour',
+	'bad_opml_file'			=> 'Votre fichier OPML n’est pas valide.',
+	'shortcuts_updated'		=> 'Les raccourcis ont été mis à jour.',
 	'shortcuts_navigation'		=> 'Navigation',
 	'shortcuts_navigation_help'	=> 'Avec le modificateur "Shift", les raccourcis de navigation s’appliquent aux flux.<br/>Avec le modificateur "Alt", les raccourcis de navigation s’appliquent aux catégories.',
 	'shortcuts_article_action'	=> 'Actions associées à l’article courant',
 	'shortcuts_other_action'	=> 'Autres actions',
-	'feeds_marked_read'		=> 'Les flux ont été marqués comme lus',
-	'updated'			=> 'Modifications enregistrées',
+	'feeds_marked_read'		=> 'Les flux ont été marqués comme lus.',
+	'updated'			=> 'Modifications enregistrées.',
 
 	'already_subscribed'		=> 'Vous êtes déjà abonné à <em>%s</em>',
-	'feed_added'			=> 'Le flux <em>%s</em> a bien été ajouté',
-	'feed_not_added'		=> '<em>%s</em> n’a pas pu être ajouté',
+	'feed_added'			=> 'Le flux <em>%s</em> a bien été ajouté.',
+	'feed_not_added'		=> '<em>%s</em> n’a pas pu être ajouté.',
 	'internal_problem_feed'		=> 'Le flux ne peut pas être ajouté. <a href="%s">Consulter les logs de FreshRSS</a> pour plus de détails.',
-	'invalid_url'			=> 'L’url <em>%s</em> est invalide',
-	'feed_actualized'		=> '<em>%s</em> a été mis à jour',
-	'n_feeds_actualized'		=> '%d flux ont été mis à jour',
-	'feeds_actualized'		=> 'Les flux ont été mis à jour',
-	'no_feed_actualized'		=> 'Aucun flux n’a pu être mis à jour',
-	'n_entries_deleted'		=> '%d articles ont été supprimés',
-	'feeds_imported_with_errors'	=> 'Vos flux ont été importés mais des erreurs sont survenues',
-	'feeds_imported'		=> 'Vos flux ont été importés et vont maintenant être actualisés',
-	'category_emptied'		=> 'La catégorie a été vidée',
-	'feed_deleted'			=> 'Le flux a été supprimé',
+	'invalid_url'			=> 'L’url <em>%s</em> est invalide.',
+	'feed_actualized'		=> '<em>%s</em> a été mis à jour.',
+	'n_feeds_actualized'		=> '%d flux ont été mis à jour.',
+	'feeds_actualized'		=> 'Les flux ont été mis à jour.',
+	'no_feed_actualized'		=> 'Aucun flux n’a pu être mis à jour.',
+	'n_entries_deleted'		=> '%d articles ont été supprimés.',
+	'feeds_imported_with_errors'	=> 'Vos flux ont été importés mais des erreurs sont survenues.',
+	'feeds_imported'		=> 'Vos flux ont été importés et vont maintenant être actualisés.',
+	'category_emptied'		=> 'La catégorie a été vidée.',
+	'feed_deleted'			=> 'Le flux a été supprimé.',
 	'feed_validator'		=> 'Vérifier la valididé du flux',
 
-	'optimization_complete'		=> 'Optimisation terminée',
+	'optimization_complete'		=> 'Optimisation terminée.',
 
 	'your_rss_feeds'		=> 'Vos flux RSS',
 	'your_favorites'		=> 'Vos favoris',
 	'public'			=> 'Public',
-	'invalid_login'			=> 'L’identifiant est invalide',
+	'invalid_login'			=> 'L’identifiant est invalide !',
 
 	// VIEWS
 	'save'				=> 'Enregistrer',
@@ -120,12 +157,12 @@ return array (
 	'category_number'		=> 'Catégorie n°%d',
 	'ask_empty'			=> 'Vider ?',
 	'number_feeds'			=> '%d flux',
-	'can_not_be_deleted'		=> 'Ne peut pas être supprimée',
+	'can_not_be_deleted'		=> 'Ne peut pas être supprimée.',
 	'add_category'			=> 'Ajouter une catégorie',
 	'new_category'			=> 'Nouvelle catégorie',
 
-	'javascript_for_shortcuts'	=> 'Le JavaScript doit être activé pour pouvoir profiter des raccourcis',
-	'javascript_should_be_activated'=> 'Le JavaScript doit être activé',
+	'javascript_for_shortcuts'	=> 'Le JavaScript doit être activé pour pouvoir profiter des raccourcis.',
+	'javascript_should_be_activated'=> 'Le JavaScript doit être activé.',
 	'shift_for_all_read'		=> '+ <code>shift</code> pour marquer tous les articles comme lus',
 	'see_on_website'		=> 'Voir sur le site d’origine',
 	'next_article'			=> 'Passer à l’article suivant',
@@ -136,7 +173,7 @@ return array (
 	'previous_page'			=> 'Passer à la page précédente',
 	'collapse_article'		=> 'Refermer',
 	'auto_share'			=> 'Partager',
-	'auto_share_help'		=> 'Si il n’y a qu’un mode de partage, celui ci est utilisé automatiquement. Sinon ils sont accessibles par leur numéro.',
+	'auto_share_help'		=> 'Sil n’y a qu’un mode de partage, celui ci est utilisé automatiquement. Sinon ils sont accessibles par leur numéro.',
 	'focus_search'			=> 'Accéder à la recherche',
 
 	'file_to_import'		=> 'Fichier à importer<br />(OPML, Json ou Zip)',
@@ -160,6 +197,7 @@ return array (
 	'by_feed'			=> 'par flux',
 	'by_default'			=> 'Par défaut',
 	'keep_history'			=> 'Nombre minimum d’articles à conserver',
+	'ttl'				=> 'Ne pas automatiquement rafraîchir plus souvent que',
 	'categorize'			=> 'Ranger dans une catégorie',
 	'truncate'			=> 'Supprimer tous les articles',
 	'advanced'			=> 'Avancé',
@@ -175,7 +213,7 @@ return array (
 	'share_name'			=> 'Nom du partage à afficher',
 	'share_url'			=> 'URL du partage à utiliser',
 	'not_yet_implemented'		=> 'Pas encore implémenté',
-	'access_protected_feeds'	=> 'La connexion permet d’accéder aux flux protégés par une authentification HTTP',
+	'access_protected_feeds'	=> 'La connexion permet d’accéder aux flux protégés par une authentification HTTP.',
 	'no_selected_feed'		=> 'Aucun flux sélectionné.',
 	'think_to_add'			=> '<a href="./?c=configure&amp;a=feed">Vous pouvez ajouter des flux</a>.',
 
@@ -202,16 +240,16 @@ return array (
 	'username'			=> 'Nom d’utilisateur',
 	'password'			=> 'Mot de passe',
 	'create'			=> 'Créer',
-	'user_created'			=> 'L’utilisateur %s a été créé',
-	'user_deleted'			=> 'L’utilisateur %s a été supprimé',
+	'user_created'			=> 'L’utilisateur %s a été créé.',
+	'user_deleted'			=> 'L’utilisateur %s a été supprimé.',
 
 	'language'			=> 'Langue',
 	'month'				=> 'mois',
 	'archiving_configuration'	=> 'Archivage',
-	'delete_articles_every'	=> 'Supprimer les articles après',
+	'delete_articles_every'		=> 'Supprimer les articles après',
 	'purge_now'			=> 'Purger maintenant',
-	'purge_completed'		=> 'Purge effectuée (%d articles supprimés)',
-	'archiving_configuration_help'	=> 'D’autres options sont disponibles dans la configuration individuelle des flux',
+	'purge_completed'		=> 'Purge effectuée (%d articles supprimés).',
+	'archiving_configuration_help'	=> 'D’autres options sont disponibles dans la configuration individuelle des flux.',
 	'reading_configuration'		=> 'Lecture',
 	'display_configuration'		=> 'Affichage',
 	'articles_per_page'		=> 'Nombre d’articles par page',
@@ -226,8 +264,9 @@ return array (
 	'bottom_line'			=> 'Ligne du bas',
 	'img_with_lazyload'		=> 'Utiliser le mode “chargement différé” pour les images',
 	'sticky_post'			=> 'Aligner l’article en haut quand il est ouvert',
+	'reading_confirm'		=> 'Afficher une confirmation lors des actions “marquer tout comme lu”',
 	'auto_read_when'		=> 'Marquer un article comme lu…',
-	'article_selected'		=> 'lorsque l’article est sélectionné',
+	'article_viewed'		=> 'lorsque l’article est affiché',
 	'article_open_on_website'	=> 'lorsque l’article est ouvert sur le site d’origine',
 	'scroll'			=> 'au défilement de la page',
 	'upon_reception'		=> 'dès la réception du nouvel article',
@@ -293,7 +332,7 @@ return array (
 	'version'			=> 'Version',
 
 	'logs'				=> 'Logs',
-	'logs_empty'			=> 'Les logs sont vides',
+	'logs_empty'			=> 'Les logs sont vides.',
 	'clear_logs'			=> 'Effacer les logs',
 
 	'forbidden_access'		=> 'L’accès vous est interdit !',

+ 1 - 1
app/i18n/install.en.php

@@ -61,7 +61,7 @@ return array (
 	'update_end'			=> 'Update process is completed, now you can go to the final step.',
 
 
-	'installation_is_ok'		=> 'The installation process was successful.<br />The final step will now attempt to delete the <kbd>./p/i/install.php</kbd> file and any database backup created during the update process.<br />You may choose to skip this step and delete <kbd>./p/i/install.php</kbd> manually.',
+	'installation_is_ok'		=> 'The installation process was successful.<br />The final step will now attempt to delete any file and database backup created during the update process.<br />You may choose to skip this step by deleting <kbd>./data/do-install.txt</kbd> manually.',
 	'finish_installation'		=> 'Complete installation',
 	'install_not_deleted'		=> 'Something went wrong; you must delete the file <em>%s</em> manually.',
 );

+ 1 - 1
app/i18n/install.fr.php

@@ -60,7 +60,7 @@ return array (
 	'update_long'			=> 'Ce processus peut prendre longtemps, selon la taille de votre base de données. Vous aurez peut-être à attendre que cette page dépasse son temps maximum d’exécution (~5 minutes) puis à la recharger.',
 	'update_end'			=> 'La mise à jour est terminée, vous pouvez maintenant passer à l’étape finale.',
 
-	'installation_is_ok'		=> 'L’installation s’est bien passée.<br />La dernière étape va maintenant tenter de supprimer le fichier <kbd>./p/i/install.php</kbd>, ainsi que d’éventuelles copies de base de données créées durant le processus de mise à jour.<br />Vous pouvez choisir de sauter cette étape et de supprimer <kbd>./p/i/install.php</kbd> manuellement.',
+	'installation_is_ok'		=> 'L’installation s’est bien passée.<br />La dernière étape va maintenant tenter de supprimer les fichiers ainsi que d’éventuelles copies de base de données créés durant le processus de mise à jour.<br />Vous pouvez choisir de sauter cette étape en supprimant <kbd>./data/do-install.txt</kbd> manuellement.',
 	'finish_installation'		=> 'Terminer l’installation',
 	'install_not_deleted'		=> 'Quelque chose s’est mal passé, vous devez supprimer le fichier <em>%s</em> à la main.',
 );

+ 90 - 50
p/i/install.php → app/install.php

@@ -3,28 +3,36 @@ if (function_exists('opcache_reset')) {
 	opcache_reset();
 }
 
-require('../../constants.php');
 define('BCRYPT_COST', 9);
 
-include(LIB_PATH . '/lib_rss.php');
-
 session_name('FreshRSS');
 session_set_cookie_params(0, dirname(empty($_SERVER['REQUEST_URI']) ? '/' : dirname($_SERVER['REQUEST_URI'])), null, false, true);
 session_start();
 
 if (isset ($_GET['step'])) {
-	define ('STEP', $_GET['step']);
+	define ('STEP', (int)$_GET['step']);
 } else {
 	define ('STEP', 1);
 }
 
 define('SQL_CREATE_DB', 'CREATE DATABASE IF NOT EXISTS %1$s DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci;');
 
-include(APP_PATH . '/sql.php');
+if (STEP === 3 && isset($_POST['type'])) {
+	$_SESSION['bd_type'] = $_POST['type'];
+}
 
-//<updates>
-define('SQL_SHOW_TABLES', 'SHOW tables;');
+if (isset($_SESSION['bd_type'])) {
+	switch ($_SESSION['bd_type']) {
+		case 'mysql':
+			include(APP_PATH . '/SQL/install.sql.mysql.php');
+			break;
+		case 'sqlite':
+			include(APP_PATH . '/SQL/install.sql.sqlite.php');
+			break;
+	}
+}
 
+//<updates>
 define('SQL_BACKUP006', 'RENAME TABLE `%1$scategory` TO `%1$scategory006`, `%1$sfeed` TO `%1$sfeed006`, `%1$sentry` TO `%1$sentry006`;');
 
 define('SQL_SHOW_COLUMNS_UPDATEv006', 'SHOW columns FROM `%1$sentry006` LIKE "id2";');
@@ -70,7 +78,9 @@ FROM `%1$sentry006` e0
 INNER JOIN `%2$sentry` e1 ON e0.id2 = e1.id
 WHERE e1.content_bin IS NULL');
 
-define('SQL_CONVERT_UPDATEv006', 'UPDATE `%1$sentry` SET content_bin=COMPRESS(?) WHERE id=?;');
+define('SQL_CONVERT_UPDATEv006', 'UPDATE `%1$sentry` SET '
+	. (isset($_SESSION['bd_type']) && $_SESSION['bd_type'] === 'mysql' ? 'content_bin=COMPRESS(?)' : 'content=?')
+	. ' WHERE id=?;');
 
 define('SQL_DROP_BACKUPv006', 'DROP TABLE IF EXISTS `%1$sentry006`, `%1$sfeed006`, `%1$scategory006`;');
 
@@ -210,21 +220,28 @@ function saveStep2 () {
 
 function saveStep3 () {
 	if (!empty ($_POST)) {
-		if (empty ($_POST['type']) ||
-		    empty ($_POST['host']) ||
-		    empty ($_POST['user']) ||
-		    empty ($_POST['base'])) {
-			$_SESSION['bd_error'] = 'Missing parameters!';
+		if ($_SESSION['bd_type'] === 'sqlite') {
+			$_SESSION['bd_base'] = $_SESSION['default_user'];
+			$_SESSION['bd_host'] = '';
+			$_SESSION['bd_user'] = '';
+			$_SESSION['bd_password'] = '';
+			$_SESSION['bd_prefix'] = '';
+			$_SESSION['bd_prefix_user'] = '';	//No prefix for SQLite
+		} else {
+			if (empty ($_POST['type']) ||
+			    empty ($_POST['host']) ||
+			    empty ($_POST['user']) ||
+			    empty ($_POST['base'])) {
+				$_SESSION['bd_error'] = 'Missing parameters!';
+			}
+			$_SESSION['bd_base'] = substr($_POST['base'], 0, 64);
+			$_SESSION['bd_host'] = $_POST['host'];
+			$_SESSION['bd_user'] = $_POST['user'];
+			$_SESSION['bd_password'] = $_POST['pass'];
+			$_SESSION['bd_prefix'] = substr($_POST['prefix'], 0, 16);
+			$_SESSION['bd_prefix_user'] = $_SESSION['bd_prefix'] . (empty($_SESSION['default_user']) ? '' : ($_SESSION['default_user'] . '_'));
 		}
 
-		$_SESSION['bd_type'] = isset ($_POST['type']) ? $_POST['type'] : 'mysql';
-		$_SESSION['bd_host'] = $_POST['host'];
-		$_SESSION['bd_user'] = $_POST['user'];
-		$_SESSION['bd_password'] = $_POST['pass'];
-		$_SESSION['bd_base'] = substr($_POST['base'], 0, 64);
-		$_SESSION['bd_prefix'] = substr($_POST['prefix'], 0, 16);
-		$_SESSION['bd_prefix_user'] = $_SESSION['bd_prefix'] . (empty($_SESSION['default_user']) ? '' : ($_SESSION['default_user'] . '_'));
-
 		$ini_array = array(
 			'general' => array(
 				'environment' => empty($_SESSION['environment']) ? 'production' : $_SESSION['environment'],
@@ -285,9 +302,7 @@ function updateDatabase($perform = false) {
 				);
 				break;
 			case 'sqlite':
-				$str = 'sqlite:' . DATA_PATH . $_SESSION['bd_base'] . '.sqlite';
-				$driver_options = null;
-				break;
+				return false;	//No update for SQLite needed so far
 			default:
 				return false;
 		}
@@ -352,8 +367,10 @@ function newPdo() {
 			);
 			break;
 		case 'sqlite':
-			$str = 'sqlite:' . DATA_PATH . $_SESSION['bd_base'] . '.sqlite';
-			$driver_options = null;
+			$str = 'sqlite:' . DATA_PATH . '/' . $_SESSION['default_user'] . '.sqlite';
+			$driver_options = array(
+				PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
+			);
 			break;
 		default:
 			return false;
@@ -364,13 +381,15 @@ function newPdo() {
 function postUpdate() {
 	$c = newPdo();
 
-	$sql = sprintf(SQL_UPDATE_HISTORYv007b, $_SESSION['bd_prefix_user']);
-	$stm = $c->prepare($sql);
-	$stm->execute();
+	if ($_SESSION['bd_type'] !== 'sqlite') {	//No update for SQLite needed yet
+		$sql = sprintf(SQL_UPDATE_HISTORYv007b, $_SESSION['bd_prefix_user']);
+		$stm = $c->prepare($sql);
+		$stm->execute();
 
-	$sql = sprintf(SQL_UPDATE_CACHED_VALUES, $_SESSION['bd_prefix_user']);
-	$stm = $c->prepare($sql);
-	$stm->execute();
+		$sql = sprintf(SQL_UPDATE_CACHED_VALUES, $_SESSION['bd_prefix_user']);
+		$stm = $c->prepare($sql);
+		$stm->execute();
+	}
 
 	//<favicons>
 	$sql = sprintf(SQL_GET_FEEDS, $_SESSION['bd_prefix_user']);
@@ -389,7 +408,7 @@ function postUpdate() {
 }
 
 function deleteInstall () {
-	$res = unlink (INDEX_PATH . '/install.php');
+	$res = unlink (DATA_PATH . '/do-install.txt');
 	if ($res) {
 		header ('Location: index.php');
 	}
@@ -647,7 +666,10 @@ function checkBD () {
 				$str = 'mysql:host=' . $_SESSION['bd_host'] . ';dbname=' . $_SESSION['bd_base'];
 				break;
 			case 'sqlite':
-				$str = 'sqlite:' . DATA_PATH . $_SESSION['bd_base'] . '.sqlite';
+				$str = 'sqlite:' . DATA_PATH . '/' . $_SESSION['default_user'] . '.sqlite';
+				$driver_options = array(
+					PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
+				);
 				break;
 			default:
 				return false;
@@ -655,20 +677,31 @@ function checkBD () {
 
 		$c = new PDO ($str, $_SESSION['bd_user'], $_SESSION['bd_password'], $driver_options);
 
-		$stm = $c->prepare(SQL_SHOW_TABLES);
-		$stm->execute();
-		$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
-		if (in_array($_SESSION['bd_prefix'] . 'entry', $res) && !in_array($_SESSION['bd_prefix'] . 'entry006', $res)) {
-			$sql = sprintf(SQL_BACKUP006, $_SESSION['bd_prefix']);	//v0.6
-			$res = $c->query($sql);	//Backup tables
+		if ($_SESSION['bd_type'] !== 'sqlite') {	//No SQL backup for SQLite
+			$stm = $c->prepare(SQL_SHOW_TABLES);
+			$stm->execute();
+			$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
+			if (in_array($_SESSION['bd_prefix'] . 'entry', $res) && !in_array($_SESSION['bd_prefix'] . 'entry006', $res)) {
+				$sql = sprintf(SQL_BACKUP006, $_SESSION['bd_prefix']);	//v0.6
+				$res = $c->query($sql);	//Backup tables
+			}
 		}
 
-		$sql = sprintf(SQL_CREATE_TABLES, $_SESSION['bd_prefix_user']);
-		$stm = $c->prepare($sql, array(PDO::ATTR_EMULATE_PREPARES => true));
-		$values = array(
-			'catName' => _t('default_category'),
-		);
-		$ok = $stm->execute($values);
+		if (defined('SQL_CREATE_TABLES')) {
+			$sql = sprintf(SQL_CREATE_TABLES, $_SESSION['bd_prefix_user'], _t('default_category'));
+			$stm = $c->prepare($sql);
+			$ok = $stm->execute();
+		} else {
+			global $SQL_CREATE_TABLES;
+			if (is_array($SQL_CREATE_TABLES)) {
+				$ok = true;
+				foreach ($SQL_CREATE_TABLES as $instruction) {
+					$sql = sprintf($instruction, $_SESSION['bd_prefix_user'], _t('default_category'));
+					$stm = $c->prepare($sql);
+					$ok &= $stm->execute();
+				}
+			}
+		}
 	} catch (PDOException $e) {
 		$ok = false;
 		$_SESSION['bd_error'] = $e->getMessage();
@@ -889,20 +922,20 @@ function printStep3 () {
 		<div class="form-group">
 			<label class="group-name" for="type"><?php echo _t ('bdd_type'); ?></label>
 			<div class="group-controls">
-				<select name="type" id="type">
+				<select name="type" id="type" onchange="mySqlShowHide()">
 				<option value="mysql"
 					<?php echo (isset($_SESSION['bd_type']) && $_SESSION['bd_type'] === 'mysql') ? 'selected="selected"' : ''; ?>>
 					MySQL
 				</option>
-				<!-- TODO : l'utilisation de SQLite n'est pas encore possible. Pour tester tout de même, décommentez ce bloc
 				<option value="sqlite"
 					<?php echo (isset($_SESSION['bd_type']) && $_SESSION['bd_type'] === 'sqlite') ? 'selected="selected"' : ''; ?>>
 					SQLite
-				</option>-->
+				</option>
 				</select>
 			</div>
 		</div>
 
+		<div id="mysql">
 		<div class="form-group">
 			<label class="group-name" for="host"><?php echo _t ('host'); ?></label>
 			<div class="group-controls">
@@ -937,6 +970,13 @@ function printStep3 () {
 				<input type="text" id="prefix" name="prefix" maxlength="16" pattern="[0-9A-Za-z_]{1,16}" value="<?php echo isset ($_SESSION['bd_prefix']) ? $_SESSION['bd_prefix'] : 'freshrss_'; ?>" />
 			</div>
 		</div>
+		</div>
+		<script>
+			function mySqlShowHide() {
+				document.getElementById('mysql').style.display = document.getElementById('type').value === 'mysql' ? 'block' : 'none';
+			}
+			mySqlShowHide();
+		</script>
 
 		<div class="form-group form-actions">
 			<div class="group-controls">
@@ -988,7 +1028,7 @@ function printStep5 () {
 
 function printStep6 () {
 ?>
-	<p class="alert alert-error"><span class="alert-head"><?php echo _t ('oops'); ?></span> <?php echo _t ('install_not_deleted', INDEX_PATH . '/install.php'); ?></p>
+	<p class="alert alert-error"><span class="alert-head"><?php echo _t ('oops'); ?></span> <?php echo _t ('install_not_deleted', DATA_PATH . '/do-install.txt'); ?></p>
 <?php
 }
 

+ 3 - 0
app/layout/aside_configure.phtml

@@ -15,6 +15,9 @@
 	<li class="item<?php echo Minz_Request::actionName () == 'shortcut' ? ' active' : ''; ?>">
 		<a href="<?php echo _url ('configure', 'shortcut'); ?>"><?php echo Minz_Translate::t ('shortcuts'); ?></a>
 	</li>
+	<li class="item<?php echo Minz_Request::actionName () == 'queries' ? ' active' : ''; ?>">
+		<a href="<?php echo _url ('configure', 'queries'); ?>"><?php echo Minz_Translate::t ('queries'); ?></a>
+	</li>
 	<li class="separator"></li>
 	<li class="item<?php echo Minz_Request::actionName () == 'users' ? ' active' : ''; ?>">
 		<a href="<?php echo _url ('configure', 'users'); ?>"><?php echo Minz_Translate::t ('users'); ?></a>

+ 9 - 0
app/layout/aside_stats.phtml

@@ -0,0 +1,9 @@
+<ul class="nav nav-list aside">
+	<li class="nav-header"><?php echo Minz_Translate::t ('stats'); ?></li>
+	<li class="item<?php echo Minz_Request::actionName () == 'index' ? ' active' : ''; ?>">
+		<a href="<?php echo _url ('stats', 'index'); ?>"><?php echo Minz_Translate::t ('stats_main'); ?></a>
+	</li>
+	<li class="item<?php echo Minz_Request::actionName () == 'idle' ? ' active' : ''; ?>">
+		<a href="<?php echo _url ('stats', 'idle'); ?>"><?php echo Minz_Translate::t ('stats_idle'); ?></a>
+	</li>
+</ul>

+ 51 - 50
app/layout/header.phtml

@@ -2,20 +2,20 @@
 if (Minz_Configuration::canLogIn()) {
 	?><ul class="nav nav-head nav-login"><?php
 	switch (Minz_Configuration::authType()) {
-		case 'form':
-			if ($this->loginOk) {
-				?><li class="item"><?php echo FreshRSS_Themes::icon('logout'); ?> <a class="signout" href="<?php echo _url ('index', 'formLogout'); ?>"><?php echo Minz_Translate::t ('logout'); ?></a></li><?php
-			} else {
-				?><li class="item"><?php echo FreshRSS_Themes::icon('login'); ?> <a class="signin" href="<?php echo _url ('index', 'formLogin'); ?>"><?php echo Minz_Translate::t ('login'); ?></a></li><?php
-			}
-			break;
-		case 'persona':
-			if ($this->loginOk) {
-				?><li class="item"><?php echo FreshRSS_Themes::icon('logout'); ?> <a class="signout" href="#"><?php echo Minz_Translate::t ('logout'); ?></a></li><?php
-			} else {
-				?><li class="item"><?php echo FreshRSS_Themes::icon('login'); ?> <a class="signin" href="#"><?php echo Minz_Translate::t ('login'); ?></a></li><?php
-			}
-			break;
+	case 'form':
+		if ($this->loginOk) {
+			?><li class="item"><?php echo _i('logout'); ?> <a class="signout" href="<?php echo _url('index', 'formLogout'); ?>"><?php echo _t('logout'); ?></a></li><?php
+		} else {
+			?><li class="item"><?php echo _i('login'); ?> <a class="signin" href="<?php echo _url('index', 'formLogin'); ?>"><?php echo _t('login'); ?></a></li><?php
+		}
+		break;
+	case 'persona':
+		if ($this->loginOk) {
+			?><li class="item"><?php echo _i('logout'); ?> <a class="signout" href="#"><?php echo _t('logout'); ?></a></li><?php
+		} else {
+			?><li class="item"><?php echo _i('login'); ?> <a class="signin" href="#"><?php echo _t('login'); ?></a></li><?php
+		}
+		break;
 	}
 	?></ul><?php
 }
@@ -24,36 +24,36 @@ if (Minz_Configuration::canLogIn()) {
 <div class="header">
 	<div class="item title">
 		<h1>
-			<a href="<?php echo _url ('index', 'index'); ?>">
-				<img class="logo" src="<?php echo Minz_Url::display ('/themes/icons/icon.svg'); ?>" alt="⊚" />
-				<?php echo Minz_Configuration::title (); ?>
+			<a href="<?php echo _url('index', 'index'); ?>">
+				<img class="logo" src="<?php echo _i('icon', true); ?>" alt="⊚" />
+				<?php echo Minz_Configuration::title(); ?>
 			</a>
 		</h1>
 	</div>
 
 	<div class="item search">
 		<?php if ($this->loginOk || Minz_Configuration::allowAnonymous()) { ?>
-		<form action="<?php echo _url ('index', 'index'); ?>" method="get">
+		<form action="<?php echo _url('index', 'index'); ?>" method="get">
 			<div class="stick">
-				<?php $search = Minz_Request::param ('search', ''); ?>
-				<input type="search" name="search" id="search" class="extend" value="<?php echo $search; ?>" placeholder="<?php echo Minz_Translate::t ('search'); ?>" />
+				<?php $search = Minz_Request::param('search', ''); ?>
+				<input type="search" name="search" id="search" class="extend" value="<?php echo $search; ?>" placeholder="<?php echo _t('search'); ?>" />
 
-				<?php $get = Minz_Request::param ('get', ''); ?>
-				<?php if($get != '') { ?>
+				<?php $get = Minz_Request::param('get', ''); ?>
+				<?php if ($get != '') { ?>
 				<input type="hidden" name="get" value="<?php echo $get; ?>" />
 				<?php } ?>
 
-				<?php $order = Minz_Request::param ('order', ''); ?>
-				<?php if($order != '') { ?>
+				<?php $order = Minz_Request::param('order', ''); ?>
+				<?php if ($order != '') { ?>
 				<input type="hidden" name="order" value="<?php echo $order; ?>" />
 				<?php } ?>
 
-				<?php $state = Minz_Request::param ('state', ''); ?>
-				<?php if($state != '') { ?>
+				<?php $state = Minz_Request::param('state', ''); ?>
+				<?php if ($state != '') { ?>
 				<input type="hidden" name="state" value="<?php echo $state; ?>" />
 				<?php } ?>
 
-				<button class="btn" type="submit"><?php echo FreshRSS_Themes::icon('search'); ?></button>
+				<button class="btn" type="submit"><?php echo _i('search'); ?></button>
 			</div>
 		</form>
 		<?php } ?>
@@ -63,31 +63,32 @@ if (Minz_Configuration::canLogIn()) {
 	<div class="item configure">
 		<div class="dropdown">
 			<div id="dropdown-configure" class="dropdown-target"></div>
-			<a class="btn dropdown-toggle" href="#dropdown-configure"><?php echo FreshRSS_Themes::icon('configure'); ?></a>
+			<a class="btn dropdown-toggle" href="#dropdown-configure"><?php echo _i('configure'); ?></a>
 			<ul class="dropdown-menu">
 				<li class="dropdown-close"><a href="#close">❌</a></li>
-				<li class="dropdown-header"><?php echo Minz_Translate::t ('configuration'); ?></li>
-				<li class="item"><a href="<?php echo _url ('configure', 'display'); ?>"><?php echo Minz_Translate::t ('display_configuration'); ?></a></li>
-				<li class="item"><a href="<?php echo _url ('configure', 'reading'); ?>"><?php echo Minz_Translate::t ('reading_configuration'); ?></a></li>
-				<li class="item"><a href="<?php echo _url ('configure', 'archiving'); ?>"><?php echo Minz_Translate::t ('archiving_configuration'); ?></a></li>
-				<li class="item"><a href="<?php echo _url ('configure', 'sharing'); ?>"><?php echo Minz_Translate::t ('sharing'); ?></a></li>
-				<li class="item"><a href="<?php echo _url ('configure', 'shortcut'); ?>"><?php echo Minz_Translate::t ('shortcuts'); ?></a></li>
+				<li class="dropdown-header"><?php echo _t('configuration'); ?></li>
+				<li class="item"><a href="<?php echo _url('configure', 'display'); ?>"><?php echo _t('display_configuration'); ?></a></li>
+				<li class="item"><a href="<?php echo _url('configure', 'reading'); ?>"><?php echo _t('reading_configuration'); ?></a></li>
+				<li class="item"><a href="<?php echo _url('configure', 'archiving'); ?>"><?php echo _t('archiving_configuration'); ?></a></li>
+				<li class="item"><a href="<?php echo _url('configure', 'sharing'); ?>"><?php echo _t('sharing'); ?></a></li>
+				<li class="item"><a href="<?php echo _url('configure', 'shortcut'); ?>"><?php echo _t('shortcuts'); ?></a></li>
+				<li class="item"><a href="<?php echo _url('configure', 'queries'); ?>"><?php echo _t('queries'); ?></a></li>
 				<li class="separator"></li>
-				<li class="item"><a href="<?php echo _url ('configure', 'users'); ?>"><?php echo Minz_Translate::t ('users'); ?></a></li>
+				<li class="item"><a href="<?php echo _url('configure', 'users'); ?>"><?php echo _t('users'); ?></a></li>
 				<li class="separator"></li>
-				<li class="item"><a href="<?php echo _url ('index', 'stats'); ?>"><?php echo Minz_Translate::t ('stats'); ?></a></li>
-				<li class="item"><a href="<?php echo _url ('index', 'logs'); ?>"><?php echo Minz_Translate::t ('logs'); ?></a></li>
-				<li class="item"><a href="<?php echo _url ('index', 'about'); ?>"><?php echo Minz_Translate::t ('about'); ?></a></li>
+				<li class="item"><a href="<?php echo _url('stats', 'index'); ?>"><?php echo _t('stats'); ?></a></li>
+				<li class="item"><a href="<?php echo _url('index', 'logs'); ?>"><?php echo _t('logs'); ?></a></li>
+				<li class="item"><a href="<?php echo _url('index', 'about'); ?>"><?php echo _t('about'); ?></a></li>
 				<?php
 				if (Minz_Configuration::canLogIn()) {
 					?><li class="separator"></li><?php
 					switch (Minz_Configuration::authType()) {
-						case 'form':
-							?><li class="item"><a class="signout" href="<?php echo _url ('index', 'formLogout'); ?>"><?php echo FreshRSS_Themes::icon('logout'), ' ', Minz_Translate::t ('logout'); ?></a></li><?php
-							break;
-						case 'persona':
-							?><li class="item"><a class="signout" href="#"><?php echo FreshRSS_Themes::icon('logout'), ' ', Minz_Translate::t ('logout'); ?></a></li><?php
-							break;
+					case 'form':
+						?><li class="item"><a class="signout" href="<?php echo _url('index', 'formLogout'); ?>"><?php echo _i('logout'), ' ', _t('logout'); ?></a></li><?php
+						break;
+					case 'persona':
+						?><li class="item"><a class="signout" href="#"><?php echo _i('logout'), ' ', _t('logout'); ?></a></li><?php
+						break;
 					}
 				} ?>
 			</ul>
@@ -96,12 +97,12 @@ if (Minz_Configuration::canLogIn()) {
 	<?php } elseif (Minz_Configuration::canLogIn()) {
 		?><div class="item configure"><?php
 		switch (Minz_Configuration::authType()) {
-			case 'form':
-				echo FreshRSS_Themes::icon('login'); ?><a class="signin" href="<?php echo _url ('index', 'formLogin'); ?>"><?php echo Minz_Translate::t ('login'); ?></a></li><?php
-				break;
-			case 'persona':
-				echo FreshRSS_Themes::icon('login'); ?><a class="signin" href="#"><?php echo Minz_Translate::t ('login'); ?></a></li><?php
-				break;
+		case 'form':
+			echo _i('login'); ?><a class="signin" href="<?php echo _url('index', 'formLogin'); ?>"><?php echo _t('login'); ?></a></li><?php
+			break;
+		case 'persona':
+			echo _i('login'); ?><a class="signin" href="#"><?php echo _t('login'); ?></a></li><?php
+			break;
 		}
 		?></div><?php
 	} ?>

+ 45 - 12
app/layout/nav_menu.phtml

@@ -7,18 +7,20 @@
 	<?php } ?>
 
 	<?php if ($this->loginOk) { ?>
-	<?php $url_state = $this->url;
-		if ($this->state & FreshRSS_Entry::STATE_READ) {
-			$url_state['params']['state'] = $this->state & ~FreshRSS_Entry::STATE_READ;
-			$checked = 'true';
-			$class = 'active';
-		} else {
-			$url_state['params']['state'] = $this->state | FreshRSS_Entry::STATE_READ;
-			$checked = 'false';
-			$class = '';
-		}
-	?>
 	<div class="stick">
+		<?php
+			$url_state = $this->url;
+
+			if ($this->state & FreshRSS_Entry::STATE_READ) {
+				$url_state['params']['state'] = $this->state & ~FreshRSS_Entry::STATE_READ;
+				$checked = 'true';
+				$class = 'active';
+			} else {
+				$url_state['params']['state'] = $this->state | FreshRSS_Entry::STATE_READ;
+				$checked = 'false';
+				$class = '';
+			}
+		?>
 		<a id="toggle-read"
 		   class="btn <?php echo $class; ?>"
 		   aria-checked="<?php echo $checked; ?>"
@@ -26,6 +28,7 @@
 		   title="<?php echo Minz_Translate::t ('show_read'); ?>">
 			<?php echo FreshRSS_Themes::icon('read'); ?>
 		</a>
+
 		<?php
 			if ($this->state & FreshRSS_Entry::STATE_NOT_READ) {
 				$url_state['params']['state'] = $this->state & ~FreshRSS_Entry::STATE_NOT_READ;
@@ -44,6 +47,7 @@
 		   title="<?php echo Minz_Translate::t ('show_not_reads'); ?>">
 			<?php echo FreshRSS_Themes::icon('unread'); ?>
 		</a>
+
 		<?php
 			if ($this->state & FreshRSS_Entry::STATE_FAVORITE) {
 				$url_state['params']['state'] = $this->state & ~FreshRSS_Entry::STATE_FAVORITE;
@@ -62,6 +66,7 @@
 		   title="<?php echo Minz_Translate::t ('show_favorite'); ?>">
 			<?php echo FreshRSS_Themes::icon('starred'); ?>
 		</a>
+
 		<?php
 			if ($this->state & FreshRSS_Entry::STATE_NOT_FAVORITE) {
 				$url_state['params']['state'] = $this->state & ~FreshRSS_Entry::STATE_NOT_FAVORITE;
@@ -80,6 +85,34 @@
 		   title="<?php echo Minz_Translate::t ('show_not_favorite'); ?>">
 			<?php echo FreshRSS_Themes::icon('non-starred'); ?>
 		</a>
+
+		<div class="dropdown">
+			<div id="dropdown-query" class="dropdown-target"></div>
+
+			<a class="dropdown-toggle btn" href="#dropdown-query"><?php echo FreshRSS_Themes::icon('down'); ?></a>
+			<ul class="dropdown-menu">
+				<li class="dropdown-close"><a href="#close">❌</a></li>
+
+				<li class="dropdown-header"><?php echo Minz_Translate::t('queries'); ?> <a class="no-mobile" href="<?php echo _url('configure', 'queries'); ?>"><?php echo FreshRSS_Themes::icon('configure'); ?></a></li>
+
+				<?php foreach ($this->conf->queries as $query) { ?>
+				<li class="item">
+					<a href="<?php echo $query['url']; ?>"><?php echo $query['name']; ?></a>
+				</li>
+				<?php } ?>
+
+				<?php if (count($this->conf->queries) > 0) { ?>
+				<li class="separator no-mobile"></li>
+				<?php } ?>
+
+				<?php
+					$url_query = $this->url;
+					$url_query['c'] = 'configure';
+					$url_query['a'] = 'addQuery';
+				?>
+				<li class="item no-mobile"><a href="<?php echo Minz_Url::display($url_query); ?>"><?php echo FreshRSS_Themes::icon('bookmark-add'); ?> <?php echo Minz_Translate::t('add_query'); ?></a></li>
+			</ul>
+		</div>
 	</div>
 	<?php
 		$get = false;
@@ -148,7 +181,7 @@
 	?>
 
 	<div class="stick" id="nav_menu_read_all">
-		<a class="read_all btn" href="<?php echo $markReadUrl; ?>"><?php echo Minz_Translate::t ('mark_read'); ?></a>
+		<a class="read_all btn<?php if ($this->conf->reading_confirm) {echo ' confirm';} ?>" href="<?php echo $markReadUrl; ?>"><?php echo Minz_Translate::t ('mark_read'); ?></a>
 		<div class="dropdown">
 			<div id="dropdown-read" class="dropdown-target"></div>
 

+ 21 - 0
app/views/configure/archiving.phtml

@@ -24,6 +24,27 @@
 				?></select> (<?php echo Minz_Translate::t('by_default'); ?>)
 			</div>
 		</div>
+		<div class="form-group">
+			<label class="group-name" for="ttl_default"><?php echo Minz_Translate::t('ttl'); ?></label>
+			<div class="group-controls">
+				<select class="number" name="ttl_default" id="ttl_default" required="required"><?php
+					$found = false;
+					foreach (array(1200 => '20min', 1500 => '25min', 1800 => '30min', 2700 => '45min',
+					                3600 => '1h', 5400 => '1.5h', 7200 => '2h', 10800 => '3h', 14400 => '4h', 18800 => '5h', 21600 => '6h', 25200 => '7h', 28800 => '8h',
+					                36000 => '10h', 43200 => '12h', 64800 => '18h',
+					                86400 => '1d', 129600 => '1.5d', 172800 => '2d', 259200 => '3d', 345600 => '4d', 432000 => '5d', 518400 => '6d',
+					                604800 => '1wk', -1 => '∞') as $v => $t) {
+						echo '<option value="' . $v . ($this->conf->ttl_default == $v ? '" selected="selected' : '') . '">' . $t . '</option>';
+						if ($this->conf->ttl_default == $v) {
+							$found = true;
+						}
+					}
+					if (!$found) {
+						echo '<option value="' . intval($this->conf->ttl_default) . '" selected="selected">' . intval($this->conf->ttl_default) . 's</option>';
+					}
+				?></select> (<?php echo Minz_Translate::t('by_default'); ?>)
+			</div>
+		</div>
 
 		<div class="form-group form-actions">
 			<div class="group-controls">

+ 21 - 0
app/views/configure/feed.phtml

@@ -103,6 +103,27 @@
 				?></select>
 			</div>
 		</div>
+		<div class="form-group">
+			<label class="group-name" for="ttl"><?php echo Minz_Translate::t('ttl'); ?></label>
+			<div class="group-controls">
+				<select class="number" name="ttl" id="ttl" required="required"><?php
+					$found = false;
+					foreach (array(-2 => Minz_Translate::t('by_default'), 900 => '15min', 1200 => '20min', 1500 => '25min', 1800 => '30min', 2700 => '45min',
+					                3600 => '1h', 5400 => '1.5h', 7200 => '2h', 10800 => '3h', 14400 => '4h', 18800 => '5h', 21600 => '6h', 25200 => '7h', 28800 => '8h',
+					                36000 => '10h', 43200 => '12h', 64800 => '18h',
+					                86400 => '1d', 129600 => '1.5d', 172800 => '2d', 259200 => '3d', 345600 => '4d', 432000 => '5d', 518400 => '6d',
+					                604800 => '1wk', 1209600 => '2wk', 1814400 => '3wk', 2419200 => '4wk', 2629744 => '1mo', -1 => '∞') as $v => $t) {
+						echo '<option value="' . $v . ($this->flux->ttl() === $v ? '" selected="selected' : '') . '">' . $t . '</option>';
+						if ($this->flux->ttl() == $v) {
+							$found = true;
+						}
+					}
+					if (!$found) {
+						echo '<option value="' . intval($this->flux->ttl()) . '" selected="selected">' . intval($this->flux->ttl()) . 's</option>';
+					}
+				?></select>
+			</div>
+		</div>
 		<div class="form-group form-actions">
 			<div class="group-controls">
 				<button class="btn btn-important"><?php echo Minz_Translate::t ('save'); ?></button>

+ 90 - 0
app/views/configure/queries.phtml

@@ -0,0 +1,90 @@
+<?php $this->partial('aside_configure'); ?>
+
+<div class="post">
+	<a href="<?php echo _url('index', 'index'); ?>"><?php echo _t('back_to_rss_feeds'); ?></a>
+
+	<form method="post" action="<?php echo _url('configure', 'queries'); ?>">
+		<legend><?php echo _t('queries'); ?></legend>
+
+		<?php foreach ($this->conf->queries as $key => $query) { ?>
+		<div class="form-group" id="query-group-<?php echo $key; ?>">
+			<label class="group-name" for="queries_<?php echo $key; ?>_name">
+				<?php echo _t('query_number', $key + 1); ?>
+			</label>
+
+			<div class="group-controls">
+				<input type="hidden" id="queries_<?php echo $key; ?>_search" name="queries[<?php echo $key; ?>][search]" value="<?php echo isset($query['search']) ? $query['search'] : ""; ?>"/>
+				<input type="hidden" id="queries_<?php echo $key; ?>_state" name="queries[<?php echo $key; ?>][state]" value="<?php echo isset($query['state']) ? $query['state'] : ""; ?>"/>
+				<input type="hidden" id="queries_<?php echo $key; ?>_order" name="queries[<?php echo $key; ?>][order]" value="<?php echo isset($query['order']) ? $query['order'] : ""; ?>"/>
+				<input type="hidden" id="queries_<?php echo $key; ?>_get" name="queries[<?php echo $key; ?>][get]" value="<?php echo isset($query['get']) ? $query['get'] : ""; ?>"/>
+
+				<div class="stick">
+					<input class="extend"
+					       type="text"
+					       id="queries_<?php echo $key; ?>_name"
+					       name="queries[<?php echo $key; ?>][name]"
+					       value="<?php echo $query['name']; ?>"
+					/>
+
+					<a class="btn" href="<?php echo $query['url']; ?>">
+						<?php echo _i('link'); ?>
+					</a>
+
+					<a class="btn btn-attention remove" href="#" data-remove="query-group-<?php echo $key; ?>">
+						<?php echo _i('close'); ?>
+					</a>
+				</div>
+
+				<?php
+					$exist = (isset($query['search']) ? 1 : 0)
+						   + (isset($query['state']) ? 1 : 0)
+						   + (isset($query['order']) ? 1 : 0)
+						   + (isset($query['get']) ? 1 : 0);
+					// If the only filter is "all" articles, we consider there is no filter
+					$exist = ($exist === 1 && isset($query['get']) && $query['get'] === 'a') ? 0 : $exist;
+				?>
+
+				<?php if ($exist === 0) { ?>
+				<div class="alert alert-warn">
+					<div class="alert-head"><?php echo _t('no_query_filter'); ?></div>
+				</div>
+				<?php } else { ?>
+				<div class="alert alert-success">
+					<div class="alert-head"><?php echo _t('query_filter'); ?></div>
+
+					<ul>
+						<?php if (isset($query['search'])) { $exist = true; ?>
+						<li class="item"><?php echo _t('query_search', $query['search']); ?></li>
+						<?php } ?>
+
+						<?php if (isset($query['state'])) { $exist = true; ?>
+						<li class="item"><?php echo _t('query_state_' . $query['state']); ?></li>
+						<?php } ?>
+
+						<?php if (isset($query['order'])) { $exist = true; ?>
+						<li class="item"><?php echo _t('query_order_' . strtolower($query['order'])); ?></li>
+						<?php } ?>
+
+						<?php if (isset($query['get'])) { $exist = true; ?>
+						<li class="item"><?php echo _t('query_get_' . $this->query_get[$key]['type'], $this->query_get[$key]['name']); ?></li>
+						<?php } ?>
+					</ul>
+				</div>
+				<?php } ?>
+			</div>
+		</div>
+		<?php } ?>
+
+		<?php if (count($this->conf->queries) > 0) { ?>
+		<div class="form-group form-actions">
+			<div class="group-controls">
+				<button type="submit" class="btn btn-important"><?php echo _t('save'); ?></button>
+				<button type="reset" class="btn"><?php echo _t('cancel'); ?></button>
+			</div>
+		</div>
+		<?php } else { ?>
+		<p class="alert alert-warn"><span class="alert-head"><?php echo _t('damn'); ?></span> <?php echo _t('no_query'); ?></p>
+		<?php } ?>
+	</form>
+
+</div>

+ 11 - 1
app/views/configure/reading.phtml

@@ -82,12 +82,22 @@
 			</div>
 		</div>
 
+		<div class="form-group">
+			<div class="group-controls">
+				<label class="checkbox" for="reading_confirm">
+					<input type="checkbox" name="reading_confirm" id="reading_confirm" value="1"<?php echo $this->conf->reading_confirm ? ' checked="checked"' : ''; ?> />
+					<?php echo Minz_Translate::t ('reading_confirm'); ?>
+					<noscript> — <strong><?php echo Minz_Translate::t ('javascript_should_be_activated'); ?></strong></noscript>
+				</label>
+			</div>
+		</div>
+
 		<div class="form-group">
 			<label class="group-name"><?php echo Minz_Translate::t ('auto_read_when'); ?></label>
 			<div class="group-controls">
 				<label class="checkbox" for="check_open_article">
 					<input type="checkbox" name="mark_open_article" id="check_open_article" value="1"<?php echo $this->conf->mark_when['article'] ? ' checked="checked"' : ''; ?> />
-					<?php echo Minz_Translate::t ('article_selected'); ?>
+					<?php echo Minz_Translate::t('article_viewed'); ?>
 				</label>
 				<label class="checkbox" for="check_open_site">
 					<input type="checkbox" name="mark_open_site" id="check_open_site" value="1"<?php echo $this->conf->mark_when['site'] ? ' checked="checked"' : ''; ?> />

+ 7 - 7
app/views/configure/sharing.phtml

@@ -4,35 +4,35 @@
 	<a href="<?php echo _url ('index', 'index'); ?>"><?php echo Minz_Translate::t ('back_to_rss_feeds'); ?></a>
 
 	<form method="post" action="<?php echo _url ('configure', 'sharing'); ?>"
-		data-simple='<div class="form-group"><label class="group-name">##label##</label><div class="group-controls"><a href="#" class="share remove btn btn-attention"><?php echo FreshRSS_Themes::icon('close'); ?></a>
+		data-simple='<div class="form-group" id="group-share-##key##"><label class="group-name">##label##</label><div class="group-controls"><a href="#" class="remove btn btn-attention" data-remove="group-share-##key##"><?php echo FreshRSS_Themes::icon('close'); ?></a>
 			<input type="hidden" id="share_##key##_type" name="share[##key##][type]" value="##type##" /></div></div>'
-		data-advanced='<div class="form-group"><label class="group-name">##label##</label><div class="group-controls">
+		data-advanced='<div class="form-group" id="group-share-##key##"><label class="group-name">##label##</label><div class="group-controls">
 			<input type="hidden" id="share_##key##_type" name="share[##key##][type]" value="##type##" />
 			<div class="stick">
 			<input type="text" id="share_##key##_name" name="share[##key##][name]" class="extend" value="" placeholder="<?php echo Minz_Translate::t ('share_name'); ?>" size="64" />
 			<input type="url" id="share_##key##_url" name="share[##key##][url]" class="extend" value="" placeholder="<?php echo Minz_Translate::t ('share_url'); ?>" size="64" />
-			<a href="#" class="share remove btn btn-attention"><?php echo FreshRSS_Themes::icon('close'); ?></a></div>
+			<a href="#" class="remove btn btn-attention" data-remove="group-share-##key##"><?php echo FreshRSS_Themes::icon('close'); ?></a></div>
 			<a target="_blank" class="btn" title="<?php echo Minz_Translate::t('more_information'); ?>" href="##help##"><?php echo FreshRSS_Themes::icon('help'); ?></a>
 			</div></div>'>
 		<legend><?php echo Minz_Translate::t ('sharing'); ?></legend>
 		<?php foreach ($this->conf->sharing as $key => $sharing): ?>
 			<?php $share = $this->conf->shares[$sharing['type']]; ?>
-			<div class="form-group">
+			<div class="form-group" id="group-share-<?php echo $key; ?>">
 				<label class="group-name">
 					<?php echo Minz_Translate::t ($sharing['type']); ?>
 				</label>
 				<div class="group-controls">
 					<input type='hidden' id='share_<?php echo $key;?>_type' name="share[<?php echo $key;?>][type]" value='<?php echo $sharing['type']?>' />
-					<?php if ($share['form'] === 'advanced'){ ?>
+					<?php if ($share['form'] === 'advanced') { ?>
 						<div class="stick">
 							<input type="text" id="share_<?php echo $key;?>_name" name="share[<?php echo $key;?>][name]" class="extend" value="<?php echo $sharing['name']?>" placeholder="<?php echo Minz_Translate::t ('share_name'); ?>" size="64" />
 							<input type="url" id="share_<?php echo $key;?>_url" name="share[<?php echo $key;?>][url]" class="extend" value="<?php echo $sharing['url']?>" placeholder="<?php echo Minz_Translate::t ('share_url'); ?>" size="64" />
-							<a href='#' class='share remove btn btn-attention'><?php echo FreshRSS_Themes::icon('close'); ?></a>
+							<a href='#' class='remove btn btn-attention' data-remove="group-share-<?php echo $key; ?>"><?php echo FreshRSS_Themes::icon('close'); ?></a>
 						</div>
 
 						<a target="_blank" class="btn" title="<?php echo Minz_Translate::t('more_information'); ?>" href="<?php echo $share['help']?>"><?php echo FreshRSS_Themes::icon('help'); ?></a>
 					<?php } else { ?>
-					<a href='#' class='share remove btn btn-attention'><?php echo FreshRSS_Themes::icon('close'); ?></a>
+					<a href='#' class='remove btn btn-attention' data-remove="group-share-<?php echo $key; ?>"><?php echo FreshRSS_Themes::icon('close'); ?></a>
 					<?php } ?>
 				</div>
 			</div>

+ 1 - 1
app/views/helpers/pagination.phtml

@@ -12,7 +12,7 @@
 	<?php $params['next'] = $this->nextId; ?>
 	<a id="load_more" href="<?php echo Minz_Url::display (array ('c' => $c, 'a' => $a, 'params' => $params)); ?>"><?php echo Minz_Translate::t ('load_more'); ?></a>
 	<?php } elseif ($markReadUrl) { ?>
-	<a id="bigMarkAsRead" href="<?php echo $markReadUrl; ?>">
+	<a id="bigMarkAsRead" href="<?php echo $markReadUrl; ?>"<?php if ($this->conf->reading_confirm) { echo ' class="confirm"';} ?>>
 		<?php echo Minz_Translate::t ('nothing_to_load'); ?><br />
 		<span class="bigTick">✔</span><br />
 		<?php echo Minz_Translate::t ('mark_all_read'); ?>

+ 4 - 2
app/views/javascript/actualize.phtml

@@ -10,7 +10,7 @@ var feeds = [<?php
 function initProgressBar(init) {
 	if (init) {
 		$("body").after("\<div id=\"actualizeProgress\" class=\"notification good\">\
-			<?php echo Minz_Translate::t ('refresh'); ?> <span class=\"progress\">0 / " + feed_count + "</span><br />\
+			<?php echo _t('refresh'); ?> <span class=\"progress\">0 / " + feed_count + "</span><br />\
 			<progress id=\"actualizeProgressBar\" value=\"0\" max=\"" + feed_count + "\"></progress>\
 		</div>");
 	} else {
@@ -24,7 +24,8 @@ function updateProgressBar(i) {
 
 function updateFeeds() {
 	if (feed_count === 0) {
-		openNotification("<?php echo Minz_Translate::t ('no_feed_to_refresh'); ?>", "good");
+		openNotification("<?php echo _t('no_feed_to_refresh'); ?>", "good");
+		ajax_loading = false;
 		return;
 	}
 	initProgressBar(true);
@@ -39,6 +40,7 @@ function updateFeed() {
 	if (feed == undefined) {
 		return;
 	}
+
 	$.ajax({
 		type: 'POST',
 		url: feed,

+ 19 - 0
app/views/stats/idle.phtml

@@ -0,0 +1,19 @@
+<?php $this->partial('aside_stats'); ?>
+
+<div class="post content">
+	<a href="<?php echo _url ('index', 'index'); ?>"><?php echo _t ('back_to_rss_feeds'); ?></a>
+
+	<h1><?php echo _t ('stats_idle'); ?></h1>
+
+	<?php foreach ($this->idleFeeds as $period => $feeds){ ?>
+		<div class="stat">
+			<h2><?php echo _t ($period); ?></h2>
+
+			<ul>
+				<?php foreach ($feeds as $feed){ ?>
+					<li><?php echo $feed; ?></li>
+				<?php } ?>
+			</ul>
+		</div>
+	<?php } ?>
+</div>

+ 127 - 0
app/views/stats/index.phtml

@@ -0,0 +1,127 @@
+<?php $this->partial('aside_stats'); ?>
+
+<div class="post content">
+	<a href="<?php echo _url ('index', 'index'); ?>"><?php echo _t ('back_to_rss_feeds'); ?></a>
+	
+	<h1><?php echo _t ('stats_main'); ?></h1>
+
+	<div class="stat">
+		<h2><?php echo _t ('stats_entry_repartition'); ?></h2>
+		<table>
+			<thead>
+				<tr>
+					<th> </th>
+					<th><?php echo _t ('main_stream'); ?></th>
+					<th><?php echo _t ('all_feeds'); ?></th>
+				</tr>
+			</thead>
+			<tbody>
+				<tr>
+					<th><?php echo _t ('status_total'); ?></th>
+					<td class="numeric"><?php echo formatNumber($this->repartition['main_stream']['total']); ?></td>
+					<td class="numeric"><?php echo formatNumber($this->repartition['all_feeds']['total']); ?></td>
+				</tr>
+				<tr>
+					<th><?php echo _t ('status_read'); ?></th>
+					<td class="numeric"><?php echo formatNumber($this->repartition['main_stream']['read']); ?></td>
+					<td class="numeric"><?php echo formatNumber($this->repartition['all_feeds']['read']); ?></td>
+				</tr>
+				<tr>
+					<th><?php echo _t ('status_unread'); ?></th>
+					<td class="numeric"><?php echo formatNumber($this->repartition['main_stream']['unread']); ?></td>
+					<td class="numeric"><?php echo formatNumber($this->repartition['all_feeds']['unread']); ?></td>
+				</tr>
+				<tr>
+					<th><?php echo _t ('status_favorites'); ?></th>
+					<td class="numeric"><?php echo formatNumber($this->repartition['main_stream']['favorite']); ?></td>
+					<td class="numeric"><?php echo formatNumber($this->repartition['all_feeds']['favorite']); ?></td>
+				</tr>
+			</tbody>
+		</table>
+	</div>
+	
+	<div class="stat">
+		<h2><?php echo _t ('stats_entry_per_day'); ?></h2>
+		<div id="statsEntryPerDay" style="height: 300px"></div>
+	</div>
+	
+	<div class="stat">
+		<h2><?php echo _t ('stats_feed_per_category'); ?></h2>
+		<div id="statsFeedPerCategory" style="height: 300px"></div>
+		<div id="statsFeedPerCategoryLegend"></div>
+	</div>
+	
+	<div class="stat">
+		<h2><?php echo _t ('stats_entry_per_category'); ?></h2>
+		<div id="statsEntryPerCategory" style="height: 300px"></div>
+		<div id="statsEntryPerCategoryLegend"></div>
+	</div>
+	
+	<div class="stat">
+		<h2><?php echo _t ('stats_top_feed'); ?></h2>
+		<table>
+			<thead>
+				<tr>
+					<th><?php echo _t ('feed'); ?></th>
+					<th><?php echo _t ('category'); ?></th>
+					<th><?php echo _t ('stats_entry_count'); ?></th>
+				</tr>
+			</thead>
+			<tbody>
+				<?php foreach ($this->topFeed as $feed): ?>
+					<tr>
+						<td><?php echo $feed['name']; ?></td>
+						<td><?php echo $feed['category']; ?></td>
+						<td class="numeric"><?php echo formatNumber($feed['count']); ?></td>
+					</tr>
+				<?php endforeach;?>
+			</tbody>
+		</table>
+	</div>
+</div>
+
+<script>
+"use strict";
+function initStats() {
+	if (!window.Flotr) {
+		if (window.console) {
+			console.log('FreshRSS waiting for Flotr…');
+		}
+		window.setTimeout(initStats, 50);
+		return;
+	}
+	// Entry per day
+	Flotr.draw(document.getElementById('statsEntryPerDay'),
+		[<?php echo $this->count ?>],
+		{
+			grid: {verticalLines: false},
+			bars: {horizontal: false, show: true},
+			xaxis: {noTicks: 6, showLabels: false, tickDecimals: 0},
+			yaxis: {min: 0},
+			mouse: {relative: true, track: true, trackDecimals: 0, trackFormatter: function(obj) {return numberFormat(obj.y);}}
+		});
+	// Feed per category
+	Flotr.draw(document.getElementById('statsFeedPerCategory'),
+		<?php echo $this->feedByCategory ?>,
+		{
+			grid: {verticalLines: false, horizontalLines: false},
+			pie: {explode: 10, show: true, labelFormatter: function(){return '';}},
+			xaxis: {showLabels: false},
+			yaxis: {showLabels: false},
+			mouse: {relative: true, track: true, trackDecimals: 0, trackFormatter: function(obj) {return obj.series.label + ' - '+ numberFormat(obj.y) + ' ('+ (obj.fraction * 100).toFixed(1) + '%)';}},
+			legend: {container: document.getElementById('statsFeedPerCategoryLegend'), noColumns: 3}
+		});
+	// Entry per category
+	Flotr.draw(document.getElementById('statsEntryPerCategory'),
+		<?php echo $this->entryByCategory ?>,
+		{
+			grid: {verticalLines: false, horizontalLines: false},
+			pie: {explode: 10, show: true, labelFormatter: function(){return '';}},
+			xaxis: {showLabels: false},
+			yaxis: {showLabels: false},
+			mouse: {relative: true, track: true, trackDecimals: 0, trackFormatter: function(obj) {return obj.series.label + ' - '+ numberFormat(obj.y) + ' ('+ (obj.fraction * 100).toFixed(1) + '%)';}},
+			legend: {container: document.getElementById('statsEntryPerCategoryLegend'), noColumns: 3}
+		});
+}
+initStats();
+</script>

+ 7 - 5
app/views/index/stats.phtml → app/views/stats/main.phtml

@@ -1,9 +1,11 @@
+<?php $this->partial('aside_stats'); ?>
+
 <div class="post content">
-	<a href="<?php echo _url ('index', 'index'); ?>"><?php echo Minz_Translate::t ('back_to_rss_feeds'); ?></a>
-	
-	<h1><?php echo Minz_Translate::t ('stats'); ?></h1>
-	
-	<div class="stat">
+        <a href="<?php echo _url ('index', 'index'); ?>"><?php echo Minz_Translate::t ('back_to_rss_feeds'); ?></a>
+        
+        <h1><?php echo Minz_Translate::t ('stats_main'); ?></h1>
+
+        <div class="stat">
 		<h2><?php echo Minz_Translate::t ('stats_entry_repartition'); ?></h2>
 		<table>
 			<thead>

+ 0 - 0
data/do-install.txt


+ 50 - 30
lib/Minz/Configuration.php

@@ -297,41 +297,61 @@ class Minz_Configuration {
 		// Base de données
 		if (isset ($ini_array['db'])) {
 			$db = $ini_array['db'];
-			if (empty($db['host'])) {
+			if (empty($db['type'])) {
 				throw new Minz_BadConfigurationException (
-					'host',
+					'type',
 					Minz_Exception::ERROR
 				);
 			}
-			if (empty($db['user'])) {
-				throw new Minz_BadConfigurationException (
-					'user',
-					Minz_Exception::ERROR
-				);
-			}
-			if (!isset ($db['password'])) {
-				throw new Minz_BadConfigurationException (
-					'password',
-					Minz_Exception::ERROR
-				);
-			}
-			if (empty($db['base'])) {
-				throw new Minz_BadConfigurationException (
-					'base',
-					Minz_Exception::ERROR
-				);
-			}
-
-			if (!empty($db['type'])) {
-				self::$db['type'] = $db['type'];
-			}
-			self::$db['host'] = $db['host'];
-			self::$db['user'] = $db['user'];
-			self::$db['password'] = $db['password'];
-			self::$db['base'] = $db['base'];
-			if (isset($db['prefix'])) {
-				self::$db['prefix'] = $db['prefix'];
+			switch ($db['type']) {
+				case 'mysql':
+					if (empty($db['host'])) {
+						throw new Minz_BadConfigurationException (
+							'host',
+							Minz_Exception::ERROR
+						);
+					}
+					if (empty($db['user'])) {
+						throw new Minz_BadConfigurationException (
+							'user',
+							Minz_Exception::ERROR
+						);
+					}
+					if (!isset($db['password'])) {
+						throw new Minz_BadConfigurationException (
+							'password',
+							Minz_Exception::ERROR
+						);
+					}
+					if (empty($db['base'])) {
+						throw new Minz_BadConfigurationException (
+							'base',
+							Minz_Exception::ERROR
+						);
+					}
+					self::$db['host'] = $db['host'];
+					self::$db['user'] = $db['user'];
+					self::$db['password'] = $db['password'];
+					self::$db['base'] = $db['base'];
+					if (isset($db['prefix'])) {
+						self::$db['prefix'] = $db['prefix'];
+					}
+					break;
+				case 'sqlite':
+					self::$db['host'] = '';
+					self::$db['user'] = '';
+					self::$db['password'] = '';
+					self::$db['base'] = '';
+					self::$db['prefix'] = '';
+					break;
+				default:
+					throw new Minz_BadConfigurationException (
+						'type',
+						Minz_Exception::ERROR
+					);
+					break;
 			}
+			self::$db['type'] = $db['type'];
 		}
 	}
 

+ 28 - 30
lib/Minz/ModelPdo.php

@@ -16,6 +16,7 @@ class Minz_ModelPdo {
 	public static $useSharedBd = true;
 	private static $sharedBd = null;
 	private static $sharedPrefix;
+	protected static $sharedDbType;
 
 	/**
 	 * $bd variable représentant la base de données
@@ -24,46 +25,57 @@ class Minz_ModelPdo {
 
 	protected $prefix;
 
+	public function dbType() {
+		return self::$sharedDbType;
+	}
+
 	/**
 	 * Créé la connexion à la base de données à l'aide des variables
 	 * HOST, BASE, USER et PASS définies dans le fichier de configuration
 	 */
-	public function __construct () {
+	public function __construct() {
 		if (self::$useSharedBd && self::$sharedBd != null) {
 			$this->bd = self::$sharedBd;
 			$this->prefix = self::$sharedPrefix;
 			return;
 		}
 
-		$db = Minz_Configuration::dataBase ();
-		$driver_options = null;
+		$db = Minz_Configuration::dataBase();
 
 		try {
 			$type = $db['type'];
-			if($type == 'mysql') {
-				$string = $type
-				        . ':host=' . $db['host']
+			if ($type === 'mysql') {
+				$string = 'mysql:host=' . $db['host']
 				        . ';dbname=' . $db['base']
 				        . ';charset=utf8';
 				$driver_options = array(
-					PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8'
+					PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
+				);
+				$this->prefix = $db['prefix'] . Minz_Session::param('currentUser', '_') . '_';
+			} elseif ($type === 'sqlite') {
+				$string = 'sqlite:' . DATA_PATH . '/' . Minz_Session::param('currentUser', '_') . '.sqlite';
+				$driver_options = array(
+					//PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
+				);
+				$this->prefix = '';
+			} else {
+				throw new Minz_PDOConnectionException(
+					'Invalid database type!',
+					$db['user'], Minz_Exception::ERROR
 				);
-			} elseif($type == 'sqlite') {
-				$string = $type . ':/' . DATA_PATH . $db['base'] . '.sqlite';	//TODO: DEBUG UTF-8 http://www.siteduzero.com/forum/sujet/sqlite-connexion-utf-8-18797
 			}
+			self::$sharedDbType = $type;
+			self::$sharedPrefix = $this->prefix;
 
-			$this->bd = new FreshPDO (
+			$this->bd = new FreshPDO(
 				$string,
 				$db['user'],
 				$db['password'],
 				$driver_options
 			);
 			self::$sharedBd = $this->bd;
-
-			$this->prefix = $db['prefix'] . Minz_Session::param('currentUser', '_') . '_';
-			self::$sharedPrefix = $this->prefix;
 		} catch (Exception $e) {
-			throw new Minz_PDOConnectionException (
+			throw new Minz_PDOConnectionException(
 				$string,
 				$db['user'], Minz_Exception::ERROR
 			);
@@ -80,20 +92,6 @@ class Minz_ModelPdo {
 		$this->bd->rollBack();
 	}
 
-	public function size($all = false) {
-		$db = Minz_Configuration::dataBase ();
-		$sql = 'SELECT SUM(data_length + index_length) FROM information_schema.TABLES WHERE table_schema = ?';
-		$values = array ($db['base']);
-		if (!$all) {
-			$sql .= ' AND table_name LIKE ?';
-			$values[] = $this->prefix . '%';
-		}
-		$stm = $this->bd->prepare ($sql);
-		$stm->execute ($values);
-		$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
-		return $res[0];
-	}
-
 	public static function clean() {
 		self::$sharedBd = null;
 		self::$sharedPrefix = '';
@@ -107,12 +105,12 @@ class FreshPDO extends PDO {
 		}
 	}
 
-	public function prepare ($statement, $driver_options = array()) {
+	public function prepare($statement, $driver_options = array()) {
 		FreshPDO::check($statement);
 		return parent::prepare($statement, $driver_options);
 	}
 
-	public function exec ($statement) {
+	public function exec($statement) {
 		FreshPDO::check($statement);
 		return parent::exec($statement);
 	}

+ 3 - 2
lib/Minz/Request.php

@@ -28,6 +28,9 @@ class Minz_Request {
 		return self::$params;
 	}
 	static function htmlspecialchars_utf8 ($p) {
+		if (is_array($p)) {
+			return array_map('self::htmlspecialchars_utf8', $p);
+		}
 		return htmlspecialchars($p, ENT_COMPAT, 'UTF-8');
 	}
 	public static function param ($key, $default = false, $specialchars = false) {
@@ -35,8 +38,6 @@ class Minz_Request {
 			$p = self::$params[$key];
 			if(is_object($p) || $specialchars) {
 				return $p;
-			} elseif(is_array($p)) {
-				return array_map('self::htmlspecialchars_utf8', $p);
 			} else {
 				return self::htmlspecialchars_utf8($p);
 			}

+ 21 - 13
lib/Minz/Translate.php

@@ -18,28 +18,28 @@ class Minz_Translate {
 	 * $translates est le tableau de correspondance
 	 * 	$key => $traduction
 	 */
-	private static $translates = array ();
+	private static $translates = array();
 	
 	/**
 	 * Inclus le fichier de langue qui va bien
 	 * l'enregistre dans $translates
 	 */
-	public static function init () {
-		$l = Minz_Configuration::language ();
-		self::$language = Minz_Session::param ('language', $l);
+	public static function init() {
+		$l = Minz_Configuration::language();
+		self::$language = Minz_Session::param('language', $l);
 		
 		$l_path = APP_PATH . '/i18n/' . self::$language . '.php';
 		
-		if (file_exists ($l_path)) {
-			self::$translates = include ($l_path);
+		if (file_exists($l_path)) {
+			self::$translates = include($l_path);
 		}
 	}
 	
 	/**
 	 * Alias de init
 	 */
-	public static function reset () {
-		self::init ();
+	public static function reset() {
+		self::init();
 	}
 	
 	/**
@@ -48,24 +48,32 @@ class Minz_Translate {
 	 * @return la valeur correspondante à la clé
 	 *       > si non présente dans le tableau, on retourne la clé elle-même
 	 */ 
-	public static function t ($key) {
+	public static function t($key) {
 		$translate = $key;
 		
-		if (isset (self::$translates[$key])) {
+		if (isset(self::$translates[$key])) {
 			$translate = self::$translates[$key];
 		}
 
-		$args = func_get_args ();
+		$args = func_get_args();
 		unset($args[0]);
 		
-		return vsprintf ($translate, $args);
+		return vsprintf($translate, $args);
 	}
 	
 	/**
 	 * Retourne la langue utilisée actuellement
 	 * @return la langue
 	 */
-	public static function language () {
+	public static function language() {
 		return self::$language;
 	}
 }
+
+function _t($key) {
+	$args = func_get_args();
+	unset($args[0]);
+	array_unshift($args, $key);
+
+	return call_user_func_array("Minz_Translate::t", $args);
+}

+ 30 - 8
lib/SimplePie/SimplePie.php

@@ -445,6 +445,13 @@ class SimplePie
 	 */
 	public $feed_url;
 
+	/**
+	 * @var string Original feed URL, or new feed URL iff HTTP 301 Moved Permanently
+	 * @see SimplePie::subscribe_url()
+	 * @access private
+	 */
+	public $permanent_url = null;	//FreshRSS
+
 	/**
 	 * @var object Instance of SimplePie_File to use as a feed
 	 * @see SimplePie::set_file()
@@ -735,6 +742,7 @@ class SimplePie
 		else
 		{
 			$this->feed_url = $this->registry->call('Misc', 'fix_protocol', array($url, 1));
+			$this->permanent_url = $this->feed_url;	//FreshRSS
 		}
 	}
 
@@ -749,6 +757,7 @@ class SimplePie
 		if ($file instanceof SimplePie_File)
 		{
 			$this->feed_url = $file->url;
+			$this->permanent_url = $this->feed_url;	//FreshRSS
 			$this->file =& $file;
 			return true;
 		}
@@ -1602,7 +1611,7 @@ class SimplePie
 		}
 
 		$this->raw_data = $file->body;
-
+		$this->permanent_url = $file->permanent_url;	//FreshRSS
 		$headers = $file->headers;
 		$sniffer = $this->registry->create('Content_Type_Sniffer', array(&$file));
 		$sniffed = $sniffer->get_type();
@@ -1788,26 +1797,39 @@ class SimplePie
 
 	/**
 	 * Get the URL for the feed
+	 * 
+	 * When the 'permanent' mode is enabled, returns the original feed URL,
+	 * except in the case of an `HTTP 301 Moved Permanently` status response,
+	 * in which case the location of the first redirection is returned.
 	 *
-	 * May or may not be different from the URL passed to {@see set_feed_url()},
+	 * When the 'permanent' mode is disabled (default),
+	 * may or may not be different from the URL passed to {@see set_feed_url()},
 	 * depending on whether auto-discovery was used.
 	 *
 	 * @since Preview Release (previously called `get_feed_url()` since SimplePie 0.8.)
-	 * @todo If we have a perm redirect we should return the new URL
-	 * @todo When we make the above change, let's support <itunes:new-feed-url> as well
+	 * @todo Support <itunes:new-feed-url>
 	 * @todo Also, |atom:link|@rel=self
+	 * @param bool $permanent Permanent mode to return only the original URL or the first redirection
+	 *  iff it is a 301 redirection
 	 * @return string|null
 	 */
-	public function subscribe_url()
+	public function subscribe_url($permanent = false)
 	{
-		if ($this->feed_url !== null)
+		if ($permanent)	//FreshRSS
 		{
-			return $this->sanitize($this->feed_url, SIMPLEPIE_CONSTRUCT_IRI);
+			if ($this->permanent_url !== null)
+			{
+				return $this->sanitize($this->permanent_url, SIMPLEPIE_CONSTRUCT_IRI);
+			}
 		}
 		else
 		{
-			return null;
+			if ($this->feed_url !== null)
+			{
+				return $this->sanitize($this->feed_url, SIMPLEPIE_CONSTRUCT_IRI);
+			}
 		}
+		return null;
 	}
 
 	/**

+ 10 - 2
lib/SimplePie/SimplePie/File.php

@@ -64,6 +64,7 @@ class SimplePie_File
 	var $redirects = 0;
 	var $error;
 	var $method = SIMPLEPIE_FILE_SOURCE_NONE;
+	var $permanent_url;	//FreshRSS
 
 	public function __construct($url, $timeout = 10, $redirects = 5, $headers = null, $useragent = null, $force_fsockopen = false)
 	{
@@ -74,6 +75,7 @@ class SimplePie_File
 			$url = SimplePie_Misc::compress_parse_url($parsed['scheme'], $idn->encode($parsed['authority']), $parsed['path'], $parsed['query'], $parsed['fragment']);
 		}
 		$this->url = $url;
+		$this->permanent_url = $url;	//FreshRSS
 		$this->useragent = $useragent;
 		if (preg_match('/^http(s)?:\/\//i', $url))
 		{
@@ -142,7 +144,10 @@ class SimplePie_File
 						{
 							$this->redirects++;
 							$location = SimplePie_Misc::absolutize_url($this->headers['location'], $url);
-							return $this->__construct($location, $timeout, $redirects, $headers, $useragent, $force_fsockopen);
+							$previousStatusCode = $this->status_code;
+							$this->__construct($location, $timeout, $redirects, $headers, $useragent, $force_fsockopen);
+							$this->permanent_url = ($previousStatusCode == 301) ? $location : $url;	//FreshRSS
+							return;
 						}
 					}
 				}
@@ -224,7 +229,10 @@ class SimplePie_File
 							{
 								$this->redirects++;
 								$location = SimplePie_Misc::absolutize_url($this->headers['location'], $url);
-								return $this->__construct($location, $timeout, $redirects, $headers, $useragent, $force_fsockopen);
+								$previousStatusCode = $this->status_code;
+								$this->__construct($location, $timeout, $redirects, $headers, $useragent, $force_fsockopen);
+								$this->permanent_url = ($previousStatusCode == 301) ? $location : $url;	//FreshRSS
+								return;
 							}
 							if (isset($this->headers['content-encoding']))
 							{

+ 1 - 7
lib/lib_rss.php

@@ -56,12 +56,6 @@ function checkUrl($url) {
 	}
 }
 
-// tiré de Shaarli de Seb Sauvage	//Format RFC 4648 base64url
-function small_hash ($txt) {
-	$t = rtrim (base64_encode (hash ('crc32', $txt, true)), '=');
-	return strtr ($t, '+/', '-_');
-}
-
 function formatNumber($n, $precision = 0) {
 	return str_replace(' ', ' ',	//Espace insécable	//TODO: remplacer par une espace _fine_ insécable
 		number_format($n, $precision, '.', ' '));	//number_format does not seem to be Unicode-compatible
@@ -115,7 +109,7 @@ function customSimplePie() {
 	$simplePie = new SimplePie();
 	$simplePie->set_useragent(Minz_Translate::t('freshrss') . '/' . FRESHRSS_VERSION . ' (' . PHP_OS . '; ' . FRESHRSS_WEBSITE . ') ' . SIMPLEPIE_NAME . '/' . SIMPLEPIE_VERSION);
 	$simplePie->set_cache_location(CACHE_PATH);
-	$simplePie->set_cache_duration(1500);
+	$simplePie->set_cache_duration(800);
 	$simplePie->strip_htmltags(array(
 		'base', 'blink', 'body', 'doctype', 'embed',
 		'font', 'form', 'frame', 'frameset', 'html',

+ 8 - 6
p/api/greader.php

@@ -327,7 +327,7 @@ function streamContents($path, $include_target, $start_time, $count, $order, $ex
 	logMe("streamContents($path, $include_target, $start_time, $count, $order, $exclude_target, $continuation)\n");
 	header('Content-Type: application/json; charset=UTF-8');
 
-	$feedDAO = new FreshRSS_FeedDAO();
+	$feedDAO = FreshRSS_Factory::createFeedDao();
 	$arrayFeedCategoryNames = $feedDAO->arrayFeedCategoryNames();
 
 	switch ($path) {
@@ -364,7 +364,7 @@ function streamContents($path, $include_target, $start_time, $count, $order, $ex
 		$count++;	//Shift by one element
 	}
 
-	$entryDAO = new FreshRSS_EntryDAO();
+	$entryDAO = FreshRSS_Factory::createEntryDao();
 	$entries = $entryDAO->listWhere($type, $include_target, $state, $order === 'o' ? 'ASC' : 'DESC', $count, $continuation, '', $start_time);
 
 	$items = array();
@@ -458,7 +458,7 @@ function streamContentsItemsIds($streamId, $start_time, $count, $order, $exclude
 			break;
 	}
 
-	$entryDAO = new FreshRSS_EntryDAO();
+	$entryDAO = FreshRSS_Factory::createEntryDao();
 	$ids = $entryDAO->listIdsWhere($type, $id, $state, $order === 'o' ? 'ASC' : 'DESC', $count, '', '', $start_time);
 
 	$itemRefs = array();
@@ -481,7 +481,7 @@ function editTag($e_ids, $a, $r) {
 		$e_ids[$i] = hex2dec(basename($e_id));	//Strip prefix 'tag:google.com,2005:reader/item/'
 	}
 
-	$entryDAO = new FreshRSS_EntryDAO();
+	$entryDAO = FreshRSS_Factory::createEntryDao();
 
 	switch ($a) {
 		case 'user/-/state/com.google/read':
@@ -512,13 +512,15 @@ function editTag($e_ids, $a, $r) {
 
 function markAllAsRead($streamId, $olderThanId) {
 	logMe("markAllAsRead($streamId, $olderThanId)\n");
-	$entryDAO = new FreshRSS_EntryDAO();
+	$entryDAO = FreshRSS_Factory::createEntryDao();
 	if (strpos($streamId, 'feed/') === 0) {
 		$f_id = basename($streamId);
 		$entryDAO->markReadFeed($f_id, $olderThanId);
 	} elseif (strpos($streamId, 'user/-/label/') === 0) {
 		$c_name = basename($streamId);
-		$entryDAO->markReadCatName($c_name, $olderThanId);
+		$categoryDAO = new FreshRSS_CategoryDAO();
+		$cat = $categoryDAO->searchByName($c_name);
+		$entryDAO->markReadCat($cat === null ? -1 : $cat->id(), $olderThanId);
 	} elseif ($streamId === 'user/-/state/com.google/reading-list') {
 		$entryDAO->markReadEntries($olderThanId, false, -1);
 	}

+ 8 - 8
p/i/index.php

@@ -18,12 +18,12 @@
 #
 # ***** END LICENSE BLOCK *****
 
-if (file_exists ('install.php')) {
-	require('install.php');
-} else {
-	require('../../constants.php');
-	require(LIB_PATH . '/lib_rss.php');	//Includes class autoloader
+require('../../constants.php');
+require(LIB_PATH . '/lib_rss.php');	//Includes class autoloader
 
+if (file_exists(DATA_PATH . '/do-install.txt')) {
+	require(APP_PATH . '/install.php');
+} else {
 	session_cache_limiter('');
 	Minz_Session::init('FreshRSS');
 	Minz_Session::_param('keepAlive', 1);	//For Persona
@@ -42,11 +42,11 @@ if (file_exists ('install.php')) {
 
 	try {
 		$front_controller = new FreshRSS();
-		$front_controller->init ();
-		$front_controller->run ();
+		$front_controller->init();
+		$front_controller->run();
 	} catch (Exception $e) {
 		echo '### Fatal error! ###<br />', "\n";
-		Minz_Log::record ($e->getMessage (), Minz_Log::ERROR);
+		Minz_Log::record($e->getMessage(), Minz_Log::ERROR);
 		echo 'See logs files.';
 	}
 }

+ 45 - 15
p/scripts/main.js

@@ -1,7 +1,8 @@
 "use strict";
 var $stream = null,
 	isCollapsed = true,
-	shares = 0;
+	shares = 0,
+	ajax_loading = false;
 
 function is_normal_mode() {
 	return $stream.hasClass('normal');
@@ -54,9 +55,11 @@ function numberFormat(nStr) {
 	return x1 + x2;
 }
 
-function incLabel(p, inc) {
+function incLabel(p, inc, spaceAfter) {
 	var i = str2int(p) + inc;
-	return i > 0 ? ' (' + numberFormat(i) + ')' : '';
+	return i > 0
+		? ((spaceAfter ? '' : ' ') + '(' + numberFormat(i) + ')' + (spaceAfter ? ' ' : ''))
+		: '';
 }
 
 function incUnreadsFeed(article, feed_id, nb) {
@@ -95,13 +98,16 @@ function incUnreadsFeed(article, feed_id, nb) {
 
 	var isCurrentView = false;
 	//Update unread: title
-	document.title = document.title.replace(/((?: \([ 0-9]+\))?)( · .*?)((?: \([ 0-9]+\))?)$/, function (m, p1, p2, p3) {
+	document.title = document.title.replace(/^((?:\([ 0-9]+\) )?)(.*? · )((?:\([ 0-9]+\) )?)/, function (m, p1, p2, p3) {
 		var $feed = $('#' + feed_id);
 		if (article || ($feed.closest('.active').length > 0 && $feed.siblings('.active').length === 0)) {
 			isCurrentView = true;
-			return incLabel(p1, nb) + p2 + incLabel(p3, feed_priority > 0 ? nb : 0);
+			return incLabel(p1, nb, true) + p2 + incLabel(p3, feed_priority > 0 ? nb : 0, true);
+		} else if ($('.all.active').length > 0) {
+			isCurrentView = feed_priority > 0;
+			return incLabel(p1, feed_priority > 0 ? nb : 0, true) + p2 + incLabel(p3, feed_priority > 0 ? nb : 0, true);
 		} else {
-			return p1 + p2 + incLabel(p3, feed_priority > 0 ? nb : 0);
+			return p1 + p2 + incLabel(p3, feed_priority > 0 ? nb : 0, true);
 		}
 	});
 	return isCurrentView;
@@ -190,7 +196,7 @@ function mark_favorite(active) {
 		var favourites = $('.favorites>a').contents().last().get(0);
 		if (favourites && favourites.textContent) {
 			favourites.textContent = favourites.textContent.replace(/((?: \([ 0-9]+\))?\s*)$/, function (m, p1) {
-				return incLabel(p1, inc);
+				return incLabel(p1, inc, false);
 			});
 		}
 
@@ -258,7 +264,7 @@ function toggleContent(new_active, old_active) {
 		}
 	}
 
-	if (auto_mark_article) {
+	if (auto_mark_article && new_active.hasClass('active')) {
 		mark_read(new_active, true);
 	}
 }
@@ -448,6 +454,7 @@ function init_posts() {
 				load_more_posts();
 			}
 		});
+		box_to_follow.scroll();
 	}
 }
 
@@ -497,7 +504,13 @@ function init_shortcuts() {
 	shortcut.add("shift+" + shortcuts.mark_read, function () {
 		// on marque tout comme lu
 		var url = $(".nav_menu a.read_all").attr("href");
-		redirect(url, false);
+		if ($(".nav_menu a.read_all").hasClass('confirm')) {
+			if (confirm(str_confirmation)) {
+				redirect(url, false);
+			}
+		} else {
+			redirect(url, false);
+		}
 	}, {
 		'disable_in_input': true
 	});
@@ -683,14 +696,22 @@ function init_actualize() {
 	var auto = false;
 
 	$("#actualize").click(function () {
+		if (ajax_loading) {
+			return false;
+		}
+
+		ajax_loading = true;
+
 		$.getScript('./?c=javascript&a=actualize').done(function () {
 			if (auto && feed_count < 1) {
 				auto = false;
-				return;
+				ajax_loading = false;
+				return false;
 			}
 
 			updateFeeds();
 		});
+
 		return false;
 	});
 
@@ -975,11 +996,6 @@ function init_print_action() {
 function init_share_observers() {
 	shares = $('.form-group:not(".form-actions")').length;
 
-	$('.post').on('click', '.share.remove', function(e) {
-		e.preventDefault();
-		$(this).parents('.form-group').remove();
-	});
-
 	$('.share.add').on('click', function(e) {
 		var opt = $(this).siblings('select').find(':selected');
 		var row = $(this).parents('form').data(opt.data('form'));
@@ -994,6 +1010,19 @@ function init_share_observers() {
 	});
 }
 
+function init_remove_observers() {
+	$('.post').on('click', 'a.remove', function(e) {
+		var remove_what = $(this).attr('data-remove');
+
+		if (remove_what !== undefined) {
+			var remove_obj = $('#' + remove_what);
+			remove_obj.remove();
+		}
+
+		return false;
+	});
+}
+
 function init_feed_observers() {
 	$('select[id="category"]').on('change', function() {
 		var detail = $('#new_category_name').parent();
@@ -1054,6 +1083,7 @@ function init_all() {
 		window.setInterval(refreshUnreads, 120000);
 	} else {
 		init_share_observers();
+		init_remove_observers();
 		init_feed_observers();
 		init_password_observers();
 	}

+ 985 - 0
p/themes/Dark/dark.css

@@ -0,0 +1,985 @@
+@charset "UTF-8";
+
+/*=== FONTS */
+@font-face {
+	font-family: "OpenSans";
+	src: url("../fonts/openSans.woff") format("woff");
+}
+
+/*=== GENERAL */
+/*============*/
+html, body {
+	height: 100%;
+	font-family: "OpenSans", "Cantarell", "Helvetica", "Arial", sans-serif;
+	background: #1c1c1c;
+	color: #888;
+}
+
+/*=== Links */
+a {
+	outline: none;
+	color: #6986B2;
+}
+
+/*=== Images */
+img.favicon {
+	background: #fff;
+	border-radius: 2px;
+}
+
+/*=== Forms */
+legend {
+	margin: 20px 0 5px;
+	padding: 5px 0;
+	font-size: 1.4em;
+	border-bottom: 1px solid #2f2f2f;
+}
+label {
+	min-height: 25px;
+	padding: 5px 0;
+	cursor: pointer;
+}
+textarea {
+	width: 360px;
+	height: 100px;
+}
+input, select, textarea {
+	min-height: 25px;
+	padding: 5px;
+	line-height: 25px;
+	vertical-align: middle;
+	background: #333;
+	border: 1px solid #000;
+	border-radius: 3px;
+	color: #999;
+	box-shadow: 0 2px 2px #1d1d1d inset;
+}
+option {
+	padding: 0 .5em;
+}
+input:focus, select:focus, textarea:focus {
+	color: #6986b2;
+	border-color: #2f2f2f;
+}
+input:invalid, select:invalid {
+	border-color: #f00;
+	box-shadow: 0 0 2px 1px #f00;
+}
+input:disabled, select:disabled {
+	background: #666;
+	color: #aaa;
+}
+input.extend {
+	transition: width 200ms linear;
+	-moz-transition: width 200ms linear;
+	-webkit-transition: width 200ms linear;
+	-o-transition: width 200ms linear;
+	-ms-transition: width 200ms linear;
+}
+
+/*=== Tables */
+table {
+	border-collapse: collapse;
+}
+
+tr, th, td {
+	padding: 0.5em;
+	border: 1px solid #333;
+}
+th {
+	background: #222;
+}
+form td,
+form th {
+	font-weight: normal;
+	text-align: center;
+}
+
+/*=== COMPONENTS */
+/*===============*/
+/*=== Forms */
+.form-group.form-actions {
+	padding: 5px 0;
+	background: #1a1a1a;
+	border-top: 1px solid #2f2f2f;
+}
+.form-group.form-actions .btn {
+	margin: 0 10px;
+}
+.form-group .group-name {
+	padding: 10px 0;
+	text-align: right;
+}
+.form-group .group-controls {
+	min-height: 25px;
+	padding: 5px 0;
+}
+.form-group table {
+	margin: 10px 0 0 220px;
+}
+
+/*=== Buttons */
+.stick {
+	vertical-align: middle;
+	font-size: 0;
+}
+.stick input,
+.stick .btn {
+	border-radius: 0;
+}
+.stick .btn:first-child,
+.stick input:first-child {
+	border-radius: 3px 0 0 3px;
+}
+.stick .btn-important:first-child {
+	border-right: 1px solid #000;
+}
+.stick .btn:last-child,
+.stick input:last-child {
+	border-radius: 0 3px 3px 0;
+}
+.stick .btn + .btn,
+.stick .btn + input,
+.stick .btn + .dropdown > .btn,
+.stick input + .btn,
+.stick input + input,
+.stick input + .dropdown > .btn,
+.stick .dropdown + .btn,
+.stick .dropdown + input,
+.stick .dropdown + .dropdown > .btn {
+	border-left: none;
+}
+.stick input:focus+input {
+	border-left: 1px solid #000;
+}
+.stick input+input:focus {
+	border-left: 1px solid #333;
+}
+.stick .btn + .dropdown > .btn {
+	border-left: none;
+	border-radius: 0 3px 3px 0;
+}
+
+.btn {
+	display: inline-block;
+	min-height: 37px;
+	min-width: 15px;
+	margin: 0;
+	padding: 5px 10px;
+	font-size: 0.9rem;
+	vertical-align: middle;
+	cursor: pointer;
+	overflow: hidden;
+	background: #111;
+	border-radius: 3px;
+	border: 1px solid #000;
+	color: #888;
+}
+a.btn {
+	min-height: 25px;
+	line-height: 25px;
+}
+.btn:hover {
+	text-decoration: none;
+	background: -moz-linear-gradient(top, #4A5D7A 0%, #26303F 100%);
+	background: -moz-linear-gradient(top, #4A5D7A 0%, #26303F 100%);
+	background: -webkit-linear-gradient(top, #4A5D7A 0%, #26303F 100%);
+	background: -o-linear-gradient(top, #4A5D7A 0%, #26303F 100%);
+	background: -ms-linear-gradient(top, #4A5D7A 0%, #26303F 100%);
+}
+.btn.active,
+.dropdown-target:target ~ .btn.dropdown-toggle {
+	background: #333;
+}
+.btn:active {
+	background: #26303F;
+}
+
+.btn-important {
+	font-weight: normal;
+	background: #26303F;
+}
+.btn-important:hover {
+	background: linear-gradient(top, #4A5D7A 0%, #26303F 100%);
+	background: -moz-linear-gradient(top, #4A5D7A 0%, #26303F 100%);
+	background: -webkit-linear-gradient(top, #4A5D7A 0%, #26303F 100%);
+	background: -o-linear-gradient(top, #4A5D7A 0%, #26303F 100%);
+	background: -ms-linear-gradient(top, #4A5D7A 0%, #26303F 100%);
+}
+.btn-important:active {
+	background: #26303F;
+}
+
+.btn-attention {
+	background: #880011;
+}
+.btn-attention:hover {
+	background: linear-gradient(top, #cc0044 0%, #880011 100%);
+	background: -moz-linear-gradient(top, #cc0044 0%, #880011 100%);
+	background: -webkit-linear-gradient(top, #cc0044 0%, #880011 100%);
+	background: -o-linear-gradient(top, #cc0044 0%, #880011 100%);
+	background: -ms-linear-gradient(top, #cc0044 0%, #880011 100%);
+}
+.btn-attention:active {
+	background: #880011;
+}
+
+/*=== Navigation */
+.nav-list .nav-header,
+.nav-list .item {
+	height: 2.5em;
+	line-height: 2.5em;
+	font-size: 0.9rem;
+}
+.nav-list .item:hover {
+	background: #26303F;
+}
+.nav-list .item.active {
+	background: #333;
+}
+.nav-list .item:hover a,
+.nav-list .item.active a {
+	color: #888;
+}
+.nav-list .disable {
+	text-align: center;
+	color: #aaa;
+	background: #fafafa;
+}
+.nav-list .item > a {
+	padding: 0 10px;
+}
+.nav-list a:hover {
+	text-decoration: none;
+}
+.nav-list .item.empty a {
+	color: #c95;
+}
+.nav-list .item:hover.empty a,
+.nav-list .item.active.empty a {
+	color: #fff;
+	background: #c95;
+}
+.nav-list .item.error a {
+	color: #a44;
+}
+.nav-list .item:hover.error a,
+.nav-list .item.active.error a {
+	color: #fff;
+	background: #a44;
+}
+
+.nav-list .nav-header {
+	padding: 0 10px;
+	font-weight: bold;
+	background: #111;
+	border-bottom: 1px solid #333;
+}
+
+.nav-list .nav-form {
+	padding: 3px;
+	text-align: center;
+}
+
+.nav-head {
+	margin: 0;
+	text-align: right;
+	background: #1c1c1c;
+	border-bottom: 1px solid #333;
+}
+.nav-head .item {
+	padding: 5px 10px;
+	font-size: 0.9rem;
+	line-height: 1.5rem;
+}
+
+/*=== Horizontal-list */
+.horizontal-list {
+	margin: 0;
+	padding: 0;
+}
+.horizontal-list .item {
+	vertical-align: middle;
+}
+
+/*=== Dropdown */
+.dropdown-menu {
+	margin: 5px 0 0;
+	padding: 5px 0;
+	font-size: 0.8rem;
+	text-align: left;
+	background: #1a1a1a;
+	border: 1px solid #888;
+	border-radius: 5px;
+}
+.dropdown-menu:after {
+	content: "";
+	position: absolute;
+	top: -6px;
+	right: 13px;
+	width: 10px;
+	height: 10px;
+	z-index: -10;
+	transform: rotate(45deg);
+	-moz-transform: rotate(45deg);
+	-webkit-transform: rotate(45deg);
+	-ms-transform: rotate(45deg);
+	background: #1a1a1a;
+	border-top: 1px solid #888;
+	border-left: 1px solid #888;
+}
+.dropdown-header {
+	padding: 0 5px 5px;
+	font-weight: bold;
+	text-align: left;
+	color: #888;
+}
+.dropdown-menu > .item > a {
+	padding: 0 25px;
+	line-height: 2.5em;
+}
+.dropdown-menu > .item > span {
+	padding: 0 25px;
+	line-height: 2em;
+}
+.dropdown-menu > .item:hover {
+	background: #26303F;
+	color: #888;
+}
+.dropdown-menu > .item[aria-checked="true"] > a:before {
+	font-weight: bold;
+	margin: 0 0 0 -14px;
+}
+.dropdown-menu > .item:hover > a {
+	text-decoration: none;
+	color: #888;
+}
+.dropdown-menu .input select,
+.dropdown-menu .input input {
+	margin: 0 auto 5px;
+	padding: 2px 5px;
+	border-radius: 3px;
+}
+
+.separator {
+	margin: 5px 0;
+	border-bottom: 1px solid #333;
+}
+
+/*=== Alerts */
+.alert {
+	margin: 15px auto;
+	padding: 10px 15px;
+	font-size: 0.9em;
+	background: #111;
+	border: 1px solid #888;
+	border-radius: 5px;
+	color: #aaa;
+}
+.alert-head {
+	font-size: 1.15em;
+}
+.alert > a {
+	text-decoration: underline;
+	color: inherit;
+}
+.alert-warn {
+	border: 1px solid #c95;
+	color: #c95;
+}
+.alert-success {
+	border: 1px solid #484;
+	color: #484;
+}
+.alert-error {
+	border: 1px solid #a44;
+	color: #a44;
+}
+
+/*=== Pagination */
+.pagination {
+	text-align: center;
+	font-size: 0.8em;
+	background: #1c1c1c;
+	color: #888;
+}
+.content .pagination {
+	margin: 0;
+	padding: 0;
+}
+.pagination .item.pager-current {
+	font-weight: bold;
+	font-size: 1.5em;
+	background: #111;
+}
+.pagination .item a {
+	display: block;
+	font-style: italic;
+	line-height: 3em;
+	text-decoration: none;
+	color: #666;
+}
+.pagination .item a:hover {
+	background-color: #111;
+}
+.pagination:first-child .item {
+	border-bottom: 1px solid #333;
+}
+.pagination:last-child .item {
+		border-top: 1px solid #333;
+}
+
+.pagination .loading,
+.pagination a:hover.loading {
+	font-size: 0;
+}
+
+/*=== STRUCTURE */
+/*===============*/
+/*=== Header */
+.header {
+	height: 85px;
+}
+.header > .item {
+	padding: 10px;
+	vertical-align: middle;
+	text-align: center;
+	border-bottom: 1px solid #333;
+}
+.header > .item.title{
+	width: 230px;
+}
+.header > .item.title h1 {
+	margin: 0.5em 0;
+}
+.header > .item.title h1 a {
+	text-decoration: none;
+}
+.header > .item.search input {
+	width: 230px;
+}
+.header .item.search input:focus {
+	width: 350px;
+}
+
+/*=== Body */
+#global {
+	height: calc(100% - 85px);
+}
+.aside {
+	border-right: 1px solid #333;
+	background: #1c1c1c;
+}
+.aside.aside_flux {
+	padding: 10px 0 50px;
+	border-right: 1px solid #333;
+	background: #1c1c1c;
+}
+
+/*=== Aside main page (categories) */
+.categories {
+	text-align: center;
+}
+.category {
+	width: 235px;
+	margin: 10px auto;
+	text-align: left;
+}
+.category .btn:first-child {
+	position: relative;
+	width: 213px;
+}
+.category.stick .btn:first-child {
+	width: 176px;
+}
+.category .btn:first-child:not([data-unread="0"]):after {
+	position: absolute;
+	top: 3px; right: 3px;
+	padding: 1px 5px;
+	background: #111;
+	color: #888;
+	border: 1px solid #000;
+	border-radius: 5px;
+}
+
+/*=== Aside main page (feeds) */
+.categories .feeds .item.active {
+	background: #333;
+}
+.categories .feeds .item.active .feed {
+	color: #888;
+}
+.categories .feeds .item.empty .feed {
+	color: #c95;
+}
+.categories .feeds .item.empty.active {
+	background: #c95;
+}
+.categories .feeds .item.empty.active .feed {
+	color: #fff;
+}
+.categories .feeds .item.error .feed {
+	color: #a44;
+}
+.categories .feeds .item.error.active {
+	background: #a44;
+}
+.categories .feeds .item.error.active .feed {
+	color: #fff;
+}
+.categories .feeds .item .feed {
+	margin: 0;
+	width: 165px;
+	line-height: 3em;
+	font-size: 0.8em;
+	text-align: left;
+	text-decoration: none;
+}
+.categories .feeds .feed:not([data-unread="0"]) {
+	font-weight: bold;
+}
+.categories .feeds .dropdown-menu:after {
+	left: 2px;
+}
+.categories .feeds .item .dropdown-target:target ~ .dropdown-toggle > .icon,
+.categories .feeds .item:hover .dropdown-toggle > .icon,
+.categories .feeds .item.active .dropdown-toggle > .icon {
+	vertical-align: middle;
+	background-color: #111;
+	border-radius: 3px;
+}
+
+/*=== Configuration pages */
+.post {
+	padding: 10px 50px;
+	font-size: 0.9em;
+}
+.post form {
+	margin: 10px 0;
+}
+.post.content {
+	max-width: 550px;
+}
+
+/*=== Prompt (centered) */
+.prompt {
+	text-align: center;
+}
+.prompt label {
+	text-align: left;
+}
+.prompt form {
+	margin: 10px auto 20px auto;
+	width: 180px;
+}
+.prompt input {
+	margin: 5px auto;
+	width: 100%;
+}
+.prompt p {
+	margin: 20px 0;
+}
+
+/*=== New article notification */
+#new-article {
+	text-align: center;
+	font-size: 0.9em;
+	background: #26303F;
+}
+#new-article:hover {
+	background: #4A5D7A;
+}
+#new-article > a {
+	line-height: 3em;
+	font-weight: bold;
+	color: #fff;
+}
+#new-article > a:hover {
+	text-decoration: none;
+}
+
+/*=== Day indication */
+.day {
+	padding: 0 10px;
+	font-weight: bold;
+	line-height: 3em;
+	border-top: 1px solid #333;
+	border-bottom: 1px solid #333;
+}
+.day .name {
+	padding: 0 10px 0 0;
+	font-size: 1.8em;
+	opacity: 0.3;
+	font-style: italic;
+	text-align: right;
+	color: #aab;
+	text-shadow: 0px -1px 0px #333;
+}
+
+/*=== Index menu */
+.nav_menu {
+	text-align: center;
+	padding: 5px 0;
+	border-bottom: 1px solid #2f2f2f;
+}
+
+/*=== Feed articles */
+.flux {
+	border-left: 2px solid #2f2f2f;
+}
+.flux:hover {
+	background: #111;
+}
+.flux.current {
+	border-left: 2px solid #0062BE;
+	background: #111;
+}
+.flux.not_read {
+	border-left: 2px solid #FF5300;
+}
+.flux.favorite {
+	border-left: 2px solid #FFC300;
+}
+
+
+.flux_header {
+	font-size: 0.8rem;
+	cursor: pointer;
+}
+.flux_header .title {
+	font-size: 0.9rem;
+}
+.flux_header .item.title a {
+	color: #888;
+}
+.flux .website .favicon {
+	margin: 5px;
+}
+.flux .date {
+	font-size: 0.7rem;
+	color: #666;
+}
+.flux:not(.current):hover .item.title {
+	background: #111;
+}
+
+.flux .bottom {
+	font-size: 0.8rem;
+	text-align: center;
+}
+
+/*=== Content of feed articles */
+.content {
+	padding: 20px 10px;
+}
+.content > h1.title > a {
+	color: #888;
+}
+
+.content hr {
+	margin: 30px 10px;
+	height: 1px;
+	background: #666;
+	border: 0;
+	box-shadow: 0 2px 5px #666;
+}
+
+.content pre {
+	margin: 10px auto;
+	padding: 10px 20px;
+	overflow: auto;
+	background: #222;
+	color: #fff;
+	border: 1px solid #000;
+	font-size: 0.9rem;
+	border-radius: 3px;
+}
+.content code {
+	padding: 2px 5px;
+	color: #dd1144;
+	background: #000;
+	border: 1px solid #333;
+	border-radius: 3px;
+}
+.content pre code {
+	background: transparent;
+	color: #fff;
+	border: none;
+}
+
+.content blockquote {
+	display: block;
+	margin: 0;
+	padding: 5px 20px;
+	border-top: 1px solid #444;
+	border-bottom: 1px solid #444;
+	background: #222;
+	color: #999;
+}
+.content blockquote p {
+	margin: 0;
+}
+
+/*=== Notification and actualize notification */
+.notification {
+	padding: 0 0 0 5px;
+	text-align: center;
+	font-weight: bold;
+	font-size: 0.9em;
+	line-height: 3em;
+	z-index: 10;
+	vertical-align: middle;
+	border-radius: 5px;
+	box-shadow: 0 0 5px #666;
+	background: #111;
+	color: #c95;
+	border: 1px solid #c95;
+}
+.notification.good {
+	border-color: #484;
+	color: #484;
+}
+.notification.bad {
+	border-color: #a44;
+	color: #a44;
+}
+.notification a.close {
+	padding: 0 15px;
+	line-height: 3em;
+}
+.notification a.close:hover {
+	background: #222;
+	border-radius: 0 3px 3px 0;
+}
+.notification.good a.close:hover {
+	background: #484;
+}
+.notification.bad a.close:hover {
+	background: #a44;
+}
+
+.notification#actualizeProgress {
+	line-height: 2em;
+}
+
+/*=== "Load more" part */
+#bigMarkAsRead {
+	text-align: center;
+	text-decoration: none;	
+}
+#bigMarkAsRead:hover {
+	background: #111;
+	color: #aaa;
+}
+
+/*=== Navigation menu (for articles) */
+#nav_entries {
+	margin: 0;
+	text-align: center;
+	line-height: 3em;
+	table-layout: fixed;
+	background: #111;
+	border-top: 1px solid #333;
+}
+
+/*=== READER VIEW */
+/*================*/
+#stream.reader .flux {
+	padding: 0 0 50px;
+	border: none;
+	background: #111;
+}
+#stream.reader .flux .author {
+	margin: 0 0 10px;
+	font-size: 90%;
+	color: #666;
+}
+
+/*=== GLOBAL VIEW */
+/*================*/
+#stream.global .box-category {
+	text-align: left;
+	background: #1a1a1a;
+	border: 1px solid #000;
+	border-radius: 5px;
+	text-align: left;
+}
+#stream.global .category {
+	margin: 0;
+}
+#stream.global .btn {
+	width: auto;
+	height: 2em;
+	margin: 0;
+	padding: 0 10px;
+	line-height: 2em;
+	font-size: 1.2rem;
+	background: #26303F;
+	border: none;
+	border-bottom: 1px solid #000;
+	border-radius: 5px 5px 0 0;
+}
+#stream.global .btn:not([data-unread="0"]) {
+	font-weight: bold;
+	background: #34495e;
+	color: #fff;
+}
+#stream.global .btn:first-child:not([data-unread="0"]):after {
+	top: 0; right: 5px;
+	font-weight: bold;
+	border: 0;
+	background: none;
+	color: #fff;
+}
+#stream.global .box-category .feeds {
+	max-height: 250px;
+}
+#stream.global .box-category .feeds .item {
+	padding: 2px 10px;
+	font-size: 0.9rem;
+}
+
+/*=== Panel */
+#panel {
+	background: #1c1c1c;
+	border: 1px solid #666;
+	border-radius: 3px;
+}
+
+/*=== DIVERS */
+/*===========*/
+.aside.aside_feed .nav-form input,
+.aside.aside_feed .nav-form select {
+	width: 140px;
+}
+.aside.aside_feed .nav-form .dropdown .dropdown-menu {
+	right: -20px;
+}
+.aside.aside_feed .nav-form .dropdown .dropdown-menu:after {
+	right: 33px;
+}
+
+/*=== STATISTICS */
+/*===============*/
+.stat {
+	margin: 10px 0 20px;
+}
+
+.stat th,
+.stat td,
+.stat tr {
+	border: none;
+}
+.stat > table td,
+.stat > table th {
+	border-bottom: 1px solid #333;
+	text-align: center;
+}
+
+/*=== LOGS */
+/*=========*/
+.logs {
+	overflow: hidden;
+	border: 1px solid #333;
+}
+.log {
+	padding: 5px 10px;
+	font-size: 0.8rem;
+	background: #111;
+	color: #888;
+}
+.log+.log {
+	border-top: 1px solid #333;
+}
+.log .date {
+	display: block;
+	font-weight: bold;
+}
+.log.error {
+	background: #a44;
+	color: #fff;
+}
+.log.warning {
+	background: #c95;
+	color: #fff;
+}
+.log.notice {
+	background: #ec9;
+	color: #000;
+}
+.log.debug {
+	background: #111;
+	color: #eee;
+}
+
+/*=== MOBILE */
+/*===========*/
+@media(max-width: 840px) {
+	.aside {
+		transition: width 200ms linear;
+		-moz-transition: width 200ms linear;
+		-webkit-transition: width 200ms linear;
+		-o-transition: width 200ms linear;
+		-ms-transition: width 200ms linear;
+	}
+	.aside .toggle_aside,
+	#panel .close {
+		position: absolute;
+		display: block;
+		top: 0; right: 0;
+		width: 30px;
+		height: 30px;
+		line-height: 30px;
+		text-align: center;
+		background: #111;
+		border-left: 1px solid #333;
+		border-bottom: 1px solid #333;
+		border-radius: 0 0 0 5px;
+	}
+
+	.nav_menu .btn {
+		margin: 5px 10px;
+	}
+	.nav_menu .stick {
+		margin: 0 10px;
+	}
+	.nav_menu .stick .btn {
+		margin: 5px 0;
+	}
+	.nav_menu .search {
+		display: inline-block;
+		max-width: 97%;
+	}
+	.nav_menu .search input {
+		max-width: 97%;
+		width: 90px;
+	}
+	.nav_menu .search input:focus {
+		width: 400px;
+	}
+
+	.day .name {
+		font-size: 1.1rem;
+	}
+
+	.pagination {
+		margin: 0 0 3.5em;
+	}
+
+	.notification {
+		border-top: none;
+		border-right: none;
+		border-left: none;
+		border-radius: 0;
+	}
+	.notification a.close {
+		display: block;
+		left: 0;
+	}
+	.notification a.close:hover {
+		opacity: 0.5;
+	}
+	.notification a.close .icon {
+		display: none;
+	}
+}

+ 0 - 926
p/themes/Dark/freshrss.css

@@ -1,926 +0,0 @@
-@charset "UTF-8";
-
-/* STRUCTURE */
-.header {
-	display: table;
-	width: 100%;
-	background: #1c1c1c;
-	table-layout: fixed;
-}
-	.header > .item {
-		display: table-cell;
-		padding: 10px 0;
-		border-bottom: 1px solid #2f2f2f;
-		vertical-align: middle;
-		text-align: center;
-	}
-		.header > .item.title {
-			width: 250px;
-			white-space: nowrap;
-		}
-			.logo {
-				display: inline-block;
-				font-size: 48px;
-				height: 32px;
-				width: 32px;
-				padding: 10px;
-			}
-			.header > .item.title h1 {
-				display: inline-block;
-				margin: 0;
-			}
-		.header > .item.search input {
-			width: 230px;
-		}
-			.header .item.search input:focus {
-				width: 330px;
-			}
-		.header > .item.configure {
-			width: 100px;
-		}
-
-.item a:hover {
-	text-decoration: none;
-}
-
-#global {
-	display: table;
-	width: 100%;
-	height: 100%;
-	background: #1c1c1c;
-	table-layout: fixed;
-}
-	.aside {
-		display: table-cell;
-		height: 100%;
-		width: 250px;
-		vertical-align: top;
-		border-right: 1px solid #2f2f2f;
-		background: #1c1c1c;
-	}
-		.aside .nav-form input {
-			width: 180px;
-		}
-		.aside.aside_flux {
-			padding: 10px 0 40px;
-		}
-		.aside.aside_feed .nav-form input {
-			width: 140px;
-		}
-		.aside.aside_feed .nav-form .dropdown .dropdown-menu {
-			right: -20px;
-		}
-		.aside.aside_feed .nav-form .dropdown .dropdown-menu:after {
-			right: 33px;
-		}
-
-	.nav-login {
-		display: none;
-	}
-
-	.nav_menu {
-		width: 100%;
-		background: #1c1c1c;
-		border-bottom: 1px solid #2f2f2f;
-		text-align: center;
-		padding: 5px 0;
-	}
-		.nav_menu .search {
-			display:none;
-		}
-
-.favicon {
-	height: 16px;
-	width: 16px;
-}
-
-.categories {
-	margin: 0;
-	padding: 0;
-	text-align: center;
-	list-style: none;
-}
-	.category {
-		display: block;
-		width: 220px;
-		margin: 10px auto;
-		text-align: left;
-		overflow: hidden;
-		white-space: nowrap;
-		text-overflow: ellipsis;
-	}
-		.category .btn:first-child {
-			width: 195px;
-			position: relative;
-		}
-		.category.stick .btn:first-child {
-			width:160px;
-		}
-		.category .btn:first-child:not([data-unread="0"]):after {
-			content: attr(data-unread);
-			position: absolute;
-			top: 3px; right: 3px;
-			padding: 1px 5px;
-			background: #1a1a1a;
-			color: #888;
-			font-size: 90%;
-			border: 1px solid #000;
-			border-radius: 5px;
-		}
-		.category + .feeds:not(.active) {
-			display:none;
-		}
-	.categories .feeds {
-		width: 100%;
-		margin: 0;
-		list-style: none;
-	}
-		.categories .feeds .item.active {
-			background: #26303F;
-		}
-		.categories .feeds .item.active .feed {
-			color: #888;
-		}
-		.categories .feeds .item.empty .feed {
-			color: #e67e22;
-		}
-		.categories .feeds .item.empty.active {
-			background: #e67e22;
-		}
-		.categories .feeds .item.empty.active .feed {
-			color: #fff;
-		}
-		.categories .feeds .item.error .feed {
-			color: #BD362F;
-		}
-		.categories .feeds .item .feed {
-			display: inline-block;
-			margin: 0;
-			width: 165px;
-			line-height: 35px;
-			font-size: 90%;
-			vertical-align: middle;
-			text-align: left;
-			overflow: hidden;
-			white-space: nowrap;
-			text-overflow: ellipsis;
-		}
-		.feed:not([data-unread="0"]):before {
-			content: "(" attr(data-unread) ") ";
-		}
-		.categories .feeds .dropdown-menu {
-			left: 0;
-		}
-		.categories .feeds .dropdown-menu:after {
-			left: 2px;
-		}
-		.categories .feeds .item .dropdown-toggle > .icon {
-			visibility: hidden;
-			cursor: pointer;
-		}
-			.categories .feeds .item .dropdown-target:target ~ .dropdown-toggle > .icon,
-			.categories .feeds .item:hover .dropdown-toggle > .icon,
-			.categories .feeds .item.active .dropdown-toggle > .icon {
-				background-color: #1c1c1c;
-				border-radius: 3px;
-				visibility: visible;
-			}
-
-.post {
-	padding: 10px 50px;
-}
-	.post form {
-		margin: 10px 0;
-	}
-
-.day {
-	min-height: 50px;
-	padding: 0 10px;
-	font-size: 130%;
-	font-weight: bold;
-	line-height: 50px;
-	background: #1c1c1c;
-	border-top: 1px solid #2f2f2f;
-}
-	#new-article + .day {
-		border-top: none;
-	}
-	.day .name {
-		position: absolute;
-		right: 0;
-		width: 50%;
-		height: 1.5em;
-		padding: 0 10px 0 0;
-		overflow: hidden;
-		color: #aab;
-		font-size: 1.8em;
-		opacity: .3;
-		text-shadow: 0px -1px 0px #333;
-		font-style: italic;
-		white-space: nowrap;
-		text-overflow: ellipsis;
-		text-align: right;
-	}
-
-#new-article {
-	display: none;
-	min-height: 40px;
-	background: #26303F;
-	text-align: center;
-}
-	#new-article:hover {
-		background: #4A5D7A;
-	}
-	#new-article > a {
-		display: block;
-		line-height: 40px;
-		color: #fff;
-		font-weight: bold;
-	}
-		#new-article > a:hover {
-			text-decoration: none;
-		}
-
-.flux {
-	border-left: 3px solid #2f2f2f;
-	background: #1c1c1c;
-}
-	.flux.not_read {
-		border-left: 3px solid #FF5300;
-		background: #1c1c1c;
-	}
-	.flux.favorite {
-		border-left: 3px solid #FFC300;
-		background: #1c1c1c;
-	}
-	.flux.current {
-		border-left: 3px solid #0062BE;
-		background: #1a1a1a;
-	}
-
-	.horizontal-list > .item:not(.title):not(.website) > a {
-		display: block;
-	}
-
-	.flux_header {
-		background: inherit;
-		height: 25px;
-		font-size: 12px;
-		border-top: 1px solid #2f2f2f;
-		cursor: pointer;
-	}
-		.flux .item {
-			line-height: 40px;
-			white-space: nowrap;
-		}
-		.flux_header > .item {
-			overflow: hidden;
-			text-overflow: ellipsis;
-		}
-		.flux .item.manage {
-			width: 40px;
-			text-align: center;
-		}
-		.flux .item.website {
-			width: 200px;
-		}
-			.website .favicon {
-				padding: 5px;
-			}
-		.flux .item.title {
-			background: inherit;
-		}
-			.flux .title a {
-				color: #888;
-				outline: none;
-			}
-			.flux.not_read .item.title,
-			.flux.current .item.title {
-				font-weight: bold;
-			}
-		.flux .item.date {
-			width: 145px;
-			padding:0 5px 0 0;
-			text-align: right;
-			font-size: 10px;
-			color: #666;
-		}
-		.link {
-			width: 40px;
-			text-align: center;
-		}
-
-#stream.reader .flux {
-	padding: 0 0 30px;
-	border: none;
-	background: #1c1c1c;
-	color: #888;
-}
-	#stream.reader .flux .author {
-		margin: 0 0 10px;
-		font-size: 90%;
-		color: #666;
-	}
-
-#stream.global {
-	text-align: center;
-}
-	#stream.global .box-category {
-		display: inline-block;
-		width: 280px;
-		margin: 20px 10px;
-		vertical-align: top;
-		background: #1a1a1a;
-		border: 1px solid #000;
-		border-radius: 5px;
-		text-align: left;
-		box-shadow: 0 0 5px #2f2f2f;
-	}
-		#stream.global .category {
-			width: 100%;
-			margin: 0;
-		}
-		#stream.global .btn {
-			display: block;
-			width: auto;
-			height: 35px;
-			margin: 0;
-			padding: 0 10px;
-			background: #26303F;
-			border: none;
-			border-bottom: 1px solid #2f2f2f;
-			border-radius: 5px 5px 0 0;
-			line-height: 35px;
-			font-size: 120%;
-		}
-			#stream.global .btn:not([data-unread="0"]) {
-				background: #34495e;
-				color: #fff;
-				font-weight: bold;
-			}
-			#stream.global .btn:first-child:not([data-unread="0"]):after {
-				top: 0; right: 5px;
-				border: 0;
-				background: none;
-				color: #fff;
-				font-weight: bold;
-				box-shadow: none;
-			}
-		#stream.global .box-category .feeds {
-			display: block;
-			max-height: 250px;
-			margin: 0;
-			list-style: none;
-			overflow: auto;
-		}
-			#stream.global .box-category .feeds .item {
-				padding: 2px 10px;
-				font-size: 90%;
-			}
-		#stream.global .box-category .feed {
-			width: 220px;
-		}
-
-.content {
-	min-height: 150px;
-	margin: 0 auto;
-	padding: 20px 10px;
-	line-height: 170%;
-	word-wrap: break-word;
-}
-	.content.large {
-		max-width: 1000px;
-	}
-	.content.medium {
-		max-width: 800px;
-	}
-	.content.thin {
-		max-width: 550px;
-	}
-	.content h1, .content h2, .content h3 {
-		margin: 20px 0 5px;
-	}
-	.content > .title {
-		font-size: x-large;
-		margin: 0;
-	}
-
-	.content p {
-		margin: 0 0 20px;
-	}
-	img.big {
-		display: block;
-		margin: 10px auto;
-	}
-	figure img.big {
-		margin: 0;
-	}
-	.content hr {
-		margin: 30px 0;
-		height: 1px;
-		background: #ddd;
-		border: 0;
-	}
-	.content pre {
-		margin: 10px auto;
-		padding: 10px;
-		overflow: auto;
-		background: #000;
-		color: #fff;
-		font-size: 110%;
-	}
-	.content q, .content blockquote {
-		display: block;
-		margin: 5px 0;
-		padding: 5px 20px;
-		font-style: italic;
-		border-left: 4px solid #ccc;
-		color: #666;
-	}
-		.content blockquote p {
-			margin: 0;
-		}
-
-#panel {
-	display: none;
-	position: fixed;
-	top: 10px; bottom: 10px;
-	left: 100px; right: 100px;
-	overflow: auto;
-	background: #1c1c1c;
-	border: 1px solid #95a5a6;
-	border-radius: 5px;
-}
-	#panel .close {
-		position: fixed;
-		top: 10px; right: 0;
-		display: inline-block;
-		width: 26px;
-		height: 26px;
-		margin: 0 10px 0 0;
-		border: 1px solid #ccc;
-		border-radius: 20px;
-		text-align: center;
-		line-height: 26px;
-		background: #fff;
-	}
-
-#overlay {
-	display: none;
-	position: fixed;
-	top: 0; bottom: 0;
-	left: 0; right: 0;
-	background: rgba(0, 0, 0, 0.9);
-}
-
-.flux_content .bottom {
-	font-size: 90%;
-	text-align: center;
-}
-
-.hide_posts > :not(.active) > .flux_content {
-	display:none;
-}
-
-/*** PAGINATION ***/
-.pagination {
-	display: table;
-	width: 100%;
-	margin: 0;
-	background: #1a1a1a;
-	text-align: center;
-	color: #888;
-	font-size: 80%;
-	line-height: 200%;
-	table-layout: fixed;
-}
-	.pagination .item {
-		display: table-cell;
-		line-height: 40px;
-	}
-		.pagination .item.pager-current {
-			font-weight: bold;
-			font-size: 140%;
-		}
-		.pagination .pager-first,
-		.pagination .pager-previous,
-		.pagination .pager-next,
-		.pagination .pager-last {
-			width: 100px;
-		}
-		.pagination .item a {
-			display: block;
-			color: #333;
-			font-style: italic;
-		}
-	.pagination:first-child .item {
-		border-bottom: 1px solid #2f2f2f;
-	}
-	.pagination:last-child .item {
-		border-top: 1px solid #2f2f2f;
-	}
-
-#nav_entries {
-	display: table;
-	width: 250px;
-	height: 40px;
-	position: fixed;
-	bottom: 0;
-	left: 0;
-	margin: 0;
-	background: #1c1c1c;
-	border-top: 1px solid #2f2f2f;
-	text-align: center;
-	line-height: 40px;
-	table-layout: fixed;
-}
-	#nav_entries .item {
-		display: table-cell;
-		width: 30%;
-	}
-		#nav_entries a {
-			display: block;
-		}
-		#nav_entries .i_up {
-			margin: 5px 0 0;
-			vertical-align: top;
-		}
-
-.loading {
-	background: url("loader.gif") center center no-repeat;
-	font-size: 0;
-}
-
-#bigMarkAsRead {
-	display: block;
-	font-style: normal;
-	padding: 32px 0 64px 0;
-	text-align: center;
-	text-decoration: none;
-}
-	#bigMarkAsRead:hover {
-		background: #1c1c1c;
-		color: #888;
-	}
-	.bigTick {
-		font-size: 72pt;
-		line-height: 1.6em;
-	}
-
-/*** NOTIFICATION ***/
-.notification {
-	position: absolute;
-	top: 10px;
-	left: 25%; right: 25%;
-	min-height: 30px;
-	padding: 10px;
-	line-height: 30px;
-	text-align: center;
-	border-radius: 5px;
-	box-shadow: 0 0 5px #666;
-	background: #1a1a1a;
-	color: #888;
-	border: 1px solid #f4f899;
-	font-weight: bold;
-	z-index: 10;
-}
-	.notification.closed {
-		display: none;
-	}
-	.notification a.close {
-		position: absolute;
-		top: -10px; right: -10px;
-		display: inline-block;
-		width: 16px;
-		height: 16px;
-		padding: 5px;
-		background: #1a1a1a;
-		border-radius: 50px;
-		line-height: 16px;
-		border:1px solid #f4f899;
-	}
-
-.toggle_aside, .btn.toggle_aside {
-	display: none;
-}
-
-#actualizeProgress {
-	position: fixed;
-}
-#actualizeProgress progress {
-	max-width: 100%;
-	vertical-align: middle;
-}
-#actualizeProgress .progress {
-	vertical-align: middle;
-}
-
-.logs {
-	border: 1px solid #aaa;
-}
-	.log {
-		padding: 5px 2%;
-		overflow: auto;
-		background: #fafafa;
-		border-bottom: 1px solid #999;
-		color: #333;
-		font-size: 90%;
-	}
-		.log .date {
-			display: block;
-		}
-	.log.error {
-		background: #fdd;
-		color: #844;
-	}
-	.log.warning {
-		background: #ffe;
-		color: #c95;
-	}
-	.log.notice {
-		background: #f4f4f4;
-		color: #aaa;
-	}
-	.log.debug {
-		background: #111;
-		color: #eee;
-	}
-
-.form-group table {
-	border-collapse:collapse;
-	margin:10px 0 0 220px;
-	text-align:center;
-}
-
-.form-group tr, .form-group th, .form-group td {
-	border:1px solid #2f2f2f;
-	font-weight:normal;
-	padding:.5em;
-}
-
-select.number option {
-	text-align:right;
-}
-
-@media(min-width: 841px) {
-	.flux:not(.current):hover .item.title {
-		max-width: calc(100% - 580px);
-		padding-right: 1.5em;
-		position: absolute;
-	}
-}
-
-@media(max-width: 840px) {
-	.header,
-	.aside .btn-important,
-	.aside .feeds .dropdown,
-	.flux_header .item.website span,
-	.item.date {
-		display: none;
-	}
-	.flux_header .item.website {
-		width: 40px;
-		text-align: center;
-	}
-		.flux_header .item.website .favicon {
-			padding: 12px;
-		}
-
-	.nav-login {
-		display: block;
-	}
-
-	.pagination {
-		margin: 0 0 40px;
-	}
-	.pagination .pager-previous, .pagination .pager-next {
-		width: 100px;
-	}
-
-	.toggle_aside, .btn.toggle_aside {
-		display: inline-block;
-	}
-	.aside {
-		position: fixed;
-		top: 0; left: 0;
-		width: 0;
-		overflow: hidden;
-		border-right: none;
-		z-index: 10;
-		transition: width 200ms linear;
-	}
-		.aside.aside_flux {
-			padding: 10px 0 0;
-		}
-		.aside:target {
-			width: 80%;
-			border-right: 1px solid #aaa;
-			overflow: auto;
-		}
-		.aside .toggle_aside {
-			position: absolute;
-			right: 0;
-			display: inline-block;
-			width: 26px;
-			height: 26px;
-			margin: 0 10px 0 0;
-			border: 1px solid #ccc;
-			border-radius: 20px;
-			text-align: center;
-			line-height: 26px;
-		}
-		.aside .categories {
-			margin: 30px 0;
-		}
-
-	#nav_entries {
-		width: 100%;
-	}
-
-	.nav_menu .btn {
-		margin: 5px 10px;
-	}
-	.nav_menu .stick {
-		margin: 0 10px;
-	}
-	.nav_menu .stick .btn {
-		margin: 5px 0;
-	}
-	.nav_menu .search {
-			display: inline-block;
-			max-width: 97%;
-		}
-		.nav_menu .search input {
-			max-width: 97%;
-			width: 90px;
-		}
-		.nav_menu .search input:focus {
-			width: 400px;
-		}
-
-	#panel {
-		left: 5px; right: 5px;
-	}
-
-	.day .date {
-		display: none;
-	}
-	.day .name {
-		height: 2.6em;
-		font-size: 1em;
-		text-shadow: none;
-	}
-
-	.notification,
-	#actualizeProgress {
-		top: 0;
-		left: 0;
-		right: 0;
-		border-radius: 0;
-		border: none;
-		border-bottom: 1px solid #f4f899;
-	}
-	.notification a.close {
-		left: 0; right: 0;
-		top: 0; bottom: 0;
-		width: auto;
-		height: auto;
-		background: transparent;
-		border: none;
-	}
-	.notification a.close .icon {
-		display: none;
-	}
-}
-
-/*** FALLBACK ***/
-.btn {
-	background: #1c1c1c;
-}
-	.btn:hover {
-		background: -moz-linear-gradient(top, #4A5D7A 0%, #26303F 100%);
-		background: -webkit-linear-gradient(top, #4A5D7A 0%, #26303F 100%);
-		background: -o-linear-gradient(top, #4A5D7A 0%, #26303F 100%);
-		background: -ms-linear-gradient(top, #4A5D7A 0%, #26303F 100%);
-	}
-	.btn-important {
-		background: #26303F;
-	}
-		.btn-important:hover {
-			background: -moz-linear-gradient(top, #4A5D7A 0%, #26303F 100%);
-			background: -webkit-linear-gradient(top, #4A5D7A 0%, #26303F 100%);
-			background: -o-linear-gradient(top, #4A5D7A 0%, #26303F 100%);
-			background: -ms-linear-gradient(top, #4A5D7A 0%, #26303F 100%);
-		}
-	.btn-attention {
-		background: #880011;
-	}
-		.btn-attention:hover {
-			background: -moz-linear-gradient(top, #cc0044 0%, #880011 100%);
-			background: -webkit-linear-gradient(top, #cc0044 0%, #880011 100%);
-			background: -o-linear-gradient(top, #cc0044 0%, #880011 100%);
-			background: -ms-linear-gradient(top, #cc0044 0%, #880011 100%);
-		}
-
-.dropdown-menu:after {
-	-moz-transform: rotate(45deg);
-	-webkit-transform: rotate(45deg);
-	-ms-transform: rotate(45deg);
-}
-
-.nav-head {
-	background: #1c1c1c;
-}
-
-input.extend {
-	-moz-transition: width 200ms linear;
-	-webkit-transition: width 200ms linear;
-	-o-transition: width 200ms linear;
-	-ms-transition: width 200ms linear;
-}
-
-@media(max-width: 840px) {
-	.aside {
-		-moz-transition: width 200ms linear;
-		 -webkit-transition: width 200ms linear;
-		-o-transition: width 200ms linear;
-		-ms-transition: width 200ms linear;
-	}
-}
-
-@media print {
-	.header,
-	.aside,
-	.nav_menu,
-	.day,
-	.flux_header,
-	.flux_content .bottom,
-	.pagination {
-		display: none;
-	}
-
-	html, body {
-		background: #fff;
-		color: #000;
-		font-family: Serif;
-		font-size: 12pt;
-	}
-
-	#global,
-	.flux_content {
-		display: block !important;
-	}
-
-	.flux_content .content {
-		width: 100% !important;
-		text-align: justify;
-	}
-
-	.flux_content .content a {
-		color: #000;
-	}
-	.flux_content .content a:after {
-		content: " (" attr(href) ") ";
-		text-decoration: underline;
-	}
-}
-
-.stat {
-	border:1px solid #2f2f2f;
-	border-radius:10px;
-	margin:10px 0;
-	padding:0 5px;
-}
-.stat > h2 {
-	border-bottom:1px solid #2f2f2f;
-	margin:0 -5px;
-	padding-left:5px;
-}
-.stat > div {
-	margin:5px 0;
-}
-.stat > table {
-	border-collapse:collapse;
-	margin:5px 0;
-	width:100%;
-}
-.stat > table > thead > tr {
-	border-bottom:2px solid #2f2f2f;
-}
-.stat > table > tbody > tr {
-	border-bottom:1px solid #2f2f2f;
-}
-.stat > table > tbody > tr:last-child {
-	border-bottom:0;
-}
-.stat > table th, .stat > table td {
-	border-left:2px solid #2f2f2f;
-	padding:5px;
-}
-.stat > table th:first-child, .stat > table td:first-child {
-	border-left:0;
-}
-.stat > table td.numeric{
-	margin:5px 0;
-	text-align:center;
-}

+ 0 - 525
p/themes/Dark/global.css

@@ -1,525 +0,0 @@
-@charset "UTF-8";
-
-/* FONTS */
-@font-face {
-	font-family: "OpenSans";
-	src: url("../fonts/openSans.woff") format("woff");
-}
-
-
-* {
-	margin: 0;
-	padding: 0;
-}
-html, body {
-	height: 100%;
-	font-size: 95%;
-	font-family: "OpenSans", "Cantarell", "Helvetica", "Arial", sans-serif;
-	color: #888;
-}
-
-/* LIENS */
-a {
-	color: #6986B2;
-	text-decoration: none;
-}
-	a:hover {
-		text-decoration: underline;
-	}
-
-/* LISTES */
-ul, ol, dl {
-	margin: 10px 0 10px 30px;
-	line-height: 190%;
-}
-	dd {
-		margin: 0 0 10px 30px;
-	}
-
-/* TITRES */
-h1, h2, h3 {
-	min-height: 40px;
-	margin: 15px 0 5px;
-	line-height: 40px;
-}
-
-/* IMG */
-figure {
-	margin: 5px 0 10px;
-	text-align: center;
-}
-	figcaption {
-		display: inline-block;
-		padding: 3px 20px;
-		color: #999;
-		font-style: italic;
-		border-bottom: 1px solid #ccc;
-	}
-img {
-	height: auto;
-	max-width: 100%;
-	vertical-align: middle;
-}
-	a img {
-		border: none;
-	}
-
-/* VIDEOS */
-iframe, embed, object, video {
-	max-width: 100%;
-}
-
-/* FORMULAIRES */
-legend {
-	display: block;
-	width: 100%;
-	margin: 20px 0 5px;
-	padding: 5px 0;
-	border-bottom: 1px solid #2f2f2f;
-	font-size: 150%;
-	clear: both;
-}
-label {
-	display: block;
-	min-height: 25px;
-	padding: 5px 0;
-	font-size: 14px;
-	line-height: 25px;
-	cursor: pointer;
-}
-input {
-	width: 180px;
-}
-textarea {
-	width: 360px;
-	height: 100px;
-}
-input, select, textarea {
-	display: inline-block;
-	max-width: 100%;
-	min-height: 25px;
-	padding: 5px;
-	background: #333;
-	border: 1px solid #000;
-	border-radius: 3px;
-	color: #999;
-	line-height: 25px;
-	vertical-align: middle;
-	box-shadow: 0 2px 2px #1d1d1d inset;
-}
-	option {
-		padding:0 .5em 0 .5em;
-	}
-	input[type="radio"],
-	input[type="checkbox"] {
-		width: 15px !important;
-		min-height: 15px !important;
-	}
-	input:focus, select:focus, textarea:focus {
-		color: #6986b2;
-		border-color: #2f2f2f;
-	}
-	input:invalid, select:invalid {
-		border-color: red;
-		box-shadow: 0 0 2px 1px red;
-	}
-	input:focus.extend {
-		width: 300px;
-		transition: width 200ms linear;
-	}
-
-.form-group {
-	margin: 0;
-}
-	.form-group:after {
-		content: "";
-		display: block;
-		clear: both;
-	}
-	.form-group.form-actions {
-		min-width: 250px;
-		padding: 5px 0;
-		background: #1a1a1a;
-		border-top: 1px solid #2f2f2f;
-	}
-		.form-group.form-actions .btn {
-			margin: 0 10px;
-		}
-	.form-group .group-name {
-		display: block;
-		float: left;
-		width: 200px;
-		padding: 10px 0;
-		text-align: right;
-	}
-	.form-group .group-controls {
-		min-width: 250px;
-		min-height: 25px;
-		margin: 0 0 0 220px;
-		padding: 5px 0;
-	}
-		.form-group .group-controls .control {
-			display: block;
-			min-height: 30px;
-			padding: 5px 0;
-			line-height: 25px;
-			font-size: 14px;
-		}
-
-.stick {
-	display: inline-block;
-	white-space: nowrap;
-	font-size: 0px;
-	vertical-align: middle;
-}
-	.stick input,
-	.stick .btn {
-		border-radius: 0;
-		font-size: 14px;
-	}
-	.stick .btn:first-child,
-	.stick input:first-child {
-		border-radius: 3px 0 0 3px;
-	}
-		.stick .btn-important:first-child {
-			border-right: 1px solid #000;
-		}
-	.stick .btn:last-child,
-	.stick input:last-child {
-		border-radius: 0 3px 3px 0;
-	}
-	.stick .btn + .btn,
-	.stick .btn + input,
-	.stick .btn + .dropdown > .btn,
-	.stick input + .btn,
-	.stick input + input,
-	.stick input + .dropdown > .btn,
-	.stick .dropdown + .btn,
-	.stick .dropdown + input,
-	.stick .dropdown + .dropdown > .btn {
-		border-left: none;
-	}
-	.stick .btn + .dropdown > .btn {
-		border-left: none;
-		border-radius: 0 3px 3px 0;
-	}
-	.stick .btn + .dropdown a {
-		font-size: 12px;
-	}
-
-.btn {
-	display: inline-block;
-	min-height: 37px;
-	min-width: 15px;
-	padding: 5px 10px;
-	background: linear-gradient(to bottom, #fff 0%, #eee 100%);
-	border-radius: 3px;
-	border: 1px solid #000;
-	color: #888;
-	line-height: 20px;
-	vertical-align: middle;
-	cursor: pointer;
-	overflow: hidden;
-}
-	a.btn {
-		min-height: 25px;
-		line-height: 25px;
-	}
-	.btn:hover {
-		background: linear-gradient(to bottom, #4A5D7A, #26303F);
-		text-decoration: none;
-	}
-	.btn.active,
-	.btn:active,
-	.dropdown-target:target ~ .btn.dropdown-toggle {
-		background: #26303F;
-	}
-
-	.btn-important {
-		background: linear-gradient(to bottom, #0084CC, #0045CC);
-		color: #888888;
-		border: 1px solid #000000;
-	}
-		.btn-important:hover {
-			background: linear-gradient(to bottom, #0066CC, #0045CC);
-		}
-		.btn-important:active {
-			background: #0044CB;
-			box-shadow: none;
-		}
-
-	.btn-attention {
-		background: linear-gradient(to bottom, #E95B57, #BD362F);
-		color: #888888;
-		border: 1px solid #000000;
-	}
-		.btn-attention:hover {
-			background: linear-gradient(to bottom, #D14641, #BD362F);
-		}
-		.btn-attention:active {
-			background: #BD362F;
-			box-shadow: none;
-		}
-
-/* NAVIGATION */
-.nav-list .nav-header,
-.nav-list .item {
-	display: block;
-	height: 35px;
-	line-height: 35px;
-}
-	.nav-list .item:hover {
-		background: #1a1a1a;
-	}
-		.nav-list .item:hover a {
-			color: #26303F;
-		}
-	.nav-list .item.active {
-		background: #26303F;
-		color: #1a1a1a;
-	}
-		.nav-list .item.active a {
-			color: #888;
-		}
-	.nav-list .disable {
-		color: #aaa;
-		background: #fafafa;
-		text-align: center;
-	}
-	.nav-list .item > * {
-		display: block;
-		padding: 0 10px;
-		overflow: hidden;
-		white-space: nowrap;
-		text-overflow: ellipsis;
-	}
-		.nav-list a:hover {
-			text-decoration: none;
-		}
-	.nav-list .item.error a {
-		color: #BD362F;
-	}
-		.nav-list .item.active.error a {
-			color: #fff;
-			background: #BD362F;
-		}
-	.nav-list .item.empty a {
-		color: #f39c12;
-	}
-		.nav-list .item.active.empty a {
-			color: #fff;
-			background: #f39c12;
-		}
-
-	.nav-list .nav-header {
-		padding: 0 10px;
-		background: #1a1a1a;
-		border-bottom: 1px solid #2f2f2f;
-		font-weight: bold;
-	}
-	.nav-list .separator {
-		display: block;
-		height: 0;
-		margin: 5px 0;
-		border-bottom: 1px solid #2f2f2f;
-	}
-
-	.nav-list .nav-form {
-		padding: 3px;
-		text-align: center;
-	}
-
-.nav-head {
-	display: block;
-	margin: 0;
-	background: linear-gradient(to bottom, #fff, #f0f0f0);
-	border-bottom: 1px solid #2f2f2f;
-	text-align: right;
-}
-	.nav-head .item {
-		display: inline-block;
-		padding: 5px 10px;
-	}
-
-/* HORIZONTAL-LIST */
-.horizontal-list {
-	display: table;
-	table-layout: fixed;
-	margin: 0;
-	padding: 0;
-	width: 100%;
-}
-	.horizontal-list .item {
-		display: table-cell;
-		vertical-align: middle;
-	}
-
-/* DROPDOWN */
-.dropdown {
-	position: relative;
-	display: inline-block;
-}
-	.dropdown-target {
-		display: none;
-	}
-
-	.dropdown-menu {
-		display: none;
-		min-width: 200px;
-		margin: 5px 0 0;
-		padding: 5px 0;
-		position: absolute;
-		right: 0px;
-		background: #1a1a1a;
-		border: 1px solid #888;
-		border-radius: 5px;
-		text-align: left;
-	}
-	.dropdown-menu:after {
-		content: "";
-		position: absolute;
-		top: -6px;
-		right: 13px;
-		width: 10px;
-		height: 10px;
-		background: #1a1a1a;
-		border-top: 1px solid #888;
-		border-left: 1px solid #888;
-		z-index: -10;
-		transform: rotate(45deg);
-	}
-		.dropdown-header {
-			display: block;
-			padding: 0 5px;
-			color: #888;
-			font-weight: bold;
-			font-size: 14px;
-			line-height: 30px;
-		}
-		.dropdown-menu .item {
-			display: block;
-			height: 30px;
-			font-size: 90%;
-			line-height: 30px;
-		}
-			.dropdown-menu > .item > a {
-				display: block;
-				padding: 0 25px;
-				line-height: 30px;
-			}
-			.dropdown-menu > .item.share > a {
-				display: list-item;
-				list-style-position:inside;
-				list-style-type:decimal;
-			}
-			.dropdown-menu > .item:hover {
-				background: #26303F;
-				color: #888;
-			}
-				.dropdown-menu > .item:hover > a {
-					color: #888;
-					text-decoration: none;
-				}
-		.dropdown-menu .input {
-			display: block;
-			height: 40px;
-			font-size: 90%;
-			line-height: 30px;
-		}
-			.dropdown-menu .input select,
-			.dropdown-menu .input input {
-				display: block;
-				height: 20px;
-				width: 95%;
-				margin: auto;
-				padding: 2px 5px;
-				border-radius: 3px;
-			}
-			.dropdown-menu .input select {
-				width: 70%;
-				height: auto;
-			}
-		.dropdown-menu .separator {
-			display: block;
-			height: 0;
-			margin: 5px 0;
-			border-bottom: 1px solid #888;
-		}
-		.dropdown-target:target ~ .dropdown-menu {
-			display: block;
-			z-index: 10;
-		}
-	.dropdown-close {
-		display: inline;
-	}
-		.dropdown-close a {
-			font-size: 0;
-			position: fixed;
-			top: 0; bottom: 0;
-			left: 0; right: 0;
-			display: block;
-			z-index: -10;
-		}
-
-/* ALERTS */
-.alert {
-	display: block;
-	width: 90%;
-	margin: 15px auto;
-	padding: 10px 15px;
-	background: #1a1a1a;
-	border: 1px solid #ccc;
-	border-right: 1px solid #aaa;
-	border-bottom: 1px solid #aaa;
-	border-radius: 5px;
-	color: #aaa;
-}
-	.alert-head {
-		margin: 0;
-		font-weight: bold;
-		font-size: 110%;
-	}
-	.alert > a {
-		color: inherit;
-		text-decoration: underline;
-	}
-	.alert-warn {
-		border: 1px solid #c95;
-		color: #c95;
-	}
-	.alert-success {
-		border: 1px solid #484;
-		color: #484;
-	}
-	.alert-error {
-		border: 1px solid #844;
-		color: #844;
-	}
-
-/* ICÔNES */
-.icon {
-	display: inline-block;
-	width: 16px;
-	height: 16px;
-	vertical-align: middle;
-	line-height: 16px;
-}
-
-/* Prompt (centré) */
-.prompt {
-	text-align: center;
-}
-	.prompt label {
-		text-align: left;
-	}
-	.prompt form {
-		margin: 1em auto 2.5em auto;
-		width: 10em;
-	}
-	.prompt input {
-		margin: .4em auto 1.1em auto;
-		width: 100%;
-	}
-	.prompt p {
-		margin: 20px 0;
-	}

+ 12 - 0
p/themes/Dark/icons/icon.svg

@@ -0,0 +1,12 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
+	<title>Logo FreshRSS</title>
+	<circle fill="#6986B2" cx="128" cy="128" r="33"/>
+	<g fill="none" stroke="#6986B2" stroke-width="24">
+		<g stroke-opacity="0.3">
+			<path d="M12,128 A116,116 0 1,1 128,244"/>
+			<path d="M54,128 A74,74 0 1,1 128,202"/>
+		</g>
+		<path d="M128,12 A116,116 0 0,1 244,128"/>
+		<path d="M128,54 A74,74 0 0,1 202,128"/>
+	</g>
+</svg>

+ 2 - 2
p/themes/Dark/metadata.json

@@ -2,6 +2,6 @@
 	"name": "Dark",
 	"author": "AD",
 	"description": "Le coté obscur du thème “Origine”",
-	"version": 0.1,
-	"files": ["global.css", "freshrss.css"]
+	"version": 0.2,
+	"files": ["template.css", "dark.css"]
 }

+ 695 - 0
p/themes/Dark/template.css

@@ -0,0 +1,695 @@
+@charset "UTF-8";
+
+/*=== GENERAL */
+/*============*/
+html, body {
+	margin: 0;
+	padding: 0;
+	font-size: 100%;
+}
+
+/*=== Links */
+a {
+	text-decoration: none;
+}
+a:hover {
+	text-decoration: underline;
+}
+
+/*=== Lists */
+ul, ol, dd {
+	margin: 0;
+	padding: 0;
+}
+
+/*=== Titles */
+h1 {
+	margin: 0.6em 0 0.3em;
+	font-size: 1.5em;
+	line-height: 1.6em;
+}
+h2 {
+	margin: 0.5em 0 0.25em;
+	font-size: 1.3em;
+	line-height: 2em;
+}
+h3 {
+	margin: 0.5em 0 0.25em;
+	font-size: 1.1em;
+	line-height: 2em;
+}
+
+/*=== Paragraphs */
+p {
+	margin: 1em 0 0.5em;
+	font-size: 1em;
+}
+
+/*=== Images */
+img {
+	height: auto;
+	max-width: 100%;
+}
+img.favicon {
+	height: 16px;
+	width: 16px;
+	vertical-align: middle;
+}
+
+/*=== Videos */
+iframe, embed, object, video {
+	max-width: 100%;
+}
+
+/*=== Forms */
+legend {
+	display: block;
+	width: 100%;
+	clear: both;
+}
+label {
+	display: block;
+}
+input {
+	width: 180px;
+}
+textarea {
+	width: 300px;
+}
+input, select, textarea {
+	display: inline-block;
+	max-width: 100%;
+}
+input[type="radio"],
+input[type="checkbox"] {
+	width: 15px !important;
+	min-height: 15px !important;
+}
+input.extend:focus {
+	width: 300px;
+}
+
+/*=== COMPONENTS */
+/*===============*/
+/*=== Forms */
+.form-group:after {
+	content: "";
+	display: block;
+	clear: both;
+}
+.form-group.form-actions {
+	min-width: 250px;
+}
+.form-group .group-name {
+	display: block;
+	float: left;
+	width: 200px;
+}
+.form-group .group-controls {
+	min-width: 250px;
+	margin: 0 0 0 220px;
+}
+.form-group .group-controls .control {
+	display: block;
+}
+
+/*=== Buttons */
+.stick {
+	display: inline-block;
+	white-space: nowrap;
+}
+.btn,
+a.btn {
+	display: inline-block;
+	cursor: pointer;
+	overflow: hidden;
+}
+.btn-important {
+	font-weight: bold;
+}
+
+/*=== Navigation */
+.nav-list .nav-header,
+.nav-list .item {
+	display: block;
+}
+.nav-list .item,
+.nav-list .item > a {
+	display: block;
+	overflow: hidden;
+	white-space: nowrap;
+	text-overflow: ellipsis;
+}
+.nav-head {
+	display: block;
+}
+.nav-head .item {
+	display: inline-block;
+}
+
+/*=== Horizontal-list */
+.horizontal-list {
+	display: table;
+	table-layout: fixed;
+	width: 100%;
+}
+.horizontal-list .item {
+	display: table-cell;
+}
+
+/*=== Dropdown */
+.dropdown {
+	position: relative;
+	display: inline-block;
+}
+.dropdown-target {
+	display: none;
+}
+.dropdown-menu {
+	display: none;
+	min-width: 200px;
+	margin: 0;
+	position: absolute;
+	right: 0;
+	background: #fff;
+	border: 1px solid #aaa;
+}
+.dropdown-header {
+	display: block;
+}
+.dropdown-menu > .item {
+	display: block;
+}
+.dropdown-menu > .item > a,
+.dropdown-menu > .item > span {
+	display: block;
+}
+.dropdown-menu > .item[aria-checked="true"] > a:before {
+	content: '✓';
+}
+.dropdown-menu .input {
+	display: block;
+}
+.dropdown-menu .input select,
+.dropdown-menu .input input {
+	display: block;
+	max-width: 95%;
+}
+.dropdown-target:target ~ .dropdown-menu {
+	display: block;
+	z-index: 10;
+}
+.dropdown-close {
+	display: inline;
+}
+.dropdown-close a {
+	font-size: 0;
+	position: fixed;
+	top: 0; bottom: 0;
+	left: 0; right: 0;
+	display: block;
+	z-index: -10;
+}
+.separator {
+	display: block;
+	height: 0;
+	border-bottom: 1px solid #aaa;
+}
+
+/*=== Alerts */
+.alert {
+	display: block;
+	width: 90%;
+}
+.group-controls .alert {
+	width: 100%
+}
+.alert-head {
+	margin: 0;
+	font-weight: bold;
+}
+.alert ul {
+	margin: 5px 20px;
+}
+
+/*=== Icons */
+.icon {
+	display: inline-block;
+	width: 16px;
+	height: 16px;
+	vertical-align: middle;
+	line-height: 16px;
+}
+
+/*=== Pagination */
+.pagination {
+	display: table;
+	width: 100%;
+	margin: 0;
+	padding: 0;
+	table-layout: fixed;
+}
+.pagination .item {
+	display: table-cell;
+}
+.pagination .pager-first,
+.pagination .pager-previous,
+.pagination .pager-next,
+.pagination .pager-last {
+	width: 100px;
+}
+
+/*=== STRUCTURE */
+/*===============*/
+/*=== Header */
+.header {
+	display: table;
+	width: 100%;
+	table-layout: fixed;
+}
+.header > .item {
+	display: table-cell;
+}
+.header > .item.title {
+	width: 250px;
+	white-space: nowrap;
+}
+.header > .item.title h1 {
+	display: inline-block;
+}
+.header > .item.title .logo {
+	display: inline-block;
+	height: 32px;
+	width: 32px;
+	vertical-align: middle;
+}
+.header > .item.configure {
+	width: 100px;
+}
+
+/*=== Body */
+#global {
+	display: table;
+	width: 100%;
+	height: 100%;
+	table-layout: fixed;
+}
+.aside {
+	display: table-cell;
+	height: 100%;
+	width: 250px;
+	vertical-align: top;
+}
+.aside.aside_flux {
+	background: #fff;
+}
+
+/*=== Aside main page (categories) */
+.categories {
+	list-style: none;
+	margin: 0;
+}
+.category {
+	display: block;
+	overflow: hidden;
+	white-space: nowrap;
+	text-overflow: ellipsis;
+}
+.category .btn:not([data-unread="0"]):after {
+	content: attr(data-unread);
+}
+
+/*=== Aside main page (feeds) */
+.categories .feeds {
+	width: 100%;
+	list-style: none;
+}
+.categories .feeds:not(.active) {
+	display: none;
+}
+.categories .feeds .feed {
+	display: inline-block;
+	overflow: hidden;
+	white-space: nowrap;
+	text-overflow: ellipsis;
+	vertical-align: middle;
+}
+.categories .feeds .feed:not([data-unread="0"]):before {
+	content: "(" attr(data-unread) ") ";
+}
+.categories .feeds .dropdown-menu {
+	left: 0;
+}
+.categories .feeds .item .dropdown-toggle > .icon {
+	visibility: hidden;
+	cursor: pointer;
+	vertical-align: top;
+}
+.categories .feeds .item .dropdown-target:target ~ .dropdown-toggle > .icon,
+.categories .feeds .item:hover .dropdown-toggle > .icon,
+.categories .feeds .item.active .dropdown-toggle > .icon {
+	visibility: visible;
+}
+
+/*=== New article notification */
+#new-article {
+	display: none;
+}
+#new-article > a {
+	display: block;
+}
+
+/*=== Day indication */
+.day .name {
+	position: absolute;
+	right: 0;
+	width: 50%;
+	overflow: hidden;
+	white-space: nowrap;
+	text-overflow: ellipsis;
+}
+
+/*=== Feed article header and footer */
+.flux_header {
+	position: relative;
+}
+.flux .item {
+	line-height: 40px;
+	white-space: nowrap;
+}
+.flux .item.manage,
+.flux .item.link {
+	width: 40px;
+	text-align: center;
+}
+.flux .item.website {
+	width: 200px;
+}
+.flux.not_read .item.title,
+.flux.current .item.title {
+	font-weight: bold;
+}
+.flux:not(.current):hover .item.title {
+	position: absolute;
+	max-width: calc(100% - 320px);
+	background: #fff;
+}
+.flux .item.title a {
+	color: #000;
+	text-decoration: none;
+}
+.flux .item.date {
+	width: 145px;
+	text-align: right;
+}
+.flux .item > a {
+	display: block;
+}
+.flux .item > a {
+	display: block;
+	text-decoration: none;
+	white-space: nowrap;
+	text-overflow: ellipsis;
+	overflow: hidden;
+}
+.flux .item.share > a {
+	display: list-item;
+	list-style-position: inside;
+	list-style-type: decimal;
+}
+
+/*=== Feed article content */
+.hide_posts > .flux:not(.active) > .flux_content {
+	display: none;
+}
+.content {
+	min-height: 20em;
+	margin: auto;
+	line-height: 1.7em;
+	word-wrap: break-word;
+}
+.content.large {
+	max-width: 1000px;
+}
+.content.medium {
+	max-width: 800px;
+}
+.content.thin {
+	max-width: 550px;
+}
+.content ul,
+.content ol,
+.content dd {
+	margin: 0 0 0 15px;
+	padding: 0 0 5px 15px;
+}
+.content pre {
+	overflow: auto;
+}
+
+/*=== Notification and actualize notification */
+.notification {
+	position: absolute;
+	top: 1em;
+	left: 25%; right: 25%;
+	z-index: 10;
+	background: #fff;
+	border: 1px solid #aaa;
+}
+.notification.closed {
+	display: none;
+}
+.notification a.close {
+	position: absolute;
+	top: 0; bottom: 0;
+	right: 0;
+	display: inline-block;
+}
+
+#actualizeProgress {
+	position: fixed;
+}
+#actualizeProgress progress {
+	max-width: 100%;
+	vertical-align: middle;
+}
+#actualizeProgress .progress {
+	vertical-align: middle;
+}
+
+/*=== Navigation menu (for articles) */
+#nav_entries {
+	position: fixed;
+	bottom: 0; left: 0;
+	display: table;
+	width: 250px;
+	background: #fff;
+	table-layout: fixed;
+}
+#nav_entries .item {
+	display: table-cell;
+	width: 30%;
+}
+#nav_entries a {
+	display: block;
+}
+
+/*=== "Load more" part */
+#load_more {
+	min-height: 40px;
+}
+.loading {
+	background: url("loader.gif") center center no-repeat;
+	font-size: 0;
+}
+#bigMarkAsRead {
+	display: block;
+	padding: 3em 0;
+	text-align: center;
+}
+.bigTick {
+	font-size: 7em;
+	line-height: 1.6em;
+}
+
+/*=== Statistiques */
+.stat > table {
+	width: 100%;
+}
+
+/*=== GLOBAL VIEW */
+/*================*/
+/*=== Category boxes */
+#stream.global .box-category {
+	display: inline-block;
+	width: 19em;
+	max-width: 95%;
+	margin: 20px 10px;
+	border: 1px solid #ccc;
+	vertical-align: top;
+}
+#stream.global .category {
+	width: 100%;
+}
+#stream.global .btn {
+	display: block;
+}
+#stream.global .box-category .feeds {
+	display: block;
+	overflow: auto;
+}
+#stream.global .box-category .feed {
+	width: 19em;
+	max-width: 90%;
+}
+
+/*=== Panel */
+#overlay {
+	display: none;
+	position: fixed;
+	top: 0; bottom: 0;
+	left: 0; right: 0;
+	background: rgba(0, 0, 0, 0.9);
+}
+#panel {
+	display: none;
+	position: fixed;
+	top: 1em; bottom: 1em;
+	left: 2em; right: 2em;
+	overflow: auto;
+	background: #fff;
+}
+#panel .close {
+	position: fixed;
+	top: 0; bottom: 0;
+	left: 0; right: 0;
+	display: block;
+}
+#panel .close img {
+	display: none;
+}
+
+/*=== DIVERS */
+/*===========*/
+.nav-login,
+.nav_menu .search,
+.nav_menu .toggle_aside {
+	display: none;
+}
+
+.aside .toggle_aside {
+	position: absolute;
+	right: 0;
+	display: none;
+	width: 30px;
+	height: 30px;
+	line-height: 30px;
+	text-align: center;
+}
+
+/*=== MOBILE */
+/*===========*/
+@media(max-width: 840px) {
+	.header,
+	.aside .btn-important,
+	.aside .feeds .dropdown,
+	.flux_header .item.website span,
+	.item.date, .day .date,
+	.dropdown-menu > .no-mobile,
+	.no-mobile {
+		display: none;
+	}
+	.nav-login {
+		display: block;
+	}
+	.nav_menu .toggle_aside,
+	.aside .toggle_aside,
+	.nav_menu .search,
+	#panel .close img {
+		display: inline-block;
+	}
+
+	.aside {
+		position: fixed;
+		top: 0; bottom: 0;
+		left: 0;
+		width: 0;
+		overflow: hidden;
+		z-index: 100;
+	}
+	.aside:target {
+		width: 90%;
+		overflow: auto;
+	}
+	.aside .categories {
+		margin: 10px 0 75px;
+	}
+
+	.flux_header .item.website {
+		width: 40px;
+	}
+
+	.flux:not(.current):hover .item.title {
+		position: relative;
+		width: auto;
+		white-space: nowrap;
+	}
+
+	.notification {
+		top: 0;
+		left: 0;
+		right: 0;
+	}
+
+	#nav_entries {
+		width: 100%;
+	}
+
+	#stream.global .box-category {
+		margin: 10px 0;
+	}
+
+	#panel {
+		top: 0; bottom: 0;
+		left: 0; right: 0;
+	}
+	#panel .close {
+		top: 0; right: 0;
+		left: auto; bottom: auto;
+		display: inline-block;
+		width: 30px;
+		height: 30px;
+	}
+}
+
+/*=== PRINTER */
+/*============*/
+@media print {
+	.header, .aside,
+	.nav_menu, .day,
+	.flux_header,
+	.flux_content .bottom,
+	.pagination,
+	#nav_entries {
+		display: none;
+	}
+	html, body {
+		background: #fff;
+		color: #000;
+		font-family: Serif;
+	}
+	#global,
+	.flux_content {
+		display: block !important;
+	}
+	.flux_content .content {
+		width: 100% !important;
+	}
+	.flux_content .content a {
+		color: #000;
+	}
+	.flux_content .content a:after {
+		content: " [" attr(href) "] ";
+		font-style: italic;
+	}
+}

+ 950 - 0
p/themes/Flat/flat.css

@@ -0,0 +1,950 @@
+@charset "UTF-8";
+
+/*=== FONTS */
+@font-face {
+	font-family: "OpenSans";
+	src: url("../fonts/openSans.woff") format("woff");
+}
+
+/*=== GENERAL */
+/*============*/
+html, body {
+	height: 100%;
+	font-family: "OpenSans", "Cantarell", "Helvetica", "Arial", sans-serif;
+	background: #fafafa;
+}
+
+/*=== Links */
+a {
+	color: #2980b9;
+	outline: none;
+}
+
+/*=== Forms */
+legend {
+	display: inline-block;
+	width: auto;
+	margin: 20px 0 5px;
+	padding: 5px 20px;
+	font-size: 1.4em;
+	clear: both;
+	background: #ecf0f1;
+	border-radius: 20px;
+}
+label {
+	min-height: 25px;
+	padding: 5px 0;
+	cursor: pointer;
+	color: #444;
+}
+textarea {
+	width: 360px;
+	height: 100px;
+}
+input, select, textarea {
+	min-height: 25px;
+	padding: 5px;
+	line-height: 25px;
+	vertical-align: middle;
+	background: #fff;
+	border: none;
+	border-bottom: 3px solid #ddd;
+	color: #666;
+	border-radius: 5px;
+}
+option {
+	padding: 0 .5em;
+}
+input:focus, select:focus, textarea:focus {
+	color: #333;
+	border-color: #2980b9;
+}
+input:invalid, select:invalid {
+	color: #f00;
+	border-color: #f00;
+	box-shadow: none;
+}
+input:disabled, select:disabled {
+	background: #eee;
+}
+input.extend {
+	transition: width 200ms linear;
+	-moz-transition: width 200ms linear;
+	-webkit-transition: width 200ms linear;
+	-o-transition: width 200ms linear;
+	-ms-transition: width 200ms linear;
+}
+
+/*=== Tables */
+table {
+	border-collapse: collapse;
+}
+
+tr, th, td {
+	padding: 0.5em;
+	border: 1px solid #ddd;
+}
+th {
+	background: #f6f6f6;
+}
+form td,
+form th {
+	font-weight: normal;
+	text-align: center;
+}
+
+/*=== COMPONENTS */
+/*===============*/
+/*=== Forms */
+.form-group {
+	padding: 5px;
+	border: 1px solid transparent;
+	border-radius: 3px;
+}
+.form-group:after {
+	content: "";
+	display: block;
+	clear: both;
+}
+.form-group:hover {
+	background: #fff;
+	border: 1px solid #eee;
+	border-radius: 3px;
+	border: 1px solid #eee;
+}
+.form-group.form-actions {
+	margin: 15px 0 25px;
+	padding: 5px 0;
+	background: #ecf0f1;
+	border-top: 3px solid #bdc3c7;
+	border-radius: 5px 5px 0 0;
+}
+.form-group.form-actions .btn {
+	margin: 0 10px;
+}
+.form-group .group-name {
+	padding: 10px 0;
+	text-align: right;
+}
+.form-group .group-controls {
+	min-height: 25px;
+	padding: 5px 0;
+}
+.form-group .group-controls .control {
+	line-height: 2.0em;
+}
+.form-group table {
+	margin: 10px 0 0 220px;
+}
+
+/*=== Buttons */
+.stick {
+	vertical-align: middle;
+	font-size: 0;
+}
+.stick input,
+.stick .btn {
+	border-radius: 0;
+}
+.stick .btn:first-child,
+.stick input:first-child {
+	border-radius: 5px 0 0 5px;
+}
+.stick .btn:last-child,
+.stick input:last-child,
+.stick .btn + .dropdown > .btn {
+	border-radius: 0 5px 5px 0;
+}
+.stick .btn + input,
+.stick input + input,
+.stick .dropdown + input {
+	border-left: 1px solid #ddd;
+}
+
+.btn {
+	display: inline-block;
+	min-height: 38px;
+	min-width: 15px;
+	margin: 0;
+	padding: 5px 10px;
+	font-size: 0.9rem;
+	vertical-align: middle;
+	cursor: pointer;
+	overflow: hidden;
+	background: #3498db;
+	border-radius: 5px;
+	border: none;
+	border-bottom: 3px solid #2980b9;
+	color: #fff;
+}
+a.btn {
+	min-height: 25px;
+	line-height: 25px;
+}
+.btn:hover {
+	text-decoration: none;
+}
+.btn.active,
+.btn:active,
+.btn:hover,
+.dropdown-target:target ~ .btn.dropdown-toggle {
+	background: #2980b9;
+}
+
+.btn-important {
+	font-weight: normal;
+	background: #e67e22;
+	color: #fff;
+	border-bottom: 3px solid #d35400;
+}
+.btn-important:hover,
+.btn-important:active {
+	background: #d35400;
+}
+
+.btn-attention {
+	background: #e74c3c;
+	color: #fff;
+	border-bottom: 3px solid #c0392b;
+}
+.btn-attention:hover,
+.btn-attention:active {
+	background: #c0392b;
+}
+
+/*=== Navigation */
+.nav-list .nav-header,
+.nav-list .item {
+	height: 2.5em;
+	line-height: 2.5em;
+	font-size: 0.9rem;
+}
+.nav-list .item:hover,
+.nav-list .item.active {
+	background: #2980b9;
+	color: #fff;
+}
+.nav-list .item:hover a,
+.nav-list .item.active a {
+	color: #fff;
+}
+.nav-list .disable {
+	text-align: center;
+	background: #fafafa;
+	color: #aaa;
+}
+.nav-list .item > a {
+	padding: 0 10px;
+}
+.nav-list a:hover {
+	text-decoration: none;
+}
+.nav-list .item.empty a {
+	color: #f39c12;
+}
+.nav-list .item:hover.empty a,
+.nav-list .item.active.empty a {
+	color: #fff;
+	background: #f39c12;
+}
+.nav-list .item.error a {
+	color: #bd362f;
+}
+.nav-list .item:hover.error a,
+.nav-list .item.active.error a {
+	color: #fff;
+	background: #bd362f;
+}
+
+.nav-list .nav-header {
+	padding: 0 10px;
+	font-weight: bold;
+	background: #34495e;
+	color: #fff;
+}
+
+.nav-list .nav-form {
+	padding: 3px;
+	text-align: center;
+}
+
+.nav-head {
+	margin: 0;
+	text-align: right;
+	background: #34495e;
+	color: #fff;
+}
+.nav-head a {
+	color: #fff;
+}
+.nav-head .item {
+	padding: 5px 10px;
+	font-size: 0.9rem;
+	line-height: 1.5rem;
+}
+
+/*=== Horizontal-list */
+.horizontal-list {
+	margin: 0;
+	padding: 0;
+}
+.horizontal-list .item {
+	vertical-align: middle;
+}
+
+/*=== Dropdown */
+.dropdown-menu {
+	margin: 5px 0 0;
+	padding: 5px 0;
+	font-size: 0.8rem;
+	text-align: left;
+	border: 1px solid #95a5a6;
+	border-radius: 3px;
+}
+.dropdown-menu:after {
+	content: "";
+	position: absolute;
+	top: -6px;
+	right: 13px;
+	width: 10px;
+	height: 10px;
+	z-index: -10;
+	transform: rotate(45deg);
+	-moz-transform: rotate(45deg);
+	-webkit-transform: rotate(45deg);
+	-ms-transform: rotate(45deg);
+	background: #fff;
+	border-top: 1px solid #95a5a6;
+	border-left: 1px solid #95a5a6;
+}
+.dropdown-header {
+	padding: 0 5px 5px;
+	font-weight: bold;
+	text-align: left;
+	color: #34495e;
+}
+.dropdown-menu > .item > a {
+	padding: 0 25px;
+	line-height: 2.5em;
+}
+.dropdown-menu > .item > span {
+	padding: 0 25px;
+	line-height: 2em;
+}
+.dropdown-menu > .item[aria-checked="true"] > a:before {
+	font-weight: bold;
+	margin: 0 0 0 -14px;
+}
+.dropdown-menu > .item:hover > a {
+	text-decoration: none;
+	background: #2980b9;
+	color: #fff;
+}
+.dropdown-menu .input select,
+.dropdown-menu .input input {
+	margin: 0 auto 5px;
+	padding: 2px 5px;
+	border-radius: 3px;
+}
+
+.separator {
+	margin: 5px 0;
+	border-bottom: 1px solid #ddd;
+}
+
+/*=== Alerts */
+.alert {
+	margin: 15px auto;
+	padding: 10px 15px;
+	font-size: 0.9em;
+	background: #f4f4f4;
+	border: 1px solid #ccc;
+	border-right: 1px solid #aaa;
+	border-bottom: 1px solid #aaa;
+	border-radius: 5px;
+	color: #aaa;
+	text-shadow: 0 0 1px #eee;
+}
+.alert-head {
+	font-size: 1.15em;
+}
+.alert > a {
+	text-decoration: underline;
+	color: inherit;
+}
+.alert-warn {
+	background: #ffe;
+	border: 1px solid #eeb;
+	color: #c95;
+}
+.alert-success {
+	background: #dfd;
+	border: 1px solid #cec;
+	color: #484;
+}
+.alert-error {
+	background: #fdd;
+	border: 1px solid #ecc;
+	color: #844;
+}
+
+/*=== Pagination */
+.pagination {
+	text-align: center;
+	font-size: 0.8em;
+	background: #ecf0f1;
+	color: #000;
+}
+.content .pagination {
+	margin: 0;
+	padding: 0;
+}
+.pagination .item.pager-current {
+	font-weight: bold;
+	font-size: 1.5em;
+	background: #34495e;
+	color: #ecf0f1;
+}
+.pagination .item a {
+	display: block;
+	font-style: italic;
+	line-height: 3em;
+	text-decoration: none;
+	color: #000;
+}
+.pagination .item a:hover {
+	background: #34495e;
+	color: #ecf0f1;
+}
+
+.pagination .loading,
+.pagination a:hover.loading {
+	font-size: 0;
+	background: url("loader.gif") center center no-repeat #34495e;
+}
+
+/*=== STRUCTURE */
+/*===============*/
+/*=== Header */
+.header {
+	height: 85px;
+	background: #ecf0f1;
+}
+.header > .item {
+	padding: 10px;
+	vertical-align: middle;
+	text-align: center;
+}
+.header > .item.title{
+	width: 230px;
+}
+.header > .item.title h1 {
+	margin: 0.5em 0;
+}
+.header > .item.title h1 a {
+	text-decoration: none;
+}
+.header > .item.search input {
+	width: 230px;
+}
+.header .item.search input:focus {
+	width: 350px;
+}
+
+/*=== Body */
+#global {
+	height: calc(100% - 85px);
+}
+.aside {
+	background: #ecf0f1;
+}
+.aside.aside_flux {
+	padding: 10px 0 50px;
+	background: #ecf0f1;
+}
+
+/*=== Aside main page (categories) */
+.categories {
+	text-align: center;
+}
+.category {
+	width: 233px;
+	margin: 10px auto;
+	text-align: left;
+}
+.category .btn:first-child {
+	position: relative;
+	width: 212px;
+}
+.category.stick .btn:first-child {
+	width: 175px;
+}
+.category .btn:first-child:not([data-unread="0"]):after {
+	position: absolute;
+	top: 5px; right: 5px;
+	padding: 1px 5px;
+	background: #3498DB;
+	color: #fff;
+	border-radius: 5px;
+}
+
+/*=== Aside main page (feeds) */
+.categories .feeds .item.active {
+	background: #2980b9;
+}
+.categories .feeds .item.active .feed,
+.categories .feeds .item.empty.active .feed {
+	color: #fff;
+}
+.categories .feeds .item.empty.active {
+	background: #f39c12;
+}
+.categories .feeds .item.error.active {
+	background: #bd362f;
+}
+.categories .feeds .item.empty .feed {
+	color: #e67e22;
+}
+.categories .feeds .item.error .feed {
+	color: #bd362f;
+}
+.categories .feeds .item .feed {
+	margin: 0;
+	width: 165px;
+	line-height: 3em;
+	font-size: 0.8em;
+	text-align: left;
+	text-decoration: none;
+}
+.categories .feeds .feed:not([data-unread="0"]) {
+	font-weight: bold;
+}
+.categories .feeds .dropdown-menu:after {
+	left: 2px;
+}
+.categories .feeds .item .dropdown-target:target ~ .dropdown-toggle > .icon,
+.categories .feeds .item:hover .dropdown-toggle > .icon,
+.categories .feeds .item.active .dropdown-toggle > .icon {
+	vertical-align: middle;
+	background-color: #95a5a6;
+	border-radius: 3px;
+}
+
+/*=== Configuration pages */
+.post {
+	padding: 10px 50px;
+	font-size: 0.9em;
+}
+.post form {
+	margin: 10px 0;
+}
+.post.content {
+	max-width: 550px;
+}
+
+/*=== Prompt (centered) */
+.prompt {
+	text-align: center;
+}
+.prompt label {
+	text-align: left;
+}
+.prompt form {
+	margin: 10px auto 20px auto;
+	width: 180px;
+}
+.prompt input {
+	margin: 5px auto;
+	width: 100%;
+}
+.prompt p {
+	margin: 20px 0;
+}
+
+/*=== New article notification */
+#new-article {
+	text-align: center;
+	font-size: 0.9em;
+	background: #3498db;
+}
+#new-article:hover {
+	background: #2980b9;
+}
+#new-article > a {
+	line-height: 3em;
+	font-weight: bold;
+	color: #fff;
+}
+#new-article > a:hover {
+	text-decoration: none;
+}
+
+/*=== Day indication */
+.day {
+	padding: 0 10px;
+	font-weight: bold;
+	line-height: 3em;
+	border-left: 2px solid #ecf0f1;
+}
+.day .name {
+	padding: 0 10px 0 0;
+	font-size: 1.8em;
+	opacity: 0.3;
+	font-style: italic;
+	text-align: right;
+	color: #aab;
+}
+
+/*=== Index menu */
+.nav_menu {
+	text-align: center;
+	padding: 5px 0;
+}
+
+/*=== Feed articles */
+.flux {
+	border-left: 2px solid #ecf0f1;
+}
+.flux:hover {
+	background: #fff;
+}
+.flux.current {
+	border-left-color: #3498db;
+}
+.flux.not_read {
+	background: #FFF3ED;
+	border-left-color: #FF5300;
+}
+.flux.not_read:not(.current):hover .item.title {
+	background: #FFF3ED;
+}
+.flux.favorite {
+	background: #FFF6DA;
+	border-left-color: #FFC300;
+}
+.flux.favorite:not(.current):hover .item.title {
+	background: #FFF6DA;
+}
+.flux.current {
+	background: #fff;
+}
+
+
+.flux_header {
+	font-size: 0.8rem;
+	cursor: pointer;
+	border-top: 1px solid #ecf0f1;
+}
+.flux_header .title {
+	font-size: 0.9rem;
+}
+.flux .website .favicon {
+	padding: 5px;
+}
+.flux .date {
+	font-size: 0.7rem;
+	color: #666;
+}
+
+.flux .bottom {
+	font-size: 0.8rem;
+	text-align: center;
+}
+
+/*=== Content of feed articles */
+.content {
+	padding: 20px 10px;
+}
+.content > h1.title > a {
+	color: #000;
+}
+
+.content hr {
+	margin: 30px 10px;
+	height: 1px;
+	background: #ddd;
+	border: 0;
+	box-shadow: 0 2px 5px #ccc;
+}
+
+.content pre {
+	margin: 10px auto;
+	padding: 10px 20px;
+	overflow: auto;
+	background: #222;
+	color: #fff;
+	font-size: 0.9rem;
+	border-radius: 3px;
+}
+.content code {
+	padding: 2px 5px;
+	color: #dd1144;
+	background: #fafafa;
+	border: 1px solid #eee;
+	border-radius: 3px;
+}
+.content pre code {
+	background: transparent;
+	color: #fff;
+	border: none;
+}
+
+.content blockquote {
+	display: block;
+	margin: 0;
+	padding: 5px 20px;
+	border-top: 1px solid #ddd;
+	border-bottom: 1px solid #ddd;
+	background: #fafafa;
+	color: #333;
+}
+.content blockquote p {
+	margin: 0;
+}
+
+/*=== Notification and actualize notification */
+.notification {
+	padding: 0 0 0 5px;
+	text-align: center;
+	font-weight: bold;
+	font-size: 0.9em;
+	line-height: 3em;
+	z-index: 10;
+	vertical-align: middle;
+	background: #ddd;
+	color: #666;
+	border-radius: 3px;
+	border: none;
+}
+.notification.good {
+	background: #1abc9c;
+	color: #fff;
+}
+.notification.bad {
+	background: #e74c3c;
+	color: #fff;
+}
+.notification a.close {
+	padding: 0 15px;
+	line-height: 3em;
+	border-radius: 0 3px 3px 0;
+}
+.notification.good a.close:hover {
+	background: #16a085;
+}
+.notification.bad a.close:hover {
+	background: #c0392b;
+}
+
+.notification#actualizeProgress {
+	line-height: 2em;
+}
+
+/*=== "Load more" part */
+#bigMarkAsRead {
+	text-align: center;
+	text-decoration: none;
+	background: #ecf0f1;
+}
+#bigMarkAsRead:hover {
+	background: #34495e;
+	color: #fff;
+}
+
+/*=== Navigation menu (for articles) */
+#nav_entries {
+	margin: 0;
+	text-align: center;
+	line-height: 3em;
+	table-layout: fixed;
+	background: #34495e;
+}
+
+/*=== READER VIEW */
+/*================*/
+#stream.reader .flux {
+	padding: 0 0 50px;
+	background: #ecf0f1;
+	color: #34495e;
+	border: none;
+}
+#stream.reader .flux .author {
+	margin: 0 0 10px;
+	font-size: 90%;
+	color: #999;
+}
+
+/*=== GLOBAL VIEW */
+/*================*/
+#stream.global .box-category {
+	text-align: left;
+	border: 1px solid #ddd;
+	border-radius: 5px;
+}
+#stream.global .category {
+	margin: 0;
+}
+#stream.global .btn {
+	width: auto;
+	height: 2em;
+	margin: 0;
+	padding: 0 10px;
+	line-height: 2em;
+	font-size: 1.2rem;
+	background: #ecf0f1;
+	color: #333;
+	border-bottom: 1px solid #ddd;
+	border-radius: 5px 5px 0 0;
+}
+#stream.global .btn:not([data-unread="0"]) {
+	font-weight: bold;
+	background: #3498db;
+	color: #fff;
+}
+#stream.global .btn:first-child:not([data-unread="0"]):after {
+	top: 0; right: 5px;
+	font-weight: bold;
+	background: none;
+	border: 0;
+	color: #fff;
+}
+#stream.global .box-category .feeds {
+	max-height: 250px;
+}
+#stream.global .box-category .feeds .item {
+	padding: 2px 10px;
+	font-size: 0.9rem;
+}
+
+/*=== DIVERS */
+/*===========*/
+.aside.aside_feed .nav-form input,
+.aside.aside_feed .nav-form select {
+	width: 140px;
+}
+.aside.aside_feed .nav-form .dropdown .dropdown-menu {
+	right: -20px;
+}
+.aside.aside_feed .nav-form .dropdown .dropdown-menu:after {
+	right: 33px;
+}
+
+/*=== STATISTICS */
+/*===============*/
+.stat {
+	margin: 10px 0 20px;
+}
+
+.stat th,
+.stat td,
+.stat tr {
+	border: none;
+}
+.stat > table td,
+.stat > table th {
+	border-bottom: 1px solid #ddd;
+	text-align: center;
+}
+
+/*=== LOGS */
+/*=========*/
+.logs {
+	overflow: hidden;
+	border: 1px solid #aaa;
+}
+.log {
+	margin: 10px 0;
+	padding: 5px 2%;
+	overflow: auto;
+	font-size: 0.8rem;
+	background: #fafafa;
+	color: #666;
+}
+
+.log > .date {
+	margin: 0 10px 0 0;
+	padding: 5px 10px;
+	border-radius: 20px;
+}
+.log.error > .date {
+	background: #e74c3c;
+	color: #fff;
+}
+.log.warning > .date {
+	background: #f39c12;
+}
+.log.notice > .date {
+	background: #ecf0f1;
+}
+.log.debug > .date {
+	background: #111;
+	color: #eee;
+}
+
+/*=== MOBILE */
+/*===========*/
+@media(max-width: 840px) {
+	.aside {
+		transition: width 200ms linear;
+		-moz-transition: width 200ms linear;
+		-webkit-transition: width 200ms linear;
+		-o-transition: width 200ms linear;
+		-ms-transition: width 200ms linear;
+	}
+	.aside .toggle_aside,
+	#panel .close {
+		position: absolute;
+		display: block;
+		top: 0; right: 0;
+		width: 32px;
+		height: 32px;
+		line-height: 30px;
+		text-align: center;
+		background: #34495e;
+		border-radius: 0 0 0 5px;
+	}
+
+	.nav_menu .btn {
+		margin: 5px 10px;
+	}
+	.nav_menu .stick {
+		margin: 0 10px;
+	}
+	.nav_menu .stick .btn {
+		margin: 5px 0;
+	}
+	.nav_menu .search {
+		display: inline-block;
+		max-width: 97%;
+	}
+	.nav_menu .search input {
+		max-width: 97%;
+		width: 90px;
+	}
+	.nav_menu .search input:focus {
+		width: 400px;
+	}
+
+	.day .name {
+		font-size: 1.1rem;
+	}
+
+	.pagination {
+		margin: 0 0 3.5em;
+	}
+
+	.notification {
+		border-radius: 0;
+	}
+	.notification a.close {
+		display: block;
+		left: 0;
+		background: transparent;
+	}
+	.notification a.close:hover {
+		opacity: 0.5;
+	}
+	.notification a.close .icon {
+		display: none;
+	}
+}

+ 0 - 900
p/themes/Flat/freshrss.css

@@ -1,900 +0,0 @@
-@charset "UTF-8";
-
-/* STRUCTURE */
-body {
-	background: #fafafa;
-}
-
-.header {
-	display: table;
-	width: 100%;
-	table-layout: fixed;
-	background: #ecf0f1;
-}
-	.header > .item {
-		display: table-cell;
-		padding: 10px 0;
-		vertical-align: middle;
-		text-align: center;
-	}
-		.header > .item.title {
-			width: 250px;
-			white-space: nowrap;
-		}
-			.logo {
-				display: inline-block;
-				font-size: 48px;
-				height: 32px;
-				width: 32px;
-				padding: 10px;
-			}
-			.header > .item.title h1 {
-				display: inline-block;
-				margin: 0;
-			}
-		.header > .item.search input {
-			width: 230px;
-		}
-			.header .item.search input:focus {
-				width: 330px;
-			}
-		.header > .item.configure {
-			width: 100px;
-		}
-
-.item a:hover {
-	text-decoration: none;
-}
-
-#global {
-	display: table;
-	width: 100%;
-	height: 100%;
-	table-layout: fixed;
-}
-	.aside {
-		display: table-cell;
-		height: 100%;
-		width: 250px;
-		vertical-align: top;
-		background: #ecf0f1;
-	}
-		.aside .nav-form input {
-			width: 180px;
-		}
-		.aside.aside_flux {
-			padding: 10px 0 40px;
-		}
-		.aside.aside_feed .nav-form input {
-			width: 140px;
-		}
-		.aside.aside_feed .nav-form .dropdown-menu {
-			right: -20px;
-		}
-		.aside.aside_feed .nav-form .dropdown-menu:after {
-			right: 33px;
-		}
-
-	.nav-login {
-		display: none;
-	}
-
-	.nav_menu {
-		width: 100%;
-		text-align: center;
-		padding: 5px 0;
-	}
-		.nav_menu .search {
-			display:none;
-		}
-
-.favicon {
-	height: 16px;
-	width: 16px;
-}
-
-.categories {
-	margin: 0;
-	padding: 0;
-	text-align: center;
-	list-style: none;
-}
-	.category {
-		display: block;
-		width: 220px;
-		margin: 10px auto;
-		text-align: left;
-		overflow: hidden;
-		white-space: nowrap;
-		text-overflow: ellipsis;
-	}
-		.category .btn:first-child {
-			width: 195px;
-			position: relative;
-		}
-		.category.stick .btn:first-child {
-			width:160px;
-		}
-		.category .btn:first-child:not([data-unread="0"]):after {
-			content: attr(data-unread);
-			position: absolute;
-			top: 5px; right: 5px;
-			padding: 0 5px;
-			color: #fff;
-			font-size: 90%;
-			background: #3498DB;
-			border-radius: 5px;
-		}
-		.category + .feeds:not(.active) {
-			display:none;
-		}
-	.categories .feeds {
-		width: 100%;
-		margin: 0 auto;
-		list-style: none;
-	}
-		.categories .feeds .item.active:after {
-			content: "⇢";
-			line-height: 35px;
-			float: right;
-		}
-		.categories .feeds .item.empty .feed {
-			color: #e67e22;
-		}
-		.categories .feeds .item.error .feed {
-			color: #BD362F;
-		}
-		.categories .feeds .item .feed {
-			display: inline-block;
-			margin: 0;
-			width: 165px;
-			line-height: 35px;
-			font-size: 90%;
-			vertical-align: middle;
-			text-align: left;
-			overflow: hidden;
-			white-space: nowrap;
-			text-overflow: ellipsis;
-		}
-		.feed:not([data-unread="0"]) {
-			font-weight:bold;
-		}
-		.feed:not([data-unread="0"]):before {
-			content: "(" attr(data-unread) ") ";
-		}
-		.categories .feeds .dropdown-menu {
-			left: 0;
-		}
-		.categories .feeds .dropdown-menu:after {
-			left: 2px;
-		}
-		.categories .feeds .item .dropdown-toggle > .icon {
-			visibility: hidden;
-			cursor: pointer;
-		}
-			.categories .feeds .item .dropdown-target:target ~ .dropdown-toggle > .icon,
-			.categories .feeds .item:hover .dropdown-toggle > .icon,
-			.categories .feeds .item.active .dropdown-toggle > .icon {
-				background-color: #95a5a6;
-				border-radius: 3px;
-				visibility: visible;
-			}
-	.categories .btn:hover .notRead,
-	.categories .btn.active .notRead {
-		background: #2980B9;
-		border-left: 3px solid #3498DB;
-	}
-
-.post {
-	padding: 10px 50px;
-}
-	.post form {
-		margin: 10px 0;
-	}
-
-.day {
-	padding: 5px 15px;
-	font-size: 130%;
-	font-weight: bold;
-	line-height: 50px;
-	border-left: 3px solid #ecf0f1;
-}
-	.day .name {
-		position: absolute;
-		right: 0;
-		width: 50%;
-		height: 1.5em;
-		padding: 0 10px 0 0;
-		overflow: hidden;
-		color: #aab;
-		font-size: 1.8em;
-		opacity: .3;
-		font-style: italic;
-		text-align: right;
-		white-space: nowrap;
-		text-overflow: ellipsis;
-		z-index: -10;
-	}
-
-#new-article {
-	display: none;
-	min-height: 40px;
-	background: #3498db;
-	text-align: center;
-}
-	#new-article:hover {
-		background: #2980b9;
-	}
-	#new-article > a {
-		display: block;
-		line-height: 40px;
-		color: #fff;
-		font-weight: bold;
-	}
-		#new-article > a:hover {
-			text-decoration: none;
-		}
-
-.flux {
-	border-left: 3px solid #ecf0f1;
-}
-	.flux:hover {
-		background: #fff;
-	}
-	.flux.not_read {
-		border-left-color: #FF5300;
-		background: #FFF3ED;
-	}
-	.flux.favorite {
-		border-left-color: #FFC300;
-		background: #FFF6DA;
-	}
-	.flux.current {
-		border-left-color: #3498db;
-		background: #fff;
-	}
-
-	.horizontal-list > .item:not(.title):not(.website) > a {
-		display: block;
-	}
-
-	.flux_header {
-		background: inherit;
-		height: 25px;
-		font-size: 12px;
-		border-top: 1px solid #ecf0f1;
-		cursor: pointer;
-	}
-		.flux .item {
-			line-height: 40px;
-			white-space: nowrap;
-		}
-		.flux_header > .item {
-			overflow: hidden;
-			text-overflow: ellipsis;
-		}
-		.flux .item.manage {
-			width: 40px;
-			text-align: center;
-		}
-		.flux .item.website {
-			width: 200px;
-		}
-			.website .favicon {
-				padding: 5px;
-			}
-		.flux .item.title {
-			background: inherit;
-		}
-			.flux .title a {
-				color: #333;
-				outline: none;
-			}
-			.flux.current .item.title,
-			.flux.not_read .item.title {
-				font-weight: bold;
-			}
-		.flux .item.date {
-			width: 145px;
-			padding:0 5px 0 0;
-			text-align: right;
-			font-size: 10px;
-			color: #666;
-		}
-		.link {
-			width: 40px;
-			text-align: center;
-		}
-
-#stream.reader .flux {
-	position: relative;
-	padding: 0 0 30px;
-	border: none;
-	background: #ecf0f1;
-	color: #34495e;
-	font-size: 120%;
-}
-	#stream.reader .flux .author {
-		margin: 0 0 10px;
-		font-size: 90%;
-		color: #aaa;
-	}
-
-#stream.global {
-	text-align: center;
-}
-	#stream.global .box-category {
-		display: inline-block;
-		width: 280px;
-		margin: 20px 10px;
-		vertical-align: top;
-		border: 1px solid #ddd;
-		border-radius: 5px;
-		text-align: left;
-	}
-		#stream.global .category {
-			width: 100%;
-			margin: 0;
-		}
-		#stream.global .btn {
-			display: block;
-			width: auto;
-			height: 35px;
-			margin: 0;
-			padding: 0 10px;
-			background: #ecf0f1;
-			color: #333;
-			border-bottom: 1px solid #ddd;
-			border-radius: 5px 5px 0 0;
-			line-height: 35px;
-			font-size: 120%;
-		}
-			#stream.global .btn:not([data-unread="0"]) {
-				background: #3498db;
-				color: #fff;
-				font-weight: bold;
-			}
-			#stream.global .btn:first-child:not([data-unread="0"]):after {
-				top: 0; right: 5px;
-				border: 0;
-				background: none;
-				color: #fff;
-				font-weight: bold;
-				box-shadow: none;
-			}
-		#stream.global .box-category .feeds {
-			display: block;
-			max-height: 250px;
-			margin: 0;
-			list-style: none;
-			overflow: auto;
-		}
-			#stream.global .box-category .feeds .item {
-				padding: 2px 10px;
-				font-size: 90%;
-			}
-		#stream.global .box-category .feed {
-			width: 220px;
-		}
-
-.content {
-	min-height: 150px;
-	margin: 0 auto;
-	padding: 20px 10px;
-	line-height: 170%;
-	word-wrap: break-word;
-}
-	.content.large {
-		max-width: 1000px;
-	}
-	.content.medium {
-		max-width: 800px;
-	}
-	.content.thin {
-		max-width: 550px;
-	}
-	.content h1, .content h2, .content h3 {
-		margin: 20px 0 5px;
-	}
-	.content > .title {
-		font-size: x-large;
-		margin: 0;
-	}
-
-	.content p {
-		margin: 0 0 20px;
-	}
-	img.big {
-		display: block;
-		margin: 10px auto;
-	}
-	figure img.big {
-		margin: 0;
-	}
-	.content hr {
-		margin: 30px 0;
-		height: 1px;
-		background: #ddd;
-		border: 0;
-	}
-	.content pre {
-		margin: 10px auto;
-		padding: 10px;
-		overflow: auto;
-		background: #000;
-		color: #fff;
-		font-size: 110%;
-	}
-	.content q, .content blockquote {
-		display: block;
-		margin: 5px 0;
-		padding: 5px 20px;
-		font-style: italic;
-		border-left: 4px solid #ccc;
-		color: #666;
-	}
-		.content blockquote p {
-			margin: 0;
-		}
-
-#panel {
-	display: none;
-	position: fixed;
-	top: 10px; bottom: 10px;
-	left: 100px; right: 100px;
-	overflow: auto;
-	background: #fff;
-	border: 1px solid #95a5a6;
-	border-radius: 5px;
-}
-	#panel .close {
-		position: fixed;
-		top: 10px; right: 0px;
-		display: inline-block;
-		width: 26px;
-		height: 26px;
-		margin: 0 10px 0 0;
-		border-radius: 3px;
-		text-align: center;
-		line-height: 24px;
-		background: #95a5a6;
-	}
-		#panel .close:hover {
-			background: #7f8c8d;
-		}
-
-#overlay {
-	display: none;
-	position: fixed;
-	top: 0; bottom: 0;
-	left: 0; right: 0;
-	background: rgba(0, 0, 0, 0.9);
-}
-
-.flux_content .bottom {
-	font-size: 90%;
-	text-align: center;
-}
-
-.hide_posts > :not(.active) > .flux_content {
-	display:none;
-}
-
-/*** PAGINATION ***/
-.pagination {
-	display: table;
-	width: 100%;
-	margin: 0;
-	background: #ecf0f1;
-	text-align: center;
-	color: #000;
-	font-size: 80%;
-	line-height: 200%;
-	table-layout: fixed;
-	font-weight: bold;
-}
-	.pagination .item {
-		display: table-cell;
-		line-height: 40px;
-		vertical-align: top;
-	}
-		.pagination .item.pager-current {
-			font-weight: bold;
-			font-size: 140%;
-			color: #ecf0f1;
-			background: #34495e;
-		}
-		.pagination .item.pager-first,
-		.pagination .item.pager-previous,
-		.pagination .item.pager-next,
-		.pagination .item.pager-last {
-			width: 100px;
-		}
-		.pagination .item a {
-			display: block;
-			color: #000;
-			font-weight: bold;
-			line-height: 40px;
-		}
-			.pagination .item a:hover {
-				color: #ecf0f1;
-				background: #34495e;
-			}
-
-#nav_entries {
-	display: table;
-	width: 250px;
-	height: 40px;
-	position: fixed;
-	bottom: 0;
-	left: 0;
-	margin: 0;
-	background: #34495e;
-	text-align: center;
-	line-height: 40px;
-	table-layout: fixed;
-}
-	#nav_entries .item {
-		display: table-cell;
-		width: 30%;
-	}
-		#nav_entries a {
-			display: block;
-		}
-		#nav_entries .i_up {
-			margin: 5px 0 0;
-			vertical-align: top;
-		}
-
-.pagination .loading,
-.pagination a:hover.loading {
-	background: url("loader.gif") center center no-repeat #34495e;
-	font-size: 0;
-}
-
-#bigMarkAsRead {
-	background: #ecf0f1;
-	display: block;
-	font-style: normal;
-	padding: 32px 0 64px 0;
-	text-align: center;
-	text-decoration: none;
-	text-shadow: 0 -1px 0 #aaa;
-}
-	#bigMarkAsRead:hover {
-		background: #34495e;
-		color: #fff;
-	}
-	.bigTick {
-		font-size: 72pt;
-		line-height: 1.6em;
-	}
-
-/*** NOTIFICATION ***/
-.notification {
-	position: absolute;
-	top: 10px;
-	left: 25%; right: 25%;
-	min-height: 30px;
-	padding: 10px;
-	line-height: 30px;
-	text-align: center;
-	border-radius: 3px;
-	background: #ddd;
-	color: #666;
-	font-weight: bold;
-	z-index: 10;
-}
-	.notification.closed {
-		display: none;
-	}
-	.notification.good {
-		background: #1abc9c;
-		color: #fff;
-	}
-	.notification.bad {
-		background: #e74c3c;
-		color: #fff;
-	}
-	.notification a.close {
-		position: absolute;
-		top: -6px; right: -6px;
-		display: inline-block;
-		width: 16px;
-		height: 16px;
-		padding: 5px;
-		border-radius: 3px;
-		line-height: 16px;
-	}
-		.notification.good a.close {
-			background: #1abc9c;
-		}
-		.notification.bad a.close {
-			background: #e74c3c;
-		}
-
-.toggle_aside, .btn.toggle_aside {
-	display: none;
-}
-
-#actualizeProgress {
-	position: fixed;
-}
-#actualizeProgress progress {
-	max-width: 100%;
-	vertical-align: middle;
-}
-#actualizeProgress .progress {
-	vertical-align: middle;
-}
-
-.logs {
-	border: 1px solid #34495e;
-}
-	.log {
-		margin: 10px 0;
-		padding: 5px 2%;
-		overflow: auto;
-		background: #fafafa;
-		color: #666;
-		font-size: 90%;
-	}
-		.log>.date {
-			margin: 0 10px 0 0;
-			padding: 5px 10px;
-			border-radius: 20px;
-		}
-			.log.error>.date {
-				background: #e74c3c;
-				color: #fff;
-			}
-			.log.warning>.date {
-				background: #f39c12;
-			}
-			.log.notice>.date {
-				background: #ecf0f1;
-			}
-			.log.debug>.date {
-				background: #111;
-				color: #eee;
-			}
-
-.form-group table {
-	border-collapse:collapse;
-	margin:10px 0 0 220px;
-	text-align:center;
-}
-
-.form-group tr, .form-group th, .form-group td {
-	font-weight:normal;
-	padding:.5em;
-}
-
-select.number option {
-	text-align:right;
-}
-
-@media(min-width: 841px) {
-	.flux:not(.current):hover .item.title {
-		max-width: calc(100% - 580px);
-		padding-right: 1.5em;
-		position: absolute;
-	}
-}
-
-@media(max-width: 840px) {
-	.header,
-	.aside .btn-important,
-	.aside .feeds .dropdown,
-	.flux_header .item.website span,
-	.item.date {
-		display: none;
-	}
-	.flux_header .item.website {
-		width: 40px;
-		text-align: center;
-	}
-		.flux_header .item.website .favicon {
-			padding: 12px;
-		}
-
-	.nav-login {
-		display: block;
-	}
-
-	.pagination {
-		margin: 0 0 40px;
-	}
-	.pagination .pager-previous, .pagination .pager-next {
-		width: 100px;
-	}
-
-	.toggle_aside, .btn.toggle_aside {
-		display: inline-block;
-	}
-	.aside {
-		position: fixed;
-		top: 0; left: 0;
-		width: 0;
-		overflow: hidden;
-		z-index: 10;
-		transition: width 200ms linear;
-		background: #ecf0f1;
-	}
-		.aside.aside_flux {
-			padding: 10px 0 0;
-		}
-		.aside:target {
-			width: 80%;
-			border-right: 1px solid #aaa;
-			overflow: auto;
-		}
-		.aside .toggle_aside {
-			position: absolute;
-			right: 10px;
-			display: inline-block;
-			width: 26px;
-			height: 26px;
-			margin: 0 10px 0 0;
-			border-radius: 3px;
-			text-align: center;
-			line-height: 24px;
-			background: #95a5a6;
-		}
-			.aside .toggle_aside:hover {
-				background: #7f8c8d;
-			}
-		.aside .categories {
-			margin: 30px 0;
-		}
-
-	#nav_entries {
-		width: 100%;
-	}
-
-	.nav_menu .btn {
-		margin: 5px 10px;
-	}
-	.nav_menu .stick {
-		margin: 0 10px;
-	}
-	.nav_menu .stick .btn {
-		margin: 5px 0;
-	}
-	.nav_menu .search {
-			display: inline-block;
-			max-width: 97%;
-		}
-		.nav_menu .search input {
-			max-width: 97%;
-			width: 90px;
-		}
-		.nav_menu .search input:focus {
-			width: 400px;
-		}
-
-	#panel {
-		left: 5px; right: 5px;
-	}
-
-	.day .date {
-		display: none;
-	}
-	.day .name {
-		height: 2.6em;
-		font-size: 1em;
-		text-shadow: none;
-	}
-
-	.notification {
-		top: 0;
-		left: 0;
-		right: 0;
-		border-radius: 0;
-	}
-	.notification a.close,
-	.notification.good a.close,
-	.notification.bad a.close {
-		left: 0; right: 0;
-		top: 0; bottom: 0;
-		width: auto;
-		height: auto;
-		background: transparent;
-		border: none;
-	}
-	.notification a.close .icon {
-		display: none;
-	}
-}
-
-/*** FALLBACK ***/
-.dropdown-menu:after {
-	-moz-transform: rotate(45deg);
-	-webkit-transform: rotate(45deg);
-	-ms-transform: rotate(45deg);
-}
-
-input.extend {
-	-moz-transition: width 200ms linear;
-	-webkit-transition: width 200ms linear;
-	-o-transition: width 200ms linear;
-	-ms-transition: width 200ms linear;
-}
-
-@media print {
-	.header,
-	.aside,
-	.nav_menu,
-	.day,
-	.flux_header,
-	.flux_content .bottom,
-	.pagination {
-		display: none;
-	}
-
-	html, body {
-		background: #fff;
-		color: #000;
-		font-family: Serif;
-		font-size: 12pt;
-	}
-
-	#global,
-	.flux_content {
-		display: block !important;
-	}
-
-	.flux_content .content {
-		width: 100% !important;
-		text-align: justify;
-	}
-
-	.flux_content .content a {
-		color: #000;
-	}
-	.flux_content .content a:after {
-		content: " (" attr(href) ") ";
-		text-decoration: underline;
-	}
-}
-
-.stat {
-	border:1px solid #aaa;
-	border-radius:10px;
-	box-shadow:2px 2px 5px #aaa;
-	margin:10px 0;
-	padding:0 5px;
-}
-.stat > h2 {
-	border-bottom:1px solid #aaa;
-	margin:0 -5px;
-	padding-left:5px;
-}
-.stat > div {
-	margin:5px 0;
-}
-.stat > table {
-	border-collapse:collapse;
-	margin:5px 0;
-	width:100%;
-}
-.stat > table > thead > tr {
-	border-bottom:2px solid #aaa;
-}
-.stat > table > tbody > tr {
-	border-bottom:1px solid #aaa;
-}
-.stat > table > tbody > tr:last-child {
-	border-bottom:0;
-}
-.stat > table th, .stat > table td {
-	border-left:2px solid #aaa;
-	padding:5px;
-}
-.stat > table th:first-child, .stat > table td:first-child {
-	border-left:0;
-}
-.stat > table td.numeric{
-	margin:5px 0;
-	text-align:center;
-}

+ 0 - 528
p/themes/Flat/global.css

@@ -1,528 +0,0 @@
-@charset "UTF-8";
-
-/* FONTS */
-@font-face {
-	font-family: "OpenSans";
-	src: url("../fonts/openSans.woff") format("woff");
-}
-
-
-* {
-	margin: 0;
-	padding: 0;
-}
-html, body {
-	height: 100%;
-	font-size: 95%;
-	font-family: "OpenSans", "Cantarell", "Helvetica", "Arial", sans-serif;
-}
-
-/* LIENS */
-a {
-	color: #2980b9;
-	text-decoration: none;
-}
-	a:hover {
-		text-decoration: underline;
-	}
-
-/* LISTES */
-ul, ol, dl {
-	margin: 10px 0 10px 30px;
-	line-height: 190%;
-}
-	dd {
-		margin: 0 0 10px 30px;
-	}
-
-/* TITRES */
-h1, h2, h3 {
-	min-height: 40px;
-	margin: 15px 0 5px;
-	line-height: 40px;
-}
-
-/* IMG */
-figure {
-	margin: 5px 0 10px;
-	text-align: center;
-}
-	figcaption {
-		display: inline-block;
-		padding: 3px 20px;
-		color: #999;
-		font-style: italic;
-		border-bottom: 1px solid #ccc;
-	}
-img {
-	height: auto;
-	max-width: 100%;
-	vertical-align: middle;
-}
-	a img {
-		border: none;
-	}
-
-/* VIDEOS */
-iframe, embed, object, video {
-	max-width: 100%;
-}
-
-/* FORMULAIRES */
-legend {
-	display: inline-block;
-	margin: 20px 0 5px;
-	padding: 5px 20px;
-	font-size: 150%;
-	clear: both;
-	background: #ecf0f1;
-	border-radius: 20px;
-}
-label {
-	display: block;
-	min-height: 25px;
-	padding: 5px 0;
-	font-size: 12px;
-	line-height: 25px;
-	cursor: pointer;
-	font-weight: bold;
-	color: #444;
-}
-input {
-	width: 180px;
-}
-textarea {
-	width: 360px;
-	height: 100px;
-}
-input, select, textarea {
-	display: inline-block;
-	max-width: 100%;
-	min-height: 25px;
-	padding: 5px;
-	background: #FFF;
-	border: none;
-	border-bottom: 3px solid #ddd;
-	color: #666;
-	line-height: 25px;
-	vertical-align: middle;
-	border-radius: 5px;
-}
-	option {
-		padding:0 .5em 0 .5em;
-	}
-	input[type="radio"],
-	input[type="checkbox"] {
-		width: 15px !important;
-		min-height: 15px !important;
-	}
-	input:focus, select:focus, textarea:focus {
-		color: #333;
-		border-color: #2980b9;
-	}
-	input:invalid, select:invalid {
-		border-color: red;
-		box-shadow: 0 0 2px 1px red;
-	}
-	input:focus.extend {
-		width: 300px;
-		transition: width 200ms linear;
-	}
-
-.form-group {
-	margin: 5px 0;
-	border: 1px solid transparent;
-}
-	.form-group:after {
-		content: "";
-		display: block;
-		clear: both;
-	}
-	.form-group:hover {
-		background: #fff;
-		border: 1px solid #eee;
-		border-radius: 3px;
-	}
-	.form-group.form-actions {
-		min-width: 250px;
-		margin: 20px 0;
-		padding: 5px 0;
-		background: #ecf0f1;
-		border-top: 3px solid #bdc3c7;
-		border-radius: 5px 5px 0 0;
-	}
-		.form-group.form-actions .btn {
-			margin: 0 10px;
-		}
-	.form-group .group-name {
-		display: block;
-		float: left;
-		width: 200px;
-		padding: 10px 0;
-		text-align: right;
-	}
-	.form-group .group-controls {
-		min-width: 250px;
-		min-height: 25px;
-		margin: 0 0 0 220px;
-		padding: 5px 0;
-	}
-		.form-group .group-controls label {
-			font-weight: normal;
-			font-size: 14px;
-			color: #000;
-		}
-		.form-group .group-controls .control {
-			display: block;
-			min-height: 30px;
-			padding: 5px 0;
-			line-height: 25px;
-			font-size: 14px;
-		}
-
-.stick {
-	display: inline-block;
-	white-space: nowrap;
-	font-size: 0px;
-	vertical-align: middle;
-}
-	.stick .btn,
-	.stick input {
-		font-size: 14px;
-		border-radius: 0;
-	}
-	.stick .btn:first-child,
-	.stick input:first-child {
-		border-radius: 5px 0 0 5px;
-	}
-	.stick .btn:last-child,
-	.stick input:last-child,
-	.stick .btn + .dropdown > .btn {
-		border-radius: 0 5px 5px 0;
-	}
-	.stick .btn + .dropdown a {
-		font-size: 12px;
-	}
-
-.btn {
-	display: inline-block;
-	min-height: 38px;
-	min-width: 18px;
-	padding: 5px 10px;
-	background: #3498db;
-	border-radius: 5px;
-	border: none;
-	border-bottom: 3px solid #2980b9;
-	color: #fff;
-	line-height: 20px;
-	vertical-align: middle;
-	cursor: pointer;
-	overflow: hidden;
-}
-	a.btn {
-		min-height: 25px;
-		line-height: 25px;
-	}
-	.btn.active,
-	.btn:active,
-	.btn:hover,
-	.dropdown-target:target ~ .btn.dropdown-toggle {
-		background: #2980b9;
-		text-decoration: none;
-	}
-
-	.btn-important {
-		background: #e67e22;
-		color: #fff;
-		border-bottom: 3px solid #d35400;
-	}
-		.btn-important:active,
-		.btn-important:hover {
-			background: #d35400;
-		}
-
-	.btn-attention {
-		background: #e74c3c;
-		color: #fff;
-		border-bottom: 3px solid #c0392b;
-	}
-		.btn-attention:hover,
-		.btn-attention:active {
-			background: #c0392b;
-		}
-
-/* NAVIGATION */
-.nav-list {
-	border-right: 1px solid #ecf0f1;
-}
-.nav-list .nav-header,
-.nav-list .item {
-	display: block;
-	height: 35px;
-	line-height: 35px;
-	margin: 5px 0;
-}
-	.nav-list .item:hover,
-	.nav-list .item.active {
-		background: #2980b9;
-		color: #fff;
-	}
-		.nav-list .item:hover a,
-		.nav-list .item.active a {
-			color: #fff;
-		}
-	.nav-list .disable {
-		color: #aaa;
-		background: #fafafa;
-		text-align: center;
-	}
-	.nav-list .item > * {
-		display: block;
-		padding: 0 10px;
-		overflow: hidden;
-		white-space: nowrap;
-		text-overflow: ellipsis;
-	}
-		.nav-list a:hover {
-			text-decoration: none;
-		}
-	.nav-list .item.error a {
-		color: #BD362F;
-	}
-		.nav-list .item:hover.error a,
-		.nav-list .item.active.error a {
-			color: #fff;
-			background: #BD362F;
-		}
-	.nav-list .item.empty a {
-		color: #f39c12;
-	}
-		.nav-list .item:hover.empty a,
-		.nav-list .item.active.empty a {
-			color: #fff;
-			background: #f39c12;
-		}
-
-	.nav-list .nav-header {
-		padding: 0 10px;
-		margin: 0;
-		color: #fff;
-		background: #34495e;
-		font-weight: bold;
-	}
-	.nav-list .separator {
-		display: block;
-		height: 0;
-		margin: 5px 0;
-		border-bottom: 1px solid #ddd;
-	}
-
-	.nav-list .nav-form {
-		padding: 3px;
-		text-align: center;
-	}
-
-.nav-head {
-	display: block;
-	margin: 0;
-	background: #34495e;
-	color: #fff;
-	text-align: right;
-}
-	.nav-head a {
-		color: #fff;
-	}
-	.nav-head .item {
-		display: inline-block;
-		padding: 5px 10px;
-	}
-
-/* HORIZONTAL-LIST */
-.horizontal-list {
-	display: table;
-	table-layout: fixed;
-	margin: 0;
-	padding: 0;
-	width: 100%;
-}
-	.horizontal-list .item {
-		display: table-cell;
-		vertical-align: middle;
-	}
-
-/* DROPDOWN */
-.dropdown {
-	position: relative;
-	display: inline-block;
-}
-	.dropdown-target {
-		display: none;
-	}
-
-	.dropdown-menu {
-		display: none;
-		min-width: 200px;
-		margin: 5px 0 0;
-		padding: 5px 0;
-		position: absolute;
-		right: 0px;
-		background: #fff;
-		border: 1px solid #95a5a6;
-		border-radius: 3px;
-		text-align: left;
-	}
-	.dropdown-menu:after {
-		content: "";
-		position: absolute;
-		top: -6px;
-		right: 13px;
-		width: 10px;
-		height: 10px;
-		background: #fff;
-		border-top: 1px solid #95a5a6;
-		border-left: 1px solid #95a5a6;
-		z-index: -10;
-		transform: rotate(45deg);
-	}
-		.dropdown-header {
-			display: block;
-			padding: 0 5px;
-			color: #34495e;
-			font-weight: bold;
-			font-size: 14px;
-			line-height: 30px;
-		}
-		.dropdown-menu > .item {
-			display: block;
-			height: 30px;
-			font-size: 90%;
-			line-height: 30px;
-		}
-			.dropdown-menu > .item > a {
-				display: block;
-				padding: 0 25px;
-				line-height: 30px;
-			}
-			.dropdown-menu > .item.share > a {
-				display: list-item;
-				list-style-position:inside;
-				list-style-type:decimal;
-			}
-			.dropdown-menu > .item:hover > a {
-				background: #2980b9;
-				color: #fff;
-			}
-				.dropdown-menu > .item:hover > a {
-					color: #fff;
-					text-decoration: none;
-				}
-		.dropdown-menu .input {
-			display: block;
-			height: 40px;
-			font-size: 90%;
-			line-height: 30px;
-		}
-			.dropdown-menu label {
-				font-weight: normal;
-			}
-			.dropdown-menu .input select,
-			.dropdown-menu .input input {
-				display: block;
-				height: 20px;
-				width: 95%;
-				margin: auto;
-				padding: 2px 5px;
-				border-radius: 3px;
-			}
-			.dropdown-menu .input select {
-				width: 70%;
-				height: auto;
-			}
-		.dropdown-menu .separator {
-			display: block;
-			height: 0;
-			margin: 5px 0;
-			border-bottom: 1px solid #95a5a6;
-		}
-		.dropdown-target:target ~ .dropdown-menu {
-			display: block;
-			z-index: 10;
-		}
-	.dropdown-close {
-		display: inline;
-	}
-		.dropdown-close a {
-			font-size: 0;
-			position: fixed;
-			top: 0; bottom: 0;
-			left: 0; right: 0;
-			display: block;
-			z-index: -10;
-		}
-
-/* ALERTS */
-.alert {
-	display: block;
-	width: 90%;
-	margin: 15px auto;
-	padding: 10px 15px;
-	background: #f4f4f4;
-	border: 1px solid #ccc;
-	border-right: 1px solid #aaa;
-	border-bottom: 1px solid #aaa;
-	border-radius: 5px;
-	color: #aaa;
-	text-shadow: 0 0 1px #eee;
-}
-	.alert-head {
-		margin: 0;
-		font-weight: bold;
-		font-size: 110%;
-	}
-	.alert > a {
-		color: inherit;
-		text-decoration: underline;
-	}
-	.alert-warn {
-		background: #ffe;
-		border: 1px solid #eeb;
-		color: #c95;
-	}
-	.alert-success {
-		background: #dfd;
-		border: 1px solid #cec;
-		color: #484;
-	}
-	.alert-error {
-		background: #fdd;
-		border: 1px solid #ecc;
-		color: #844;
-	}
-
-/* ICÔNES */
-.icon {
-	display: inline-block;
-	width: 16px;
-	height: 16px;
-	vertical-align: middle;
-	line-height: 16px;
-}
-
-/* Prompt (centré) */
-.prompt {
-	text-align: center;
-}
-	.prompt label {
-		text-align: left;
-	}
-	.prompt form {
-		margin: 1em auto 2.5em auto;
-		width: 10em;
-	}
-	.prompt input {
-		margin: .4em auto 1.1em auto;
-		width: 100%;
-	}
-	.prompt p {
-		margin: 20px 0;
-	}

+ 12 - 0
p/themes/Flat/icons/icon.svg

@@ -0,0 +1,12 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
+	<title>Logo FreshRSS</title>
+	<circle fill="#2980b9" cx="128" cy="128" r="33"/>
+	<g fill="none" stroke="#2980b9" stroke-width="24">
+		<g stroke-opacity="0.3">
+			<path d="M12,128 A116,116 0 1,1 128,244"/>
+			<path d="M54,128 A74,74 0 1,1 128,202"/>
+		</g>
+		<path d="M128,12 A116,116 0 0,1 244,128"/>
+		<path d="M128,54 A74,74 0 0,1 202,128"/>
+	</g>
+</svg>

+ 7 - 0
p/themes/Flat/icons/key.svg

@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg">
+<g transform="translate(-340.99994,-257)" fill="#ffffff">
+<path style="block-progression:tb;color:#000000;direction:ltr;text-indent:0;text-align:start;enable-background:accumulate;text-transform:none;" d="m346,260c-2.7496,0-5,2.2504-5,5s2.2504,5,5,5c1.5862,0,2.9034-0.84459,3.8125-2h4.8438,0.75l0.21875-0.75,1.0312-4,0.3125-1.25h-1.2812-5.875c-0.90914-1.1554-2.2263-2-3.8125-2zm0,2c1.1158,0,2.0379,0.59507,2.5625,1.5l0.3125,0.5h0.5625,4.9688l-0.53125,2h-4.4375-0.5625l-0.3125,0.5c-0.52462,0.90493-1.4466,1.5-2.5625,1.5-1.6687,0-3-1.3313-3-3s1.3313-3,3-3z"/>
+<path opacity="0.35" style="enable-background:accumulate;color:#000000;" d="M355.5,265,350,265,349.44,267,355,267z" fill-rule="nonzero"/>
+<path style="enable-background:accumulate;color:#000000;" d="m346,265c0,0.55228-0.44772,1-1,1s-1-0.44772-1-1,0.44772-1,1-1,1,0.44772,1,1z" fill-rule="nonzero"/>
+</g>
+</svg>

+ 2 - 2
p/themes/Flat/metadata.json

@@ -2,6 +2,6 @@
   "name": "Flat design",
   "author": "Marien Fressinaud",
   "description": "Thème plat pour FreshRSS",
-  "version": 0.1,
-  "files": ["global.css", "freshrss.css"]
+  "version": 0.2,
+  "files": ["template.css", "flat.css"]
 }

+ 695 - 0
p/themes/Flat/template.css

@@ -0,0 +1,695 @@
+@charset "UTF-8";
+
+/*=== GENERAL */
+/*============*/
+html, body {
+	margin: 0;
+	padding: 0;
+	font-size: 100%;
+}
+
+/*=== Links */
+a {
+	text-decoration: none;
+}
+a:hover {
+	text-decoration: underline;
+}
+
+/*=== Lists */
+ul, ol, dd {
+	margin: 0;
+	padding: 0;
+}
+
+/*=== Titles */
+h1 {
+	margin: 0.6em 0 0.3em;
+	font-size: 1.5em;
+	line-height: 1.6em;
+}
+h2 {
+	margin: 0.5em 0 0.25em;
+	font-size: 1.3em;
+	line-height: 2em;
+}
+h3 {
+	margin: 0.5em 0 0.25em;
+	font-size: 1.1em;
+	line-height: 2em;
+}
+
+/*=== Paragraphs */
+p {
+	margin: 1em 0 0.5em;
+	font-size: 1em;
+}
+
+/*=== Images */
+img {
+	height: auto;
+	max-width: 100%;
+}
+img.favicon {
+	height: 16px;
+	width: 16px;
+	vertical-align: middle;
+}
+
+/*=== Videos */
+iframe, embed, object, video {
+	max-width: 100%;
+}
+
+/*=== Forms */
+legend {
+	display: block;
+	width: 100%;
+	clear: both;
+}
+label {
+	display: block;
+}
+input {
+	width: 180px;
+}
+textarea {
+	width: 300px;
+}
+input, select, textarea {
+	display: inline-block;
+	max-width: 100%;
+}
+input[type="radio"],
+input[type="checkbox"] {
+	width: 15px !important;
+	min-height: 15px !important;
+}
+input.extend:focus {
+	width: 300px;
+}
+
+/*=== COMPONENTS */
+/*===============*/
+/*=== Forms */
+.form-group:after {
+	content: "";
+	display: block;
+	clear: both;
+}
+.form-group.form-actions {
+	min-width: 250px;
+}
+.form-group .group-name {
+	display: block;
+	float: left;
+	width: 200px;
+}
+.form-group .group-controls {
+	min-width: 250px;
+	margin: 0 0 0 220px;
+}
+.form-group .group-controls .control {
+	display: block;
+}
+
+/*=== Buttons */
+.stick {
+	display: inline-block;
+	white-space: nowrap;
+}
+.btn,
+a.btn {
+	display: inline-block;
+	cursor: pointer;
+	overflow: hidden;
+}
+.btn-important {
+	font-weight: bold;
+}
+
+/*=== Navigation */
+.nav-list .nav-header,
+.nav-list .item {
+	display: block;
+}
+.nav-list .item,
+.nav-list .item > a {
+	display: block;
+	overflow: hidden;
+	white-space: nowrap;
+	text-overflow: ellipsis;
+}
+.nav-head {
+	display: block;
+}
+.nav-head .item {
+	display: inline-block;
+}
+
+/*=== Horizontal-list */
+.horizontal-list {
+	display: table;
+	table-layout: fixed;
+	width: 100%;
+}
+.horizontal-list .item {
+	display: table-cell;
+}
+
+/*=== Dropdown */
+.dropdown {
+	position: relative;
+	display: inline-block;
+}
+.dropdown-target {
+	display: none;
+}
+.dropdown-menu {
+	display: none;
+	min-width: 200px;
+	margin: 0;
+	position: absolute;
+	right: 0;
+	background: #fff;
+	border: 1px solid #aaa;
+}
+.dropdown-header {
+	display: block;
+}
+.dropdown-menu > .item {
+	display: block;
+}
+.dropdown-menu > .item > a,
+.dropdown-menu > .item > span {
+	display: block;
+}
+.dropdown-menu > .item[aria-checked="true"] > a:before {
+	content: '✓';
+}
+.dropdown-menu .input {
+	display: block;
+}
+.dropdown-menu .input select,
+.dropdown-menu .input input {
+	display: block;
+	max-width: 95%;
+}
+.dropdown-target:target ~ .dropdown-menu {
+	display: block;
+	z-index: 10;
+}
+.dropdown-close {
+	display: inline;
+}
+.dropdown-close a {
+	font-size: 0;
+	position: fixed;
+	top: 0; bottom: 0;
+	left: 0; right: 0;
+	display: block;
+	z-index: -10;
+}
+.separator {
+	display: block;
+	height: 0;
+	border-bottom: 1px solid #aaa;
+}
+
+/*=== Alerts */
+.alert {
+	display: block;
+	width: 90%;
+}
+.group-controls .alert {
+	width: 100%
+}
+.alert-head {
+	margin: 0;
+	font-weight: bold;
+}
+.alert ul {
+	margin: 5px 20px;
+}
+
+/*=== Icons */
+.icon {
+	display: inline-block;
+	width: 16px;
+	height: 16px;
+	vertical-align: middle;
+	line-height: 16px;
+}
+
+/*=== Pagination */
+.pagination {
+	display: table;
+	width: 100%;
+	margin: 0;
+	padding: 0;
+	table-layout: fixed;
+}
+.pagination .item {
+	display: table-cell;
+}
+.pagination .pager-first,
+.pagination .pager-previous,
+.pagination .pager-next,
+.pagination .pager-last {
+	width: 100px;
+}
+
+/*=== STRUCTURE */
+/*===============*/
+/*=== Header */
+.header {
+	display: table;
+	width: 100%;
+	table-layout: fixed;
+}
+.header > .item {
+	display: table-cell;
+}
+.header > .item.title {
+	width: 250px;
+	white-space: nowrap;
+}
+.header > .item.title h1 {
+	display: inline-block;
+}
+.header > .item.title .logo {
+	display: inline-block;
+	height: 32px;
+	width: 32px;
+	vertical-align: middle;
+}
+.header > .item.configure {
+	width: 100px;
+}
+
+/*=== Body */
+#global {
+	display: table;
+	width: 100%;
+	height: 100%;
+	table-layout: fixed;
+}
+.aside {
+	display: table-cell;
+	height: 100%;
+	width: 250px;
+	vertical-align: top;
+}
+.aside.aside_flux {
+	background: #fff;
+}
+
+/*=== Aside main page (categories) */
+.categories {
+	list-style: none;
+	margin: 0;
+}
+.category {
+	display: block;
+	overflow: hidden;
+	white-space: nowrap;
+	text-overflow: ellipsis;
+}
+.category .btn:not([data-unread="0"]):after {
+	content: attr(data-unread);
+}
+
+/*=== Aside main page (feeds) */
+.categories .feeds {
+	width: 100%;
+	list-style: none;
+}
+.categories .feeds:not(.active) {
+	display: none;
+}
+.categories .feeds .feed {
+	display: inline-block;
+	overflow: hidden;
+	white-space: nowrap;
+	text-overflow: ellipsis;
+	vertical-align: middle;
+}
+.categories .feeds .feed:not([data-unread="0"]):before {
+	content: "(" attr(data-unread) ") ";
+}
+.categories .feeds .dropdown-menu {
+	left: 0;
+}
+.categories .feeds .item .dropdown-toggle > .icon {
+	visibility: hidden;
+	cursor: pointer;
+	vertical-align: top;
+}
+.categories .feeds .item .dropdown-target:target ~ .dropdown-toggle > .icon,
+.categories .feeds .item:hover .dropdown-toggle > .icon,
+.categories .feeds .item.active .dropdown-toggle > .icon {
+	visibility: visible;
+}
+
+/*=== New article notification */
+#new-article {
+	display: none;
+}
+#new-article > a {
+	display: block;
+}
+
+/*=== Day indication */
+.day .name {
+	position: absolute;
+	right: 0;
+	width: 50%;
+	overflow: hidden;
+	white-space: nowrap;
+	text-overflow: ellipsis;
+}
+
+/*=== Feed article header and footer */
+.flux_header {
+	position: relative;
+}
+.flux .item {
+	line-height: 40px;
+	white-space: nowrap;
+}
+.flux .item.manage,
+.flux .item.link {
+	width: 40px;
+	text-align: center;
+}
+.flux .item.website {
+	width: 200px;
+}
+.flux.not_read .item.title,
+.flux.current .item.title {
+	font-weight: bold;
+}
+.flux:not(.current):hover .item.title {
+	position: absolute;
+	max-width: calc(100% - 320px);
+	background: #fff;
+}
+.flux .item.title a {
+	color: #000;
+	text-decoration: none;
+}
+.flux .item.date {
+	width: 145px;
+	text-align: right;
+}
+.flux .item > a {
+	display: block;
+}
+.flux .item > a {
+	display: block;
+	text-decoration: none;
+	white-space: nowrap;
+	text-overflow: ellipsis;
+	overflow: hidden;
+}
+.flux .item.share > a {
+	display: list-item;
+	list-style-position: inside;
+	list-style-type: decimal;
+}
+
+/*=== Feed article content */
+.hide_posts > .flux:not(.active) > .flux_content {
+	display: none;
+}
+.content {
+	min-height: 20em;
+	margin: auto;
+	line-height: 1.7em;
+	word-wrap: break-word;
+}
+.content.large {
+	max-width: 1000px;
+}
+.content.medium {
+	max-width: 800px;
+}
+.content.thin {
+	max-width: 550px;
+}
+.content ul,
+.content ol,
+.content dd {
+	margin: 0 0 0 15px;
+	padding: 0 0 5px 15px;
+}
+.content pre {
+	overflow: auto;
+}
+
+/*=== Notification and actualize notification */
+.notification {
+	position: absolute;
+	top: 1em;
+	left: 25%; right: 25%;
+	z-index: 10;
+	background: #fff;
+	border: 1px solid #aaa;
+}
+.notification.closed {
+	display: none;
+}
+.notification a.close {
+	position: absolute;
+	top: 0; bottom: 0;
+	right: 0;
+	display: inline-block;
+}
+
+#actualizeProgress {
+	position: fixed;
+}
+#actualizeProgress progress {
+	max-width: 100%;
+	vertical-align: middle;
+}
+#actualizeProgress .progress {
+	vertical-align: middle;
+}
+
+/*=== Navigation menu (for articles) */
+#nav_entries {
+	position: fixed;
+	bottom: 0; left: 0;
+	display: table;
+	width: 250px;
+	background: #fff;
+	table-layout: fixed;
+}
+#nav_entries .item {
+	display: table-cell;
+	width: 30%;
+}
+#nav_entries a {
+	display: block;
+}
+
+/*=== "Load more" part */
+#load_more {
+	min-height: 40px;
+}
+.loading {
+	background: url("loader.gif") center center no-repeat;
+	font-size: 0;
+}
+#bigMarkAsRead {
+	display: block;
+	padding: 3em 0;
+	text-align: center;
+}
+.bigTick {
+	font-size: 7em;
+	line-height: 1.6em;
+}
+
+/*=== Statistiques */
+.stat > table {
+	width: 100%;
+}
+
+/*=== GLOBAL VIEW */
+/*================*/
+/*=== Category boxes */
+#stream.global .box-category {
+	display: inline-block;
+	width: 19em;
+	max-width: 95%;
+	margin: 20px 10px;
+	border: 1px solid #ccc;
+	vertical-align: top;
+}
+#stream.global .category {
+	width: 100%;
+}
+#stream.global .btn {
+	display: block;
+}
+#stream.global .box-category .feeds {
+	display: block;
+	overflow: auto;
+}
+#stream.global .box-category .feed {
+	width: 19em;
+	max-width: 90%;
+}
+
+/*=== Panel */
+#overlay {
+	display: none;
+	position: fixed;
+	top: 0; bottom: 0;
+	left: 0; right: 0;
+	background: rgba(0, 0, 0, 0.9);
+}
+#panel {
+	display: none;
+	position: fixed;
+	top: 1em; bottom: 1em;
+	left: 2em; right: 2em;
+	overflow: auto;
+	background: #fff;
+}
+#panel .close {
+	position: fixed;
+	top: 0; bottom: 0;
+	left: 0; right: 0;
+	display: block;
+}
+#panel .close img {
+	display: none;
+}
+
+/*=== DIVERS */
+/*===========*/
+.nav-login,
+.nav_menu .search,
+.nav_menu .toggle_aside {
+	display: none;
+}
+
+.aside .toggle_aside {
+	position: absolute;
+	right: 0;
+	display: none;
+	width: 30px;
+	height: 30px;
+	line-height: 30px;
+	text-align: center;
+}
+
+/*=== MOBILE */
+/*===========*/
+@media(max-width: 840px) {
+	.header,
+	.aside .btn-important,
+	.aside .feeds .dropdown,
+	.flux_header .item.website span,
+	.item.date, .day .date,
+	.dropdown-menu > .no-mobile,
+	.no-mobile {
+		display: none;
+	}
+	.nav-login {
+		display: block;
+	}
+	.nav_menu .toggle_aside,
+	.aside .toggle_aside,
+	.nav_menu .search,
+	#panel .close img {
+		display: inline-block;
+	}
+
+	.aside {
+		position: fixed;
+		top: 0; bottom: 0;
+		left: 0;
+		width: 0;
+		overflow: hidden;
+		z-index: 100;
+	}
+	.aside:target {
+		width: 90%;
+		overflow: auto;
+	}
+	.aside .categories {
+		margin: 10px 0 75px;
+	}
+
+	.flux_header .item.website {
+		width: 40px;
+	}
+
+	.flux:not(.current):hover .item.title {
+		position: relative;
+		width: auto;
+		white-space: nowrap;
+	}
+
+	.notification {
+		top: 0;
+		left: 0;
+		right: 0;
+	}
+
+	#nav_entries {
+		width: 100%;
+	}
+
+	#stream.global .box-category {
+		margin: 10px 0;
+	}
+
+	#panel {
+		top: 0; bottom: 0;
+		left: 0; right: 0;
+	}
+	#panel .close {
+		top: 0; right: 0;
+		left: auto; bottom: auto;
+		display: inline-block;
+		width: 30px;
+		height: 30px;
+	}
+}
+
+/*=== PRINTER */
+/*============*/
+@media print {
+	.header, .aside,
+	.nav_menu, .day,
+	.flux_header,
+	.flux_content .bottom,
+	.pagination,
+	#nav_entries {
+		display: none;
+	}
+	html, body {
+		background: #fff;
+		color: #000;
+		font-family: Serif;
+	}
+	#global,
+	.flux_content {
+		display: block !important;
+	}
+	.flux_content .content {
+		width: 100% !important;
+	}
+	.flux_content .content a {
+		color: #000;
+	}
+	.flux_content .content a:after {
+		content: " [" attr(href) "] ";
+		font-style: italic;
+	}
+}

+ 16 - 12
p/themes/Origine/origine.css

@@ -11,11 +11,13 @@
 html, body {
 	height: 100%;
 	font-family: "OpenSans", "Cantarell", "Helvetica", "Arial", sans-serif;
+	background: #fafafa;
 }
 
 /*=== Links */
 a {
 	color: #0062be;
+	outline: none;
 }
 
 /*=== Forms */
@@ -362,6 +364,10 @@ a.btn {
 	padding: 0 25px;
 	line-height: 2.5em;
 }
+.dropdown-menu > .item > span {
+	padding: 0 25px;
+	line-height: 2em;
+}
 .dropdown-menu > .item:hover {
 	background: #0062BE;
 	color: #fff;
@@ -400,7 +406,7 @@ a.btn {
 	font-size: 0.9em;
 }
 .alert-head {
-	font-size: 1.2em;
+	font-size: 1.15em;
 }
 .alert > a {
 	color: inherit;
@@ -464,15 +470,20 @@ a.btn {
 /*===============*/
 /*=== Header */
 .header {
+	height: 85px;
 	background: #f4f4f4;
 }
 .header > .item {
-	padding: 10px 0;
+	padding: 10px;
 	border-bottom: 1px solid #aaa;
 	vertical-align: middle;
 	text-align: center;
 }
+.header > .item.title{
+	width: 230px;
+}
 .header > .item.title h1 {
+	margin: 0.5em 0;
 	text-shadow: 1px -1px 0 #ccc;
 }
 .header > .item.title h1 a {
@@ -487,14 +498,14 @@ a.btn {
 
 /*=== Body */
 #global {
-	background: #fafafa;
+	height: calc(100% - 85px);
 }
 .aside {
 	border-right: 1px solid #aaa;
 	background: #fff;
 }
 .aside.aside_flux {
-	padding: 10px 0 40px;
+	padding: 10px 0 50px;
 }
 
 /*=== Aside main page (categories) */
@@ -690,9 +701,6 @@ a.btn {
 	color: #666;
 	font-size: 0.7rem;
 }
-.flux:not(.current):hover .item.title {
-	top: 1px;
-}
 
 .flux .bottom {
 	font-size: 0.8rem;
@@ -920,7 +928,7 @@ a.btn {
 	padding: 5px 10px;
 	background: #fafafa;
 	color: #333;
-	font-size: 90%;
+	font-size: 0.8rem;
 }
 .log+.log {
 	border-top: 1px solid #aaa;
@@ -998,10 +1006,6 @@ a.btn {
 		text-shadow: none;
 	}
 
-	.flux_header .item.website .favicon {
-		padding: 12px;
-	}
-
 	.pagination {
 		margin: 0 0 3.5em;
 	}

+ 11 - 2
p/themes/Origine/template.css

@@ -180,7 +180,8 @@ a.btn {
 .dropdown-menu > .item {
 	display: block;
 }
-.dropdown-menu > .item > a {
+.dropdown-menu > .item > a,
+.dropdown-menu > .item > span {
 	display: block;
 }
 .dropdown-menu > .item[aria-checked="true"] > a:before {
@@ -220,10 +221,16 @@ a.btn {
 	display: block;
 	width: 90%;
 }
+.group-controls .alert {
+	width: 100%
+}
 .alert-head {
 	margin: 0;
 	font-weight: bold;
 }
+.alert ul {
+	margin: 5px 20px;
+}
 
 /*=== Icons */
 .icon {
@@ -587,7 +594,9 @@ a.btn {
 	.aside .btn-important,
 	.aside .feeds .dropdown,
 	.flux_header .item.website span,
-	.item.date, .day .date {
+	.item.date, .day .date,
+	.dropdown-menu > .no-mobile,
+	.no-mobile {
 		display: none;
 	}
 	.nav-login {

+ 12 - 0
p/themes/base-theme/README.md

@@ -0,0 +1,12 @@
+FreshRSS-base-theme
+===================
+
+A base theme for [FreshRSS](http://freshrss.org)
+
+1. Custom ```base.css``` file with colors, backgrounds and borders
+2. Change information in ```metadata.json``` file (at least, give a name!)
+3. Choose your new theme in FreshRSS configuration
+4. Enjoy your wonderful theme!
+
+Don't hesitate to share your theme with us [on Github](https://github.com/marienfressinaud/FreshRSS/issues) :)
+

+ 762 - 0
p/themes/base-theme/base.css

@@ -0,0 +1,762 @@
+@charset "UTF-8";
+
+/*=== FONTS */
+@font-face {
+	font-family: "OpenSans";
+	src: url("../fonts/openSans.woff") format("woff");
+}
+
+/*=== GENERAL */
+/*============*/
+html, body {
+	height: 100%;
+	font-family: "OpenSans", "Cantarell", "Helvetica", "Arial", sans-serif;
+}
+
+/*=== Links */
+a {
+	outline: none;
+}
+
+/*=== Forms */
+legend {
+	margin: 20px 0 5px;
+	padding: 5px 0;
+	font-size: 1.4em;
+}
+label {
+	min-height: 25px;
+	padding: 5px 0;
+	cursor: pointer;
+}
+textarea {
+	width: 360px;
+	height: 100px;
+}
+input, select, textarea {
+	min-height: 25px;
+	padding: 5px;
+	line-height: 25px;
+	vertical-align: middle;
+}
+option {
+	padding: 0 .5em;
+}
+input:focus, select:focus, textarea:focus {
+}
+input:invalid, select:invalid {
+}
+input:disabled, select:disabled {
+}
+input.extend {
+	transition: width 200ms linear;
+	-moz-transition: width 200ms linear;
+	-webkit-transition: width 200ms linear;
+	-o-transition: width 200ms linear;
+	-ms-transition: width 200ms linear;
+}
+
+/*=== Tables */
+table {
+	border-collapse: collapse;
+}
+
+tr, th, td {
+	padding: 0.5em;
+}
+th {
+}
+form td,
+form th {
+	font-weight: normal;
+	text-align: center;
+}
+
+/*=== COMPONENTS */
+/*===============*/
+/*=== Forms */
+.form-group.form-actions {
+	padding: 5px 0;
+}
+.form-group.form-actions .btn {
+	margin: 0 10px;
+}
+.form-group .group-name {
+	padding: 10px 0;
+	text-align: right;
+}
+.form-group .group-controls {
+	min-height: 25px;
+	padding: 5px 0;
+}
+.form-group table {
+	margin: 10px 0 0 220px;
+}
+
+/*=== Buttons */
+.stick {
+	vertical-align: middle;
+	font-size: 0;
+}
+.stick input,
+.stick .btn {
+}
+.stick .btn:first-child,
+.stick input:first-child {
+}
+.stick .btn-important:first-child {
+}
+.stick .btn:last-child,
+.stick input:last-child {
+}
+.stick .btn + .btn,
+.stick .btn + input,
+.stick .btn + .dropdown > .btn,
+.stick input + .btn,
+.stick input + input,
+.stick input + .dropdown > .btn,
+.stick .dropdown + .btn,
+.stick .dropdown + input,
+.stick .dropdown + .dropdown > .btn {
+}
+.stick input + .btn {
+}
+.stick .btn + .dropdown > .btn {
+}
+
+.btn {
+	display: inline-block;
+	min-height: 37px;
+	min-width: 15px;
+	margin: 0;
+	padding: 5px 10px;
+	font-size: 0.9rem;
+	vertical-align: middle;
+	cursor: pointer;
+	overflow: hidden;
+}
+a.btn {
+	min-height: 25px;
+	line-height: 25px;
+}
+.btn:hover {
+	text-decoration: none;
+}
+.btn.active,
+.btn:active,
+.dropdown-target:target ~ .btn.dropdown-toggle {
+}
+
+.btn-important {
+	font-weight: normal;
+}
+.btn-important:hover {
+}
+.btn-important:active {
+}
+
+.btn-attention {
+}
+.btn-attention:hover {
+}
+.btn-attention:active {
+}
+
+/*=== Navigation */
+.nav-list .nav-header,
+.nav-list .item {
+	height: 2.5em;
+	line-height: 2.5em;
+	font-size: 0.9rem;
+}
+.nav-list .item:hover {
+}
+.nav-list .item:hover a {
+}
+.nav-list .item.active {
+}
+.nav-list .item.active a {
+}
+.nav-list .disable {
+	text-align: center;
+}
+.nav-list .item > a {
+	padding: 0 10px;
+}
+.nav-list a:hover {
+	text-decoration: none;
+}
+.nav-list .item.empty a {
+}
+.nav-list .item.active.empty a {
+}
+.nav-list .item.error a {
+}
+.nav-list .item.active.error a {
+}
+
+.nav-list .nav-header {
+	padding: 0 10px;
+	font-weight: bold;
+}
+
+.nav-list .nav-form {
+	padding: 3px;
+	text-align: center;
+}
+
+.nav-head {
+	margin: 0;
+	text-align: right;
+}
+.nav-head .item {
+	padding: 5px 10px;
+	font-size: 0.9rem;
+	line-height: 1.5rem;
+}
+
+/*=== Horizontal-list */
+.horizontal-list {
+	margin: 0;
+	padding: 0;
+}
+.horizontal-list .item {
+	vertical-align: middle;
+}
+
+/*=== Dropdown */
+.dropdown-menu {
+	margin: 5px 0 0;
+	padding: 5px 0;
+	font-size: 0.8rem;
+	text-align: left;
+}
+.dropdown-menu:after {
+	content: "";
+	position: absolute;
+	top: -6px;
+	right: 13px;
+	width: 10px;
+	height: 10px;
+	z-index: -10;
+	transform: rotate(45deg);
+	-moz-transform: rotate(45deg);
+	-webkit-transform: rotate(45deg);
+	-ms-transform: rotate(45deg);
+}
+.dropdown-header {
+	padding: 0 5px 5px;
+	font-weight: bold;
+	text-align: left;
+}
+.dropdown-menu > .item {
+}
+.dropdown-menu > .item > a {
+	padding: 0 25px;
+	line-height: 2.5em;
+}
+.dropdown-menu > .item > span {
+	padding: 0 25px;
+	line-height: 2em;
+}
+.dropdown-menu > .item:hover {
+}
+.dropdown-menu > .item[aria-checked="true"] > a:before {
+	font-weight: bold;
+	margin: 0 0 0 -14px;
+}
+.dropdown-menu > .item:hover > a {
+	text-decoration: none;
+}
+.dropdown-menu .input select,
+.dropdown-menu .input input {
+	margin: 0 auto 5px;
+	padding: 2px 5px;
+}
+
+.separator {
+	margin: 5px 0;
+}
+
+/*=== Alerts */
+.alert {
+	margin: 15px auto;
+	padding: 10px 15px;
+	font-size: 0.9em;
+}
+.alert-head {
+	font-size: 1.15em;
+}
+.alert > a {
+	text-decoration: underline;
+}
+.alert-warn {
+}
+.alert-success {
+}
+.alert-error {
+}
+
+/*=== Pagination */
+.pagination {
+	text-align: center;
+	font-size: 0.8em;
+}
+.content .pagination {
+	margin: 0;
+	padding: 0;
+}
+.pagination .item.pager-current {
+	font-weight: bold;
+	font-size: 1.5em;
+}
+.pagination .item a {
+	display: block;
+	font-style: italic;
+	line-height: 3em;
+	text-decoration: none;
+}
+.pagination .item a:hover {
+}
+.pagination:first-child .item {
+}
+.pagination:last-child .item {
+}
+
+.pagination .loading,
+.pagination a:hover.loading {
+	font-size: 0;
+}
+
+/*=== STRUCTURE */
+/*===============*/
+/*=== Header */
+.header {
+	height: 85px;
+}
+.header > .item {
+	padding: 10px;
+	vertical-align: middle;
+	text-align: center;
+}
+.header > .item.title{
+	width: 230px;
+}
+.header > .item.title h1 {
+	margin: 0.5em 0;
+}
+.header > .item.title h1 a {
+	text-decoration: none;
+}
+.header > .item.search input {
+	width: 230px;
+}
+.header .item.search input:focus {
+	width: 350px;
+}
+
+/*=== Body */
+#global {
+	height: calc(100% - 85px);
+}
+.aside {
+}
+.aside.aside_flux {
+	padding: 10px 0 50px;
+}
+
+/*=== Aside main page (categories) */
+.categories {
+	text-align: center;
+}
+.category {
+	width: 235px;
+	margin: 10px auto;
+	text-align: left;
+}
+.category .btn:first-child {
+	position: relative;
+	width: 213px;
+}
+.category.stick .btn:first-child {
+	width: 176px;
+}
+.category .btn:first-child:not([data-unread="0"]):after {
+	position: absolute;
+	top: 3px; right: 3px;
+	padding: 1px 5px;
+}
+
+/*=== Aside main page (feeds) */
+.categories .feeds .item.active {
+}
+.categories .feeds .item.active .feed {
+}
+.categories .feeds .item.empty .feed {
+}
+.categories .feeds .item.empty.active {
+}
+.categories .feeds .item.empty.active .feed {
+}
+.categories .feeds .item.error .feed {
+}
+.categories .feeds .item .feed {
+	margin: 0;
+	width: 165px;
+	line-height: 3em;
+	font-size: 0.8em;
+	text-align: left;
+	text-decoration: none;
+}
+.categories .feeds .feed:not([data-unread="0"]) {
+	font-weight: bold;
+}
+.categories .feeds .dropdown-menu:after {
+	left: 2px;
+}
+.categories .feeds .item .dropdown-target:target ~ .dropdown-toggle > .icon,
+.categories .feeds .item:hover .dropdown-toggle > .icon,
+.categories .feeds .item.active .dropdown-toggle > .icon {
+	vertical-align: middle;
+}
+
+/*=== Configuration pages */
+.post {
+	padding: 10px 50px;
+	font-size: 0.9em;
+}
+.post form {
+	margin: 10px 0;
+}
+.post.content {
+	max-width: 550px;
+}
+
+/*=== Prompt (centered) */
+.prompt {
+	text-align: center;
+}
+.prompt label {
+	text-align: left;
+}
+.prompt form {
+	margin: 10px auto 20px auto;
+	width: 180px;
+}
+.prompt input {
+	margin: 5px auto;
+	width: 100%;
+}
+.prompt p {
+	margin: 20px 0;
+}
+
+/*=== New article notification */
+#new-article {
+	text-align: center;
+	font-size: 0.9em;
+}
+#new-article:hover {
+}
+#new-article > a {
+	line-height: 3em;
+	font-weight: bold;
+}
+#new-article > a:hover {
+	text-decoration: none;
+}
+
+/*=== Day indication */
+.day {
+	padding: 0 10px;
+	font-weight: bold;
+	line-height: 3em;
+}
+#new-article + .day {
+}
+.day .name {
+	padding: 0 10px 0 0;
+	font-size: 1.8em;
+	opacity: 0.3;
+	font-style: italic;
+	text-align: right;
+}
+
+/*=== Index menu */
+.nav_menu {
+	text-align: center;
+	padding: 5px 0;
+}
+
+/*=== Feed articles */
+.flux {
+}
+.flux:hover {
+}
+.flux.current {
+}
+.flux.not_read {
+}
+.flux.not_read:not(.current):hover .item.title {
+}
+.flux.favorite {
+}
+.flux.favorite:not(.current):hover .item.title {
+}
+.flux.current {
+}
+
+
+.flux_header {
+	font-size: 0.8rem;
+	cursor: pointer;
+}
+.flux_header .title {
+	font-size: 0.9rem;
+}
+.flux .website .favicon {
+	padding: 5px;
+}
+.flux .date {
+	font-size: 0.7rem;
+}
+.flux:not(.current):hover .item.title {
+}
+
+.flux .bottom {
+	font-size: 0.8rem;
+	text-align: center;
+}
+
+/*=== Content of feed articles */
+.content {
+	padding: 20px 10px;
+}
+.content > h1.title > a {
+}
+
+.content hr {
+	margin: 30px 10px;
+	height: 1px;
+}
+
+.content pre {
+	margin: 10px auto;
+	padding: 10px 20px;
+	overflow: auto;
+	font-size: 0.9rem;
+}
+.content code {
+	padding: 2px 5px;
+}
+.content pre code {
+}
+
+.content blockquote {
+	display: block;
+	margin: 0;
+	padding: 5px 20px;
+}
+.content blockquote p {
+	margin: 0;
+}
+
+/*=== Notification and actualize notification */
+.notification {
+	padding: 0 0 0 5px;
+	text-align: center;
+	font-weight: bold;
+	font-size: 0.9em;
+	line-height: 3em;
+	z-index: 10;
+	vertical-align: middle;
+}
+.notification.good {
+}
+.notification.bad {
+}
+.notification a.close {
+	padding: 0 15px;
+	line-height: 3em;
+}
+.notification.good a.close:hover {
+}
+.notification.bad a.close:hover {
+}
+
+.notification#actualizeProgress {
+	line-height: 2em;
+}
+
+/*=== "Load more" part */
+#bigMarkAsRead {
+	text-align: center;
+	text-decoration: none;
+}
+#bigMarkAsRead:hover {
+}
+#bigMarkAsRead:hover .bigTick {
+}
+
+/*=== Navigation menu (for articles) */
+#nav_entries {
+	margin: 0;
+	text-align: center;
+	line-height: 3em;
+	table-layout: fixed;
+}
+
+/*=== READER VIEW */
+/*================*/
+#stream.reader .flux {
+	padding: 0 0 50px;
+}
+#stream.reader .flux .author {
+	margin: 0 0 10px;
+	font-size: 90%;
+}
+
+/*=== GLOBAL VIEW */
+/*================*/
+#stream.global .box-category {
+	text-align: left;
+}
+#stream.global .category {
+	margin: 0;
+}
+#stream.global .btn {
+	width: auto;
+	height: 2em;
+	margin: 0;
+	padding: 0 10px;
+	line-height: 2em;
+	font-size: 1.2rem;
+}
+#stream.global .btn:not([data-unread="0"]) {
+	font-weight: bold;
+}
+#stream.global .btn:first-child:not([data-unread="0"]):after {
+	top: 0; right: 5px;
+	font-weight: bold;
+}
+#stream.global .box-category .feeds {
+	max-height: 250px;
+}
+#stream.global .box-category .feeds .item {
+	padding: 2px 10px;
+	font-size: 0.9rem;
+}
+
+/*=== DIVERS */
+/*===========*/
+.aside.aside_feed .nav-form input,
+.aside.aside_feed .nav-form select {
+	width: 140px;
+}
+.aside.aside_feed .nav-form .dropdown .dropdown-menu {
+	right: -20px;
+}
+.aside.aside_feed .nav-form .dropdown .dropdown-menu:after {
+	right: 33px;
+}
+
+/*=== STATISTICS */
+/*===============*/
+.stat {
+	margin: 10px 0 20px;
+}
+
+.stat th,
+.stat td,
+.stat tr {
+}
+.stat > table td,
+.stat > table th {
+	text-align: center;
+}
+
+/*=== LOGS */
+/*=========*/
+.logs {
+	overflow: hidden;
+}
+.log {
+	padding: 5px 10px;
+	font-size: 0.8rem;
+}
+.log+.log {
+}
+.log .date {
+	display: block;
+	font-weight: bold;
+}
+.log.error {
+}
+.log.warning {
+}
+.log.notice {
+}
+.log.debug {
+}
+
+/*=== MOBILE */
+/*===========*/
+@media(max-width: 840px) {
+	.aside {
+		transition: width 200ms linear;
+		-moz-transition: width 200ms linear;
+		-webkit-transition: width 200ms linear;
+		-o-transition: width 200ms linear;
+		-ms-transition: width 200ms linear;
+	}
+	.aside .toggle_aside,
+	#panel .close {
+		position: absolute;
+		display: block;
+		top: 0; right: 0;
+		width: 30px;
+		height: 30px;
+		line-height: 30px;
+		text-align: center;
+	}
+
+	.nav_menu .btn {
+		margin: 5px 10px;
+	}
+	.nav_menu .stick {
+		margin: 0 10px;
+	}
+	.nav_menu .stick .btn {
+		margin: 5px 0;
+	}
+	.nav_menu .search {
+		display: inline-block;
+		max-width: 97%;
+	}
+	.nav_menu .search input {
+		max-width: 97%;
+		width: 90px;
+	}
+	.nav_menu .search input:focus {
+		width: 400px;
+	}
+
+	.day .name {
+		font-size: 1.1rem;
+	}
+
+	.pagination {
+		margin: 0 0 3.5em;
+	}
+
+	.notification a.close {
+		display: block;
+		left: 0;
+	}
+	.notification a.close:hover {
+		opacity: 0.5;
+	}
+	.notification a.close .icon {
+		display: none;
+	}
+}

+ 7 - 0
p/themes/base-theme/metadata.json

@@ -0,0 +1,7 @@
+{
+  "name": "",
+  "author": "Your name",
+  "description": "A wonderful base theme",
+  "version": 0.1,
+  "files": ["template.css", "base.css"]
+}

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

@@ -0,0 +1,695 @@
+@charset "UTF-8";
+
+/*=== GENERAL */
+/*============*/
+html, body {
+	margin: 0;
+	padding: 0;
+	font-size: 100%;
+}
+
+/*=== Links */
+a {
+	text-decoration: none;
+}
+a:hover {
+	text-decoration: underline;
+}
+
+/*=== Lists */
+ul, ol, dd {
+	margin: 0;
+	padding: 0;
+}
+
+/*=== Titles */
+h1 {
+	margin: 0.6em 0 0.3em;
+	font-size: 1.5em;
+	line-height: 1.6em;
+}
+h2 {
+	margin: 0.5em 0 0.25em;
+	font-size: 1.3em;
+	line-height: 2em;
+}
+h3 {
+	margin: 0.5em 0 0.25em;
+	font-size: 1.1em;
+	line-height: 2em;
+}
+
+/*=== Paragraphs */
+p {
+	margin: 1em 0 0.5em;
+	font-size: 1em;
+}
+
+/*=== Images */
+img {
+	height: auto;
+	max-width: 100%;
+}
+img.favicon {
+	height: 16px;
+	width: 16px;
+	vertical-align: middle;
+}
+
+/*=== Videos */
+iframe, embed, object, video {
+	max-width: 100%;
+}
+
+/*=== Forms */
+legend {
+	display: block;
+	width: 100%;
+	clear: both;
+}
+label {
+	display: block;
+}
+input {
+	width: 180px;
+}
+textarea {
+	width: 300px;
+}
+input, select, textarea {
+	display: inline-block;
+	max-width: 100%;
+}
+input[type="radio"],
+input[type="checkbox"] {
+	width: 15px !important;
+	min-height: 15px !important;
+}
+input.extend:focus {
+	width: 300px;
+}
+
+/*=== COMPONENTS */
+/*===============*/
+/*=== Forms */
+.form-group:after {
+	content: "";
+	display: block;
+	clear: both;
+}
+.form-group.form-actions {
+	min-width: 250px;
+}
+.form-group .group-name {
+	display: block;
+	float: left;
+	width: 200px;
+}
+.form-group .group-controls {
+	min-width: 250px;
+	margin: 0 0 0 220px;
+}
+.form-group .group-controls .control {
+	display: block;
+}
+
+/*=== Buttons */
+.stick {
+	display: inline-block;
+	white-space: nowrap;
+}
+.btn,
+a.btn {
+	display: inline-block;
+	cursor: pointer;
+	overflow: hidden;
+}
+.btn-important {
+	font-weight: bold;
+}
+
+/*=== Navigation */
+.nav-list .nav-header,
+.nav-list .item {
+	display: block;
+}
+.nav-list .item,
+.nav-list .item > a {
+	display: block;
+	overflow: hidden;
+	white-space: nowrap;
+	text-overflow: ellipsis;
+}
+.nav-head {
+	display: block;
+}
+.nav-head .item {
+	display: inline-block;
+}
+
+/*=== Horizontal-list */
+.horizontal-list {
+	display: table;
+	table-layout: fixed;
+	width: 100%;
+}
+.horizontal-list .item {
+	display: table-cell;
+}
+
+/*=== Dropdown */
+.dropdown {
+	position: relative;
+	display: inline-block;
+}
+.dropdown-target {
+	display: none;
+}
+.dropdown-menu {
+	display: none;
+	min-width: 200px;
+	margin: 0;
+	position: absolute;
+	right: 0;
+	background: #fff;
+	border: 1px solid #aaa;
+}
+.dropdown-header {
+	display: block;
+}
+.dropdown-menu > .item {
+	display: block;
+}
+.dropdown-menu > .item > a,
+.dropdown-menu > .item > span {
+	display: block;
+}
+.dropdown-menu > .item[aria-checked="true"] > a:before {
+	content: '✓';
+}
+.dropdown-menu .input {
+	display: block;
+}
+.dropdown-menu .input select,
+.dropdown-menu .input input {
+	display: block;
+	max-width: 95%;
+}
+.dropdown-target:target ~ .dropdown-menu {
+	display: block;
+	z-index: 10;
+}
+.dropdown-close {
+	display: inline;
+}
+.dropdown-close a {
+	font-size: 0;
+	position: fixed;
+	top: 0; bottom: 0;
+	left: 0; right: 0;
+	display: block;
+	z-index: -10;
+}
+.separator {
+	display: block;
+	height: 0;
+	border-bottom: 1px solid #aaa;
+}
+
+/*=== Alerts */
+.alert {
+	display: block;
+	width: 90%;
+}
+.group-controls .alert {
+	width: 100%
+}
+.alert-head {
+	margin: 0;
+	font-weight: bold;
+}
+.alert ul {
+	margin: 5px 20px;
+}
+
+/*=== Icons */
+.icon {
+	display: inline-block;
+	width: 16px;
+	height: 16px;
+	vertical-align: middle;
+	line-height: 16px;
+}
+
+/*=== Pagination */
+.pagination {
+	display: table;
+	width: 100%;
+	margin: 0;
+	padding: 0;
+	table-layout: fixed;
+}
+.pagination .item {
+	display: table-cell;
+}
+.pagination .pager-first,
+.pagination .pager-previous,
+.pagination .pager-next,
+.pagination .pager-last {
+	width: 100px;
+}
+
+/*=== STRUCTURE */
+/*===============*/
+/*=== Header */
+.header {
+	display: table;
+	width: 100%;
+	table-layout: fixed;
+}
+.header > .item {
+	display: table-cell;
+}
+.header > .item.title {
+	width: 250px;
+	white-space: nowrap;
+}
+.header > .item.title h1 {
+	display: inline-block;
+}
+.header > .item.title .logo {
+	display: inline-block;
+	height: 32px;
+	width: 32px;
+	vertical-align: middle;
+}
+.header > .item.configure {
+	width: 100px;
+}
+
+/*=== Body */
+#global {
+	display: table;
+	width: 100%;
+	height: 100%;
+	table-layout: fixed;
+}
+.aside {
+	display: table-cell;
+	height: 100%;
+	width: 250px;
+	vertical-align: top;
+}
+.aside.aside_flux {
+	background: #fff;
+}
+
+/*=== Aside main page (categories) */
+.categories {
+	list-style: none;
+	margin: 0;
+}
+.category {
+	display: block;
+	overflow: hidden;
+	white-space: nowrap;
+	text-overflow: ellipsis;
+}
+.category .btn:not([data-unread="0"]):after {
+	content: attr(data-unread);
+}
+
+/*=== Aside main page (feeds) */
+.categories .feeds {
+	width: 100%;
+	list-style: none;
+}
+.categories .feeds:not(.active) {
+	display: none;
+}
+.categories .feeds .feed {
+	display: inline-block;
+	overflow: hidden;
+	white-space: nowrap;
+	text-overflow: ellipsis;
+	vertical-align: middle;
+}
+.categories .feeds .feed:not([data-unread="0"]):before {
+	content: "(" attr(data-unread) ") ";
+}
+.categories .feeds .dropdown-menu {
+	left: 0;
+}
+.categories .feeds .item .dropdown-toggle > .icon {
+	visibility: hidden;
+	cursor: pointer;
+	vertical-align: top;
+}
+.categories .feeds .item .dropdown-target:target ~ .dropdown-toggle > .icon,
+.categories .feeds .item:hover .dropdown-toggle > .icon,
+.categories .feeds .item.active .dropdown-toggle > .icon {
+	visibility: visible;
+}
+
+/*=== New article notification */
+#new-article {
+	display: none;
+}
+#new-article > a {
+	display: block;
+}
+
+/*=== Day indication */
+.day .name {
+	position: absolute;
+	right: 0;
+	width: 50%;
+	overflow: hidden;
+	white-space: nowrap;
+	text-overflow: ellipsis;
+}
+
+/*=== Feed article header and footer */
+.flux_header {
+	position: relative;
+}
+.flux .item {
+	line-height: 40px;
+	white-space: nowrap;
+}
+.flux .item.manage,
+.flux .item.link {
+	width: 40px;
+	text-align: center;
+}
+.flux .item.website {
+	width: 200px;
+}
+.flux.not_read .item.title,
+.flux.current .item.title {
+	font-weight: bold;
+}
+.flux:not(.current):hover .item.title {
+	position: absolute;
+	max-width: calc(100% - 320px);
+	background: #fff;
+}
+.flux .item.title a {
+	color: #000;
+	text-decoration: none;
+}
+.flux .item.date {
+	width: 145px;
+	text-align: right;
+}
+.flux .item > a {
+	display: block;
+}
+.flux .item > a {
+	display: block;
+	text-decoration: none;
+	white-space: nowrap;
+	text-overflow: ellipsis;
+	overflow: hidden;
+}
+.flux .item.share > a {
+	display: list-item;
+	list-style-position: inside;
+	list-style-type: decimal;
+}
+
+/*=== Feed article content */
+.hide_posts > .flux:not(.active) > .flux_content {
+	display: none;
+}
+.content {
+	min-height: 20em;
+	margin: auto;
+	line-height: 1.7em;
+	word-wrap: break-word;
+}
+.content.large {
+	max-width: 1000px;
+}
+.content.medium {
+	max-width: 800px;
+}
+.content.thin {
+	max-width: 550px;
+}
+.content ul,
+.content ol,
+.content dd {
+	margin: 0 0 0 15px;
+	padding: 0 0 5px 15px;
+}
+.content pre {
+	overflow: auto;
+}
+
+/*=== Notification and actualize notification */
+.notification {
+	position: absolute;
+	top: 1em;
+	left: 25%; right: 25%;
+	z-index: 10;
+	background: #fff;
+	border: 1px solid #aaa;
+}
+.notification.closed {
+	display: none;
+}
+.notification a.close {
+	position: absolute;
+	top: 0; bottom: 0;
+	right: 0;
+	display: inline-block;
+}
+
+#actualizeProgress {
+	position: fixed;
+}
+#actualizeProgress progress {
+	max-width: 100%;
+	vertical-align: middle;
+}
+#actualizeProgress .progress {
+	vertical-align: middle;
+}
+
+/*=== Navigation menu (for articles) */
+#nav_entries {
+	position: fixed;
+	bottom: 0; left: 0;
+	display: table;
+	width: 250px;
+	background: #fff;
+	table-layout: fixed;
+}
+#nav_entries .item {
+	display: table-cell;
+	width: 30%;
+}
+#nav_entries a {
+	display: block;
+}
+
+/*=== "Load more" part */
+#load_more {
+	min-height: 40px;
+}
+.loading {
+	background: url("loader.gif") center center no-repeat;
+	font-size: 0;
+}
+#bigMarkAsRead {
+	display: block;
+	padding: 3em 0;
+	text-align: center;
+}
+.bigTick {
+	font-size: 7em;
+	line-height: 1.6em;
+}
+
+/*=== Statistiques */
+.stat > table {
+	width: 100%;
+}
+
+/*=== GLOBAL VIEW */
+/*================*/
+/*=== Category boxes */
+#stream.global .box-category {
+	display: inline-block;
+	width: 19em;
+	max-width: 95%;
+	margin: 20px 10px;
+	border: 1px solid #ccc;
+	vertical-align: top;
+}
+#stream.global .category {
+	width: 100%;
+}
+#stream.global .btn {
+	display: block;
+}
+#stream.global .box-category .feeds {
+	display: block;
+	overflow: auto;
+}
+#stream.global .box-category .feed {
+	width: 19em;
+	max-width: 90%;
+}
+
+/*=== Panel */
+#overlay {
+	display: none;
+	position: fixed;
+	top: 0; bottom: 0;
+	left: 0; right: 0;
+	background: rgba(0, 0, 0, 0.9);
+}
+#panel {
+	display: none;
+	position: fixed;
+	top: 1em; bottom: 1em;
+	left: 2em; right: 2em;
+	overflow: auto;
+	background: #fff;
+}
+#panel .close {
+	position: fixed;
+	top: 0; bottom: 0;
+	left: 0; right: 0;
+	display: block;
+}
+#panel .close img {
+	display: none;
+}
+
+/*=== DIVERS */
+/*===========*/
+.nav-login,
+.nav_menu .search,
+.nav_menu .toggle_aside {
+	display: none;
+}
+
+.aside .toggle_aside {
+	position: absolute;
+	right: 0;
+	display: none;
+	width: 30px;
+	height: 30px;
+	line-height: 30px;
+	text-align: center;
+}
+
+/*=== MOBILE */
+/*===========*/
+@media(max-width: 840px) {
+	.header,
+	.aside .btn-important,
+	.aside .feeds .dropdown,
+	.flux_header .item.website span,
+	.item.date, .day .date,
+	.dropdown-menu > .no-mobile,
+	.no-mobile {
+		display: none;
+	}
+	.nav-login {
+		display: block;
+	}
+	.nav_menu .toggle_aside,
+	.aside .toggle_aside,
+	.nav_menu .search,
+	#panel .close img {
+		display: inline-block;
+	}
+
+	.aside {
+		position: fixed;
+		top: 0; bottom: 0;
+		left: 0;
+		width: 0;
+		overflow: hidden;
+		z-index: 100;
+	}
+	.aside:target {
+		width: 90%;
+		overflow: auto;
+	}
+	.aside .categories {
+		margin: 10px 0 75px;
+	}
+
+	.flux_header .item.website {
+		width: 40px;
+	}
+
+	.flux:not(.current):hover .item.title {
+		position: relative;
+		width: auto;
+		white-space: nowrap;
+	}
+
+	.notification {
+		top: 0;
+		left: 0;
+		right: 0;
+	}
+
+	#nav_entries {
+		width: 100%;
+	}
+
+	#stream.global .box-category {
+		margin: 10px 0;
+	}
+
+	#panel {
+		top: 0; bottom: 0;
+		left: 0; right: 0;
+	}
+	#panel .close {
+		top: 0; right: 0;
+		left: auto; bottom: auto;
+		display: inline-block;
+		width: 30px;
+		height: 30px;
+	}
+}
+
+/*=== PRINTER */
+/*============*/
+@media print {
+	.header, .aside,
+	.nav_menu, .day,
+	.flux_header,
+	.flux_content .bottom,
+	.pagination,
+	#nav_entries {
+		display: none;
+	}
+	html, body {
+		background: #fff;
+		color: #000;
+		font-family: Serif;
+	}
+	#global,
+	.flux_content {
+		display: block !important;
+	}
+	.flux_content .content {
+		width: 100% !important;
+	}
+	.flux_content .content a {
+		color: #000;
+	}
+	.flux_content .content a:after {
+		content: " [" attr(href) "] ";
+		font-style: italic;
+	}
+}

+ 6 - 0
p/themes/icons/bookmark-add.svg

@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16">
+<g transform="translate(-141.0002,-807)" fill="#bebebe">
+<path d="m143,807,0,13,4-4,4,4,0-4,0-1-2,0,0-4,2,0,0-4z"/>
+<path d="m152,810,0,2-2,0,0,2,2,0,0,2,2,0,0-2,2,0,0-2-2,0,0-2-2,0z"/>
+</g>
+</svg>