Przeglądaj źródła

Merge branch '634-refactor-with-context' into dev

Marien Fressinaud 11 lat temu
rodzic
commit
3e2d34c867

+ 1 - 4
app/Controllers/configureController.php

@@ -104,7 +104,7 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 		if (Minz_Request::isPost()) {
 			FreshRSS_Context::$conf->_posts_per_page(Minz_Request::param('posts_per_page', 10));
 			FreshRSS_Context::$conf->_view_mode(Minz_Request::param('view_mode', 'normal'));
-			FreshRSS_Context::$conf->_default_view((int)Minz_Request::param('default_view', FreshRSS_Entry::STATE_ALL));
+			FreshRSS_Context::$conf->_default_view(Minz_Request::param('default_view', 'adaptive'));
 			FreshRSS_Context::$conf->_auto_load_more(Minz_Request::param('auto_load_more', false));
 			FreshRSS_Context::$conf->_display_posts(Minz_Request::param('display_posts', false));
 			FreshRSS_Context::$conf->_display_categories(Minz_Request::param('display_categories', false));
@@ -338,9 +338,6 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 				unset($query[$key]);
 			}
 		}
-		if (!empty($query['state']) && $query['state'] & FreshRSS_Entry::STATE_STRICT) {
-			$query['state'] -= FreshRSS_Entry::STATE_STRICT;
-		}
 		$queries[] = $query;
 		FreshRSS_Context::$conf->_queries($queries);
 		FreshRSS_Context::$conf->save();

+ 3 - 11
app/Controllers/entryController.php

@@ -17,14 +17,6 @@ class FreshRSS_entry_Controller extends Minz_ActionController {
 			);
 		}
 
-		// Keep parameter information (output) to do a correct redirection at
-		// the end.
-		$this->params = array();
-		$output = Minz_Request::param('output', '');
-		if ($output != '' && FreshRSS_Context::$conf->view_mode !== $output) {
-			$this->params['output'] = $output;
-		}
-
 		// If ajax request, we do not print layout
 		$this->ajax = Minz_Request::param('ajax');
 		if ($this->ajax) {
@@ -53,6 +45,7 @@ class FreshRSS_entry_Controller extends Minz_ActionController {
 		$get = Minz_Request::param('get');
 		$next_get = Minz_Request::param('nextGet', $get);
 		$id_max = Minz_Request::param('idMax', 0);
+		$params = array();
 
 		$entryDAO = FreshRSS_Factory::createEntryDao();
 		if ($id === false) {
@@ -86,7 +79,7 @@ class FreshRSS_entry_Controller extends Minz_ActionController {
 					// Redirect to the correct page (category, feed or starred)
 					// Not "a" because it is the default value if nothing is
 					// given.
-					$this->params['get'] = $next_get;
+					$params['get'] = $next_get;
 				}
 			}
 		} else {
@@ -98,7 +91,7 @@ class FreshRSS_entry_Controller extends Minz_ActionController {
 			Minz_Request::good(_t('feeds_marked_read'), array(
 				'c' => 'index',
 				'a' => 'index',
-				'params' => $this->params,
+				'params' => $params,
 			), true);
 		}
 	}
@@ -123,7 +116,6 @@ class FreshRSS_entry_Controller extends Minz_ActionController {
 			Minz_Request::forward(array(
 				'c' => 'index',
 				'a' => 'index',
-				'params' => $this->params,
 			), true);
 		}
 	}

+ 1 - 1
app/Controllers/errorController.php

@@ -37,7 +37,7 @@ class FreshRSS_error_Controller extends Minz_ActionController {
 		if ($this->view->errorMessage == '') {
 			switch($code_int) {
 			case 403:
-				$this->view->errorMessage = _t('forbidden_access');
+				$this->view->errorMessage = _t('access_denied');
 				break;
 			case 404:
 			default:

+ 177 - 174
app/Controllers/indexController.php

@@ -1,217 +1,220 @@
 <?php
 
+/**
+ * This class handles main actions of FreshRSS.
+ */
 class FreshRSS_index_Controller extends Minz_ActionController {
-	private $nb_not_read_cat = 0;
 
+	/**
+	 * This action only redirect on the default view mode (normal or global)
+	 */
 	public function indexAction() {
-		$output = Minz_Request::param('output');
-		$token = FreshRSS_Context::$conf->token;
+		$prefered_output = FreshRSS_Context::$conf->view_mode;
+		Minz_Request::forward(array(
+			'c' => 'index',
+			'a' => $prefered_output
+		));
+	}
 
-		// check if user is logged in
+	/**
+	 * This action displays the normal view of FreshRSS.
+	 */
+	public function normalAction() {
 		if (!FreshRSS_Auth::hasAccess() && !Minz_Configuration::allowAnonymous()) {
-			$token_param = Minz_Request::param('token', '');
-			$token_is_ok = ($token != '' && $token === $token_param);
-			if ($output === 'rss' && !$token_is_ok) {
-				Minz_Error::error(
-					403,
-					array('error' => array(_t('access_denied')))
-				);
-				return;
-			} elseif ($output !== 'rss') {
-				// "hard" redirection is not required, just ask dispatcher to
-				// forward to the login form without 302 redirection
-				Minz_Request::forward(array('c' => 'auth', 'a' => 'login'));
-				return;
-			}
+			Minz_Request::forward(array('c' => 'auth', 'a' => 'login'));
+			return;
 		}
 
-		$params = Minz_Request::params();
-		if (isset($params['search'])) {
-			$params['search'] = urlencode($params['search']);
+		try {
+			$this->updateContext();
+		} catch (FreshRSS_Context_Exception $e) {
+			Minz_Error::error(404);
 		}
 
-		$this->view->url = array(
-			'c' => 'index',
-			'a' => 'index',
-			'params' => $params
-		);
+		try {
+			$entries = $this->listEntriesByContext();
+
+			$nb_entries = count($entries);
+			if ($nb_entries > FreshRSS_Context::$number) {
+				// We have more elements for pagination
+				$last_entry = array_pop($entries);
+				FreshRSS_Context::$next_id = $last_entry->id();
+			}
+
+			$first_entry = $nb_entries > 0 ? $entries[0] : null;
+			FreshRSS_Context::$id_max = $first_entry === null ?
+			                            (time() - 1) . '000000' :
+			                            $first_entry->id();
+			if (FreshRSS_Context::$order === 'ASC') {
+				// In this case we do not know but we guess id_max
+				$id_max = (time() - 1) . '000000';
+				if (strcmp($id_max, FreshRSS_Context::$id_max) > 0) {
+					FreshRSS_Context::$id_max = $id_max;
+				}
+			}
+
+			$this->view->entries = $entries;
+		} catch (FreshRSS_EntriesGetter_Exception $e) {
+			Minz_Log::notice($e->getMessage());
+			Minz_Error::error(404);
+		}
+
+		$this->view->categories = FreshRSS_Context::$categories;
 
-		if ($output === 'rss') {
-			// no layout for RSS output
-			$this->view->_useLayout(false);
-			header('Content-Type: application/rss+xml; charset=utf-8');
-		} elseif ($output === 'global') {
-			Minz_View::appendScript(Minz_Url::display('/scripts/global_view.js?' . @filemtime(PUBLIC_PATH . '/scripts/global_view.js')));
+		$this->view->rss_title = FreshRSS_Context::$name . ' | ' . Minz_View::title();
+		$title = FreshRSS_Context::$name;
+		if (FreshRSS_Context::$get_unread > 0) {
+			$title = '(' . FreshRSS_Context::$get_unread . ') ' . $title;
 		}
+		Minz_View::prependTitle($title . ' · ');
+	}
 
-		$catDAO = new FreshRSS_CategoryDAO();
-		$entryDAO = FreshRSS_Factory::createEntryDao();
+	/**
+	 * This action displays the reader view of FreshRSS.
+	 *
+	 * @todo: change this view into specific CSS rules?
+	 */
+	public function readerAction() {
+		$this->normalAction();
+	}
 
-		$this->view->cat_aside = $catDAO->listCategories();
-		$this->view->nb_favorites = $entryDAO->countUnreadReadFavorites();
-		$this->view->nb_not_read = FreshRSS_CategoryDAO::CountUnreads($this->view->cat_aside, 1);
-		$this->view->currentName = '';
-
-		$this->view->get_c = '';
-		$this->view->get_f = '';
-
-		$get = Minz_Request::param('get', 'a');
-		$getType = $get[0];
-		$getId = substr($get, 2);
-		if (!$this->checkAndProcessType($getType, $getId)) {
-			Minz_Log::debug('Not found [' . $getType . '][' . $getId . ']');
-			Minz_Error::error(
-				404,
-				array('error' => array(_t('page_not_found')))
-			);
+	/**
+	 * This action displays the global view of FreshRSS.
+	 */
+	public function globalAction() {
+		if (!FreshRSS_Auth::hasAccess() && !Minz_Configuration::allowAnonymous()) {
+			Minz_Request::forward(array('c' => 'auth', 'a' => 'login'));
 			return;
 		}
 
-		// mise à jour des titres
-		$this->view->rss_title = $this->view->currentName . ' | ' . Minz_View::title();
-		Minz_View::prependTitle(
-			($this->nb_not_read_cat > 0 ? '(' . formatNumber($this->nb_not_read_cat) . ') ' : '') .
-			$this->view->currentName .
-			' · '
-		);
+		Minz_View::appendScript(Minz_Url::display('/scripts/global_view.js?' . @filemtime(PUBLIC_PATH . '/scripts/global_view.js')));
 
-		// On récupère les différents éléments de filtrage
-		$this->view->state = Minz_Request::param('state', FreshRSS_Context::$conf->default_view);
-		$state_param = Minz_Request::param('state', null);
-		$filter = Minz_Request::param('search', '');
-		$this->view->order = $order = Minz_Request::param('order', FreshRSS_Context::$conf->sort_order);
-		$nb = Minz_Request::param('nb', FreshRSS_Context::$conf->posts_per_page);
-		$first = Minz_Request::param('next', '');
-
-		$ajax_request = Minz_Request::param('ajax', false);
-		if ($output === 'reader') {
-			$nb = max(1, round($nb / 2));
+		try {
+			$this->updateContext();
+		} catch (FreshRSS_Context_Exception $e) {
+			Minz_Error::error(404);
 		}
 
-		if ($this->view->state === FreshRSS_Entry::STATE_NOT_READ) {	//Any unread article in this category at all?
-			switch ($getType) {
-			case 'a':
-				$hasUnread = $this->view->nb_not_read > 0;
-				break;
-			case 's':
-				// This is deprecated. The favorite button does not exist anymore
-				$hasUnread = $this->view->nb_favorites['unread'] > 0;
-				break;
-			case 'c':
-				$hasUnread = (!isset($this->view->cat_aside[$getId]) ||
-				              $this->view->cat_aside[$getId]->nbNotRead() > 0);
-				break;
-			case 'f':
-				$myFeed = FreshRSS_CategoryDAO::findFeed($this->view->cat_aside, $getId);
-				$hasUnread = ($myFeed === null) || ($myFeed->nbNotRead() > 0);
-				break;
-			default:
-				$hasUnread = true;
-				break;
-			}
-			if (!$hasUnread && ($state_param === null)) {
-				$this->view->state = FreshRSS_Entry::STATE_ALL;
-			}
+		$this->view->categories = FreshRSS_Context::$categories;
+
+		$this->view->rss_title = FreshRSS_Context::$name . ' | ' . Minz_View::title();
+		$title = _t('gen.title.global_view');
+		if (FreshRSS_Context::$get_unread > 0) {
+			$title = '(' . FreshRSS_Context::$get_unread . ') ' . $title;
 		}
+		Minz_View::prependTitle($title . ' · ');
+	}
 
-		$this->view->today = @strtotime('today');
+	/**
+	 * This action displays the RSS feed of FreshRSS.
+	 */
+	public function rssAction() {
+		$token = FreshRSS_Context::$conf->token;
+		$token_param = Minz_Request::param('token', '');
+		$token_is_ok = ($token != '' && $token === $token_param);
+
+		// Check if user has access.
+		if (!FreshRSS_Auth::hasAccess() &&
+				!Minz_Configuration::allowAnonymous() &&
+				!$token_is_ok) {
+			Minz_Error::error(403);
+		}
 
 		try {
-			$entries = $entryDAO->listWhere($getType, $getId, $this->view->state, $order, $nb + 1, $first, $filter);
-
-			// Si on a récupéré aucun article "non lus"
-			// on essaye de récupérer tous les articles
-			if ($this->view->state === FreshRSS_Entry::STATE_NOT_READ && empty($entries) && ($state_param === null) && ($filter == '')) {
-				Minz_Log::debug('Conflicting information about nbNotRead!');
-				$feedDAO = FreshRSS_Factory::createFeedDao();
-				try {
-					$feedDAO->updateCachedValues();
-				} catch (Exception $ex) {
-					Minz_Log::notice('Failed to automatically correct nbNotRead! ' + $ex->getMessage());
-				}
-				$this->view->state = FreshRSS_Entry::STATE_ALL;
-				$entries = $entryDAO->listWhere($getType, $getId, $this->view->state, $order, $nb, $first, $filter);
-			}
-			Minz_Request::_param('state', $this->view->state);
-
-			if (count($entries) <= $nb) {
-				$this->view->nextId  = '';
-			} else {	//We have more elements for pagination
-				$lastEntry = array_pop($entries);
-				$this->view->nextId  = $lastEntry->id();
-			}
+			$this->updateContext();
+		} catch (FreshRSS_Context_Exception $e) {
+			Minz_Error::error(404);
+		}
 
-			$this->view->entries = $entries;
+		try {
+			$this->view->entries = $this->listEntriesByContext();
 		} catch (FreshRSS_EntriesGetter_Exception $e) {
 			Minz_Log::notice($e->getMessage());
-			Minz_Error::error(
-				404,
-				array('error' => array(_t('page_not_found')))
-			);
+			Minz_Error::error(404);
 		}
+
+		// No layout for RSS output.
+		$this->view->rss_title = FreshRSS_Context::$name . ' | ' . Minz_View::title();
+		$this->view->_useLayout(false);
+		header('Content-Type: application/rss+xml; charset=utf-8');
 	}
 
-	/*
-	 * Vérifie que la catégorie / flux sélectionné existe
-	 * + Initialise correctement les variables de vue get_c et get_f
-	 * + Met à jour la variable $this->nb_not_read_cat
+	/**
+	 * This action updates the Context object by using request parameters.
+	 *
+	 * Parameters are:
+	 *   - state (default: conf->default_view)
+	 *   - search (default: empty string)
+	 *   - order (default: conf->sort_order)
+	 *   - nb (default: conf->posts_per_page)
+	 *   - next (default: empty string)
 	 */
-	private function checkAndProcessType($getType, $getId) {
-		switch($getType) {
-		case 'a':
-			$this->view->currentName = _t('your_rss_feeds');
-			$this->nb_not_read_cat = $this->view->nb_not_read;
-			$this->view->get_c = $getType;
-			return true;
-		case 's':
-			$this->view->currentName = _t('your_favorites');
-			$this->nb_not_read_cat = $this->view->nb_favorites['unread'];
-			$this->view->get_c = $getType;
-			return true;
-		case 'c':
-			$cat = isset($this->view->cat_aside[$getId]) ? $this->view->cat_aside[$getId] : null;
-			if ($cat === null) {
-				$catDAO = new FreshRSS_CategoryDAO();
-				$cat = $catDAO->searchById($getId);
-			}
-			if ($cat) {
-				$this->view->currentName = $cat->name();
-				$this->nb_not_read_cat = $cat->nbNotRead();
-				$this->view->get_c = $getId;
-				return true;
-			} else {
-				return false;
-			}
-		case 'f':
-			$feed = FreshRSS_CategoryDAO::findFeed($this->view->cat_aside, $getId);
-			if (empty($feed)) {
-				$feedDAO = FreshRSS_Factory::createFeedDao();
-				$feed = $feedDAO->searchById($getId);
-			}
-			if ($feed) {
-				$this->view->currentName = $feed->name();
-				$this->nb_not_read_cat = $feed->nbNotRead();
-				$this->view->get_f = $getId;
-				$this->view->get_c = $feed->category();
-				return true;
-			} else {
-				return false;
-			}
-		default:
-			return false;
+	private function updateContext() {
+		// Update number of read / unread variables.
+		$entryDAO = FreshRSS_Factory::createEntryDao();
+		FreshRSS_Context::$total_starred = $entryDAO->countUnreadReadFavorites();
+		FreshRSS_Context::$total_unread = FreshRSS_CategoryDAO::CountUnreads(
+			FreshRSS_Context::$categories, 1
+		);
+
+		FreshRSS_Context::_get(Minz_Request::param('get', 'a'));
+
+		FreshRSS_Context::$state = Minz_Request::param(
+			'state', FreshRSS_Context::$conf->default_state
+		);
+		$state_forced_by_user = Minz_Request::param('state', false) !== false;
+		if (FreshRSS_Context::$conf->default_view === 'adaptive' &&
+				FreshRSS_Context::$get_unread <= 0 &&
+				!FreshRSS_Context::isStateEnabled(FreshRSS_Entry::STATE_READ) &&
+				!$state_forced_by_user) {
+			FreshRSS_Context::$state |= FreshRSS_Entry::STATE_READ;
 		}
+
+		FreshRSS_Context::$search = Minz_Request::param('search', '');
+		FreshRSS_Context::$order = Minz_Request::param(
+			'order', FreshRSS_Context::$conf->sort_order
+		);
+		FreshRSS_Context::$number = Minz_Request::param(
+			'nb', FreshRSS_Context::$conf->posts_per_page
+		);
+		FreshRSS_Context::$first_id = Minz_Request::param('next', '');
 	}
-	
+
+	/**
+	 * This method returns a list of entries based on the Context object.
+	 */
+	private function listEntriesByContext() {
+		$entryDAO = FreshRSS_Factory::createEntryDao();
+
+		$get = FreshRSS_Context::currentGet(true);
+		if (count($get) > 1) {
+			$type = $get[0];
+			$id = $get[1];
+		} else {
+			$type = $get;
+			$id = '';
+		}
+
+		return $entryDAO->listWhere(
+			$type, $id, FreshRSS_Context::$state, FreshRSS_Context::$order,
+			FreshRSS_Context::$number + 1, FreshRSS_Context::$first_id,
+			FreshRSS_Context::$search
+		);
+	}
+
+	/**
+	 * This action displays the about page of FreshRSS.
+	 */
 	public function aboutAction() {
 		Minz_View::prependTitle(_t('about') . ' · ');
 	}
 
+	/**
+	 * This action displays logs of FreshRSS for the current user.
+	 */
 	public function logsAction() {
 		if (!FreshRSS_Auth::hasAccess()) {
-			Minz_Error::error(
-				403,
-				array('error' => array(_t('access_denied')))
-			);
+			Minz_Error::error(403);
 		}
 
 		Minz_View::prependTitle(_t('logs') . ' · ');

+ 10 - 0
app/Exceptions/ContextException.php

@@ -0,0 +1,10 @@
+<?php
+
+/**
+ * An exception raised when a context is invalid
+ */
+class FreshRSS_Context_Exception extends Exception {
+	public function __construct($message) {
+		parent::__construct($message);
+	}
+}

+ 4 - 10
app/FreshRSS.php

@@ -26,21 +26,15 @@ class FreshRSS extends Minz_FrontController {
 		// Load context and configuration.
 		FreshRSS_Context::init();
 
-		$this->loadParamsView();
+		// Init i18n.
+		Minz_Session::_param('language', FreshRSS_Context::$conf->language);
+		Minz_Translate::init();
+
 		$this->loadStylesAndScripts();
 		$this->loadNotifications();
 		$this->loadExtensions();
 	}
 
-	private function loadParamsView() {
-		// TODO: outputs should be different actions.
-		$output = Minz_Request::param('output', '');
-		if (($output === '') || ($output !== 'normal' && $output !== 'rss' && $output !== 'reader' && $output !== 'global')) {
-			$output = FreshRSS_Context::$conf->view_mode;
-			Minz_Request::_param('output', $output);
-		}
-	}
-
 	private function loadStylesAndScripts() {
 		$theme = FreshRSS_Themes::load(FreshRSS_Context::$conf->theme);
 		if ($theme) {

+ 13 - 8
app/Models/Configuration.php

@@ -14,7 +14,8 @@ class FreshRSS_Configuration {
 		'apiPasswordHash' => '',	//CRYPT_BLOWFISH
 		'posts_per_page' => 20,
 		'view_mode' => 'normal',
-		'default_view' => FreshRSS_Entry::STATE_NOT_READ,
+		'default_view' => 'adaptive',
+		'default_state' => FreshRSS_Entry::STATE_NOT_READ,
 		'auto_load_more' => true,
 		'display_posts' => false,
 		'display_categories' => false,
@@ -153,18 +154,22 @@ class FreshRSS_Configuration {
 	}
 	public function _default_view($value) {
 		switch ($value) {
-		case FreshRSS_Entry::STATE_ALL:
-			// left blank on purpose
-		case FreshRSS_Entry::STATE_NOT_READ:
-			// left blank on purpose
-		case FreshRSS_Entry::STATE_STRICT + FreshRSS_Entry::STATE_NOT_READ:
+		case 'all':
 			$this->data['default_view'] = $value;
+			$this->data['default_state'] = (FreshRSS_Entry::STATE_READ +
+			                                FreshRSS_Entry::STATE_NOT_READ);
 			break;
+		case 'adaptive':
+		case 'unread':
 		default:
-			$this->data['default_view'] = FreshRSS_Entry::STATE_ALL;
-			break;
+			$this->data['default_view'] = $value;
+			$this->data['default_state'] = FreshRSS_Entry::STATE_NOT_READ;
 		}
 	}
+	public function _default_state($value) {
+		$this->data['default_state'] = (int)$value;
+	}
+
 	public function _display_posts($value) {
 		$this->data['display_posts'] = ((bool)$value) && $value !== 'no';
 	}

+ 243 - 7
app/Models/Context.php

@@ -6,8 +6,39 @@
  */
 class FreshRSS_Context {
 	public static $conf = null;
+	public static $categories = array();
+
+	public static $name = '';
+
+	public static $total_unread = 0;
+	public static $total_starred = array(
+		'all' => 0,
+		'read' => 0,
+		'unread' => 0,
+	);
+
+	public static $get_unread = 0;
+	public static $current_get = array(
+		'all' => false,
+		'starred' => false,
+		'feed' => false,
+		'category' => false,
+	);
+	public static $next_get = 'a';
+
 	public static $state = 0;
+	public static $order = 'DESC';
+	public static $number = 0;
+	public static $search = '';
+	public static $first_id = '';
+	public static $next_id = '';
+	public static $id_max = '';
 
+	/**
+	 * Initialize the context.
+	 *
+	 * Set the correct $conf and $categories variables.
+	 */
 	public static function init() {
 		// Init configuration.
 		$current_user = Minz_Session::param('currentUser');
@@ -18,15 +49,220 @@ class FreshRSS_Context {
 			die($e->getMessage());
 		}
 
-		// Init i18n.
-		Minz_Session::_param('language', self::$conf->language);
-		Minz_Translate::init();
-
-		// Get the current state.
-		self::$state = self::$conf->default_view;
+		$catDAO = new FreshRSS_CategoryDAO();
+		self::$categories = $catDAO->listCategories();
 	}
 
-	public static function stateEnabled($state) {
+	/**
+	 * Returns if the current state includes $state parameter.
+	 */
+	public static function isStateEnabled($state) {
 		return self::$state & $state;
 	}
+
+	/**
+	 * Returns the current state with or without $state parameter.
+	 */
+	public static function getRevertState($state) {
+		if (self::$state & $state) {
+			return self::$state & ~$state;
+		} else {
+			return self::$state | $state;
+		}
+	}
+
+	/**
+	 * Return the current get as a string or an array.
+	 *
+	 * If $array is true, the first item of the returned value is 'f' or 'c' and
+	 * the second is the id.
+	 */
+	public static function currentGet($array = false) {
+		if (self::$current_get['all']) {
+			return 'a';
+		} elseif (self::$current_get['starred']) {
+			return 's';
+		} elseif (self::$current_get['feed']) {
+			if ($array) {
+				return array('f', self::$current_get['feed']);
+			} else {
+				return 'f_' . self::$current_get['feed'];
+			}
+		} elseif (self::$current_get['category']) {
+			if ($array) {
+				return array('c', self::$current_get['category']);
+			} else {
+				return 'c_' . self::$current_get['category'];
+			}
+		}
+	}
+
+	/**
+	 * Return true if $get parameter correspond to the $current_get attribute.
+	 */
+	public static function isCurrentGet($get) {
+		$type = $get[0];
+		$id = substr($get, 2);
+
+		switch($type) {
+		case 'a':
+			return self::$current_get['all'];
+		case 's':
+			return self::$current_get['starred'];
+		case 'f':
+			return self::$current_get['feed'] == $id;
+		case 'c':
+			return self::$current_get['category'] == $id;
+		default:
+			return false;
+		}
+	}
+
+	/**
+	 * Set the current $get attribute.
+	 *
+	 * Valid $get parameter are:
+	 *   - a
+	 *   - s
+	 *   - f_<feed id>
+	 *   - c_<category id>
+	 *
+	 * $name and $get_unread attributes are also updated as $next_get
+	 * Raise an exception if id or $get is invalid.
+	 */
+	public static function _get($get) {
+		$type = $get[0];
+		$id = substr($get, 2);
+		$nb_unread = 0;
+
+		switch($type) {
+		case 'a':
+			self::$current_get['all'] = true;
+			self::$name = _t('your_rss_feeds');
+			self::$get_unread = self::$total_unread;
+			break;
+		case 's':
+			self::$current_get['starred'] = true;
+			self::$name = _t('your_favorites');
+			self::$get_unread = self::$total_starred['unread'];
+
+			// Update state if favorite is not yet enabled.
+			self::$state = self::$state | FreshRSS_Entry::STATE_FAVORITE;
+			break;
+		case 'f':
+			// We try to find the corresponding feed.
+			$feed = FreshRSS_CategoryDAO::findFeed(self::$categories, $id);
+			if ($feed === null) {
+				$feedDAO = FreshRSS_Factory::createFeedDao();
+				$feed = $feedDAO->searchById($id);
+
+				if (!$feed) {
+					throw new FreshRSS_Context_Exception('Invalid feed: ' . $id);
+				}
+			}
+
+			self::$current_get['feed'] = $id;
+			self::$current_get['category'] = $feed->category();
+			self::$name = $feed->name();
+			self::$get_unread = $feed->nbNotRead();
+			break;
+		case 'c':
+			// We try to find the corresponding category.
+			self::$current_get['category'] = $id;
+			if (!isset(self::$categories[$id])) {
+				$catDAO = new FreshRSS_CategoryDAO();
+				$cat = $catDAO->searchById($id);
+
+				if (!$cat) {
+					throw new FreshRSS_Context_Exception('Invalid category: ' . $id);
+				}
+			} else {
+				$cat = self::$categories[$id];
+			}
+
+			self::$name = $cat->name();
+			self::$get_unread = $cat->nbNotRead();
+			break;
+		default:
+			throw new FreshRSS_Context_Exception('Invalid getter: ' . $get);
+		}
+
+		self::_nextGet();
+	}
+
+	/**
+	 * Set the value of $next_get attribute.
+	 */
+	public static function _nextGet() {
+		$get = self::currentGet();
+		// By default, $next_get == $get
+		self::$next_get = $get;
+
+		if (self::$conf->onread_jump_next && strlen($get) > 2) {
+			$another_unread_id = '';
+			$found_current_get = false;
+			switch ($get[0]) {
+			case 'f':
+				// We search the next feed with at least one unread article in
+				// same category as the currend feed.
+				foreach (self::$categories as $cat) {
+					if ($cat->id() != self::$current_get['category']) {
+						// We look into the category of the current feed!
+						continue;
+					}
+
+					foreach ($cat->feeds() as $feed) {
+						if ($feed->id() == self::$current_get['feed']) {
+							// Here is our current feed! Fine, the next one will
+							// be a potential candidate.
+							$found_current_get = true;
+							continue;
+						}
+
+						if ($feed->nbNotRead() > 0) {
+							$another_unread_id = $feed->id();
+							if ($found_current_get) {
+								// We have found our current feed and now we
+								// have an feed with unread articles. Leave the
+								// loop!
+								break;
+							}
+						}
+					}
+					break;
+				}
+
+				// If no feed have been found, next_get is the current category.
+				self::$next_get = empty($another_unread_id) ?
+				                  'c_' . self::$current_get['category'] :
+				                  'f_' . $another_unread_id;
+				break;
+			case 'c':
+				// We search the next category with at least one unread article.
+				foreach (self::$categories as $cat) {
+					if ($cat->id() == self::$current_get['category']) {
+						// Here is our current category! Next one could be our
+						// champion if it has unread articles.
+						$found_current_get = true;
+						continue;
+					}
+
+					if ($cat->nbNotRead() > 0) {
+						$another_unread_id = $cat->id();
+						if ($found_current_get) {
+							// Unread articles and the current category has
+							// already been found? Leave the loop!
+							break;
+						}
+					}
+				}
+
+				// No unread category? The main stream will be our destination!
+				self::$next_get = empty($another_unread_id) ?
+				                  'a' :
+				                  'c_' . $another_unread_id;
+				break;
+			}
+		}
+	}
 }

+ 1 - 2
app/Models/Entry.php

@@ -1,12 +1,11 @@
 <?php
 
 class FreshRSS_Entry extends Minz_Model {
-	const STATE_ALL = 0;
 	const STATE_READ = 1;
 	const STATE_NOT_READ = 2;
+	const STATE_ALL = 3;
 	const STATE_FAVORITE = 4;
 	const STATE_NOT_FAVORITE = 8;
-	const STATE_STRICT = 16;
 
 	private $id = 0;
 	private $guid;

+ 0 - 2
app/Models/EntryDAO.php

@@ -333,8 +333,6 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
 		if ($state & FreshRSS_Entry::STATE_NOT_READ) {
 			if (!($state & FreshRSS_Entry::STATE_READ)) {
 				$where .= 'AND e1.is_read=0 ';
-			} elseif ($state & FreshRSS_Entry::STATE_STRICT) {
-				$where .= 'AND e1.is_read=0 ';
 			}
 		}
 		elseif ($state & FreshRSS_Entry::STATE_READ) {

+ 95 - 0
app/layout/aside_feed.phtml

@@ -0,0 +1,95 @@
+<?php
+	$class = '';
+	if (FreshRSS_Context::$conf->hide_read_feeds &&
+			FreshRSS_Context::isStateEnabled(FreshRSS_Entry::STATE_NOT_READ) &&
+			!FreshRSS_Context::isStateEnabled(FreshRSS_Entry::STATE_READ)) {
+		$class = ' state_unread';
+	}
+?>
+
+<div class="aside aside_feed<?php echo $class; ?>" id="aside_feed">
+	<a class="toggle_aside" href="#close"><?php echo _i('close'); ?></a>
+
+	<?php if (FreshRSS_Auth::hasAccess()) { ?>
+	<div class="stick configure-feeds no-mobile">
+		<a class="btn btn-important" href="<?php echo _url('subscription', 'index'); ?>"><?php echo _t('subscription_management'); ?></a>
+		<a class="btn btn-important" href="<?php echo _url('importExport', 'index'); ?>"><?php echo _i('import'); ?></a>
+	</div>
+	<?php } elseif (Minz_Configuration::needsLogin()) { ?>
+	<a href="<?php echo _url('index', 'about'); ?>"><?php echo _t('about_freshrss'); ?></a>
+	<?php } ?>
+
+	<form id="mark-read-aside" method="post" style="display: none"></form>
+
+	<ul class="tree">
+		<li class="tree-folder category all<?php echo FreshRSS_Context::isCurrentGet('a') ? ' active' : ''; ?>">
+			<div class="tree-folder-title">
+				<?php echo _i('all'); ?> <a class="title" data-unread="<?php echo format_number(FreshRSS_Context::$total_unread); ?>" href="<?php echo _url('index', 'index'); ?>"><?php echo _t('main_stream'); ?></a>
+			</div>
+		</li>
+
+		<li class="tree-folder category favorites<?php echo FreshRSS_Context::isCurrentGet('s') ? ' active' : ''; ?>">
+			<div class="tree-folder-title">
+				<?php echo _i('bookmark'); ?> <a class="title" data-unread="<?php echo format_number(FreshRSS_Context::$total_starred['unread']); ?>" href="<?php echo _url('index', 'index', 'get', 's'); ?>"><?php echo _t('favorite_feeds', format_number(FreshRSS_Context::$total_starred['all'])); ?></a>
+			</div>
+		</li>
+
+		<?php
+			foreach ($this->categories as $cat) {
+				$feeds = $cat->feeds();
+				if (!empty($feeds)) {
+					$c_active = FreshRSS_Context::isCurrentGet('c_' . $cat->id());
+					$c_show = $c_active && (!FreshRSS_Context::$conf->display_categories ||
+					                        FreshRSS_Context::$current_get['feed']);
+		?>
+		<li class="tree-folder category<?php echo $c_active ? ' active' : ''; ?>" data-unread="<?php echo $cat->nbNotRead(); ?>">
+			<div class="tree-folder-title">
+				<a class="dropdown-toggle" href="#"><?php echo _i($c_show ? 'up' : 'down'); ?></a>
+				<a class="title" data-unread="<?php echo format_number($cat->nbNotRead()); ?>" href="<?php echo _url('index', 'index', 'get', 'c_' . $cat->id()); ?>"><?php echo $cat->name(); ?></a>
+			</div>
+
+			<ul class="tree-folder-items<?php echo $c_show ? ' active' : ''; ?>">
+				<?php
+					foreach ($feeds as $feed) {
+						$f_active = FreshRSS_Context::isCurrentGet('f_' . $feed->id());
+				?>
+				<li id="f_<?php echo $feed->id(); ?>" class="item feed<?php echo $f_active ? ' active' : ''; ?><?php echo $feed->inError() ? ' error' : ''; ?><?php echo $feed->nbEntries() <= 0 ? ' empty' : ''; ?>" data-unread="<?php echo $feed->nbNotRead(); ?>" data-priority="<?php echo $feed->priority(); ?>">
+					<div class="dropdown no-mobile">
+						<div class="dropdown-target"></div>
+						<a class="dropdown-toggle" data-fweb="<?php echo $feed->website(); ?>"><?php echo _i('configure'); ?></a>
+						<?php /* feed_config_template */ ?>
+					</div>
+					<img class="favicon" src="<?php echo $feed->favicon(); ?>" alt="✇" /> <a class="item-title" data-unread="<?php echo format_number($feed->nbNotRead()); ?>" href="<?php echo _url('index', 'index', 'get', 'f_' . $feed->id()); ?>"><?php echo $feed->name(); ?></a>
+				</li>
+				<?php } ?>
+			</ul>
+		</li>
+		<?php
+				}
+			}
+		?>
+	</ul>
+</div>
+
+<script id="feed_config_template" type="text/html">
+	<ul class="dropdown-menu">
+		<li class="dropdown-close"><a href="#close">❌</a></li>
+		<li class="item"><a href="<?php echo _url('index', 'index', 'get', 'f_!!!!!!'); ?>"><?php echo _t('filter'); ?></a></li>
+		<?php if (FreshRSS_Auth::hasAccess()) { ?>
+		<li class="item"><a href="<?php echo _url('stats', 'repartition', 'id', '!!!!!!'); ?>"><?php echo _t('stats'); ?></a></li>
+		<?php } ?>
+		<li class="item"><a target="_blank" href="http://example.net/"><?php echo _t('see_website'); ?></a></li>
+		<?php if (FreshRSS_Auth::hasAccess()) { ?>
+		<li class="separator"></li>
+		<li class="item"><a href="<?php echo _url('subscription', 'index', 'id', '!!!!!!'); ?>"><?php echo _t('administration'); ?></a></li>
+		<li class="item"><a href="<?php echo _url('feed', 'actualize', 'id', '!!!!!!'); ?>"><?php echo _t('actualize'); ?></a></li>
+		<li class="item">
+			<?php $confirm = FreshRSS_Context::$conf->reading_confirm ? 'confirm' : ''; ?>
+			<button class="read_all as-link <?php echo $confirm; ?>"
+			        form="mark-read-aside"
+			        formaction="<?php echo _url('entry', 'read', 'get', 'f_!!!!!!'); ?>"
+			        type="submit"><?php echo _t('mark_read'); ?></button>
+		</li>
+		<?php } ?>
+	</ul>
+</script>

+ 0 - 103
app/layout/aside_flux.phtml

@@ -1,103 +0,0 @@
-<div class="aside aside_flux<?php if (FreshRSS_Context::$conf->hide_read_feeds && ($this->state & FreshRSS_Entry::STATE_NOT_READ) && !($this->state & FreshRSS_Entry::STATE_READ)) echo ' state_unread'; ?>" id="aside_flux">
-	<a class="toggle_aside" href="#close"><?php echo _i('close'); ?></a>
-
-	<ul class="categories">
-		<?php if (FreshRSS_Auth::hasAccess()) { ?>
-		<form id="mark-read-aside" method="post" style="display: none"></form>
-
-		<li>
-			<div class="stick configure-feeds">
-				<a class="btn btn-important" href="<?php echo _url('subscription', 'index'); ?>"><?php echo _t('subscription_management'); ?></a>
-				<a class="btn btn-important" href="<?php echo _url('importExport', 'index'); ?>"><?php echo _i('import'); ?></a>
-			</div>
-		</li>
-		<?php } elseif (Minz_Configuration::needsLogin()) { ?>
-		<li><a href="<?php echo _url('index', 'about'); ?>"><?php echo _t('about_freshrss'); ?></a></li>
-		<?php } ?>
-
-		<?php
-			$arUrl = array('c' => 'index', 'a' => 'index', 'params' => array());
-			if (FreshRSS_Context::$conf->view_mode !== Minz_Request::param('output', 'normal')) {
-				$arUrl['params']['output'] = 'normal';
-			}
-		?>
-		<li>
-			<div class="category all<?php echo $this->get_c == 'a' ? ' active' : ''; ?>">
-				<a data-unread="<?php echo formatNumber($this->nb_not_read); ?>" class="btn<?php echo $this->get_c == 'a' ? ' active' : ''; ?>" href="<?php echo Minz_Url::display($arUrl); ?>">
-					<?php echo _i('all'); ?>
-					<?php echo _t('main_stream'); ?>
-				</a>
-			</div>
-		</li>
-
-		<li>
-			<div class="category favorites<?php echo $this->get_c == 's' ? ' active' : ''; ?>">
-				<a data-unread="<?php echo formatNumber($this->nb_favorites['unread']); ?>" class="btn<?php echo $this->get_c == 's' ? ' active' : ''; ?>" href="<?php $arUrl['params']['get'] = 's'; echo Minz_Url::display($arUrl); ?>">
-					<?php echo _i('bookmark'); ?>
-					<?php echo _t('favorite_feeds', formatNumber($this->nb_favorites['all'])); ?>
-				</a>
-			</div>
-		</li>
-
-		<?php
-		foreach ($this->cat_aside as $cat) {
-			$feeds = $cat->feeds();
-			if (!empty($feeds)) {
-				$c_active = false;
-				$c_show = false;
-				if ($this->get_c == $cat->id()) {
-					$c_active = true;
-					if (!FreshRSS_Context::$conf->display_categories || $this->get_f) {
-						$c_show = true;
-					}
-				}
-				?><li data-unread="<?php echo $cat->nbNotRead(); ?>"<?php if ($c_active) echo ' class="active"'; ?>><?php
-				?><div class="category stick<?php echo $c_active ? ' active' : ''; ?>"><?php
-					?><a data-unread="<?php echo formatNumber($cat->nbNotRead()); ?>" class="btn<?php echo $c_active ? ' active' : ''; ?>" href="<?php $arUrl['params']['get'] = 'c_' . $cat->id(); echo Minz_Url::display($arUrl); ?>"><?php echo $cat->name(); ?></a><?php
-					?><a class="btn dropdown-toggle" href="#"><?php echo _i($c_show ? 'up' : 'down'); ?></a><?php
-				?></div><?php
-				?><ul class="feeds<?php echo $c_show ? ' active' : ''; ?>"><?php
-				foreach ($feeds as $feed) {
-					$feed_id = $feed->id();
-					$nbEntries = $feed->nbEntries();
-					$f_active = ($this->get_f == $feed_id);
-					?><li id="f_<?php echo $feed_id; ?>" class="item<?php echo $f_active ? ' active' : ''; ?><?php echo $feed->inError() ? ' error' : ''; ?><?php echo $nbEntries == 0 ? ' empty' : ''; ?>" data-unread="<?php echo $feed->nbNotRead(); ?>"><?php
-						?><div class="dropdown"><?php
-							?><div class="dropdown-target"></div><?php
-							?><a class="dropdown-toggle" data-fweb="<?php echo $feed->website(); ?>"><?php echo _i('configure'); ?></a><?php
-							/* feed_config_template */
-						?></div><?php
-						?> <img class="favicon" src="<?php echo $feed->favicon(); ?>" alt="✇" /> <?php
-						?><a class="feed" data-unread="<?php echo formatNumber($feed->nbNotRead()); ?>" data-priority="<?php echo $feed->priority(); ?>" href="<?php $arUrl['params']['get'] = 'f_' . $feed_id; echo Minz_Url::display($arUrl); ?>"><?php echo $feed->name(); ?></a><?php
-					?></li><?php
-				}
-				?></ul><?php
-				?></li><?php
-			}
-		} ?>
-	</ul>
-	<span class="aside_flux_ender"><!-- For fixed menu --></span>
-</div>
-
-<script id="feed_config_template" type="text/html">
-	<ul class="dropdown-menu">
-		<li class="dropdown-close"><a href="#close">❌</a></li>
-		<li class="item"><a href="<?php echo _url('index', 'index', 'get', 'f_!!!!!!'); ?>"><?php echo _t('filter'); ?></a></li>
-		<?php if (FreshRSS_Auth::hasAccess()) { ?>
-		<li class="item"><a href="<?php echo _url('stats', 'repartition', 'id', '!!!!!!'); ?>"><?php echo _t('stats'); ?></a></li>
-		<?php } ?>
-		<li class="item"><a target="_blank" href="http://example.net/"><?php echo _t('see_website'); ?></a></li>
-		<?php if (FreshRSS_Auth::hasAccess()) { ?>
-		<li class="separator"></li>
-		<li class="item"><a href="<?php echo _url('subscription', 'index', 'id', '!!!!!!'); ?>"><?php echo _t('administration'); ?></a></li>
-		<li class="item"><a href="<?php echo _url('feed', 'actualize', 'id', '!!!!!!'); ?>"><?php echo _t('actualize'); ?></a></li>
-		<li class="item">
-			<?php $confirm = FreshRSS_Context::$conf->reading_confirm ? 'confirm' : ''; ?>
-			<button class="read_all as-link <?php echo $confirm; ?>"
-			        form="mark-read-aside"
-			        formaction="<?php echo _url('entry', 'read', 'get', 'f_!!!!!!'); ?>"
-			        type="submit"><?php echo _t('mark_read'); ?></button>
-		</li>
-		<?php } ?>
-	</ul>
-</script>

+ 1 - 1
app/layout/aside_subscription.phtml

@@ -1,4 +1,4 @@
-<ul class="nav nav-list aside aside_feed">
+<ul class="nav nav-list aside">
 	<li class="nav-header"><?php echo _t('subscription_management'); ?></li>
 
 	<li class="item<?php echo Minz_Request::controllerName() == 'subscription' ? ' active' : ''; ?>">

+ 10 - 9
app/layout/layout.phtml

@@ -10,21 +10,22 @@
 <?php $this->renderHelper('javascript_vars'); ?>
 		//]]></script>
 <?php
-	if (!empty($this->nextId)) {
-		$params = Minz_Request::params();
-		$params['next'] = $this->nextId;
-		$params['ajax'] = 1;
+	$url_base = Minz_Request::currentRequest();
+	if (FreshRSS_Context::$next_id !== '') {
+		$url_next = $url_base;
+		$url_next['params']['next'] = FreshRSS_Context::$next_id;
+		$url_next['params']['ajax'] = 1;
 ?>
-		<link id="prefetch" rel="next prefetch" href="<?php echo Minz_Url::display(array('c' => Minz_Request::controllerName(), 'a' => Minz_Request::actionName(), 'params' => $params)); ?>" />
+		<link id="prefetch" rel="next prefetch" href="<?php echo Minz_Url::display($url_next); ?>" />
 <?php } ?>
 		<link rel="shortcut icon" id="favicon" type="image/x-icon" sizes="16x16 64x64" href="<?php echo Minz_Url::display('/favicon.ico'); ?>" />
 		<link rel="icon msapplication-TileImage apple-touch-icon" type="image/png" sizes="256x256" href="<?php echo Minz_Url::display('/themes/icons/favicon-256.png'); ?>" />
 <?php
-	if (isset($this->url)) {
-		$rss_url = $this->url;
-		$rss_url['params']['output'] = 'rss';
+	if (isset($this->rss_title)) {
+		$url_rss = $url_base;
+		$url_rss['a'] = 'rss';
 ?>
-		<link rel="alternate" type="application/rss+xml" title="<?php echo $this->rss_title; ?>" href="<?php echo Minz_Url::display($rss_url); ?>" />
+		<link rel="alternate" type="application/rss+xml" title="<?php echo $this->rss_title; ?>" href="<?php echo Minz_Url::display($url_rss); ?>" />
 <?php } ?>
 		<link rel="prefetch" href="<?php echo FreshRSS_Themes::icon('starred', true); ?>">
 		<link rel="prefetch" href="<?php echo FreshRSS_Themes::icon('non-starred', true); ?>">

+ 49 - 156
app/layout/nav_menu.phtml

@@ -1,90 +1,31 @@
-<?php
-	$actual_view = Minz_Request::param('output', 'normal');
-?>
+<?php $actual_view = Minz_Request::actionName(); ?>
+
 <div class="nav_menu">
 	<?php if ($actual_view === 'normal') { ?>
-	<a class="btn toggle_aside" href="#aside_flux"><?php echo _i('category'); ?></a>
+	<a class="btn toggle_aside" href="#aside_feed"><?php echo _i('category'); ?></a>
 	<?php } ?>
 
 	<?php if (FreshRSS_Auth::hasAccess()) { ?>
 	<div id="nav_menu_actions" 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; ?>"
-		   href="<?php echo Minz_Url::display($url_state); ?>"
-		   title="<?php echo _t('show_read'); ?>">
-			<?php echo _i('read'); ?>
-		</a>
-
-		<?php
-			if ($this->state & FreshRSS_Entry::STATE_NOT_READ) {
-				$url_state['params']['state'] = $this->state & ~FreshRSS_Entry::STATE_NOT_READ;
-				$checked = 'true';
-				$class = 'active';
-			} else {
-				$url_state['params']['state'] = $this->state | FreshRSS_Entry::STATE_NOT_READ;
-				$checked = 'false';
-				$class = '';
-			}
-		?>
-		<a id="toggle-unread"
-		   class="btn <?php echo $class; ?>"
-		   aria-checked="<?php echo $checked; ?>"
-		   href="<?php echo Minz_Url::display($url_state); ?>"
-		   title="<?php echo _t('show_not_reads'); ?>">
-			<?php echo _i('unread'); ?>
-		</a>
-
-		<?php
-			if ($this->state & FreshRSS_Entry::STATE_FAVORITE || $this->get_c == 's') {
-				$url_state['params']['state'] = $this->state & ~FreshRSS_Entry::STATE_FAVORITE;
-				$checked = 'true';
-				$class = 'active';
-			} else {
-				$url_state['params']['state'] = $this->state | FreshRSS_Entry::STATE_FAVORITE;
-				$checked = 'false';
-				$class = '';
-			}
-		?>
-		<a id="toggle-favorite"
-		   class="btn <?php echo $class; ?>"
-		   aria-checked="<?php echo $checked; ?>"
-		   href="<?php echo Minz_Url::display($url_state); ?>"
-		   title="<?php echo _t('show_favorite'); ?>">
-			<?php echo _i('starred'); ?>
-		</a>
-
-		<?php
-			if ($this->state & FreshRSS_Entry::STATE_NOT_FAVORITE) {
-				$url_state['params']['state'] = $this->state & ~FreshRSS_Entry::STATE_NOT_FAVORITE;
-				$checked = 'true';
-				$class = 'active';
-			} else {
-				$url_state['params']['state'] = $this->state | FreshRSS_Entry::STATE_NOT_FAVORITE;
-				$checked = 'false';
-				$class = '';
-			}
+			$states = array(
+				'read' => FreshRSS_Entry::STATE_READ,
+				'unread' => FreshRSS_Entry::STATE_NOT_READ,
+				'starred' => FreshRSS_Entry::STATE_FAVORITE,
+				'non-starred' => FreshRSS_Entry::STATE_NOT_FAVORITE,
+			);
+
+			foreach ($states as $state_str => $state) {
+				$state_enabled = FreshRSS_Context::isStateEnabled($state);
+				$url_state = Minz_Request::currentRequest();
+				$url_state['params']['state'] = FreshRSS_Context::getRevertState($state);
 		?>
-		<a id="toggle-not-favorite"
-		   class="btn <?php echo $class; ?>"
-		   aria-checked="<?php echo $checked; ?>"
-		   href="<?php echo Minz_Url::display($url_state); ?>"
-		   title="<?php echo _t('show_not_favorite'); ?>">
-			<?php echo _i('non-starred'); ?>
-		</a>
+		<a id="toggle-<?php echo $state_str; ?>"
+		   class="btn <?php echo $state_enabled ? 'active' : ''; ?>"
+		   aria-checked="<?php echo $state_enabled ? 'true' : 'false'; ?>"
+		   title="<?php echo _t($state_str); ?>"
+		   href="<?php echo Minz_Url::display($url_state); ?>"><?php echo _i($state_str); ?></a>
+		<?php } ?>
 
 		<div class="dropdown">
 			<div id="dropdown-query" class="dropdown-target"></div>
@@ -109,7 +50,7 @@
 				<?php } ?>
 
 				<?php
-					$url_query = $this->url;
+					$url_query = Minz_Request::currentRequest();;
 					$url_query['c'] = 'configure';
 					$url_query['a'] = 'addQuery';
 				?>
@@ -117,74 +58,25 @@
 			</ul>
 		</div>
 	</div>
+
 	<?php
-		$get = false;
+		$get = FreshRSS_Context::currentGet();
 		$string_mark = _t('mark_all_read');
-		if ($this->get_f) {
-			$get = 'f_' . $this->get_f;
+		if ($get[0] == 'f') {
 			$string_mark = _t('mark_feed_read');
-		} elseif ($this->get_c && $this->get_c != 'a') {
-			if ($this->get_c === 's') {
-				$get = 's';
-			} else {
-				$get = 'c_' . $this->get_c;
-			}
+		} elseif ($get[0] == 'c') {
 			$string_mark = _t('mark_cat_read');
 		}
-		$nextGet = $get;
-		if (FreshRSS_Context::$conf->onread_jump_next && strlen($get) > 2) {
-			$anotherUnreadId = '';
-			$foundCurrent = false;
-			switch ($get[0]) {
-			case 'c':
-				foreach ($this->cat_aside as $cat) {
-					if ($cat->id() == $this->get_c) {
-						$foundCurrent = true;
-						continue;
-					}
-					if ($cat->nbNotRead() <= 0) continue;
-					$anotherUnreadId = $cat->id();
-					if ($foundCurrent) break;
-				}
-				$nextGet = empty($anotherUnreadId) ? 'a' : 'c_' . $anotherUnreadId;
-				break;
-			case 'f':
-				foreach ($this->cat_aside as $cat) {
-					if ($cat->id() == $this->get_c) {
-						foreach ($cat->feeds() as $feed) {
-							if ($feed->id() == $this->get_f) {
-								$foundCurrent = true;
-								continue;
-							}
-							if ($feed->nbNotRead() <= 0) continue;
-							$anotherUnreadId = $feed->id();
-							if ($foundCurrent) break;
-						}
-						break;
-					}
-				}
-				$nextGet = empty($anotherUnreadId) ? 'c_' . $this->get_c : 'f_' . $anotherUnreadId;
-				break;
-			}
-		}
 
-		$p = isset($this->entries[0]) ? $this->entries[0] : null;
-		$idMax = $p === null ? (time() - 1) . '000000' : $p->id();
-
-		if ($this->order === 'ASC') {	//In this case we do not know but we guess idMax
-			$idMax2 = (time() - 1) . '000000';
-			if (strcmp($idMax2, $idMax) > 0) {
-				$idMax = $idMax2;
-			}
-		}
-
-		$arUrl = array('c' => 'entry', 'a' => 'read', 'params' => array('get' => $get, 'nextGet' => $nextGet, 'idMax' => $idMax));
-		$output = Minz_Request::param('output', '');
-		if ($output != '' && FreshRSS_Context::$conf->view_mode !== $output) {
-			$arUrl['params']['output'] = $output;
-		}
-		$markReadUrl = Minz_Url::display($arUrl);
-		Minz_Session::_param('markReadUrl', $markReadUrl);
+		$mark_read_url = array(
+			'c' => 'entry',
+			'a' => 'read',
+			'params' => array(
+				'get' => $get,
+				'nextGet' => FreshRSS_Context::$next_get,
+				'idMax' => FreshRSS_Context::$id_max,
+			)
+		);
 	?>
 
 	<form id="mark-read-menu" method="post" style="display: none"></form>
@@ -193,7 +85,7 @@
 		<?php $confirm = FreshRSS_Context::$conf->reading_confirm ? 'confirm' : ''; ?>
 		<button class="read_all btn <?php echo $confirm; ?>"
 		        form="mark-read-menu"
-		        formaction="<?php echo $markReadUrl; ?>"
+		        formaction="<?php echo Minz_Url::display($mark_read_url); ?>"
 		        type="submit"><?php echo _t('mark_read'); ?></button>
 
 		<div class="dropdown">
@@ -206,15 +98,16 @@
 				<li class="item">
 					<button class="as-link <?php echo $confirm; ?>"
 					        form="mark-read-menu"
-					        formaction="<?php echo $markReadUrl; ?>"
+					        formaction="<?php echo Minz_Url::display($mark_read_url); ?>"
 					        type="submit"><?php echo $string_mark; ?></button>
 				</li>
 				<li class="separator"></li>
 <?php
-	$mark_before_today = $arUrl;
-	$mark_before_today['params']['idMax'] = $this->today . '000000';
-	$mark_before_one_week = $arUrl;
-	$mark_before_one_week['params']['idMax'] = ($this->today - 604800) . '000000';
+	$today = @strtotime('today');
+	$mark_before_today = $mark_read_url;
+	$mark_before_today['params']['idMax'] = $today . '000000';
+	$mark_before_one_week = $mark_read_url;
+	$mark_before_one_week['params']['idMax'] = ($today - 604800) . '000000';
 ?>
 				<li class="item">
 					<button class="as-link <?php echo $confirm; ?>"
@@ -233,25 +126,25 @@
 	</div>
 	<?php } ?>
 
-	<?php $url_output = $this->url; ?>
+	<?php $url_output = Minz_Request::currentRequest(); ?>
 	<div class="stick" id="nav_menu_views">
-		<?php $url_output['params']['output'] = 'normal'; ?>
+		<?php $url_output['a'] = 'normal'; ?>
 		<a class="view_normal btn <?php echo $actual_view == 'normal'? 'active' : ''; ?>" title="<?php echo _t('normal_view'); ?>" href="<?php echo Minz_Url::display($url_output); ?>">
 			<?php echo _i("view-normal"); ?>
 		</a>
 
-		<?php $url_output['params']['output'] = 'global'; ?>
+		<?php $url_output['a'] = 'global'; ?>
 		<a class="view_global btn <?php echo $actual_view == 'global'? 'active' : ''; ?>" title="<?php echo _t('global_view'); ?>" href="<?php echo Minz_Url::display($url_output); ?>">
 			<?php echo _i("view-global"); ?>
 		</a>
 
-		<?php $url_output['params']['output'] = 'reader'; ?>
-		<a class="view_reader btn <?php echo $actual_view == 'reader'? 'active' : ''; ?>" title="<?php echo _t('reader_view'); ?>" href="<?php echo Minz_Url::display($url_output); ?>">
+		<?php $url_output['a'] = 'reader'; ?>
+		<a class="view_reader btn <?php echo $actual_view == 'reader'? 'active' : ''; ?>" title="<?php echo _t('global_view'); ?>" href="<?php echo Minz_Url::display($url_output); ?>">
 			<?php echo _i("view-reader"); ?>
 		</a>
 
 		<?php
-			$url_output['params']['output'] = 'rss';
+			$url_output['a'] = 'rss';
 			if (FreshRSS_Context::$conf->token) {
 				$url_output['params']['token'] = FreshRSS_Context::$conf->token;
 			}
@@ -284,7 +177,7 @@
 	</div>
 	
 	<?php
-		if ($this->order === 'DESC') {
+		if (FreshRSS_Context::$order === 'DESC') {
 			$order = 'ASC';
 			$icon = 'up';
 			$title = 'older_first';
@@ -293,7 +186,7 @@
 			$icon = 'down';
 			$title = 'newer_first';
 		}
-		$url_order = $this->url;
+		$url_order = Minz_Request::currentRequest();
 		$url_order['params']['order'] = $order;
 	?>
 	<a id="toggle-order" class="btn" href="<?php echo Minz_Url::display($url_order); ?>" title="<?php echo _t($title); ?>">

+ 3 - 3
app/views/configure/reading.phtml

@@ -39,9 +39,9 @@
 			<label class="group-name" for="view_mode"><?php echo _t('articles_to_display'); ?></label>
 			<div class="group-controls">
 				<select name="default_view" id="default_view">
-					<option value="<?php echo FreshRSS_Entry::STATE_NOT_READ; ?>"<?php echo FreshRSS_Context::$conf->default_view === FreshRSS_Entry::STATE_NOT_READ ? ' selected="selected"' : ''; ?>><?php echo _t('show_adaptive'); ?></option>
-					<option value="<?php echo FreshRSS_Entry::STATE_ALL; ?>"<?php echo FreshRSS_Context::$conf->default_view === FreshRSS_Entry::STATE_ALL ? ' selected="selected"' : ''; ?>><?php echo _t('show_all_articles'); ?></option>
-					<option value="<?php echo FreshRSS_Entry::STATE_STRICT + FreshRSS_Entry::STATE_NOT_READ; ?>"<?php echo FreshRSS_Context::$conf->default_view === FreshRSS_Entry::STATE_STRICT + FreshRSS_Entry::STATE_NOT_READ ? ' selected="selected"' : ''; ?>><?php echo _t('show_not_reads'); ?></option>
+					<option value="adaptive"<?php echo FreshRSS_Context::$conf->default_view === 'adaptive' ? ' selected="selected"' : ''; ?>><?php echo _t('show_adaptive'); ?></option>
+					<option value="all"<?php echo FreshRSS_Context::$conf->default_view === 'all' ? ' selected="selected"' : ''; ?>><?php echo _t('show_all_articles'); ?></option>
+					<option value="unread"<?php echo FreshRSS_Context::$conf->default_view === 'unread' ? ' selected="selected"' : ''; ?>><?php echo _t('show_not_reads'); ?></option>
 				</select>
 			</div>
 		</div>

+ 17 - 13
app/views/helpers/pagination.phtml

@@ -1,28 +1,32 @@
 <?php
-	$c = Minz_Request::controllerName();
-	$a = Minz_Request::actionName();
-	$params = Minz_Request::params();
-	$markReadUrl = Minz_Session::param('markReadUrl');
-	Minz_Session::_param('markReadUrl', false);
+	$url_next = Minz_Request::currentRequest();
+	$url_next['params']['next'] = FreshRSS_Context::$next_id;
+	$url_next['params']['ajax'] = 1;
+
+	$url_mark_read = array(
+		'c' => 'entry',
+		'a' => 'read',
+		'params' => array(
+			'get' => FreshRSS_Context::currentGet(),
+			'nextGet' => FreshRSS_Context::$next_get,
+			'idMax' => FreshRSS_Context::$id_max,
+		)
+	);
 ?>
 
 <form id="mark-read-pagination" method="post" style="display: none"></form>
 
 <ul class="pagination">
 	<li class="item pager-next">
-	<?php if (!empty($this->nextId)) { ?>
-		<?php
-			$params['next'] = $this->nextId;
-			$params['ajax'] = 1;
-		?>
-		<a id="load_more" href="<?php echo Minz_Url::display(array('c' => $c, 'a' => $a, 'params' => $params)); ?>">
+	<?php if (FreshRSS_Context::$next_id) { ?>
+		<a id="load_more" href="<?php echo Minz_Url::display($url_next); ?>">
 			<?php echo _t('load_more'); ?>
 		</a>
-	<?php } elseif ($markReadUrl) { ?>
+	<?php } elseif ($url_mark_read) { ?>
 		<button id="bigMarkAsRead"
 		        class="as-link <?php echo FreshRSS_Context::$conf->reading_confirm ? 'confirm' : ''; ?>"
 		        form="mark-read-pagination"
-		        formaction="<?php echo $markReadUrl; ?>"
+		        formaction="<?php echo Minz_Url::display($url_mark_read); ?>"
 		        type="submit">
 			<?php echo _t('nothing_to_load'); ?><br />
 			<span class="bigTick">✓</span><br />

+ 0 - 53
app/views/helpers/view/global_view.phtml

@@ -1,53 +0,0 @@
-<?php $this->partial('nav_menu'); ?>
-
-<?php if (!empty($this->entries)) { ?>
-<div id="stream" class="global categories">
-<?php
-	$arUrl = array('c' => 'index', 'a' => 'index', 'params' => array());
-	if (FreshRSS_Context::$conf->view_mode !== 'normal') {
-		$arUrl['params']['output'] = 'normal';
-	}
-	$p = Minz_Request::param('state', '');
-	if (($p != '') && (FreshRSS_Context::$conf->default_view !== $p)) {
-		$arUrl['params']['state'] = $p;
-	}
-
-	foreach ($this->cat_aside as $cat) {
-		$feeds = $cat->feeds();
-		if (!empty($feeds)) {
-?>
-	<div class="box-category">
-		<div class="category">
-			<a data-unread="<?php echo formatNumber($cat->nbNotRead()); ?>" class="btn" href="<?php $arUrl['params']['get'] = 'c_' . $cat->id(); echo Minz_Url::display($arUrl); ?>">
-			<?php echo $cat->name(); ?>
-			</a>
-		</div>
-		<ul class="feeds">
-			<?php foreach ($feeds as $feed) { ?>
-			<?php $not_read = $feed->nbNotRead(); ?>
-			<li id="f_<?php echo $feed->id(); ?>" class="item<?php echo $feed->inError() ? ' error' : ''; ?><?php echo $feed->nbEntries() == 0 ? ' empty' : ''; ?>">
-				<img class="favicon" src="<?php echo $feed->favicon(); ?>" alt="✇" />
-				<a class="feed" data-unread="<?php echo formatNumber($feed->nbNotRead()); ?>" data-priority="<?php echo $feed->priority(); ?>" href="<?php $arUrl['params']['get'] = 'f_' . $feed->id(); echo Minz_Url::display($arUrl); ?>">
-				<?php echo $feed->name(); ?>
-				</a>
-			</li>
-			<?php } ?>
-		</ul>
-	</div>
-<?php
-		}
-	}
-?>
-</div>
-
-<div id="overlay"></div>
-<div id="panel"<?php echo FreshRSS_Context::$conf->display_posts ? '' : ' class="hide_posts"'; ?>>
-	<a class="close" href="#"><?php echo _i('close'); ?></a>
-</div>
-
-<?php } else { ?>
-<div id="stream" class="prompt alert alert-warn global">
-	<h2><?php echo _t('no_feed_to_display'); ?></h2>
-	<a href="<?php echo _url('subscription', 'index'); ?>"><?php echo _t('think_to_add'); ?></a><br /><br />
-</div>
-<?php } ?>

+ 53 - 0
app/views/index/global.phtml

@@ -0,0 +1,53 @@
+<?php
+	$this->partial('nav_menu');
+
+	$class = '';
+	if (FreshRSS_Context::$conf->hide_read_feeds &&
+			FreshRSS_Context::isStateEnabled(FreshRSS_Entry::STATE_NOT_READ) &&
+			!FreshRSS_Context::isStateEnabled(FreshRSS_Entry::STATE_READ)) {
+		$class = ' state_unread';
+	}
+?>
+
+<div id="stream" class="global<?php echo $class; ?>">
+<?php
+	$url_base = array(
+		'c' => 'index',
+		'a' => 'index',
+		'params' => Minz_Request::params()
+	);
+
+	foreach ($this->categories as $cat) {
+		$feeds = $cat->feeds();
+		$url_base['params']['get'] = 'c_' . $cat->id();
+
+		if (!empty($feeds)) {
+?>
+	<div class="box category" data-unread="<?php echo $cat->nbNotRead(); ?>">
+		<div class="box-title"><a class="title" data-unread="<?php echo format_number($cat->nbNotRead()); ?>" href="<?php echo Minz_Url::display($url_base); ?>"><?php echo $cat->name(); ?></a></div>
+
+		<ul class="box-content">
+			<?php
+				foreach ($feeds as $feed) {
+					$nb_not_read = $feed->nbNotRead();
+					$error = $feed->inError() ? ' error' : '';
+					$empty = $feed->nbEntries() === 0 ? ' empty' : '';
+					$url_base['params']['get'] = 'f_' . $feed->id();
+			?>
+			<li id="f_<?php echo $feed->id(); ?>" class="item feed<?php echo $error, $empty; ?>" data-unread="<?php echo $feed->nbNotRead(); ?>" data-priority="<?php echo $feed->priority(); ?>">
+				<img class="favicon" src="<?php echo $feed->favicon(); ?>" alt="✇" />
+				<a class="item-title" data-unread="<?php echo format_number($feed->nbNotRead()); ?>" href="<?php echo Minz_Url::display($url_base); ?>"><?php echo $feed->name(); ?></a>
+			</li>
+			<?php } ?>
+		</ul>
+	</div>
+<?php
+		}
+	}
+?>
+</div>
+
+<div id="overlay"></div>
+<div id="panel"<?php echo FreshRSS_Context::$conf->display_posts ? '' : ' class="hide_posts"'; ?>>
+	<a class="close" href="#"><?php echo _i('close'); ?></a>
+</div>

+ 0 - 2
app/views/index/index.phtml

@@ -7,8 +7,6 @@ if (FreshRSS_Auth::hasAccess() || Minz_Configuration::allowAnonymous()) {
 		$this->renderHelper('view/normal_view');
 	} elseif ($output === 'reader') {
 		$this->renderHelper('view/reader_view');
-	} elseif ($output === 'global') {
-		$this->renderHelper('view/global_view');
 	} elseif ($output === 'rss') {
 		$this->renderHelper('view/rss_view');
 	} else {

+ 11 - 9
app/views/helpers/view/normal_view.phtml → app/views/index/normal.phtml

@@ -1,6 +1,6 @@
 <?php
 
-$this->partial('aside_flux');
+$this->partial('aside_feed');
 $this->partial('nav_menu');
 
 if (!empty($this->entries)) {
@@ -26,33 +26,35 @@ if (!empty($this->entries)) {
 	$bottomline_link = FreshRSS_Context::$conf->bottomline_link;
 
 	$content_width = FreshRSS_Context::$conf->content_width;
+
+	$today = @strtotime('today');
 ?>
 
 <div id="stream" class="normal<?php echo $hidePosts ? ' hide_posts' : ''; ?>"><?php
 	?><div id="new-article">
-		<a href="<?php echo Minz_Url::display($this->url); ?>"><?php echo _t('new_article'); ?></a>
+		<a href="<?php echo Minz_Url::display(Minz_Request::currentRequest()); ?>"><?php echo _t('new_article'); ?></a>
 	</div><?php
 	foreach ($this->entries as $item) {
-		if ($display_today && $item->isDay(FreshRSS_Days::TODAY, $this->today)) {
+		if ($display_today && $item->isDay(FreshRSS_Days::TODAY, $today)) {
 			?><div class="day" id="day_today"><?php
 				echo _t('today');
 				?><span class="date"> — <?php echo timestamptodate(time(), false); ?></span><?php
-				?><span class="name"><?php echo $this->currentName; ?></span><?php
+				?><span class="name"><?php echo FreshRSS_Context::$name; ?></span><?php
 			?></div><?php
 			$display_today = false;
 		}
-		if ($display_yesterday && $item->isDay(FreshRSS_Days::YESTERDAY, $this->today)) {
+		if ($display_yesterday && $item->isDay(FreshRSS_Days::YESTERDAY, $today)) {
 			?><div class="day" id="day_yesterday"><?php
 				echo _t('yesterday');
 				?><span class="date"> — <?php echo timestamptodate(time() - 86400, false); ?></span><?php
-				?><span class="name"><?php echo $this->currentName; ?></span><?php
+				?><span class="name"><?php echo FreshRSS_Context::$name; ?></span><?php
 			?></div><?php
 			$display_yesterday = false;
 		}
-		if ($display_others && $item->isDay(FreshRSS_Days::BEFORE_YESTERDAY, $this->today)) {
+		if ($display_others && $item->isDay(FreshRSS_Days::BEFORE_YESTERDAY, $today)) {
 			?><div class="day" id="day_before_yesterday"><?php
 				echo _t('before_yesterday');
-				?><span class="name"><?php echo $this->currentName; ?></span><?php
+				?><span class="name"><?php echo FreshRSS_Context::$name; ?></span><?php
 			?></div><?php
 			$display_others = false;
 		}
@@ -80,7 +82,7 @@ if (!empty($this->entries)) {
 					?></li><?php
 				}
 			}
-			$feed = FreshRSS_CategoryDAO::findFeed($this->cat_aside, $item->feed());	//We most likely already have the feed object in cache
+			$feed = FreshRSS_CategoryDAO::findFeed($this->categories, $item->feed());	//We most likely already have the feed object in cache
 			if ($feed == null) {
 				$feed = $item->feed(true);
 				if ($feed == null) {

+ 1 - 1
app/views/helpers/view/reader_view.phtml → app/views/index/reader.phtml

@@ -13,7 +13,7 @@ if (!empty($this->entries)) {
 		<div class="flux_content">
 			<div class="content <?php echo $content_width; ?>">
 				<?php
-					$feed = FreshRSS_CategoryDAO::findFeed($this->cat_aside, $item->feed());	//We most likely already have the feed object in cache
+					$feed = FreshRSS_CategoryDAO::findFeed($this->categories, $item->feed());	//We most likely already have the feed object in cache
 					if (empty($feed)) $feed = $item->feed(true);
 				?>
 				<a href="<?php echo $item->link(); ?>">

+ 0 - 0
app/views/helpers/view/rss_view.phtml → app/views/index/rss.phtml


+ 7 - 0
lib/Minz/Request.php

@@ -45,6 +45,13 @@ class Minz_Request {
 	public static function defaultActionName() {
 		return self::$default_action_name;
 	}
+	public static function currentRequest() {
+		return array(
+			'c' => self::$controller_name,
+			'a' => self::$action_name,
+			'params' => self::$params,
+		);
+	}
 
 	/**
 	 * Setteurs

+ 4 - 0
lib/lib_rss.php

@@ -60,6 +60,10 @@ 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
 }
+function format_number($n, $precision = 0) {
+	// TODO: coding style, prefer THIS function. Remove formatNumber.
+	return formatNumber($n, $precision);
+}
 
 function formatBytes($bytes, $precision = 2, $system = 'IEC') {
 	if ($system === 'IEC') {

+ 3 - 2
p/scripts/global_view.js

@@ -9,7 +9,7 @@ function load_panel(link) {
 	panel_loading = true;
 
 	$.get(link, function (data) {
-		$("#panel").append($(".nav_menu, #stream .day, #stream .flux, #stream .pagination", data));
+		$("#panel").append($(".nav_menu, #stream .day, #stream .flux, #stream .pagination, #stream.prompt", data));
 
 		$("#panel .nav_menu").children().not("#nav_menu_read_all").remove();
 
@@ -50,7 +50,8 @@ function init_close_panel() {
 }
 
 function init_global_view() {
-	$("#stream .box-category a").click(function () {
+	// TODO: should be based on generic classes.
+	$(".box a").click(function () {
 		var link = $(this).attr("href");
 
 		load_panel(link);

+ 36 - 35
p/scripts/main.js

@@ -56,31 +56,31 @@ function incLabel(p, inc, spaceAfter) {
 
 function incUnreadsFeed(article, feed_id, nb) {
 	//Update unread: feed
-	var elem = $('#' + feed_id + '>.feed').get(0),
+	var elem = $('#' + feed_id).get(0),
 		feed_unreads = elem ? str2int(elem.getAttribute('data-unread')) : 0,
 		feed_priority = elem ? str2int(elem.getAttribute('data-priority')) : 0;
 	if (elem) {
-		elem.setAttribute('data-unread', numberFormat(feed_unreads + nb));
-		elem = $(elem).closest('li').get(0);
+		elem.setAttribute('data-unread', feed_unreads + nb);
+		elem = $(elem).children('.item-title').get(0);
 		if (elem) {
-			elem.setAttribute('data-unread', feed_unreads + nb);
+			elem.setAttribute('data-unread', numberFormat(feed_unreads + nb));
 		}
 	}
 
 	//Update unread: category
-	elem = $('#' + feed_id).parent().prevAll('.category').children(':first').get(0);
+	elem = $('#' + feed_id).parents('.category').get(0);
 	feed_unreads = elem ? str2int(elem.getAttribute('data-unread')) : 0;
 	if (elem) {
-		elem.setAttribute('data-unread', numberFormat(feed_unreads + nb));
-		elem = $(elem).closest('li').get(0);
+		elem.setAttribute('data-unread', feed_unreads + nb);
+		elem = $(elem).find('.title').get(0);
 		if (elem) {
-			elem.setAttribute('data-unread', feed_unreads + nb);
+			elem.setAttribute('data-unread', numberFormat(feed_unreads + nb));
 		}
 	}
 
 	//Update unread: all
 	if (feed_priority > 0) {
-		elem = $('#aside_flux .all').children(':first').get(0);
+		elem = $('#aside_feed .all .title').get(0);
 		if (elem) {
 			feed_unreads = elem ? str2int(elem.getAttribute('data-unread')) : 0;
 			elem.setAttribute('data-unread', numberFormat(feed_unreads + nb));
@@ -89,7 +89,7 @@ function incUnreadsFeed(article, feed_id, nb) {
 
 	//Update unread: favourites
 	if (article && article.closest('div').hasClass('favorite')) {
-		elem = $('#aside_flux .favorites').children(':first').get(0);
+		elem = $('#aside_feed .favorites .title').get(0);
 		if (elem) {
 			feed_unreads = elem ? str2int(elem.getAttribute('data-unread')) : 0;
 			elem.setAttribute('data-unread', numberFormat(feed_unreads + nb));
@@ -97,7 +97,7 @@ function incUnreadsFeed(article, feed_id, nb) {
 	}
 
 	var isCurrentView = false;
-	//Update unread: title
+	// Update unread: title
 	document.title = document.title.replace(/^((?:\([ 0-9]+\) )?)/, function (m, p1) {
 		var $feed = $('#' + feed_id);
 		if (article || ($feed.closest('.active').length > 0 && $feed.siblings('.active').length === 0)) {
@@ -194,7 +194,7 @@ function mark_favorite(active) {
 		}
 		$b.find('.icon').replaceWith(data.icon);
 
-		var favourites = $('.favorites>a').contents().last().get(0);
+		var favourites = $('#aside_feed .favorites .title').contents().last().get(0);
 		if (favourites && favourites.textContent) {
 			favourites.textContent = favourites.textContent.replace(/((?: \([ 0-9]+\))?\s*)$/, function (m, p1) {
 				return incLabel(p1, inc, false);
@@ -202,7 +202,7 @@ function mark_favorite(active) {
 		}
 
 		if (active.closest('div').hasClass('not_read')) {
-			var elem = $('#aside_flux .favorites').children(':first').get(0),
+			var elem = $('#aside_feed .favorites .title').get(0),
 				feed_unreads = elem ? str2int(elem.getAttribute('data-unread')) : 0;
 			if (elem) {
 				elem.setAttribute('data-unread', numberFormat(feed_unreads + inc));
@@ -292,42 +292,42 @@ function next_entry() {
 }
 
 function prev_feed() {
-	var active_feed = $("#aside_flux .feeds li.active");
+	var active_feed = $("#aside_feed .tree-folder-items .item.active");
 	if (active_feed.length > 0) {
-		active_feed.prevAll(':visible:first').find('a.feed').each(function(){this.click();});
+		active_feed.prevAll(':visible:first').find('a').each(function(){this.click();});
 	} else {
 		last_feed();
 	}
 }
 
 function next_feed() {
-	var active_feed = $("#aside_flux .feeds li.active");
+	var active_feed = $("#aside_feed .tree-folder-items .item.active");
 	if (active_feed.length > 0) {
-		active_feed.nextAll(':visible:first').find('a.feed').each(function(){this.click();});
+		active_feed.nextAll(':visible:first').find('a').each(function(){this.click();});
 	} else {
 		first_feed();
 	}
 }
 
 function first_feed() {
-	var feed = $("#aside_flux .feeds.active li:visible:first");
+	var feed = $("#aside_feed .tree-folder-items.active .item:visible:first");
 	if (feed.length > 0) {
 		feed.find('a')[1].click();
 	}
 }
 
 function last_feed() {
-	var feed = $("#aside_flux .feeds.active li:visible:last");
+	var feed = $("#aside_feed .tree-folder-items.active .item:visible:last");
 	if (feed.length > 0) {
 		feed.find('a')[1].click();
 	}
 }
 
 function prev_category() {
-	var active_cat = $("#aside_flux .category.stick.active");
+	var active_cat = $("#aside_feed .tree-folder.active");
 
 	if (active_cat.length > 0) {
-		var prev_cat = active_cat.parent('li').prevAll(':visible:first').find('.category.stick a.btn');
+		var prev_cat = active_cat.prevAll(':visible:first').find('.tree-folder-title .title');
 		if (prev_cat.length > 0) {
 			prev_cat[0].click();
 		}
@@ -338,10 +338,10 @@ function prev_category() {
 }
 
 function next_category() {
-	var active_cat = $("#aside_flux .category.stick.active");
+	var active_cat = $("#aside_feed .tree-folder.active");
 
 	if (active_cat.length > 0) {
-		var next_cat = active_cat.parent('li').nextAll(':visible:first').find('.category.stick a.btn');
+		var next_cat = active_cat.nextAll(':visible:first').find('.tree-folder-title .title');
 		if (next_cat.length > 0) {
 			next_cat[0].click();
 		}
@@ -352,16 +352,16 @@ function next_category() {
 }
 
 function first_category() {
-	var cat = $("#aside_flux .category.stick:visible:first");
+	var cat = $("#aside_feed .tree-folder:visible:first");
 	if (cat.length > 0) {
-		cat.find('a.btn')[0].click();
+		cat.find('.tree-folder-title .title')[0].click();
 	}
 }
 
 function last_category() {
-	var cat = $("#aside_flux .category.stick:visible:last");
+	var cat = $("#aside_feed .tree-folder:visible:last");
 	if (cat.length > 0) {
-		cat.find('a.btn')[0].click();
+		cat.find('.tree-folder-title .title')[0].click();
 	}
 }
 
@@ -487,7 +487,8 @@ function init_column_categories() {
 	if (context['current_view'] !== 'normal') {
 		return;
 	}
-	$('#aside_flux').on('click', '.category>a.dropdown-toggle', function () {
+
+	$('#aside_feed').on('click', '.tree-folder>.tree-folder-title>a.dropdown-toggle', function () {
 		$(this).children().each(function() {
 			if (this.alt === '▽') {
 				this.src = this.src.replace('/icons/down.', '/icons/up.');
@@ -497,12 +498,12 @@ function init_column_categories() {
 				this.alt = '▽';
 			}
 		});
-		$(this).parent().next(".feeds").slideToggle();
+		$(this).parent().next(".tree-folder-items").slideToggle();
 		return false;
 	});
-	$('#aside_flux').on('click', '.feeds .dropdown-toggle', function () {
+	$('#aside_feed').on('click', '.tree-folder-items .item .dropdown-toggle', function () {
 		if ($(this).nextAll('.dropdown-menu').length === 0) {
-			var feed_id = $(this).closest('li').attr('id').substr(2),
+			var feed_id = $(this).closest('.item').attr('id').substr(2),
 				feed_web = $(this).data('fweb'),
 				template = $('#feed_config_template').html().replace(/!!!!!!/g, feed_id).replace('http://example.net/', feed_web);
 			$(this).attr('href', '#dropdown-' + feed_id).prev('.dropdown-target').attr('id', 'dropdown-' + feed_id).parent().append(template);
@@ -875,12 +876,12 @@ function init_notifs_html5() {
 
 function refreshUnreads() {
 	$.getJSON('./?c=javascript&a=nbUnreadsPerFeed').done(function (data) {
-		var isAll = $('.category.all > .active').length > 0,
+		var isAll = $('.category.all.active').length > 0,
 		    new_articles = false;
 
 		$.each(data, function(feed_id, nbUnreads) {
 			feed_id = 'f_' + feed_id;
-			var elem = $('#' + feed_id + '>.feed').get(0),
+			var elem = $('#' + feed_id).get(0),
 				feed_unreads = elem ? str2int(elem.getAttribute('data-unread')) : 0;
 
 			if ((incUnreadsFeed(null, feed_id, nbUnreads - feed_unreads) || isAll) &&	//Update of current view?
@@ -890,7 +891,7 @@ function refreshUnreads() {
 			};
 		});
 
-		var nb_unreads = str2int($('.category.all>a').attr('data-unread'));
+		var nb_unreads = str2int($('.category.all .title').attr('data-unread'));
 
 		if (nb_unreads > 0 && new_articles) {
 			faviconNbUnread(nb_unreads);
@@ -1135,7 +1136,7 @@ function init_password_observers() {
 
 function faviconNbUnread(n) {
 	if (typeof n === 'undefined') {
-		n = str2int($('.category.all>a').attr('data-unread'));
+		n = str2int($('.category.all .title').attr('data-unread'));
 	}
 	//http://remysharp.com/2010/08/24/dynamic-favicons/
 	var canvas = document.createElement('canvas'),

+ 103 - 91
p/themes/Dark/dark.css

@@ -464,6 +464,48 @@ a.btn {
 	visibility: visible;
 }
 
+/*=== Tree */
+.tree {
+	margin: 10px 0;
+}
+.tree-folder-title {
+	position: relative;
+	padding: 0 10px;
+	line-height: 2.5rem;
+	font-size: 1rem;
+	background: #1c1c1c;
+	font-weight: bold;
+}
+.tree-folder-title .title {
+	background: inherit;
+	color: #888;
+}
+.tree-folder-title .title:hover {
+	text-decoration: none;
+}
+.tree-folder.active .tree-folder-title {
+	background: #2c2c2c;
+}
+.tree-folder-items {
+	border-top: 1px solid #222;
+	border-bottom: 1px solid #222;
+	background: #161616;
+}
+.tree-folder-items > .item {
+	padding: 0 10px;
+	line-height: 2.5rem;
+	font-size: 0.8rem;
+}
+.tree-folder-items > .item.active {
+	background: #1c1c1c;
+}
+.tree-folder-items > .item > a {
+	text-decoration: none;
+}
+.tree-folder-items > .item.active > a {
+	color: #888;
+}
+
 /*=== STRUCTURE */
 /*===============*/
 /*=== Header */
@@ -500,81 +542,60 @@ a.btn {
 	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 {
+.aside.aside_feed {
+	padding: 10px 0;
 	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;
+.aside.aside_feed .tree {
+	position: sticky;
+	top: 0;
+	margin: 10px 0 50px;
 }
-.category .btn:first-child:not([data-unread="0"]):after {
+
+/*=== Aside main page (categories) */
+.aside_feed .tree-folder-title > .title: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;
+	right: 0;
+	margin: 10px 0;
+	padding: 0 10px;
+	font-size: 0.9rem;
+	line-height: 1.5rem;
+	background: inherit;
+	border-left: 1px solid #666;
 }
 
 /*=== Aside main page (feeds) */
-.categories .feeds .item.active {
-	background: #333;
+.feed.item.empty.active {
+	background: #c95;
 }
-.categories .feeds .item.active .feed {
-	color: #888;
+.feed.item.error.active {
+	background: #a44;
 }
-.categories .feeds .item.empty .feed {
+.feed.item.empty,
+.feed.item.empty > a {
 	color: #c95;
 }
-.categories .feeds .item.empty.active {
-	background: #c95;
-}
-.categories .feeds .item.error .feed {
+.feed.item.error,
+.feed.item.error > a {
 	color: #a44;
 }
-.categories .feeds .item.error.active {
-	background: #a44;
+.feed.item.empty.active,
+.feed.item.empty.active > a {
+	color: #111;
 }
-.categories .feeds .item.empty.active .feed,
-.categories .feeds .item.error.active .feed {
+.feed.item.error.active,
+.feed.item.error.active > a {
 	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 {
+.aside_feed .tree-folder-items .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 {
+.aside_feed .tree-folder-items .item .dropdown-target:target ~ .dropdown-toggle > .icon,
+.aside_feed .tree-folder-items .item:hover .dropdown-toggle > .icon,
+.aside_feed .tree-folder-items .item.active .dropdown-toggle > .icon {
+	border-radius: 3px;
 	vertical-align: middle;
 	background-color: #111;
-	border-radius: 3px;
 }
 
 /*=== Configuration pages */
@@ -825,46 +846,34 @@ a.btn {
 
 /*=== GLOBAL VIEW */
 /*================*/
-#stream.global .box-category {
-	text-align: left;
-	background: #1a1a1a;
-	border: 1px solid #000;
-	border-radius: 5px;
+.box.category .box-title .title {
+	font-weight: normal;
+	text-decoration: none;
 	text-align: left;
+	color: #888;
 }
-#stream.global .category {
-	margin: 0;
+.box.category:not([data-unread="0"]) .box-title {
+	background: #34495E;
 }
-#stream.global .btn {
-	width: auto;
-	height: 2em;
-	margin: 0;
-	padding: 0 10px;
-	line-height: 2em;
-	font-size: 1.2rem;
+.box.category:not([data-unread="0"]) .box-title:active {
 	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;
+.box.category:not([data-unread="0"]) .box-title .title {
 	color: #fff;
-}
-#stream.global .btn:first-child:not([data-unread="0"]):after {
-	top: 0; right: 5px;
 	font-weight: bold;
+}
+.box.category .title:not([data-unread="0"]):after {
+	position: absolute;
+	top: 5px; right: 10px;
 	border: 0;
 	background: none;
-	color: #fff;
-}
-#stream.global .box-category .feeds {
-	max-height: 250px;
+	font-weight: bold;
+	box-shadow: none;
+	text-shadow: none;
 }
-#stream.global .box-category .feeds .item {
+.box.category .item.feed {
 	padding: 2px 10px;
-	font-size: 0.9rem;
+	font-size: 0.8rem;
 }
 
 /*=== Panel */
@@ -963,17 +972,20 @@ a.btn {
 	}
 	.aside .toggle_aside,
 	#panel .close {
-		position: absolute;
 		display: block;
-		top: 0; right: 0;
-		width: 30px;
-		height: 30px;
-		line-height: 30px;
+		width: 100%;
+		height: 50px;
+		line-height: 50px;
 		text-align: center;
 		background: #111;
-		border-left: 1px solid #333;
 		border-bottom: 1px solid #333;
-		border-radius: 0 0 0 5px;
+	}
+
+	.aside.aside_feed {
+		padding: 0;
+	}
+	.aside.aside_feed .tree {
+		position: static;
 	}
 
 	.nav_menu .btn {

+ 98 - 86
p/themes/Flat/flat.css

@@ -473,6 +473,44 @@ a.btn {
 	visibility: visible;
 }
 
+/*=== Tree */
+.tree {
+	margin: 10px 0;
+}
+.tree-folder-title {
+	position: relative;
+	padding: 0 10px;
+	background: #34495e;
+	line-height: 2.5rem;
+	font-size: 1rem;
+	font-weight: bold;
+}
+.tree-folder-title .title {
+	background: inherit;
+	color: #fff;
+}
+.tree-folder-title .title:hover {
+	text-decoration: none;
+}
+.tree-folder.active .tree-folder-title {
+	background: #2980b9;
+}
+.tree-folder-items {
+	background: #2c3e50;
+}
+.tree-folder-items > .item {
+	padding: 0 10px;
+	line-height: 2.5rem;
+	font-size: 0.8rem;
+}
+.tree-folder-items > .item.active {
+	background: #2980b9;
+}
+.tree-folder-items > .item > a {
+	text-decoration: none;
+	color: #fff;
+}
+
 /*=== STRUCTURE */
 /*===============*/
 /*=== Header */
@@ -508,77 +546,58 @@ a.btn {
 .aside {
 	background: #ecf0f1;
 }
-.aside.aside_flux {
-	padding: 10px 0 50px;
-	background: #ecf0f1;
-}
-
-/*=== Aside main page (categories) */
-.categories {
+.aside.aside_feed {
+	padding: 10px 0;
 	text-align: center;
+	background: #34495e;
+	border-radius: 0 10px 0 0;
 }
-.category {
-	width: 233px;
-	margin: 10px auto;
-	text-align: left;
-}
-.category .btn:first-child {
-	position: relative;
-	width: 212px;
-}
-.category.stick .btn:first-child {
-	width: 175px;
+.aside.aside_feed .tree {
+	position: sticky;
+	top: 0;
+	margin: 10px 0 50px;
 }
-.category .btn:first-child:not([data-unread="0"]):after {
+
+/*=== Aside main page (categories) */
+.aside_feed .tree-folder-title > .title:not([data-unread="0"]):after {
 	position: absolute;
-	top: 5px; right: 5px;
-	padding: 1px 5px;
-	background: #3498DB;
-	color: #fff;
-	border-radius: 5px;
+	right: 0;
+	margin: 10px 0;
+	padding: 0 10px;
+	font-size: 0.9rem;
+	line-height: 1.5rem;
+	background: inherit;
 }
 
 /*=== Aside main page (feeds) */
-.categories .feeds .item.active {
-	background: #2980b9;
-}
-.categories .feeds .item.empty.active {
+.feed.item.empty.active {
 	background: #f39c12;
 }
-.categories .feeds .item.error.active {
+.feed.item.error.active {
 	background: #bd362f;
 }
-.categories .feeds .item.empty .feed {
+.feed.item.empty,
+.feed.item.empty > a {
 	color: #e67e22;
 }
-.categories .feeds .item.error .feed {
+.feed.item.error,
+.feed.item.error > a {
 	color: #bd362f;
 }
-.categories .feeds .item.active .feed,
-.categories .feeds .item.empty.active .feed,
-.categories .feeds .item.error.active .feed {
+.feed.item.empty.active,
+.feed.item.error.active,
+.feed.item.empty.active > a,
+.feed.item.error.active > a {
 	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 {
+.aside_feed .tree-folder-items .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;
+.aside_feed .tree-folder-items .item .dropdown-target:target ~ .dropdown-toggle > .icon,
+.aside_feed .tree-folder-items .item:hover .dropdown-toggle > .icon,
+.aside_feed .tree-folder-items .item.active .dropdown-toggle > .icon {
 	border-radius: 3px;
+	vertical-align: middle;
 }
 
 /*=== Configuration pages */
@@ -833,44 +852,33 @@ a.btn {
 
 /*=== GLOBAL VIEW */
 /*================*/
-#stream.global .box-category {
+.box.category .box-title .title {
+	font-weight: normal;
+	text-decoration: none;
 	text-align: left;
-	border: 1px solid #ddd;
-	border-radius: 5px;
 }
-#stream.global .category {
-	margin: 0;
+.box.category:not([data-unread="0"]) .box-title {
+	background: #3498db;
 }
-#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;
+.box.category:not([data-unread="0"]) .box-title:active {
+	background: #2980b9;
 }
-#stream.global .btn:not([data-unread="0"]) {
+.box.category:not([data-unread="0"]) .box-title .title {
 	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;
+.box.category .title:not([data-unread="0"]):after {
+	position: absolute;
+	top: 5px; right: 10px;
 	border: 0;
-	color: #fff;
-}
-#stream.global .box-category .feeds {
-	max-height: 250px;
+	background: none;
+	font-weight: bold;
+	box-shadow: none;
+	text-shadow: none;
 }
-#stream.global .box-category .feeds .item {
+.box.category .item.feed {
 	padding: 2px 10px;
-	font-size: 0.9rem;
+	font-size: 0.8rem;
 }
 
 /*=== DIVERS */
@@ -961,15 +969,19 @@ a.btn {
 	}
 	.aside .toggle_aside,
 	#panel .close {
-		position: absolute;
 		display: block;
-		top: 0; right: 0;
-		width: 32px;
-		height: 32px;
-		line-height: 30px;
+		width: 100%;
+		height: 50px;
+		line-height: 50px;
 		text-align: center;
-		background: #34495e;
-		border-radius: 0 0 0 5px;
+		background: #2c3e50;
+	}
+
+	.aside.aside_feed {
+		padding: 0;
+	}
+	.aside.aside_feed .tree {
+		position: static;
 	}
 
 	.nav_menu .btn {

+ 101 - 87
p/themes/Origine/origine.css

@@ -498,6 +498,51 @@ a.btn {
 	visibility: visible;
 }
 
+/*=== Tree */
+.tree {
+	margin: 10px 0;
+}
+.tree-folder-title {
+	position: relative;
+	padding: 0 10px;
+	background: #fff;
+	line-height: 2.5rem;
+	font-size: 1rem;
+	font-weight: bold;
+}
+.tree-folder-title .title {
+	background: inherit;
+	color: #444;
+}
+.tree-folder-title .title:hover {
+	text-decoration: none;
+}
+.tree-folder.active .tree-folder-title {
+	background: #f0f0f0;
+}
+.tree-folder.active .tree-folder-title .title {
+	color: #0062BE;
+}
+.tree-folder-items {
+	border-top: 1px solid #ccc;
+	border-bottom: 1px solid #ccc;
+	background: #f6f6f6;
+}
+.tree-folder-items > .item {
+	padding: 0 10px;
+	line-height: 2.5rem;
+	font-size: 0.8rem;
+}
+.tree-folder-items > .item.active {
+	background: #0062be;
+}
+.tree-folder-items > .item > a {
+	text-decoration: none;
+}
+.tree-folder-items > .item.active > a {
+	color: #fff;
+}
+
 /*=== STRUCTURE */
 /*===============*/
 /*=== Header */
@@ -536,76 +581,56 @@ a.btn {
 	border-right: 1px solid #aaa;
 	background: #fff;
 }
-.aside.aside_flux {
-	padding: 10px 0 50px;
-}
-
-/*=== Aside main page (categories) */
-.categories {
+.aside.aside_feed {
+	padding: 10px 0;
 	text-align: center;
+	background: #fff;
 }
-.category {
-	width: 235px;
-	margin: 10px auto;
-	text-align: left;
-}
-.category .btn:first-child {
-	position: relative;
-	width: 213px;
-}
-.category.stick .btn:first-child {
-	width: 176px;
+.aside.aside_feed .tree {
+	position: sticky;
+	top: 0;
+	margin: 10px 0 50px;
 }
-.category .btn:first-child:not([data-unread="0"]):after {
+
+/*=== Aside main page (categories) */
+.aside_feed .category .title:not([data-unread="0"]):after {
 	position: absolute;
-	top: 3px; right: 3px;
-	padding: 1px 5px;
-	background: #ccc;
-	color: #fff;
-	border: 1px solid #bbb;
-	border-radius: 5px;
-	box-shadow: 1px 3px 3px #aaa inset;
-	text-shadow: 0 0 1px #aaa;
+	right: 0;
+	margin: 10px 0;
+	padding: 0 10px;
+	font-size: 0.9rem;
+	line-height: 1.5rem;
+	background: inherit;
+	border-left: 1px solid #aaa;
 }
 
 /*=== Aside main page (feeds) */
-.categories .feeds .item.active {
-	background: #0062BE;
-}
-.categories .feeds .item.empty.active {
+.feed.item.empty.active {
 	background: #e67e22;
 }
-.categories .feeds .item.error.active {
-	background: #BD362F;
+.feed.item.error.active {
+	background: #bd362f;
 }
-.categories .feeds .item.empty .feed {
+.feed.item.empty,
+.feed.item.empty > a {
 	color: #e67e22;
 }
-.categories .feeds .item.error .feed {
-	color: #BD362F;
+.feed.item.error,
+.feed.item.error > a {
+	color: #bd362f;
 }
-.categories .feeds .item.active .feed,
-.categories .feeds .item.empty.active .feed,
-.categories .feeds .item.error.active .feed {
+.feed.item.empty.active,
+.feed.item.error.active,
+.feed.item.empty.active > a,
+.feed.item.error.active > a {
 	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 {
+.aside_feed .tree-folder-items .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 {
+.aside_feed .tree-folder-items .item .dropdown-target:target ~ .dropdown-toggle > .icon,
+.aside_feed .tree-folder-items .item:hover .dropdown-toggle > .icon,
+.aside_feed .tree-folder-items .item.active .dropdown-toggle > .icon {
 	background-color: #fff;
 	border-radius: 3px;
 	vertical-align: middle;
@@ -873,35 +898,24 @@ a.btn {
 
 /*=== GLOBAL VIEW */
 /*================*/
-#stream.global .box-category {
-	background: #fff;
-	border-radius: 5px;
+.box.category .box-title .title {
+	font-weight: normal;
+	text-decoration: none;
 	text-align: left;
-	box-shadow: 0 0 3px #bbb;
 }
-#stream.global .category {
-	margin: 0;
+.box.category:not([data-unread="0"]) .box-title {
+	background: #0084CC;
 }
-#stream.global .btn {
-	width: auto;
-	height: 2em;
-	margin: 0;
-	padding: 0 10px;
-	background: #f6f6f6;
-	border: none;
-	border-bottom: 1px solid #ddd;
-	border-radius: 5px 5px 0 0;
-	line-height: 2em;
-	font-size: 1.2rem;
+.box.category:not([data-unread="0"]) .box-title:active {
+	background: #3498db;
 }
-#stream.global .btn:not([data-unread="0"]) {
-	background: #0084CC;
+.box.category:not([data-unread="0"]) .box-title .title {
 	color: #fff;
 	font-weight: bold;
-	text-shadow: none;
 }
-#stream.global .btn:first-child:not([data-unread="0"]):after {
-	top: 0; right: 5px;
+.box.category .title:not([data-unread="0"]):after {
+	position: absolute;
+	top: 5px; right: 10px;
 	border: 0;
 	background: none;
 	color: #fff;
@@ -909,12 +923,9 @@ a.btn {
 	box-shadow: none;
 	text-shadow: none;
 }
-#stream.global .box-category .feeds {
-	max-height: 250px;
-}
-#stream.global .box-category .feeds .item {
+.box.category .item.feed {
 	padding: 2px 10px;
-	font-size: 0.9rem;
+	font-size: 0.8rem;
 }
 
 /*=== DIVERS */
@@ -1008,17 +1019,20 @@ a.btn {
 	}
 	.aside .toggle_aside,
 	#panel .close {
-		position: absolute;
 		display: block;
-		top: 0; right: 0;
-		width: 30px;
-		height: 30px;
-		line-height: 30px;
+		width: 100%;
+		height: 50px;
+		line-height: 50px;
 		text-align: center;
 		background: #f6f6f6;
-		border-left: 1px solid #ddd;
 		border-bottom: 1px solid #ddd;
-		border-radius: 0 0 0 5px;
+	}
+
+	.aside.aside_feed {
+		padding: 0;
+	}
+	.aside.aside_feed .tree {
+		position: static;
 	}
 
 	.nav_menu .btn {

+ 111 - 136
p/themes/Pafat/pafat.css

@@ -192,48 +192,12 @@ a.btn {
 	text-decoration: none;
 }
 
-
-.category.stick .btn {
-	background:#5bc0de;
-	color : #FFF;
-	border-color :#5bc0de;
-}
-
-.category.stick .btn:first-child:hover, .category.stick .btn:last-child:hover, .category.stick .btn.active:first-child, .category.stick.active .btn:last-child {
-	background:#39b3d7;
-	border-color : #39b3d7;
-}
-
-
 .btn.active,
 .btn:active,
 .dropdown-target:target ~ .btn.dropdown-toggle {
 	background: #eee;
 }
 
-.category.all > .btn  {
-	background: #428bca;
-	color : #FFF;
-	border-color : #428bca;
-}
-
-.category.all > .btn:hover  {
-	background: #3276b1;
-	border-color : #3276b1;
-}
-
-.category.favorites > .btn {
-	background:#f0ad4e;
-	border-color: #f0ad4e;
-	color : #fff;
-}
-
-.category.favorites > .btn:hover {
-	background: #ed9c28;
-	border-color : #ed9c28;
-	color : white;
-}
-
 .btn-important {
 	background: #5cb85c;
 	color: #fff;
@@ -520,6 +484,51 @@ a.btn {
 	visibility: visible;
 }
 
+/*=== Tree */
+.tree {
+	margin: 10px 0;
+}
+.tree-folder-title {
+	position: relative;
+	margin: 5px;
+	padding: 0 10px;
+	line-height: 2rem;
+	font-size: 0.9rem;
+	background: #5bc0de;
+	color: #fff;
+	border-top: 1px solid transparent;
+	border-bottom: 1px solid transparent;
+	border-radius: 5px;
+}
+.tree-folder-title .title {
+	background: inherit;
+	color: #fff;
+}
+.tree-folder-title .title:hover {
+	text-decoration: none;
+}
+.tree-folder.active .tree-folder-title {
+	background: #39b3d7;
+	font-weight: bold;
+	font-size: 1rem;
+	border-top: 1px solid #666;
+	border-bottom: 1px solid #666;
+}
+.tree-folder-items > .item {
+	padding: 0 10px;
+	line-height: 2.5rem;
+	font-size: 0.8rem;
+}
+.tree-folder-items > .item.active {
+	background: #5cb85c;
+}
+.tree-folder-items > .item > a {
+	text-decoration: none;
+}
+.tree-folder-items > .item.active > a {
+	color: #fff;
+}
+
 /*=== STRUCTURE */
 /*===============*/
 /*=== Header */
@@ -572,75 +581,69 @@ a.btn {
 	border-right: 1px solid #aaa;
 	background: #fff;
 }
-.aside.aside_flux {
-	padding: 10px 0 50px;
+.aside.aside_feed {
+	padding: 10px 0;
+	text-align: center;
+}
+.aside.aside_feed .tree {
+	position: sticky;
+	top: 5px;
+	margin: 10px 0 50px;
 }
 
 /*=== Aside main page (categories) */
-.categories {
-	text-align: center;
+.aside_feed .tree-folder-title > .title:not([data-unread="0"]):after {
+	position: absolute;
+	top: 0.25rem; right: 3px;
+	padding: 0px 5px;
+	border: 1px solid #fff;
+	border-radius: 3px;
+	font-size: 0.8rem;
+	line-height: 1.5rem;
 }
-.category {
-	width: 215px;
-	margin: 10px auto;
-	text-align: left;
+.aside_feed .tree-folder.all .tree-folder-title {
+	background: #428bca;
 }
-.category .btn:first-child {
-	position: relative;
-	width: 203px;
+.aside_feed .tree-folder.all.active .tree-folder-title {
+	background: #3276b1;
 }
-.category.stick .btn:first-child {
-	width: 176px;
+.aside_feed .tree-folder.favorites .tree-folder-title {
+	background: #f0ad4e;
 }
-.category .btn:first-child:not([data-unread="0"]):after {
-	position: absolute;
-	top: 2px; right: 3px;
-	padding: 0px 3px;
-	border: 1px solid ;
-	border-radius: 3px;
-	font-size:10pt;
-	line-height : 20px;
+.aside_feed .tree-folder.favorites.active .tree-folder-title {
+	background: #ed9c28;
 }
 
 /*=== Aside main page (feeds) */
-.categories .feeds .item.active {
-	background: #5cb85c;
+.feed.item.empty.active {
+	background: #e67e22;
 }
-.categories .feeds .item.active .feed {
-	color: #fff;
+.feed.item.error.active {
+	background: #bd362f;
 }
-.categories .feeds .item.empty .feed {
+.feed.item.empty,
+.feed.item.empty > a {
 	color: #e67e22;
 }
-.categories .feeds .item.empty.active {
-	background: #e67e22;
+.feed.item.error,
+.feed.item.error > a {
+	color: #bd362f;
 }
-.categories .feeds .item.empty.active .feed {
+.feed.item.empty.active,
+.feed.item.error.active,
+.feed.item.empty.active > a,
+.feed.item.error.active > a {
 	color: #fff;
 }
-.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 {
+.aside_feed .tree-folder-items .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 {
-	background-color: #fff;
+.aside_feed .tree-folder-items .item .dropdown-target:target ~ .dropdown-toggle > .icon,
+.aside_feed .tree-folder-items .item:hover .dropdown-toggle > .icon,
+.aside_feed .tree-folder-items .item.active .dropdown-toggle > .icon {
 	border-radius: 3px;
 	vertical-align: middle;
+	background-color: #fff;
 }
 
 /*=== Configuration pages */
@@ -906,63 +909,32 @@ a.btn {
 
 /*=== GLOBAL VIEW */
 /*================*/
-#stream.global .box-category {
-	background: #fff;
-	border:none;
+.box.category .box-title .title {
+	font-weight: normal;
+	text-decoration: none;
 	text-align: left;
 }
-
-#stream.global .category {
-	margin: 0;
+.box.category:not([data-unread="0"]) .box-title {
+	background: #5BC0DE;
 }
-
-#stream.global .category:first-child {
-	margin: 0;
-}
-
-
-#stream.global .btn {
-	width: auto;
-	height: 2em;
-	margin: 0;
-	padding: 0 10px;
-	background: #f6f6f6;
-	border-bottom: 1px solid #aaa;
-	border-radius: 5px 5px 0 0;
-	line-height: 2em;
-	font-size: 1.2rem;
-}
-
-#stream.global .btn:not([data-unread="0"]) {
-	background: #5bc0de;
-	border-color : #5bc0de;
-	color: #fff;
+.box.category:not([data-unread="0"]) .box-title .title {
 	font-weight: bold;
-	text-shadow: none;
-
+	color: #fff;
 }
-
-
-#stream.global .btn:first-child:not([data-unread="0"]):after {
-	top: 0; right: 5px;
+.box.category .title:not([data-unread="0"]):after {
+	position: absolute;
+	top: 5px; right: 10px;
 	border: 0;
 	background: none;
-	color: #fff;
 	font-weight: bold;
 	box-shadow: none;
 	text-shadow: none;
+	font-size: 0.8rem;
+	line-height: 1.6rem;
 }
-
-#stream.global .box-category .feeds {
-	max-height: 250px;
-	width: 302px;
-	border : solid #aaa 1px;
-	border-top : none;
-}
-
-#stream.global .box-category .feeds .item {
+.box.category .item.feed {
 	padding: 2px 10px;
-	font-size: 0.9rem;
+	font-size: 0.8rem;
 }
 
 /*=== DIVERS */
@@ -1056,17 +1028,20 @@ a.btn {
 	}
 	.aside .toggle_aside,
 	#panel .close {
-		position: absolute;
 		display: block;
-		top: 0; right: 0;
-		width: 30px;
-		height: 30px;
-		line-height: 30px;
+		width: 100%;
+		height: 40px;
+		line-height: 40px;
 		text-align: center;
 		background: #f6f6f6;
-		border-left: 1px solid #ddd;
 		border-bottom: 1px solid #ddd;
-		border-radius: 0 0 0 5px;
+	}
+
+	.aside.aside_feed {
+		padding: 0;
+	}
+	.aside.aside_feed .tree {
+		position: static;
 	}
 
 	.nav_menu .btn {

+ 103 - 123
p/themes/Screwdriver/screwdriver.css

@@ -531,6 +531,54 @@ a.btn {
 	visibility: visible;
 }
 
+/*=== Tree */
+.tree {
+	margin: 10px 0;
+}
+.tree-folder-title {
+	position: relative;
+	padding: 0 10px;
+	line-height: 2.5rem;
+	font-size: 0.9rem;
+}
+.tree-folder-title .title {
+	background: inherit;
+	color: #fff;
+}
+.tree-folder-title .title:hover {
+	text-decoration: none;
+}
+.tree-folder.active .tree-folder-title {
+	background: linear-gradient(180deg, #222 0%, #171717 100%) #171717;
+	background: -webkit-linear-gradient(top, #222 0%, #171717 100%);
+	box-shadow: 0px 1px #171717, 0px 1px rgba(255, 255, 255, 0.08) inset;
+	text-shadow: 0 0 2px rgba(255,255,255,0.28);
+	color: #fff;
+}
+.tree-folder-items {
+    background: #171717;
+    padding: 8px 0;
+	box-shadow: 0 4px 4px #171717 inset, 0 1px rgba(255,255,255,0.08),0 -1px rgba(255,255,255,0.08);
+}
+.tree-folder-items > .item {
+	padding: 0 10px;
+	line-height: 2.5rem;
+	font-size: 0.8rem;
+}
+.tree-folder-items > .item.active {
+	background: linear-gradient(180deg, #222 0%, #171717 100%) #171717;
+	background: -webkit-linear-gradient(top, #222 0%, #171717 100%);
+	border-radius: 4px;
+	margin: 0px 8px;
+	box-shadow: 0px 1px #171717, 0px 1px rgba(255, 255, 255, 0.08) inset, 0 2px 2px #111;
+}
+.tree-folder-items > .item > a {
+	text-decoration: none;
+	color: #fff;
+}
+.tree-folder-items > .item.active > a {
+}
+
 /*=== STRUCTURE */
 /*===============*/
 /*=== Header */
@@ -569,7 +617,7 @@ a.btn {
 /*=== Body */
 #global {
 	background:#EDE7DE;
-	height: calc(100% - 85px);
+	height: calc(100% - 60px);
 }
 .aside {
 	border-radius: 0px 12px 0px 0px;
@@ -578,103 +626,43 @@ a.btn {
 	background: #222;
 	width: 235px;
 }
-.aside.aside_flux {
-	padding: 10px 0 50px;
-	background: #222;
-}
-
-/*=== Aside main page (categories) */
-.categories {
+.aside.aside_feed {
+	padding: 10px 0;
 	text-align: center;
 }
-.categories .btn-important {
-	border: none;
-}
-.category {
-	width: 235px;
-	margin: 10px auto 0;
-	text-align: left;
-}
-#aside_flux ul.feeds{
-    box-shadow: 0 4px 4px #171717 inset, 0 1px rgba(255,255,255,0.08),0 -1px rgba(255,255,255,0.08);
-}
-ul.feeds{
-    background:#171717;
-    padding:8px 0;
-    box-shadow: 0 4px 4px #EDE7DE inset;
-}
-ul.feeds.active{
-	box-shadow: 0 0 0 #171717 inset, 0 -2px 2px #111 inset,0 1px rgba(255,255,255,0.08),0 -1px rgba(255,255,255,0);
-}
-.category.stick.active{
-	background: linear-gradient(180deg, #222 0%, #171717 100%) #171717;
-	background: -webkit-linear-gradient(top, #222 0%, #171717 100%);
-	box-shadow: 0px 1px #171717, 0px 1px rgba(255, 255, 255, 0.08) inset;
-}
-.category .btn {
-	color: #fff;
-	border: none;
-	background: transparent;
-}
-.category .btn:first-child {
-	position: relative;
-	width: 213px;
-	background: transparent;
-}
-.category.stick .btn:first-child {
-	width: 176px;
+.aside.aside_feed .tree {
+	position: sticky;
+	top: 0;
+	margin: 10px 0 50px;
 }
-.category .btn:first-child:not([data-unread="0"]):after {
+
+/*=== Aside main page (categories) */
+.aside_feed .tree-folder-title > .title:not([data-unread="0"]):after {
 	position: absolute;
-	top: 3px; right: 3px;
+	right: 3px;
 	padding: 1px 5px;
-	background: transparent;
 	color: #fff;
 	text-shadow: 0 1px rgba(255,255,255,0.08);
 }
+.aside_feed .btn-important {
+	border: none;
+}
 
 /*=== Aside main page (feeds) */
-.categories .feeds .item.active {
-	background: linear-gradient(180deg, #222 0%, #171717 100%) #171717;
-	background: -webkit-linear-gradient(top, #222 0%, #171717 100%);
-	border-radius: 4px;
-	margin: 0px 8px;
-	box-shadow: 0px 1px #171717, 0px 1px rgba(255, 255, 255, 0.08) inset, 0 2px 2px #111;
-}
-.categories .feeds .item.active .feed {
-	color: #fff;
-}
-.categories .feeds .item.empty .feed {
+.feed.item.empty,
+.feed.item.empty > a {
 	color: #e67e22;
 }
-.categories .feeds .item.empty.active {
-	background: #e67e22;
-}
-.categories .feeds .item.empty.active .feed {
-	color: #fff;
-}
-.categories .feeds .item.error .feed {
+.feed.item.error,
+.feed.item.error > a {
 	color: #BD362F;
 }
-.categories .feeds .item .feed {
-	margin: 0;
-	width: 165px;
-	line-height: 3em;
-	font-size: 0.8em;
-	text-align: left;
-	text-decoration: none;
-	color:#ccc;
-}
-.categories .feeds .feed:not([data-unread="0"]) {
-	font-weight: bold;
-}
-.categories .feeds .dropdown-menu:after {
+.aside_feed .tree-folder-items .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 {
-	background-color: transparent;
+.aside_feed .tree-folder-items .item .dropdown-target:target ~ .dropdown-toggle > .icon,
+.aside_feed .tree-folder-items .item:hover .dropdown-toggle > .icon,
+.aside_feed .tree-folder-items .item.active .dropdown-toggle > .icon {
 	border-radius: 3px;
 	vertical-align: middle;
 }
@@ -968,63 +956,50 @@ opacity: 1;
 
 /*=== GLOBAL VIEW */
 /*================*/
-#stream.global{
-	background:#222;
+#stream.global {
+	background: #222;
 	padding: 24px 0;
 	box-shadow: 0 1px #fff, 0 -2px 2px #171717 inset, 0 2px 2px #171717 inset;
 }
-#stream.global .box-category {
-	background: #fff;
-	border-radius: 4px 4px 0 0;
-	text-align: left;
-	box-shadow: 0 0 4px #171717;
-	overflow:hidden;
-}
-#stream.global .category {
-	margin: 0;
-}
-#stream.global .btn {
-	width: auto;
-	height: 2em;
-	margin: 0;
-	padding: 0 10px;
+
+.box.category .box-title {
 	background: linear-gradient(0deg, #EDE7DE 0%, #fff 100%) #171717;
 	background: -webkit-linear-gradient(bottom, #EDE7DE 0%, #fff 100%);
-	border: none;
 	box-shadow: 0px -1px #fff inset,0 -2px #ccc inset;
 	border-radius: none;
 	line-height: 2em;
 	font-size: 1.2rem;
-	color:#888;
 	text-shadow:0 1px #ccc;
 }
-#stream.global .btn:not([data-unread="0"]) {
+.box.category .box-title .title {
+	font-weight: normal;
+	text-decoration: none;
+	text-align: left;
+	color: #888;
+}
+.box.category:not([data-unread="0"]) .box-title {
+}
+.box.category:not([data-unread="0"]) .box-title:active {
+}
+.box.category:not([data-unread="0"]) .box-title .title {
 	color: #222;
 	font-weight: bold;
 }
-#stream.global .btn:first-child:not([data-unread="0"]):after {
-	top: 0;
-	right: 5px;
+.box.category .title:not([data-unread="0"]):after {
+	position: absolute;
+	top: 5px; right: 10px;
 	border: 0;
 	background: none;
-	color: #222;
 	font-weight: bold;
-	box-shadow: none;
-	text-shadow: none;
 }
-#stream.global .box-category .feeds {
-	max-height: 250px;
-	color:#222;
-	background:#EDE7DE;
-}
-#stream.global .box-category .feeds .item {
+.box.category .item.feed {
 	padding: 2px 10px;
-	font-size: 0.9rem;
-	overflow:hidden;
+	font-size: 0.8rem;
 }
-#stream.global .box-category .feed {
-	color:#222;
+.box.category .item.feed:not(.empty):not(.error) .item-title {
+	color: #222;
 }
+
 /*=== PANEL */
 /*===========*/
 #panel {
@@ -1135,12 +1110,10 @@ opacity: 1;
 	}
 	.aside .toggle_aside,
 	#panel .close {
-		position: absolute;
 		display: block;
-		top: 0; right: 0;
-		width: 30px;
-		height: 30px;
-		line-height: 30px;
+		width: 100%;
+		height: 40px;
+		line-height: 40px;
 		text-align: center;
 		background: #171717;
 		box-shadow: 0 1px rgba(255,255,255,0.08);
@@ -1151,6 +1124,13 @@ opacity: 1;
 		margin: 20px 0 0;
 	}
 
+	.aside.aside_feed {
+		padding: 0;
+	}
+	.aside.aside_feed .tree {
+		position: static;
+	}
+
 	.nav_menu .btn {
 		margin: 5px 10px;
 	}

+ 90 - 67
p/themes/base-theme/base.css

@@ -353,6 +353,42 @@ a.btn {
 	visibility: visible;
 }
 
+/*=== Tree */
+.tree {
+	margin: 10px 0;
+}
+.tree-folder-title {
+	position: relative;
+	padding: 0 10px;
+	line-height: 2.5rem;
+	font-size: 1rem;
+}
+.tree-folder-title .title {
+	background: inherit;
+}
+.tree-folder-title .title:hover {
+	text-decoration: none;
+}
+.tree-folder.active .tree-folder-title {
+	font-weight: bold;
+}
+.tree-folder.active .tree-folder-title .title {
+}
+.tree-folder-items {
+}
+.tree-folder-items > .item {
+	padding: 0 10px;
+	line-height: 2.5rem;
+	font-size: 0.8rem;
+}
+.tree-folder-items > .item.active {
+}
+.tree-folder-items > .item > a {
+	text-decoration: none;
+}
+.tree-folder-items > .item.active > a {
+}
+
 /*=== STRUCTURE */
 /*===============*/
 /*=== Header */
@@ -386,64 +422,49 @@ a.btn {
 }
 .aside {
 }
-.aside.aside_flux {
-	padding: 10px 0 50px;
-}
-
-/*=== Aside main page (categories) */
-.categories {
+.aside.aside_feed {
+	padding: 10px 0;
 	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;
+.aside.aside_feed .tree {
+	position: sticky;
+	top: 0;
+	margin: 10px 0 50px;
 }
-.category .btn:first-child:not([data-unread="0"]):after {
+
+/*=== Aside main page (categories) */
+.aside_feed .tree-folder-title > .title:not([data-unread="0"]):after {
 	position: absolute;
-	top: 3px; right: 3px;
-	padding: 1px 5px;
+	right: 0;
+	margin: 10px 0;
+	padding: 0 10px;
+	font-size: 0.9rem;
+	line-height: 1.5rem;
 }
 
 /*=== Aside main page (feeds) */
-.categories .feeds .item.active {
-}
-.categories .feeds .item.empty.active {
+.feed.item.empty.active {
 }
-.categories .feeds .item.error.active {
+.feed.item.error.active {
 }
-.categories .feeds .item.empty .feed {
+.feed.item.empty,
+.feed.item.empty > a {
 }
-.categories .feeds .item.error .feed {
-}
-.categories .feeds .item.active .feed,
-.categories .feeds .item.empty.active .feed,
-.categories .feeds .item.error.active .feed {
-}
-.categories .feeds .item .feed {
-	margin: 0;
-	width: 165px;
-	line-height: 3em;
-	font-size: 0.8em;
-	text-align: left;
-	text-decoration: none;
+.feed.item.error,
+.feed.item.error > a {
 }
-.categories .feeds .feed:not([data-unread="0"]) {
-	font-weight: bold;
+.feed.item.empty.active,
+.feed.item.error.active,
+.feed.item.empty.active > a,
+.feed.item.error.active > a {
 }
-.categories .feeds .dropdown-menu:after {
+.aside_feed .tree-folder-items .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 {
+.aside_feed .tree-folder-items .item .dropdown-target:target ~ .dropdown-toggle > .icon,
+.aside_feed .tree-folder-items .item:hover .dropdown-toggle > .icon,
+.aside_feed .tree-folder-items .item.active .dropdown-toggle > .icon {
+	border-radius: 3px;
 	vertical-align: middle;
 }
 
@@ -645,33 +666,30 @@ a.btn {
 
 /*=== GLOBAL VIEW */
 /*================*/
-#stream.global .box-category {
+.box.category .box-title .title {
+	font-weight: normal;
+	text-decoration: none;
 	text-align: left;
 }
-#stream.global .category {
-	margin: 0;
+.box.category:not([data-unread="0"]) .box-title {
 }
-#stream.global .btn {
-	width: auto;
-	height: 2em;
-	margin: 0;
-	padding: 0 10px;
-	line-height: 2em;
-	font-size: 1.2rem;
+.box.category:not([data-unread="0"]) .box-title:active {
 }
-#stream.global .btn:not([data-unread="0"]) {
+.box.category:not([data-unread="0"]) .box-title .title {
 	font-weight: bold;
 }
-#stream.global .btn:first-child:not([data-unread="0"]):after {
-	top: 0; right: 5px;
+.box.category .title:not([data-unread="0"]):after {
+	position: absolute;
+	top: 5px; right: 10px;
+	border: 0;
+	background: none;
 	font-weight: bold;
+	box-shadow: none;
+	text-shadow: none;
 }
-#stream.global .box-category .feeds {
-	max-height: 250px;
-}
-#stream.global .box-category .feeds .item {
+.box.category .item.feed {
 	padding: 2px 10px;
-	font-size: 0.9rem;
+	font-size: 0.8rem;
 }
 
 /*=== DIVERS */
@@ -750,15 +768,20 @@ a.btn {
 	}
 	.aside .toggle_aside,
 	#panel .close {
-		position: absolute;
 		display: block;
-		top: 0; right: 0;
-		width: 30px;
-		height: 30px;
-		line-height: 30px;
+		width: 100%;
+		height: 50px;
+		line-height: 50px;
 		text-align: center;
 	}
 
+	.aside.aside_feed {
+		padding: 0;
+	}
+	.aside.aside_feed .tree {
+		position: static;
+	}
+
 	.nav_menu .btn {
 		margin: 5px 10px;
 	}

+ 66 - 84
p/themes/base-theme/template.css

@@ -179,6 +179,7 @@ a.btn {
 .dropdown {
 	position: relative;
 	display: inline-block;
+	vertical-align: middle;
 }
 .dropdown-target {
 	display: none;
@@ -289,10 +290,14 @@ a.btn {
 	vertical-align: top;
 }
 .box .box-title {
+	position: relative;
 	font-size: 1.2rem;
 	font-weight: bold;
 	text-align: center;
 }
+.box .box-title a {
+	display: block;
+}
 .box .box-title form {
 	margin: 0;
 }
@@ -325,6 +330,39 @@ a.btn {
 	cursor: grab;
 }
 
+/*=== Tree */
+.tree {
+	margin: 0;
+	padding: 0;
+	list-style: none;
+	text-align: left;
+}
+.tree-folder-items {
+	padding: 0;
+	list-style: none;
+}
+.tree-folder-title {
+	display: block;
+	overflow: hidden;
+	white-space: nowrap;
+	text-overflow: ellipsis;
+}
+.tree-folder-title .title {
+	vertical-align: middle;
+}
+.tree-folder-items > .item {
+	display: block;
+	white-space: nowrap;
+}
+.tree-folder-items > .item > a {
+	display: inline-block;
+	vertical-align: middle;
+	width: calc(100% - 32px);
+	overflow: hidden;
+	white-space: nowrap;
+	text-overflow: ellipsis;
+}
+
 /*=== STRUCTURE */
 /*===============*/
 /*=== Header */
@@ -366,57 +404,21 @@ a.btn {
 	width: 250px;
 	vertical-align: top;
 }
-.aside.aside_flux {
-	background: #fff;
-}
-
-/*=== Aside main page (categories) */
-.categories {
-	list-style: none;
-	margin: 0;
-}
-.state_unread li:not(.active)[data-unread="0"] {
-	display: none;
-}
-.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) {
+.aside_feed .tree-folder-items: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 {
+.aside_feed .tree-folder-items .dropdown-menu {
 	left: 0;
 }
-.categories .feeds .item .dropdown-toggle > .icon {
+.aside_feed .tree-folder-items .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 {
+.aside_feed .tree-folder-items .item .dropdown-target:target ~ .dropdown-toggle > .icon,
+.aside_feed .tree-folder-items .item:hover .dropdown-toggle > .icon,
+.aside_feed .tree-folder-items .item.active .dropdown-toggle > .icon {
 	visibility: visible;
 }
 
@@ -603,32 +605,13 @@ br + br + br {
 
 /*=== GLOBAL VIEW */
 /*================*/
-/*=== Category boxes */
-
-/* TODO <delete> */
-#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 {
+	text-align: center;
 }
-#stream.global .box-category .feed {
-	width: 19em;
-	max-width: 90%;
+
+#stream.global .box {
+	text-align: left;
 }
-/* TODO </delete */
 
 /*=== Panel */
 #overlay {
@@ -685,39 +668,45 @@ br + br + br {
 
 /*=== DIVERS */
 /*===========*/
+.category .title:not([data-unread="0"]):after {
+	content: attr(data-unread);
+}
+.feed .item-title:not([data-unread="0"]):before {
+	content: "(" attr(data-unread) ") ";
+}
+.feed .item-title:not([data-unread="0"]) {
+	font-weight: bold;
+}
+
+.state_unread .category:not(.active)[data-unread="0"],
+.state_unread .feed:not(.active)[data-unread="0"] {
+	display: none;
+}
+
 .nav-login,
 .nav_menu .search,
+.aside .toggle_aside,
 .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;
 	}
+	.aside .toggle_aside,
 	.nav-login {
 		display: block;
 	}
 	.nav_menu .toggle_aside,
-	.aside .toggle_aside,
 	.nav_menu .search,
 	#panel .close img {
 		display: inline-block;
@@ -735,9 +724,6 @@ br + br + br {
 		width: 90%;
 		overflow: auto;
 	}
-	.aside .categories {
-		margin: 10px 0 75px;
-	}
 
 	.flux_header .item.website {
 		width: 40px;
@@ -759,10 +745,6 @@ br + br + br {
 		width: 100%;
 	}
 
-	#stream.global .box-category {
-		margin: 10px 0;
-	}
-
 	#panel {
 		top: 0; bottom: 0;
 		left: 0; right: 0;