Răsfoiți Sursa

Merge branch 'dev' into beta

Marien Fressinaud 11 ani în urmă
părinte
comite
8d7ac978f9
76 a modificat fișierele cu 6929 adăugiri și 4022 ștergeri
  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>