Răsfoiți Sursa

Merge branch 'dev' into beta

Conflicts:
	README.md
Marien Fressinaud 11 ani în urmă
părinte
comite
07444280d1
100 a modificat fișierele cu 3688 adăugiri și 680 ștergeri
  1. 20 0
      CHANGELOG
  2. 101 0
      README.fr.md
  3. 66 65
      README.md
  4. 2 0
      app/Controllers/configureController.php
  5. 32 20
      app/Controllers/errorController.php
  6. 158 114
      app/Controllers/importExportController.php
  7. 47 8
      app/Controllers/indexController.php
  8. 35 18
      app/Controllers/statsController.php
  9. 2 2
      app/Controllers/usersController.php
  10. 38 11
      app/FreshRSS.php
  11. 8 0
      app/Models/Configuration.php
  12. 8 6
      app/Models/EntryDAO.php
  13. 2 2
      app/Models/EntryDAOSQLite.php
  14. 9 3
      app/Models/Feed.php
  15. 130 2
      app/Models/StatsDAO.php
  16. 27 1
      app/Models/StatsDAOSQLite.php
  17. 24 15
      app/Models/UserDAO.php
  18. 1 0
      app/SQL/install.sql.sqlite.php
  19. 45 12
      app/i18n/en.php
  20. 34 1
      app/i18n/fr.php
  21. 2 2
      app/i18n/install.en.php
  22. 2 2
      app/i18n/install.fr.php
  23. 18 10
      app/install.php
  24. 12 5
      app/layout/aside_flux.phtml
  25. 3 0
      app/layout/aside_stats.phtml
  26. 1 1
      app/layout/layout.phtml
  27. 12 6
      app/layout/nav_menu.phtml
  28. 27 8
      app/views/configure/reading.phtml
  29. 2 11
      app/views/error/index.phtml
  30. 3 3
      app/views/helpers/export/opml.phtml
  31. 2 1
      app/views/helpers/javascript_vars.phtml
  32. 1 1
      app/views/helpers/pagination.phtml
  33. 9 8
      app/views/helpers/view/normal_view.phtml
  34. 6 12
      app/views/helpers/view/reader_view.phtml
  35. 0 0
      app/views/importExport/export.phtml
  36. 20 11
      app/views/importExport/index.phtml
  37. 15 8
      app/views/index/formLogin.phtml
  38. 10 11
      app/views/javascript/actualize.phtml
  39. 13 7
      app/views/stats/idle.phtml
  40. 0 127
      app/views/stats/main.phtml
  41. 114 0
      app/views/stats/repartition.phtml
  42. 1 0
      data/tokens/.gitignore
  43. 13 0
      data/tokens/index.html
  44. 14 3
      lib/Minz/Helper.php
  45. 12 8
      lib/Minz/ModelPdo.php
  46. 69 49
      lib/Minz/Request.php
  47. 48 26
      lib/Minz/Session.php
  48. 1 1
      lib/Minz/Translate.php
  49. 6 18
      lib/lib_rss.php
  50. 1 0
      p/api/greader.php
  51. 1 0
      p/i/.gitignore
  52. 0 14
      p/scripts/jquery.lazyload.min.js
  53. 122 24
      p/scripts/main.js
  54. 2 4
      p/themes/Dark/dark.css
  55. 3 0
      p/themes/Dark/template.css
  56. 6 5
      p/themes/Flat/flat.css
  57. 3 0
      p/themes/Flat/template.css
  58. 11 9
      p/themes/Origine/origine.css
  59. 3 0
      p/themes/Origine/template.css
  60. 36 0
      p/themes/Screwdriver/README.md
  61. 5 0
      p/themes/Screwdriver/icons/add.svg
  62. 7 0
      p/themes/Screwdriver/icons/all.svg
  63. BIN
      p/themes/Screwdriver/icons/apple-touch-icon.png
  64. 6 0
      p/themes/Screwdriver/icons/bookmark-add.svg
  65. 60 0
      p/themes/Screwdriver/icons/bookmark.svg
  66. 7 0
      p/themes/Screwdriver/icons/category-white.svg
  67. 7 0
      p/themes/Screwdriver/icons/category.svg
  68. 7 0
      p/themes/Screwdriver/icons/close.svg
  69. 5 0
      p/themes/Screwdriver/icons/configure.svg
  70. 5 0
      p/themes/Screwdriver/icons/down.svg
  71. BIN
      p/themes/Screwdriver/icons/favicon-16-32-48-64.ico
  72. BIN
      p/themes/Screwdriver/icons/favicon-256.png
  73. 13 0
      p/themes/Screwdriver/icons/favicon.svg
  74. BIN
      p/themes/Screwdriver/icons/grey.gif
  75. 7 0
      p/themes/Screwdriver/icons/help.svg
  76. 12 0
      p/themes/Screwdriver/icons/icon.svg
  77. 7 0
      p/themes/Screwdriver/icons/key.svg
  78. 4 0
      p/themes/Screwdriver/icons/link.svg
  79. 6 0
      p/themes/Screwdriver/icons/login.svg
  80. 6 0
      p/themes/Screwdriver/icons/logout.svg
  81. 5 0
      p/themes/Screwdriver/icons/next.svg
  82. 2 0
      p/themes/Screwdriver/icons/non-starred.svg
  83. 5 0
      p/themes/Screwdriver/icons/prev.svg
  84. 57 0
      p/themes/Screwdriver/icons/read.svg
  85. 5 0
      p/themes/Screwdriver/icons/refresh.svg
  86. 6 0
      p/themes/Screwdriver/icons/rss.svg
  87. 6 0
      p/themes/Screwdriver/icons/search.svg
  88. 8 0
      p/themes/Screwdriver/icons/share.svg
  89. 60 0
      p/themes/Screwdriver/icons/starred.svg
  90. 5 0
      p/themes/Screwdriver/icons/tag.svg
  91. 65 0
      p/themes/Screwdriver/icons/unread.svg
  92. 5 0
      p/themes/Screwdriver/icons/up.svg
  93. 1 0
      p/themes/Screwdriver/icons/view-global.svg
  94. 1 0
      p/themes/Screwdriver/icons/view-normal.svg
  95. 1 0
      p/themes/Screwdriver/icons/view-reader.svg
  96. BIN
      p/themes/Screwdriver/loader.gif
  97. 7 0
      p/themes/Screwdriver/metadata.json
  98. 1170 0
      p/themes/Screwdriver/screwdriver.css
  99. 695 0
      p/themes/Screwdriver/template.css
  100. 7 5
      p/themes/base-theme/base.css

+ 20 - 0
CHANGELOG

@@ -1,5 +1,25 @@
 # Journal des modifications
 
+## 2014-08-xx FreshRSS 0.7.4
+
+* UI
+	* Hide categories/feeds with unread articles when showing only unread articles
+	* Dynamic favicon showing the number of unread articles
+	* New theme: Screwdriver by Mister aiR
+* Statistics
+	* New page with article repartition
+	* Improvements
+* Security
+	* Basic protection against XSRF (Cross-Site Request Forgery) based on HTTP Referer (POST requests only)
+* API
+	* Compatible with lighttpd
+* Misc.
+	* Changed lazyload implementation
+	* Support of HTML5 notifications for new upcoming articles
+	* Add option to stay logged in
+* Bux fixes in export function, add/remove users, keyboard shortcuts, etc.
+
+
 ## 2014-07-21 FreshRSS 0.7.3
 
 * New options

+ 101 - 0
README.fr.md

@@ -0,0 +1,101 @@
+* [English version](README.md)
+
+# FreshRSS
+FreshRSS est un agrégateur de flux RSS à auto-héberger à l’image de [Leed](http://projet.idleman.fr/leed/) ou de [Kriss Feed](http://tontof.net/kriss/feed/).
+
+Il se veut léger et facile à prendre en main tout en étant un outil puissant et paramétrable.
+
+Il permet de gérer plusieurs utilisateurs, et dispose d’un mode de lecture anonyme.
+
+* Site officiel : http://freshrss.org
+* Démo : http://demo.freshrss.org/
+* Développeur : Marien Fressinaud <dev@marienfressinaud.fr>
+* Version actuelle : 0.8-dev
+* Date de publication 2014-0x-xx
+* License [GNU AGPL 3](http://www.gnu.org/licenses/agpl-3.0.html)
+
+![Logo de FreshRSS](http://marienfressinaud.fr/data/images/freshrss/freshrss_title.png)
+
+# Note sur les branches
+**Ce logiciel est encore en développement !** Veuillez vous assurer d'utiliser la branche qui vous correspond :
+
+* Utilisez [la branche master](https://github.com/marienfressinaud/FreshRSS/tree/master/) si vous visez la stabilité.
+* [La branche beta](https://github.com/marienfressinaud/FreshRSS/tree/beta) est celle par défaut : les nouveautés y sont ajoutées environ tous les mois.
+* Pour les développeurs et ceux qui savent ce qu'ils font, [la branche dev](https://github.com/marienfressinaud/FreshRSS/tree/dev) vous ouvre les bras !
+
+# Disclaimer
+Cette application a été développée pour s’adapter à des besoins personnels et non professionnels.
+Je ne garantis en aucun cas la sécurité de celle-ci, ni son bon fonctionnement.
+Je m’engage néanmoins à répondre dans la mesure du possible aux demandes d’évolution si celles-ci me semblent justifiées.
+Privilégiez pour cela des demandes sur GitHub
+(https://github.com/marienfressinaud/FreshRSS/issues) ou par mail (dev@marienfressinaud.fr)
+
+# Pré-requis
+* Serveur modeste, par exemple sous Linux ou Windows
+	* Fonctionne même sur un Raspberry Pi avec des temps de réponse < 1s (testé sur 150 flux, 22k articles, soit 32Mo de données partiellement compressées)
+* Serveur Web Apache2 (recommandé), ou nginx, lighttpd (non testé sur les autres)
+* PHP 5.2.1+ (PHP 5.3.7+ recommandé)
+	* Requis : [PDO_MySQL](http://php.net/pdo-mysql) ou [PDO_SQLite](http://php.net/pdo-sqlite), [cURL](http://php.net/curl), [GMP](http://php.net/gmp) (seulement pour accès API sur platformes < 64 bits)
+	* Recommandés : [JSON](http://php.net/json), [mbstring](http://php.net/mbstring), [zlib](http://php.net/zlib), [Zip](http://php.net/zip)
+* MySQL 5.0.3+ (recommandé) ou SQLite 3.7.4+
+* Un navigateur Web récent tel Firefox 4+, Chrome, Opera, Safari, Internet Explorer 9+
+	* Fonctionne aussi sur mobile
+
+![Capture d’écran de FreshRSS](http://marienfressinaud.fr/data/images/freshrss/freshrss_default-design.png)
+
+# Installation
+1. Récupérez l’application FreshRSS via la commande git ou [en téléchargeant l’archive](https://github.com/marienfressinaud/FreshRSS/archive/master.zip)
+2. Placez l’application sur votre serveur (la partie à exposer au Web est le répertoire `./p/`)
+3. Le serveur Web doit avoir les droits d’écriture dans le répertoire `./data/`
+4. Accédez à FreshRSS à travers votre navigateur Web et suivez les instructions d’installation
+5. Tout devrait fonctionner :) En cas de problème, n’hésitez pas à me contacter.
+
+# Contrôle d’accès
+Il est requis pour le mode multi-utilisateur, et recommandé dans tous les cas, de limiter l’accès à votre FreshRSS. Au choix :
+* En utilisant l’identification par formulaire (requiert JavaScript, et PHP 5.3.7+ recommandé – fonctionne avec certaines versions de PHP 5.3.3+)
+* En utilisant l’identification par [Mozilla Persona](https://login.persona.org/about) incluse dans FreshRSS
+* En utilisant un contrôle d’accès HTTP défini par votre serveur Web
+	* Voir par exemple la [documentation d’Apache sur l’authentification](http://httpd.apache.org/docs/trunk/howto/auth.html)
+		* Créer dans ce cas un fichier `./p/i/.htaccess` avec un fichier `.htpasswd` correspondant.
+
+# Rafraîchissement automatique des flux
+* Vous pouvez ajouter une tâche Cron lançant régulièrement le script d’actualisation automatique des flux.
+Consultez la documentation de Cron de votre système d’exploitation ([Debian/Ubuntu](http://doc.ubuntu-fr.org/cron), [Red Hat/Fedora](http://doc.fedora-fr.org/wiki/CRON_:_Configuration_de_t%C3%A2ches_automatis%C3%A9es), [Slackware](http://docs.slackware.com/fr:slackbook:process_control?#cron), [Gentoo](http://wiki.gentoo.org/wiki/Cron/fr), [Arch Linux](http://wiki.archlinux.fr/Cron)…).
+C’est une bonne idée d’utiliser le même utilisateur que votre serveur Web (souvent “www-data”).
+Par exemple, pour exécuter le script toutes les heures :
+
+```
+7 * * * * php /chemin/vers/FreshRSS/app/actualize_script.php > /tmp/FreshRSS.log 2>&1
+```
+
+# Conseils
+* Pour une meilleure sécurité, faites en sorte que seul le répertoire `./p/` soit accessible depuis le Web, par exemple en faisant pointer un sous-domaine sur le répertoire `./p/`.
+	* En particulier, les données personnelles se trouvent dans le répertoire `./data/`.
+* Le fichier `./constants.php` définit les chemins d’accès aux répertoires clés de l’application. Si vous les bougez, tout se passe ici.
+* En cas de problème, les logs peuvent être utile à lire, soit depuis l’interface de FreshRSS, soit manuellement depuis `./data/log/*.log`.
+
+# Sauvegarde
+* Il faut conserver vos fichiers `./data/config.php` ainsi que `./data/*_user.php` et éventuellement `./data/persona/`
+* Vous pouvez exporter votre liste de flux depuis FreshRSS au format OPML
+* Pour sauvegarder les articles eux-même, vous pouvez utiliser [phpMyAdmin](http://www.phpmyadmin.net) ou les outils de MySQL :
+
+```bash
+mysqldump -u utilisateur -p --databases freshrss > freshrss.sql
+```
+
+
+# Bibliothèques incluses
+* [SimplePie](http://simplepie.org/)
+* [MINZ](https://github.com/marienfressinaud/MINZ)
+* [php-http-304](http://alexandre.alapetite.fr/doc-alex/php-http-304/)
+* [jQuery](http://jquery.com/)
+* [keyboard_shortcuts](http://www.openjs.com/scripts/events/keyboard_shortcuts/)
+* [flotr2](http://www.humblesoftware.com/flotr2)
+
+## Uniquement pour certaines options
+* [bcrypt.js](https://github.com/dcodeIO/bcrypt.js)
+* [phpQuery](http://code.google.com/p/phpquery/)
+
+## Si les fonctions natives ne sont pas disponibles
+* [Services_JSON](http://pear.php.net/pepr/pepr-proposal-show.php?id=198)
+* [password_compat](https://github.com/ircmaxell/password_compat)

+ 66 - 65
README.md

@@ -1,88 +1,90 @@
+* [Version française](README.fr.md)
+
 # FreshRSS
-FreshRSS est un agrégateur de flux RSS à auto-héberger à l’image de [Leed](http://projet.idleman.fr/leed/) ou de [Kriss Feed](http://tontof.net/kriss/feed/).
+FreshRSS is a self-hosted RSS feed agregator like [Leed](http://projet.idleman.fr/leed/) or [Kriss Feed](http://tontof.net/kriss/feed/).
 
-Il se veut léger et facile à prendre en main tout en étant un outil puissant et paramétrable.
+It is at the same time light-weight, easy to work with, powerful and customizable.
 
-Il permet de gérer plusieurs utilisateurs, et dispose d’un mode de lecture anonyme.
+It is a multi-user application with an anonymous reading mode.
 
-* Site officiel : http://freshrss.org
-* Démo : http://demo.freshrss.org/
-* Développeur : Marien Fressinaud <dev@marienfressinaud.fr>
-* Version actuelle : 0.7.3
-* Date de publication 2014-07-21
+* Official website: http://freshrss.org
+* Demo: http://demo.freshrss.org/
+* Developer: Marien Fressinaud <dev@marienfressinaud.fr>
+* Current version: 0.8-dev
+* Publication date: 2014-0x-xx
 * License [GNU AGPL 3](http://www.gnu.org/licenses/agpl-3.0.html)
 
-![Logo de FreshRSS](http://marienfressinaud.fr/data/images/freshrss/freshrss_title.png)
+![FreshRSS logo](http://marienfressinaud.fr/data/images/freshrss/freshrss_title.png)
 
-# Note sur les branches
-**Ce logiciel est encore en développement !** Veuillez vous assurer d'utiliser la branche qui vous correspond :
+# Note on branches
+**This application is still in development!** Please use the branch that suits your needs:
 
-* Utilisez [la branche master](https://github.com/marienfressinaud/FreshRSS/tree/master/) si vous visez la stabilité.
-* [La branche beta](https://github.com/marienfressinaud/FreshRSS/tree/beta) est celle par défaut : les nouveautés y sont ajoutées environ tous les mois.
-* Pour les développeurs et ceux qui savent ce qu'ils font, [la branche dev](https://github.com/marienfressinaud/FreshRSS/tree/dev) vous ouvre les bras !
+* Use [the master branch](https://github.com/marienfressinaud/FreshRSS/tree/master/) if you need a stable version.
+* [The beta branch](https://github.com/marienfressinaud/FreshRSS/tree/beta) is the default branch: new features are added on a monthly basis.
+* For developers and tech savvy persons, [the dev branch](https://github.com/marienfressinaud/FreshRSS/tree/dev) is waiting for you!
 
 # Disclaimer
-Cette application a été développée pour s’adapter à des besoins personnels et non professionnels.
-Je ne garantis en aucun cas la sécurité de celle-ci, ni son bon fonctionnement.
-Je m’engage néanmoins à répondre dans la mesure du possible aux demandes d’évolution si celles-ci me semblent justifiées.
-Privilégiez pour cela des demandes sur GitHub
-(https://github.com/marienfressinaud/FreshRSS/issues) ou par mail (dev@marienfressinaud.fr)
-
-# Pré-requis
-* Serveur modeste, par exemple sous Linux ou Windows
-	* Fonctionne même sur un Raspberry Pi avec des temps de réponse < 1s (testé sur 150 flux, 22k articles, soit 32Mo de données partiellement compressées)
-* Serveur Web Apache2 ou Nginx (non testé sur les autres)
-* PHP 5.2.1+ (PHP 5.3.7+ recommandé)
-	* Requis : [PDO_MySQL](http://php.net/pdo-mysql), [cURL](http://php.net/curl), [LibXML](http://php.net/xml), [PCRE](http://php.net/pcre), [ctype](http://php.net/ctype)
-	* Recommandés : [JSON](http://php.net/json), [zlib](http://php.net/zlib), [mbstring](http://php.net/mbstring), [iconv](http://php.net/iconv), [Zip](http://php.net/zip)
-* MySQL 5.0.3+ (recommandé) ou SQLite 3.7.4+ (en bêta)
-* Un navigateur Web récent tel Firefox 4+, Chrome, Opera, Safari, Internet Explorer 9+
-	* Fonctionne aussi sur mobile
-
-![Capture d’écran de FreshRSS](http://marienfressinaud.fr/data/images/freshrss/freshrss_default-design.png)
+This application was developed to fulfill personal needs not professional needs.
+There is no guarantee neither on its security nor its proper functioning.
+If there is feature requests which I think are good for the project, I'll do my best to include them.
+The best way is to open issues on GitHub
+(https://github.com/marienfressinaud/FreshRSS/issues) or by email (dev@marienfressinaud.fr)
+
+# Requirements
+* Light server running Linux or Windows
+	* It even works on Raspberry Pi with response time under a second (tested with 150 feeds, 22k articles, or 32Mo of compressed data)
+* A web server: Apache2 (recommanded), nginx, lighttpd (not tested on others)
+* PHP 5.2.1+ (PHP 5.3.7+ recommanded)
+	* Required extensions: [PDO_MySQL](http://php.net/pdo-mysql) or [PDO_SQLite](http://php.net/pdo-sqlite), [cURL](http://php.net/curl), [GMP](http://php.net/gmp) (only for API access on platforms under 64 bits)
+	* Recommanded extensions : [JSON](http://php.net/json), [mbstring](http://php.net/mbstring), [zlib](http://php.net/zlib), [Zip](http://php.net/zip)
+* MySQL 5.0.3+ (recommanded) ou SQLite 3.7.4+
+* A recent browser like Firefox 4+, Chrome, Opera, Safari, Internet Explorer 9+
+	* Works on mobile
+
+![FreshRSS screenshot](http://marienfressinaud.fr/data/images/freshrss/freshrss_default-design.png)
 
 # Installation
-1. Récupérez l’application FreshRSS via la commande git ou [en téléchargeant l’archive](https://github.com/marienfressinaud/FreshRSS/archive/master.zip)
-2. Placez l’application sur votre serveur (la partie à exposer au Web est le répertoire `./p/`)
-3. Le serveur Web doit avoir les droits d’écriture dans le répertoire `./data/`
-4. Accédez à FreshRSS à travers votre navigateur Web et suivez les instructions d’installation
-5. Tout devrait fonctionner :) En cas de problème, n’hésitez pas à me contacter.
-
-# Contrôle d’accès
-Il est requis pour le mode multi-utilisateur, et recommandé dans tous les cas, de limiter l’accès à votre FreshRSS. Au choix :
-* En utilisant l’identification par formulaire (requiert JavaScript, et PHP 5.3.7+ recommandé – fonctionne avec certaines versions de PHP 5.3.3+)
-* En utilisant l’identification par [Mozilla Persona](https://login.persona.org/about) incluse dans FreshRSS
-* En utilisant un contrôle d’accès HTTP défini par votre serveur Web
-	* Voir par exemple la [documentation d’Apache sur l’authentification](http://httpd.apache.org/docs/trunk/howto/auth.html)
-		* Créer dans ce cas un fichier `./p/i/.htaccess` avec un fichier `.htpasswd` correspondant.
-
-# Rafraîchissement automatique des flux
-* Vous pouvez ajouter une tâche Cron lançant régulièrement le script d’actualisation automatique des flux.
-Consultez la documentation de Cron de votre système d’exploitation ([Debian/Ubuntu](http://doc.ubuntu-fr.org/cron), [Red Hat/Fedora](http://doc.fedora-fr.org/wiki/CRON_:_Configuration_de_t%C3%A2ches_automatis%C3%A9es), [Slackware](http://docs.slackware.com/fr:slackbook:process_control?#cron), [Gentoo](http://wiki.gentoo.org/wiki/Cron/fr), [Arch Linux](http://wiki.archlinux.fr/Cron)…).
-C’est une bonne idée d’utiliser le même utilisateur que votre serveur Web (souvent “www-data”).
-Par exemple, pour exécuter le script toutes les heures :
+1. Get FreshRSS with git or [by downloading the archive](https://github.com/marienfressinaud/FreshRSS/archive/master.zip)
+2. Dump the application on your server (expose only the `./p/` folder)
+3. Add write access on `./data/` folder to the webserver user
+4. Access FreshRSS with your browser and follow the installation process
+5. Every thing should be working :) If you encounter any problem, feel free to contact me.
+
+# Access control
+It is needed for the multi-user mode to limit access to FreshRSS. You can:
+* use form authentication (need JavaScript and PHP 5.3.7+, works with some PHP 5.3.3+)
+* use [Mozilla Persona](https://login.persona.org/about) authentication included in FreshRSS
+* use HTTP authentication supported by your web server
+	* See [Apache documentation](http://httpd.apache.org/docs/trunk/howto/auth.html)
+		* In that case, create a `./p/i/.htaccess` file with a matching `.htpasswd` file.
+
+# Automatic feed update
+* You can add a Cron job to launch the update script.
+Check the Cron documentation related to your distribution ([Debian/Ubuntu](https://help.ubuntu.com/community/CronHowto), [Red Hat/Fedora](https://fedoraproject.org/wiki/Administration_Guide_Draft/Cron), [Slackware](http://docs.slackware.com/fr:slackbook:process_control?#cron), [Gentoo](https://wiki.gentoo.org/wiki/Cron), [Arch Linux](https://wiki.archlinux.org/index.php/Cron)…).
+It’s a good idea to use the web server user .
+For example, if you want to run the script every hour:
 
 ```
 7 * * * * php /chemin/vers/FreshRSS/app/actualize_script.php > /tmp/FreshRSS.log 2>&1
 ```
 
-# Conseils
-* Pour une meilleure sécurité, faites en sorte que seul le répertoire `./p/` soit accessible depuis le Web, par exemple en faisant pointer un sous-domaine sur le répertoire `./p/`.
-	* En particulier, les données personnelles se trouvent dans le répertoire `./data/`.
-* Le fichier `./constants.php` définit les chemins d’accès aux répertoires clés de l’application. Si vous les bougez, tout se passe ici.
-* En cas de problème, les logs peuvent être utile à lire, soit depuis l’interface de FreshRSS, soit manuellement depuis `./data/log/*.log`.
+# Advices
+* For a better security, expose only the `./p/` folder on the web.
+	* Be aware that the `./data/` folder contain all personal data, so it is a bad idea to expose it.
+* The `./constants.php` file define access to application folder. If you want to customize your installation, every thing happens here.
+* If you encounter some problem, logs are accessibles from the interface or manually in `./data/log/*.log` files.
 
-# Sauvegarde
-* Il faut conserver vos fichiers `./data/config.php` ainsi que `./data/*_user.php` et éventuellement `./data/persona/`
-* Vous pouvez exporter votre liste de flux depuis FreshRSS au format OPML
-* Pour sauvegarder les articles eux-même, vous pouvez utiliser [phpMyAdmin](http://www.phpmyadmin.net) ou les outils de MySQL :
+# Backup
+* You need to keep `./data/config.php`, `./data/*_user.php` and `./data/persona/` files
+* You can export your feed list in OPML format from FreshRSS
+* To save articles, you can use [phpMyAdmin](http://www.phpmyadmin.net) or MySQL tools:
 
 ```bash
-mysqldump -u utilisateur -p --databases freshrss > freshrss.sql
+mysqldump -u user -p --databases freshrss > freshrss.sql
 ```
 
 
-# Bibliothèques incluses
+# Included libraries
 * [SimplePie](http://simplepie.org/)
 * [MINZ](https://github.com/marienfressinaud/MINZ)
 * [php-http-304](http://alexandre.alapetite.fr/doc-alex/php-http-304/)
@@ -90,11 +92,10 @@ mysqldump -u utilisateur -p --databases freshrss > freshrss.sql
 * [keyboard_shortcuts](http://www.openjs.com/scripts/events/keyboard_shortcuts/)
 * [flotr2](http://www.humblesoftware.com/flotr2)
 
-## Uniquement pour certaines options
+## Only for some options
 * [bcrypt.js](https://github.com/dcodeIO/bcrypt.js)
 * [phpQuery](http://code.google.com/p/phpquery/)
-* [Lazy Load](http://www.appelsiini.net/projects/lazyload)
 
-## Si les fonctions natives ne sont pas disponibles
+## If native functions are not available
 * [Services_JSON](http://pear.php.net/pepr/pepr-proposal-show.php?id=198)
 * [password_compat](https://github.com/ircmaxell/password_compat)

+ 2 - 0
app/Controllers/configureController.php

@@ -184,6 +184,8 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 			$this->view->conf->_default_view((int)Minz_Request::param('default_view', FreshRSS_Entry::STATE_ALL));
 			$this->view->conf->_auto_load_more(Minz_Request::param('auto_load_more', false));
 			$this->view->conf->_display_posts(Minz_Request::param('display_posts', false));
+			$this->view->conf->_display_categories(Minz_Request::param('display_categories', false));
+			$this->view->conf->_hide_read_feeds(Minz_Request::param('hide_read_feeds', false));
 			$this->view->conf->_onread_jump_next(Minz_Request::param('onread_jump_next', false));
 			$this->view->conf->_lazyload(Minz_Request::param('lazyload', false));
 			$this->view->conf->_sticky_post(Minz_Request::param('sticky_post', false));

+ 32 - 20
app/Controllers/errorController.php

@@ -1,26 +1,38 @@
 <?php
 
 class FreshRSS_error_Controller extends Minz_ActionController {
-	public function indexAction () {
-		switch (Minz_Request::param ('code')) {
-		case 403:
-			$this->view->code = 'Error 403 - Forbidden';
-			break;
-		case 404:
-			$this->view->code = 'Error 404 - Not found';
-			break;
-		case 500:
-			$this->view->code = 'Error 500 - Internal Server Error';
-			break;
-		case 503:
-			$this->view->code = 'Error 503 - Service Unavailable';
-			break;
-		default:
-			$this->view->code = 'Error 404 - Not found';
+	public function indexAction() {
+		switch (Minz_Request::param('code')) {
+			case 403:
+				$this->view->code = 'Error 403 - Forbidden';
+				break;
+			case 404:
+				$this->view->code = 'Error 404 - Not found';
+				break;
+			case 500:
+				$this->view->code = 'Error 500 - Internal Server Error';
+				break;
+			case 503:
+				$this->view->code = 'Error 503 - Service Unavailable';
+				break;
+			default:
+				$this->view->code = 'Error 404 - Not found';
 		}
-		
-		$this->view->logs = Minz_Request::param ('logs');
-		
-		Minz_View::prependTitle ($this->view->code . ' · ');
+
+		$errors = Minz_Request::param('logs', array());
+		$this->view->errorMessage = trim(implode($errors));
+		if ($this->view->errorMessage == '') {
+			switch(Minz_Request::param('code')) {
+				case 403:
+					$this->view->errorMessage = Minz_Translate::t('forbidden_access');
+					break;
+				case 404:
+				default:
+					$this->view->errorMessage = Minz_Translate::t('page_not_found');
+					break;
+			}
+		}
+
+		Minz_View::prependTitle($this->view->code . ' · ');
 	}
 }

+ 158 - 114
app/Controllers/importExportController.php

@@ -5,7 +5,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 		if (!$this->view->loginOk) {
 			Minz_Error::error(
 				403,
-				array('error' => array(Minz_Translate::t('access_denied')))
+				array('error' => array(_t('access_denied')))
 			);
 		}
 
@@ -20,33 +20,51 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 		$this->view->categories = $this->catDAO->listCategories();
 		$this->view->feeds = $this->feedDAO->listFeeds();
 
-		Minz_View::prependTitle(Minz_Translate::t('import_export') . ' · ');
+		Minz_View::prependTitle(_t('import_export') . ' · ');
 	}
 
 	public function importAction() {
-		if (Minz_Request::isPost() && $_FILES['file']['error'] == 0) {
-			@set_time_limit(300);
+		if (!Minz_Request::isPost()) {
+			Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true);
+		}
 
-			$file = $_FILES['file'];
-			$type_file = $this->guessFileType($file['name']);
+		$file = $_FILES['file'];
+		$status_file = $file['error'];
 
-			$list_files = array(
-				'opml' => array(),
-				'json_starred' => array(),
-				'json_feed' => array()
-			);
+		if ($status_file !== 0) {
+			Minz_Log::error('File cannot be uploaded. Error code: ' . $status_file);
+			Minz_Request::bad(_t('file_cannot_be_uploaded'),
+			                  array('c' => 'importExport', 'a' => 'index'));
+		}
+
+		@set_time_limit(300);
 
-			// We try to list all files according to their type
-			// A zip file is first opened and then its files are listed
-			$list = array();
-			if ($type_file === 'zip') {
-				$zip = zip_open($file['tmp_name']);
+		$type_file = $this->guessFileType($file['name']);
 
-				while (($zipfile = zip_read($zip)) !== false) {
-					$type_zipfile = $this->guessFileType(
-						zip_entry_name($zipfile)
-					);
+		$list_files = array(
+			'opml' => array(),
+			'json_starred' => array(),
+			'json_feed' => array()
+		);
+
+		// We try to list all files according to their type
+		$list = array();
+		if ($type_file === 'zip' && extension_loaded('zip')) {
+			$zip = zip_open($file['tmp_name']);
+
+			if (!is_resource($zip)) {
+				// zip_open cannot open file: something is wrong
+				Minz_Log::error('Zip archive cannot be imported. Error code: ' . $zip);
+				Minz_Request::bad(_t('zip_error'),
+				                  array('c' => 'importExport', 'a' => 'index'));
+			}
 
+			while (($zipfile = zip_read($zip)) !== false) {
+				if (!is_resource($zipfile)) {
+					// zip_entry() can also return an error code!
+					Minz_Log::error('Zip file cannot be imported. Error code: ' . $zipfile);
+				} else {
+					$type_zipfile = $this->guessFileType(zip_entry_name($zipfile));
 					if ($type_file !== 'unknown') {
 						$list_files[$type_zipfile][] = zip_entry_read(
 							$zipfile,
@@ -54,59 +72,37 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 						);
 					}
 				}
-
-				zip_close($zip);
-			} elseif ($type_file !== 'unknown') {
-				$list_files[$type_file][] = file_get_contents(
-					$file['tmp_name']
-				);
-			}
-
-			// Import different files.
-			// OPML first(so categories and feeds are imported)
-			// Starred articles then so the "favourite" status is already set
-			// And finally all other files.
-			$error = false;
-			foreach ($list_files['opml'] as $opml_file) {
-				$error = $this->importOpml($opml_file);
-			}
-			foreach ($list_files['json_starred'] as $article_file) {
-				$error = $this->importArticles($article_file, true);
-			}
-			foreach ($list_files['json_feed'] as $article_file) {
-				$error = $this->importArticles($article_file);
 			}
 
-			// And finally, we get import status and redirect to the home page
-			$notif = null;
-			if ($error === true) {
-				$content_notif = Minz_Translate::t(
-					'feeds_imported_with_errors'
-				);
-			} else {
-				$content_notif = Minz_Translate::t(
-					'feeds_imported'
-				);
-			}
-
-			Minz_Session::_param('notification', array(
-				'type' => 'good',
-				'content' => $content_notif
-			));
-			Minz_Session::_param('actualize_feeds', true);
+			zip_close($zip);
+		} elseif ($type_file === 'zip') {
+			// Zip extension is not loaded
+			Minz_Request::bad(_t('no_zip_extension'),
+			                  array('c' => 'importExport', 'a' => 'index'));
+		} elseif ($type_file !== 'unknown') {
+			$list_files[$type_file][] = file_get_contents($file['tmp_name']);
+		}
 
-			Minz_Request::forward(array(
-				'c' => 'index',
-				'a' => 'index'
-			), true);
+		// Import file contents.
+		// OPML first(so categories and feeds are imported)
+		// Starred articles then so the "favourite" status is already set
+		// And finally all other files.
+		$error = false;
+		foreach ($list_files['opml'] as $opml_file) {
+			$error = $this->importOpml($opml_file);
+		}
+		foreach ($list_files['json_starred'] as $article_file) {
+			$error = $this->importArticles($article_file, true);
+		}
+		foreach ($list_files['json_feed'] as $article_file) {
+			$error = $this->importArticles($article_file);
 		}
 
-		// What are you doing? you have to call this controller
-		// with a POST request!
-		Minz_Request::forward(array(
-			'c' => 'importExport',
-			'a' => 'index'
-		));
+		// And finally, we get import status and redirect to the home page
+		Minz_Session::_param('actualize_feeds', true);
+		$content_notif = $error === true ? _t('feeds_imported_with_errors') :
+		                                   _t('feeds_imported');
+		Minz_Request::good($content_notif);
 	}
 
 	private function guessFileType($filename) {
@@ -120,7 +116,8 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 		} elseif (substr_compare($filename, '.opml', -5) === 0 ||
 		          substr_compare($filename, '.xml', -4) === 0) {
 			return 'opml';
-		} elseif (strcmp($filename, 'starred.json') === 0) {
+		} elseif (substr_compare($filename, '.json', -5) === 0 &&
+		          strpos($filename, 'starred') !== false) {
 			return 'json_starred';
 		} elseif (substr_compare($filename, '.json', -5) === 0 &&
 		          strpos($filename, 'feed_') === 0) {
@@ -176,15 +173,15 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 		}
 
 		// We get different useful information
-		$url = html_chars_utf8($feed_elt['xmlUrl']);
-		$name = html_chars_utf8($feed_elt['text']);
+		$url = Minz_Helper::htmlspecialchars_utf8($feed_elt['xmlUrl']);
+		$name = Minz_Helper::htmlspecialchars_utf8($feed_elt['text']);
 		$website = '';
 		if (isset($feed_elt['htmlUrl'])) {
-			$website = html_chars_utf8($feed_elt['htmlUrl']);
+			$website = Minz_Helper::htmlspecialchars_utf8($feed_elt['htmlUrl']);
 		}
 		$description = '';
 		if (isset($feed_elt['description'])) {
-			$description = html_chars_utf8($feed_elt['description']);
+			$description = Minz_Helper::htmlspecialchars_utf8($feed_elt['description']);
 		}
 
 		$error = false;
@@ -210,7 +207,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 
 	private function addCategoryOpml($cat_elt, $parent_cat) {
 		// Create a new Category object
-		$cat = new FreshRSS_Category(html_chars_utf8($cat_elt['text']));
+		$cat = new FreshRSS_Category(Minz_Helper::htmlspecialchars_utf8($cat_elt['text']));
 
 		$id = $this->catDAO->addCategoryObject($cat);
 		$error = ($id === false);
@@ -287,7 +284,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 		$url = $origin[$key];
 		$name = $origin['title'];
 		$website = $origin['htmlUrl'];
-		$error = false;
+
 		try {
 			// Create a Feed object and add it in DB
 			$feed = new FreshRSS_Feed($url);
@@ -311,44 +308,53 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 	}
 
 	public function exportAction() {
-		if (Minz_Request::isPost()) {
-			$this->view->_useLayout(false);
-
-			$export_opml = Minz_Request::param('export_opml', false);
-			$export_starred = Minz_Request::param('export_starred', false);
-			$export_feeds = Minz_Request::param('export_feeds', false);
-
-			// From https://stackoverflow.com/questions/1061710/php-zip-files-on-the-fly
-			$file = tempnam('tmp', 'zip');
-			$zip = new ZipArchive();
-			$zip->open($file, ZipArchive::OVERWRITE);
-
-			// Stuff with content
-			if ($export_opml) {
-				$zip->addFromString(
-					'feeds.opml', $this->generateOpml()
-				);
-			}
-			if ($export_starred) {
-				$zip->addFromString(
-					'starred.json', $this->generateArticles('starred')
-				);
-			}
-			foreach ($export_feeds as $feed_id) {
-				$feed = $this->feedDAO->searchById($feed_id);
-				$zip->addFromString(
-					'feed_' . $feed->category() . '_' . $feed->id() . '.json',
-					$this->generateArticles('feed', $feed)
+		if (!Minz_Request::isPost()) {
+			Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true);
+		}
+
+		$this->view->_useLayout(false);
+
+		$export_opml = Minz_Request::param('export_opml', false);
+		$export_starred = Minz_Request::param('export_starred', false);
+		$export_feeds = Minz_Request::param('export_feeds', array());
+
+		$export_files = array();
+		if ($export_opml) {
+			$export_files['feeds.opml'] = $this->generateOpml();
+		}
+
+		if ($export_starred) {
+			$export_files['starred.json'] = $this->generateArticles('starred');
+		}
+
+		foreach ($export_feeds as $feed_id) {
+			$feed = $this->feedDAO->searchById($feed_id);
+			if ($feed) {
+				$filename = 'feed_' . $feed->category() . '_'
+				          . $feed->id() . '.json';
+				$export_files[$filename] = $this->generateArticles(
+					'feed', $feed
 				);
 			}
+		}
 
-			// Close and send to user
-			$zip->close();
-			header('Content-Type: application/zip');
-			header('Content-Length: ' . filesize($file));
-			header('Content-Disposition: attachment; filename="freshrss_export.zip"');
-			readfile($file);
-			unlink($file);
+		$nb_files = count($export_files);
+		if ($nb_files > 1) {
+			// If there are more than 1 file to export, we need a zip archive.
+			try {
+				$this->exportZip($export_files);
+			} catch (Exception $e) {
+				# Oops, there is no Zip extension!
+				Minz_Request::bad(_t('export_no_zip_extension'),
+				                  array('c' => 'importExport', 'a' => 'index'));
+			}
+		} elseif ($nb_files === 1) {
+			// Only one file? Guess its type and export it.
+			$filename = key($export_files);
+			$type = $this->guessFileType($filename);
+			$this->exportFile('freshrss_' . $filename, $export_files[$filename], $type);
+		} else {
+			Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true);
 		}
 	}
 
@@ -367,7 +373,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 		$this->view->categories = $this->catDAO->listCategories();
 
 		if ($type == 'starred') {
-			$this->view->list_title = Minz_Translate::t('starred_list');
+			$this->view->list_title = _t('starred_list');
 			$this->view->type = 'starred';
 			$unread_fav = $this->entryDAO->countUnreadReadFavorites();
 			$this->view->entries = $this->entryDAO->listWhere(
@@ -375,9 +381,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 				$unread_fav['all']
 			);
 		} elseif ($type == 'feed' && !is_null($feed)) {
-			$this->view->list_title = Minz_Translate::t(
-				'feed_list', $feed->name()
-			);
+			$this->view->list_title = _t('feed_list', $feed->name());
 			$this->view->type = 'feed/' . $feed->id();
 			$this->view->entries = $this->entryDAO->listWhere(
 				'f', $feed->id(), FreshRSS_Entry::STATE_ALL, 'ASC',
@@ -388,4 +392,44 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 
 		return $this->view->helperToString('export/articles');
 	}
+
+	private function exportZip($files) {
+		if (!extension_loaded('zip')) {
+			throw new Exception();
+		}
+
+		// From https://stackoverflow.com/questions/1061710/php-zip-files-on-the-fly
+		$zip_file = tempnam('tmp', 'zip');
+		$zip = new ZipArchive();
+		$zip->open($zip_file, ZipArchive::OVERWRITE);
+
+		foreach ($files as $filename => $content) {
+			$zip->addFromString($filename, $content);
+		}
+
+		// Close and send to user
+		$zip->close();
+		header('Content-Type: application/zip');
+		header('Content-Length: ' . filesize($zip_file));
+		header('Content-Disposition: attachment; filename="freshrss_export.zip"');
+		readfile($zip_file);
+		unlink($zip_file);
+	}
+
+	private function exportFile($filename, $content, $type) {
+		if ($type === 'unknown') {
+			return;
+		}
+
+		$content_type = '';
+		if ($type === 'opml') {
+			$content_type = "text/opml";
+		} elseif ($type === 'json_feed' || $type === 'json_starred') {
+			$content_type = "text/json";
+		}
+
+		header('Content-Type: ' . $content_type . '; charset=utf-8');
+		header('Content-disposition: attachment; filename=' . $filename);
+		print($content);
+	}
 }

+ 47 - 8
app/Controllers/indexController.php

@@ -69,9 +69,6 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 
 		// mise à jour des titres
 		$this->view->rss_title = $this->view->currentName . ' | ' . Minz_View::title();
-		if ($this->view->nb_not_read > 0) {
-			Minz_View::prependTitle('(' . formatNumber($this->view->nb_not_read) . ') ');
-		}
 		Minz_View::prependTitle(
 			($this->nb_not_read_cat > 0 ? '(' . formatNumber($this->nb_not_read_cat) . ') ' : '') .
 			$this->view->currentName .
@@ -79,14 +76,14 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 		);
 
 		// On récupère les différents éléments de filtrage
-		$this->view->state = $state = Minz_Request::param ('state', $this->view->conf->default_view);
+		$this->view->state = Minz_Request::param('state', $this->view->conf->default_view);
 		$state_param = Minz_Request::param ('state', null);
 		$filter = Minz_Request::param ('search', '');
 		$this->view->order = $order = Minz_Request::param ('order', $this->view->conf->sort_order);
 		$nb = Minz_Request::param ('nb', $this->view->conf->posts_per_page);
 		$first = Minz_Request::param ('next', '');
 
-		if ($state === FreshRSS_Entry::STATE_NOT_READ) {	//Any unread article in this category at all?
+		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;
@@ -107,7 +104,7 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 					break;
 			}
 			if (!$hasUnread && ($state_param === null)) {
-				$this->view->state = $state = FreshRSS_Entry::STATE_ALL;
+				$this->view->state = FreshRSS_Entry::STATE_ALL;
 			}
 		}
 
@@ -120,11 +117,11 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 		$keepHistoryDefault = $this->view->conf->keep_history_default;
 
 		try {
-			$entries = $entryDAO->listWhere($getType, $getId, $state, $order, $nb + 1, $first, $filter, $date_min, true, $keepHistoryDefault);
+			$entries = $entryDAO->listWhere($getType, $getId, $this->view->state, $order, $nb + 1, $first, $filter, $date_min, true, $keepHistoryDefault);
 
 			// Si on a récupéré aucun article "non lus"
 			// on essaye de récupérer tous les articles
-			if ($state === FreshRSS_Entry::STATE_NOT_READ && empty($entries) && ($state_param === null) && ($filter == '')) {
+			if ($this->view->state === FreshRSS_Entry::STATE_NOT_READ && empty($entries) && ($state_param === null) && ($filter == '')) {
 				Minz_Log::record('Conflicting information about nbNotRead!', Minz_Log::DEBUG);
 				$feedDAO = FreshRSS_Factory::createFeedDao();
 				try {
@@ -135,6 +132,7 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 				$this->view->state = FreshRSS_Entry::STATE_ALL;
 				$entries = $entryDAO->listWhere($getType, $getId, $this->view->state, $order, $nb, $first, $filter, $date_min, true, $keepHistoryDefault);
 			}
+			Minz_Request::_param('state', $this->view->state);
 
 			if (count($entries) <= $nb) {
 				$this->view->nextId  = '';
@@ -298,6 +296,41 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 		Minz_Session::_param('passwordHash');
 	}
 
+	private static function makeLongTermCookie($username, $passwordHash) {
+		do {
+			$token = sha1(Minz_Configuration::salt() . $username . uniqid(mt_rand(), true));
+			$tokenFile = DATA_PATH . '/tokens/' . $token . '.txt';
+		} while (file_exists($tokenFile));
+		if (@file_put_contents($tokenFile, $username . "\t" . $passwordHash) === false) {
+			return false;
+		}
+		$expire = time() + 2629744;	//1 month	//TODO: Use a configuration instead
+		Minz_Session::setLongTermCookie('FreshRSS_login', $token, $expire);
+		Minz_Session::_param('token', $token);
+		return $token;
+	}
+
+	private static function deleteLongTermCookie() {
+		Minz_Session::deleteLongTermCookie('FreshRSS_login');
+		$token = Minz_Session::param('token', null);
+		if (ctype_alnum($token)) {
+			@unlink(DATA_PATH . '/tokens/' . $token . '.txt');
+		}
+		Minz_Session::_param('token');
+		if (rand(0, 10) === 1) {
+			self::purgeTokens();
+		}
+	}
+
+	private static function purgeTokens() {
+		$oldest = time() - 2629744;	//1 month	//TODO: Use a configuration instead
+		foreach (new DirectoryIterator(DATA_PATH . '/tokens/') as $fileInfo) {
+			if ($fileInfo->getExtension() === 'txt' && $fileInfo->getMTime() < $oldest) {
+				@unlink($fileInfo->getPathname());
+			}
+		}
+	}
+
 	public function formLoginAction () {
 		if (Minz_Request::isPost()) {
 			$ok = false;
@@ -315,6 +348,11 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 					if ($ok) {
 						Minz_Session::_param('currentUser', $username);
 						Minz_Session::_param('passwordHash', $s);
+						if (Minz_Request::param('keep_logged_in', false)) {
+							self::makeLongTermCookie($username, $s);
+						} else {
+							self::deleteLongTermCookie();
+						}
 					} else {
 						Minz_Log::record('Password mismatch for user ' . $username . ', nonce=' . $nonce . ', c=' . $c, Minz_Log::WARNING);
 					}
@@ -374,6 +412,7 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 		Minz_Session::_param('currentUser');
 		Minz_Session::_param('mail');
 		Minz_Session::_param('passwordHash');
+		self::deleteLongTermCookie();
 		Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true);
 	}
 }

+ 35 - 18
app/Controllers/statsController.php

@@ -4,9 +4,9 @@ class FreshRSS_stats_Controller extends Minz_ActionController {
 
 	public function indexAction() {
 		$statsDAO = FreshRSS_Factory::createStatsDAO();
-		Minz_View::appendScript (Minz_Url::display ('/scripts/flotr2.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/flotr2.min.js')));
+		Minz_View::appendScript(Minz_Url::display('/scripts/flotr2.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/flotr2.min.js')));
 		$this->view->repartition = $statsDAO->calculateEntryRepartition();
-		$this->view->count = ($statsDAO->calculateEntryCount());
+		$this->view->count = $statsDAO->calculateEntryCount();
 		$this->view->feedByCategory = $statsDAO->calculateFeedByCategory();
 		$this->view->entryByCategory = $statsDAO->calculateEntryByCategory();
 		$this->view->topFeed = $statsDAO->calculateTopFeed();
@@ -15,7 +15,13 @@ class FreshRSS_stats_Controller extends Minz_ActionController {
 	public function idleAction() {
 		$statsDAO = FreshRSS_Factory::createStatsDAO();
 		$feeds = $statsDAO->calculateFeedLastDate();
-		$idleFeeds = array();
+		$idleFeeds = array(
+			'last_year' => array(),
+			'last_6_month' => array(),
+			'last_3_month' => array(),
+			'last_month' => array(),
+			'last_week' => array(),
+		);
 		$now = new \DateTime();
 		$feedDate = clone $now;
 		$lastWeek = clone $now;
@@ -34,26 +40,37 @@ class FreshRSS_stats_Controller extends Minz_ActionController {
 			if ($feedDate >= $lastWeek) {
 				continue;
 			}
-			if ($feedDate < $lastWeek) {
-				$idleFeeds['last_week'][] = $feed['name'];
-			}
-			if ($feedDate < $lastMonth) {
-				$idleFeeds['last_month'][] = $feed['name'];
-			}
-			if ($feedDate < $last3Month) {
-				$idleFeeds['last_3_month'][] = $feed['name'];
-			}
-			if ($feedDate < $last6Month) {
-				$idleFeeds['last_6_month'][] = $feed['name'];
-			}
 			if ($feedDate < $lastYear) {
-				$idleFeeds['last_year'][] = $feed['name'];
+				$idleFeeds['last_year'][] = $feed;
+			} elseif ($feedDate < $last6Month) {
+				$idleFeeds['last_6_month'][] = $feed;
+			} elseif ($feedDate < $last3Month) {
+				$idleFeeds['last_3_month'][] = $feed;
+			} elseif ($feedDate < $lastMonth) {
+				$idleFeeds['last_month'][] = $feed;
+			} elseif ($feedDate < $lastWeek) {
+				$idleFeeds['last_week'][] = $feed;
 			}
 		}
 
-		$this->view->idleFeeds = array_reverse($idleFeeds);
+		$this->view->idleFeeds = $idleFeeds;
 	}
-	
+
+	public function repartitionAction() {
+		$statsDAO = FreshRSS_Factory::createStatsDAO();
+		$categoryDAO = new FreshRSS_CategoryDAO();
+		$feedDAO = FreshRSS_Factory::createFeedDao();
+		Minz_View::appendScript(Minz_Url::display('/scripts/flotr2.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/flotr2.min.js')));
+		$id = Minz_Request::param ('id', null);
+		$this->view->categories = $categoryDAO->listCategories();
+		$this->view->feed = $feedDAO->searchById($id);
+		$this->view->days = $statsDAO->getDays();
+		$this->view->months = $statsDAO->getMonths();
+		$this->view->repartitionHour = $statsDAO->calculateEntryRepartitionPerFeedPerHour($id);
+		$this->view->repartitionDayOfWeek = $statsDAO->calculateEntryRepartitionPerFeedPerDayOfWeek($id);
+		$this->view->repartitionMonth = $statsDAO->calculateEntryRepartitionPerFeedPerMonth($id);
+	}
+
 	public function firstAction() {
 		if (!$this->view->loginOk) {
 			Minz_Error::error(

+ 2 - 2
app/Controllers/usersController.php

@@ -100,7 +100,7 @@ class FreshRSS_users_Controller extends Minz_ActionController {
 	public function createAction() {
 		if (Minz_Request::isPost() && Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) {
 			$db = Minz_Configuration::dataBase();
-			require_once(APP_PATH . '/SQL/sql.' . $db['type'] . '.php');
+			require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php');
 
 			$new_user_language = Minz_Request::param('new_user_language', $this->view->conf->language);
 			if (!in_array($new_user_language, $this->view->conf->availableLanguages())) {
@@ -172,7 +172,7 @@ class FreshRSS_users_Controller extends Minz_ActionController {
 	public function deleteAction() {
 		if (Minz_Request::isPost() && Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) {
 			$db = Minz_Configuration::dataBase();
-			require_once(APP_PATH . '/SQL/sql.' . $db['type'] . '.php');
+			require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php');
 
 			$username = Minz_Request::param('username');
 			$ok = ctype_alnum($username);

+ 38 - 11
app/FreshRSS.php

@@ -6,17 +6,49 @@ class FreshRSS extends Minz_FrontController {
 		}
 		$loginOk = $this->accessControl(Minz_Session::param('currentUser', ''));
 		$this->loadParamsView();
+		if (Minz_Request::isPost() && (empty($_SERVER['HTTP_REFERER']) ||
+			Minz_Request::getDomainName() !== parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST))) {
+			$loginOk = false;	//Basic protection against XSRF attacks
+			Minz_Error::error(
+				403,
+				array('error' => array(Minz_Translate::t('access_denied') . ' [HTTP_REFERER=' .
+					htmlspecialchars(empty($_SERVER['HTTP_REFERER']) ? '' : $_SERVER['HTTP_REFERER']) . ']'))
+			);
+		}
+		Minz_View::_param('loginOk', $loginOk);
 		$this->loadStylesAndScripts($loginOk);	//TODO: Do not load that when not needed, e.g. some Ajax requests
 		$this->loadNotifications();
 	}
 
+	private static function getCredentialsFromLongTermCookie() {
+		$token = Minz_Session::getLongTermCookie('FreshRSS_login');
+		if (!ctype_alnum($token)) {
+			return array();
+		}
+		$tokenFile = DATA_PATH . '/tokens/' . $token . '.txt';
+		$mtime = @filemtime($tokenFile);
+		if ($mtime + 2629744 < time()) {	//1 month	//TODO: Use a configuration instead
+			@unlink($tokenFile);
+			return array(); 	//Expired or token does not exist
+		}
+		$credentials = @file_get_contents($tokenFile);
+		return $credentials === false ? array() : explode("\t", $credentials, 2);
+	}
+
 	private function accessControl($currentUser) {
 		if ($currentUser == '') {
 			switch (Minz_Configuration::authType()) {
 				case 'form':
-					$currentUser = Minz_Configuration::defaultUser();
-					Minz_Session::_param('passwordHash');
-					$loginOk = false;
+					$credentials = self::getCredentialsFromLongTermCookie();
+					if (isset($credentials[1])) {
+						$currentUser = trim($credentials[0]);
+						Minz_Session::_param('passwordHash', trim($credentials[1]));
+					}
+					$loginOk = $currentUser != '';
+					if (!$loginOk) {
+						$currentUser = Minz_Configuration::defaultUser();
+						Minz_Session::_param('passwordHash');
+					}
 					break;
 				case 'http_auth':
 					$currentUser = httpAuthUser();
@@ -95,7 +127,6 @@ class FreshRSS extends Minz_FrontController {
 					break;
 			}
 		}
-		Minz_View::_param ('loginOk', $loginOk);
 		return $loginOk;
 	}
 
@@ -127,13 +158,9 @@ class FreshRSS extends Minz_FrontController {
 				Minz_View::appendScript('https://login.persona.org/include.js');
 				break;
 		}
-		$includeLazyLoad = $this->conf->lazyload && ($this->conf->display_posts || Minz_Request::param ('output') === 'reader');
-		Minz_View::appendScript (Minz_Url::display ('/scripts/jquery.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/jquery.min.js')), false, !$includeLazyLoad, !$includeLazyLoad);
-		if ($includeLazyLoad) {
-			Minz_View::appendScript (Minz_Url::display ('/scripts/jquery.lazyload.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/jquery.lazyload.min.js')));
-		}
-		Minz_View::appendScript (Minz_Url::display ('/scripts/shortcut.js?' . @filemtime(PUBLIC_PATH . '/scripts/shortcut.js')));
-		Minz_View::appendScript (Minz_Url::display ('/scripts/main.js?' . @filemtime(PUBLIC_PATH . '/scripts/main.js')));
+		Minz_View::appendScript(Minz_Url::display('/scripts/jquery.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/jquery.min.js')));
+		Minz_View::appendScript(Minz_Url::display('/scripts/shortcut.js?' . @filemtime(PUBLIC_PATH . '/scripts/shortcut.js')));
+		Minz_View::appendScript(Minz_Url::display('/scripts/main.js?' . @filemtime(PUBLIC_PATH . '/scripts/main.js')));
 	}
 
 	private function loadNotifications () {

+ 8 - 0
app/Models/Configuration.php

@@ -17,6 +17,8 @@ class FreshRSS_Configuration {
 		'default_view' => FreshRSS_Entry::STATE_NOT_READ,
 		'auto_load_more' => true,
 		'display_posts' => false,
+		'display_categories' => false,
+		'hide_read_feeds' => true,
 		'onread_jump_next' => true,
 		'lazyload' => true,
 		'sticky_post' => true,
@@ -141,6 +143,12 @@ class FreshRSS_Configuration {
 	public function _display_posts ($value) {
 		$this->data['display_posts'] = ((bool)$value) && $value !== 'no';
 	}
+	public function _display_categories ($value) {
+		$this->data['display_categories'] = ((bool)$value) && $value !== 'no';
+	}
+	public function _hide_read_feeds($value) {
+		$this->data['hide_read_feeds'] = (bool)$value;
+	}
 	public function _onread_jump_next ($value) {
 		$this->data['onread_jump_next'] = ((bool)$value) && $value !== 'no';
 	}

+ 8 - 6
app/Models/EntryDAO.php

@@ -17,7 +17,9 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
 	}
 
 	public function addEntry($valuesTmp, $preparedStatement = null) {
-		$stm = $preparedStatement === null ? addEntryPrepare() : $preparedStatement;
+		$stm = $preparedStatement === null ?
+				FreshRSS_EntryDAO::addEntryPrepare() :
+				$preparedStatement;
 
 		$values = array(
 			$valuesTmp['id'],
@@ -63,7 +65,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
 		}
 
 		if (!isset($existingGuids[$entry->guid()]) &&
-				($feedHistory != 0 || $eDate  >= $date_min)) {
+				($feedHistory != 0 || $eDate >= $date_min || $entry->isFavorite())) {
 			$values = $entry->toArray();
 
 			$useDeclaredDate = empty($existingGuids);
@@ -173,7 +175,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
 	public function markReadEntries($idMax = 0, $onlyFavorites = false, $priorityMin = 0) {
 		if ($idMax == 0) {
 			$idMax = time() . '000000';
-			Minz_Log::record($nb . 'Calling markReadEntries(0) is deprecated!', Minz_Log::DEBUG);
+			Minz_Log::record('Calling markReadEntries(0) is deprecated!', Minz_Log::DEBUG);
 		}
 
 		$sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id '
@@ -201,7 +203,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
 	public function markReadCat($id, $idMax = 0) {
 		if ($idMax == 0) {
 			$idMax = time() . '000000';
-			Minz_Log::record($nb . 'Calling markReadCat(0) is deprecated!', Minz_Log::DEBUG);
+			Minz_Log::record('Calling markReadCat(0) is deprecated!', Minz_Log::DEBUG);
 		}
 
 		$sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id '
@@ -224,11 +226,11 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
 	public function markReadFeed($id, $idMax = 0) {
 		if ($idMax == 0) {
 			$idMax = time() . '000000';
-			Minz_Log::record($nb . 'Calling markReadFeed(0) is deprecated!', Minz_Log::DEBUG);
+			Minz_Log::record('Calling markReadFeed(0) is deprecated!', Minz_Log::DEBUG);
 		}
 		$this->bd->beginTransaction();
 
-		$sql = 'UPDATE `' . $this->prefix . 'entry`  '
+		$sql = 'UPDATE `' . $this->prefix . 'entry` '
 			 . 'SET is_read=1 '
 			 . 'WHERE id_feed=? AND is_read=0 AND id <= ?';
 		$values = array($id, $idMax);

+ 2 - 2
app/Models/EntryDAOSQLite.php

@@ -72,7 +72,7 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
 	public function markReadEntries($idMax = 0, $onlyFavorites = false, $priorityMin = 0) {
 		if ($idMax == 0) {
 			$idMax = time() . '000000';
-			Minz_Log::record($nb . 'Calling markReadEntries(0) is deprecated!', Minz_Log::DEBUG);
+			Minz_Log::record('Calling markReadEntries(0) is deprecated!', Minz_Log::DEBUG);
 		}
 
 		$sql = 'UPDATE `' . $this->prefix . 'entry` SET is_read=1 WHERE is_read=0 AND id <= ?';
@@ -98,7 +98,7 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
 	public function markReadCat($id, $idMax = 0) {
 		if ($idMax == 0) {
 			$idMax = time() . '000000';
-			Minz_Log::record($nb . 'Calling markReadCat(0) is deprecated!', Minz_Log::DEBUG);
+			Minz_Log::record('Calling markReadCat(0) is deprecated!', Minz_Log::DEBUG);
 		}
 
 		$sql = 'UPDATE `' . $this->prefix . 'entry` '

+ 9 - 3
app/Models/Feed.php

@@ -28,6 +28,12 @@ class FreshRSS_Feed extends Minz_Model {
 		}
 	}
 
+	public static function example() {
+		$f = new FreshRSS_Feed('http://example.net/', false);
+		$f->faviconPrepare();
+		return $f;
+	}
+
 	public function id() {
 		return $this->id;
 	}
@@ -277,11 +283,11 @@ class FreshRSS_Feed extends Minz_Model {
 					$elinks[$elink] = '1';
 					$mime = strtolower($enclosure->get_type());
 					if (strpos($mime, 'image/') === 0) {
-						$content .= '<br /><img src="' . $elink . '" alt="" />';
+						$content .= '<br /><img lazyload="" postpone="" src="' . $elink . '" alt="" />';
 					} elseif (strpos($mime, 'audio/') === 0) {
-						$content .= '<br /><audio src="' . $elink . '" controls="controls" />';
+						$content .= '<br /><audio lazyload="" postpone="" preload="none" src="' . $elink . '" controls="controls" />';
 					} elseif (strpos($mime, 'video/') === 0) {
-						$content .= '<br /><video src="' . $elink . '" controls="controls" />';
+						$content .= '<br /><video lazyload="" postpone="" preload="none" src="' . $elink . '" controls="controls" />';
 					}
 				}
 			}

+ 130 - 2
app/Models/StatsDAO.php

@@ -85,9 +85,83 @@ SQL;
 	 * @return array
 	 */
 	protected function initEntryCountArray() {
+		return $this->initStatsArray(-self::ENTRY_COUNT_PERIOD, -1);
+	}
+
+	/**
+	 * Calculates the number of article per hour of the day per feed
+	 *
+	 * @param integer $feed id
+	 * @return string
+	 */
+	public function calculateEntryRepartitionPerFeedPerHour($feed = null) {
+		return $this->calculateEntryRepartitionPerFeedPerPeriod('%H', $feed);
+	}
+
+	/**
+	 * Calculates the number of article per day of week per feed
+	 *
+	 * @param integer $feed id
+	 * @return string
+	 */
+	public function calculateEntryRepartitionPerFeedPerDayOfWeek($feed = null) {
+		return $this->calculateEntryRepartitionPerFeedPerPeriod('%w', $feed);
+	}
+
+	/**
+	 * Calculates the number of article per month per feed
+	 *
+	 * @param integer $feed
+	 * @return string
+	 */
+	public function calculateEntryRepartitionPerFeedPerMonth($feed = null) {
+		return $this->calculateEntryRepartitionPerFeedPerPeriod('%m', $feed);
+	}
+
+	/**
+	 * Calculates the number of article per period per feed
+	 *
+	 * @param string $period format string to use for grouping
+	 * @param integer $feed id
+	 * @return string
+	 */
+	protected function calculateEntryRepartitionPerFeedPerPeriod($period, $feed = null) {
+		if ($feed) {
+			$restrict = "WHERE e.id_feed = {$feed}";
+		} else {
+			$restrict = '';
+		}
+		$sql = <<<SQL
+SELECT DATE_FORMAT(FROM_UNIXTIME(e.date), '{$period}') AS period
+, COUNT(1) AS count
+FROM {$this->prefix}entry AS e
+{$restrict}
+GROUP BY period
+ORDER BY period ASC
+SQL;
+
+		$stm = $this->bd->prepare($sql);
+		$stm->execute();
+		$res = $stm->fetchAll(PDO::FETCH_NAMED);
+
+		foreach ($res as $value) {
+			$repartition[(int) $value['period']] = (int) $value['count'];
+		}
+
+		return $this->convertToSerie($repartition);
+	}
+
+	/**
+	 * Initialize an array for statistics depending on a range
+	 *
+	 * @param integer $min
+	 * @param integer $max
+	 * @return array
+	 */
+	protected function initStatsArray($min, $max) {
 		return array_map(function () {
 			return 0;
-		}, array_flip(range(-self::ENTRY_COUNT_PERIOD, -1)));
+		}, array_flip(range($min, $max)));
 	}
 
 	/**
@@ -170,7 +244,8 @@ SQL;
 	 */
 	public function calculateFeedLastDate() {
 		$sql = <<<SQL
-SELECT MAX(f.name) AS name
+SELECT MAX(f.id) as id
+, MAX(f.name) AS name
 , MAX(date) AS last_date
 FROM {$this->prefix}feed AS f,
 {$this->prefix}entry AS e
@@ -204,4 +279,57 @@ SQL;
 		return json_encode($serie);
 	}
 
+	/**
+	 * Gets days ready for graphs
+	 *
+	 * @return string
+	 */
+	public function getDays() {
+		return $this->convertToTranslatedJson(array(
+			'sun',
+			'mon',
+			'tue',
+			'wed',
+			'thu',
+			'fri',
+			'sat',
+		));
+	}
+
+	/**
+	 * Gets months ready for graphs
+	 *
+	 * @return string
+	 */
+	public function getMonths() {
+		return $this->convertToTranslatedJson(array(
+			'jan',
+			'feb',
+			'mar',
+			'apr',
+			'may',
+			'jun',
+			'jul',
+			'aug',
+			'sep',
+			'oct',
+			'nov',
+			'dec',
+		));
+	}
+
+	/**
+	 * Translates array content and encode it as JSON
+	 *
+	 * @param array $data
+	 * @return string
+	 */
+	private function convertToTranslatedJson($data = array()) {
+		$translated = array_map(function ($a) {
+			return Minz_Translate::t($a);
+		}, $data);
+
+		return json_encode($translated);
+	}
+
 }

+ 27 - 1
app/Models/StatsDAOSQLite.php

@@ -28,10 +28,36 @@ SQL;
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 
 		foreach ($res as $value) {
-			$count[(int)$value['day']] = (int) $value['count'];
+			$count[(int) $value['day']] = (int) $value['count'];
 		}
 
 		return $this->convertToSerie($count);
 	}
 
+	protected function calculateEntryRepartitionPerFeedPerPeriod($period, $feed = null) {
+		if ($feed) {
+			$restrict = "WHERE e.id_feed = {$feed}";
+		} else {
+			$restrict = '';
+		}
+		$sql = <<<SQL
+SELECT strftime('{$period}', e.date, 'unixepoch') AS period
+, COUNT(1) AS count
+FROM {$this->prefix}entry AS e
+{$restrict}
+GROUP BY period
+ORDER BY period ASC
+SQL;
+
+		$stm = $this->bd->prepare($sql);
+		$stm->execute();
+		$res = $stm->fetchAll(PDO::FETCH_NAMED);
+
+		foreach ($res as $value) {
+			$repartition[(int) $value['period']] = (int) $value['count'];
+		}
+
+		return $this->convertToSerie($repartition);
+	}
+
 }

+ 24 - 15
app/Models/UserDAO.php

@@ -3,19 +3,22 @@
 class FreshRSS_UserDAO extends Minz_ModelPdo {
 	public function createUser($username) {
 		$db = Minz_Configuration::dataBase();
-		require_once(APP_PATH . '/SQL/sql.' . $db['type'] . '.php');
-		
-		if (defined('SQL_CREATE_TABLES')) {
+		require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php');
+
+		$userPDO = new Minz_ModelPdo($username);
+
+		$ok = false;
+		if (defined('SQL_CREATE_TABLES')) {	//E.g. MySQL
 			$sql = sprintf(SQL_CREATE_TABLES, $db['prefix'] . $username . '_', Minz_Translate::t('default_category'));
-			$stm = $c->prepare($sql);
+			$stm = $userPDO->bd->prepare($sql);
 			$ok = $stm && $stm->execute();
-		} else {
+		} else {	//E.g. SQLite
 			global $SQL_CREATE_TABLES;
 			if (is_array($SQL_CREATE_TABLES)) {
 				$ok = true;
 				foreach ($SQL_CREATE_TABLES as $instruction) {
 					$sql = sprintf($instruction, '', Minz_Translate::t('default_category'));
-					$stm = $c->prepare($sql);
+					$stm = $userPDO->bd->prepare($sql);
 					$ok &= ($stm && $stm->execute());
 				}
 			}
@@ -24,7 +27,7 @@ class FreshRSS_UserDAO extends Minz_ModelPdo {
 		if ($ok) {
 			return true;
 		} else {
-			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			$info = empty($stm) ? array(2 => 'syntax error') : $stm->errorInfo();
 			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
 			return false;
 		}
@@ -32,16 +35,22 @@ class FreshRSS_UserDAO extends Minz_ModelPdo {
 
 	public function deleteUser($username) {
 		$db = Minz_Configuration::dataBase();
-		require_once(APP_PATH . '/SQL/sql.' . $db['type'] . '.php');
+		require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php');
 
-		$sql = sprintf(SQL_DROP_TABLES, $db['prefix'] . $username . '_');
-		$stm = $this->bd->prepare($sql);
-		if ($stm && $stm->execute()) {
-			return true;
+		if ($db['type'] === 'sqlite') {
+			return unlink(DATA_PATH . '/' . $username . '.sqlite');
 		} else {
-			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
-			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
-			return false;
+			$userPDO = new Minz_ModelPdo($username);
+
+			$sql = sprintf(SQL_DROP_TABLES, $db['prefix'] . $username . '_');
+			$stm = $userPDO->bd->prepare($sql);
+			if ($stm && $stm->execute()) {
+				return true;
+			} else {
+				$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+				Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
+				return false;
+			}
 		}
 	}
 }

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

@@ -1,4 +1,5 @@
 <?php
+global $SQL_CREATE_TABLES;
 $SQL_CREATE_TABLES = array(
 'CREATE TABLE IF NOT EXISTS `%1$scategory` (
 	`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,

+ 45 - 12
app/i18n/en.php

@@ -3,6 +3,7 @@
 return array (
 	// LAYOUT
 	'login'				=> 'Login',
+	'keep_logged_in'		=> 'Keep me logged in <small>(1 month)</small>',
 	'login_with_persona'		=> 'Login with Persona',
 	'logout'			=> 'Logout',
 	'search'			=> 'Search words or #tags',
@@ -48,6 +49,10 @@ return array (
 	'stats'				=> 'Statistics',
 	'stats_idle'			=> 'Idle feeds',
 	'stats_main'			=> 'Main statistics',
+	'stats_repartition'		=> 'Articles repartition',
+	'stats_entry_per_hour'		=> 'Per hour',
+	'stats_entry_per_day_of_week'	=> 'Per day of week',
+	'stats_entry_per_month'		=> 'Per month',
     
 	'last_week'			=> 'Last week',
 	'last_month'			=> 'Last month',
@@ -177,10 +182,15 @@ return array (
 	'focus_search'			=> 'Access search box',
 
 	'file_to_import'		=> 'File to import<br />(OPML, Json or Zip)',
+	'file_to_import_no_zip'		=> 'File to import<br />(OPML or Json)',
 	'import'			=> 'Import',
+	'file_cannot_be_uploaded'	=> 'File cannot be uploaded!',
+	'zip_error'			=> 'An error occured during Zip import.',
+	'no_zip_extension'		=> 'Zip extension is not present on your server.',
 	'export'			=> 'Export',
 	'export_opml'			=> 'Export list of feeds (OPML)',
 	'export_starred'		=> 'Export your favourites',
+	'export_no_zip_extension'	=> 'Zip extension is not present on your server. Please try to export files one by one.',
 	'starred_list'			=> 'List of favourite articles',
 	'feed_list'			=> 'List of %s articles',
 	'or'				=> 'or',
@@ -257,6 +267,8 @@ return array (
 	'sort_order'			=> 'Sort order',
 	'auto_load_more'		=> 'Load next articles at the page bottom',
 	'display_articles_unfolded'	=> 'Show articles unfolded by default',
+	'display_categories_unfolded'	=> 'Show categories folded by default',
+	'hide_read_feeds'		=> 'Hide categories &amp; feeds with no unread article (only in “unread articles” display mode)',
 	'after_onread'			=> 'After “mark all as read”,',
 	'jump_next'			=> 'jump to next unread sibling (feed or category)',
 	'article_icons'			=> 'Article icons',
@@ -339,20 +351,41 @@ return array (
 	'login_required'		=> 'Login required:',
 
 	'confirm_action'		=> 'Are you sure you want to perform this action? It cannot be cancelled!',
+	'notif_title_new_articles'	=> 'FreshRSS: new articles!',
+	'notif_body_new_articles'	=> 'There are \d new articles to read on FreshRSS.',
 
 	// DATE
-	'january'			=> 'january',
-	'february'			=> 'february',
-	'march'				=> 'march',
-	'april'				=> 'april',
-	'may'				=> 'may',
-	'june'				=> 'june',
-	'july'				=> 'july',
-	'august'			=> 'august',
-	'september'			=> 'september',
-	'october'			=> 'october',
-	'november'			=> 'november',
-	'december'			=> 'december',
+	'january'			=> 'January',
+	'february'			=> 'February',
+	'march'				=> 'March',
+	'april'				=> 'April',
+	'may'				=> 'May',
+	'june'				=> 'June',
+	'july'				=> 'July',
+	'august'			=> 'August',
+	'september'			=> 'September',
+	'october'			=> 'October',
+	'november'			=> 'November',
+	'december'			=> 'December',
+	'january'			=> 'Jan',
+	'february'			=> 'Feb',
+	'march'				=> 'Mar',
+	'april'				=> 'Apr',
+	'may'				=> 'May',
+	'june'				=> 'Jun',
+	'july'				=> 'Jul',
+	'august'			=> 'Aug',
+	'september'			=> 'Sep',
+	'october'			=> 'Oct',
+	'november'			=> 'Nov',
+	'december'			=> 'Dec',
+	'sun'				=> 'Sun',
+	'mon'				=> 'Mon',
+	'tue'				=> 'Tue',
+	'wed'				=> 'Wed',
+	'thu'				=> 'Thu',
+	'fri'				=> 'Fri',
+	'sat'				=> 'Sat',
 	// special format for date() function
 	'Jan'				=> '\J\a\n\u\a\r\y',
 	'Feb'				=> '\F\e\b\r\u\a\r\y',

+ 34 - 1
app/i18n/fr.php

@@ -3,6 +3,7 @@
 return array (
 	// LAYOUT
 	'login'				=> 'Connexion',
+	'keep_logged_in'		=> 'Rester connecté <small>(1 mois)</small>',
 	'login_with_persona'		=> 'Connexion avec Persona',
 	'logout'			=> 'Déconnexion',
 	'search'			=> 'Rechercher des mots ou des #tags',
@@ -48,6 +49,10 @@ return array (
 	'stats'				=> 'Statistiques',
 	'stats_idle'			=> 'Flux inactifs',
 	'stats_main'			=> 'Statistiques principales',
+	'stats_repartition'		=> 'Répartition des articles',
+	'stats_entry_per_hour'		=> 'Par heure',
+	'stats_entry_per_day_of_week'	=> 'Par jour de la semaine',
+	'stats_entry_per_month'		=> 'Par mois',
 
 	'last_week'			=> 'La dernière semaine',
 	'last_month'			=> 'Le dernier mois',
@@ -177,10 +182,15 @@ return array (
 	'focus_search'			=> 'Accéder à la recherche',
 
 	'file_to_import'		=> 'Fichier à importer<br />(OPML, Json ou Zip)',
+	'file_to_import_no_zip'		=> 'Fichier à importer<br />(OPML ou Json)',
 	'import'			=> 'Importer',
+	'file_cannot_be_uploaded'	=> 'Le fichier ne peut pas être téléchargé!',
+	'zip_error'			=> 'Une erreur est survenue durant l’import du fichier Zip.',
+	'no_zip_extension'		=> 'L’extension Zip n’est pas présente sur votre serveur.',
 	'export'			=> 'Exporter',
 	'export_opml'			=> 'Exporter la liste des flux (OPML)',
 	'export_starred'		=> 'Exporter les favoris',
+	'export_no_zip_extension'	=> 'L’extension Zip n’est pas présente sur votre serveur. Veuillez essayer d’exporter les fichiers un par un.',
 	'starred_list'			=> 'Liste des articles favoris',
 	'feed_list'			=> 'Liste des articles de %s',
 	'or'				=> 'ou',
@@ -224,7 +234,7 @@ return array (
 	'persona_connection_email'	=> 'Adresse courriel de connexion<br /><small>(pour <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>',
 	'allow_anonymous'		=> 'Autoriser la lecture anonyme des articles de l’utilisateur par défaut (%s)',
 	'allow_anonymous_refresh'	=> 'Autoriser le rafraîchissement anonyme des flux',
-	'unsafe_autologin'		=> 'Autoriser les connexion automatiques non-sûres au format : ',
+	'unsafe_autologin'		=> 'Autoriser les connexions automatiques non-sûres au format : ',
 	'api_enabled'			=> 'Autoriser l’accès par <abbr>API</abbr> <small>(nécessaire pour les applis mobiles)</small>',
 	'auth_token'			=> 'Jeton d’identification',
 	'explain_token'			=> 'Permet d’accéder à la sortie RSS de l’utilisateur par défaut sans besoin de s’authentifier.<br /><kbd>%s?output=rss&token=%s</kbd>',
@@ -257,6 +267,8 @@ return array (
 	'sort_order'			=> 'Ordre de tri',
 	'auto_load_more'		=> 'Charger les articles suivants en bas de page',
 	'display_articles_unfolded'	=> 'Afficher les articles dépliés par défaut',
+	'display_categories_unfolded'	=> 'Afficher les catégories pliées par défaut',
+	'hide_read_feeds'		=> 'Cacher les catégories &amp; flux sans article non-lu (uniquement en affichage “articles non lus”)',
 	'after_onread'			=> 'Après “marquer tout comme lu”,',
 	'jump_next'			=> 'sauter au prochain voisin non lu (flux ou catégorie)',
 	'article_icons'			=> 'Icônes d’article',
@@ -339,6 +351,8 @@ return array (
 	'login_required'		=> 'Accès protégé par mot de passe :',
 
 	'confirm_action'		=> 'Êtes-vous sûr(e) de vouloir continuer ? Cette action ne peut être annulée !',
+	'notif_title_new_articles'	=> 'FreshRSS : nouveaux articles !',
+	'notif_body_new_articles'	=> 'Il y a \d nouveaux articles à lire sur FreshRSS.',
 
 	// DATE
 	'january'			=> 'janvier',
@@ -353,6 +367,25 @@ return array (
 	'october'			=> 'octobre',
 	'november'			=> 'novembre',
 	'december'			=> 'décembre',
+	'jan'				=> 'jan.',
+	'feb'				=> 'fév.',
+	'mar'				=> 'mar.',
+	'apr'				=> 'avr.',
+	'may'				=> 'mai.',
+	'jun'				=> 'juin',
+	'jul'				=> 'jui.',
+	'aug'				=> 'août',
+	'sep'				=> 'sep.',
+	'oct'				=> 'oct.',
+	'nov'				=> 'nov.',
+	'dec'				=> 'déc.',
+	'sun'				=> 'dim.',
+	'mon'				=> 'lun.',
+	'tue'				=> 'mar.',
+	'wed'				=> 'mer.',
+	'thu'				=> 'jeu.',
+	'fri'				=> 'ven.',
+	'sat'				=> 'sam.',
 	// format spécial pour la fonction date()
 	'Jan'				=> '\j\a\n\v\i\e\r',
 	'Feb'				=> '\f\é\v\r\i\e\r',

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

@@ -28,8 +28,8 @@ return array (
 	'minz_is_nok'			=> 'You lack the Minz framework. You should execute <em>build.sh</em> script or <a href="https://github.com/marienfressinaud/MINZ">download it on Github</a> and install in <em>%s</em> directory the content of its <em>/lib</em> directory.',
 	'curl_is_ok'			=> 'You have version %s of cURL',
 	'curl_is_nok'			=> 'You lack cURL (php5-curl package)',
-	'pdomysql_is_ok'		=> 'You have PDO and its driver for MySQL',
-	'pdomysql_is_nok'		=> 'You lack PDO or its driver for MySQL (php5-mysql package)',
+	'pdo_is_ok'			=> 'You have PDO and at least one of the supported drivers (pdo_mysql, pdo_sqlite)',
+	'pdo_is_nok'			=> 'You lack PDO or one of the supported drivers (pdo_mysql, pdo_sqlite)',
 	'dom_is_ok'			=> 'You have the required library to browse the DOM',
 	'dom_is_nok'			=> 'You lack a required library to browse the DOM (php-xml package)',
 	'pcre_is_ok'			=> 'You have the required library for regular expressions (PCRE)',

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

@@ -28,8 +28,8 @@ return array (
 	'minz_is_nok'			=> 'Vous ne disposez pas de la librairie Minz. Vous devriez exécuter le script <em>build.sh</em> ou bien <a href="https://github.com/marienfressinaud/MINZ">la télécharger sur Github</a> et installer dans le répertoire <em>%s</em> le contenu de son répertoire <em>/lib</em>.',
 	'curl_is_ok'			=> 'Vous disposez de cURL dans sa version %s',
 	'curl_is_nok'			=> 'Vous ne disposez pas de cURL (paquet php5-curl)',
-	'pdomysql_is_ok'		=> 'Vous disposez de PDO et de son driver pour MySQL (paquet php5-mysql)',
-	'pdomysql_is_nok'		=> 'Vous ne disposez pas de PDO ou de son driver pour MySQL',
+	'pdo_is_ok'			=> 'Vous disposez de PDO et d’au moins un des drivers supportés (pdo_mysql, pdo_sqlite)',
+	'pdo_is_nok'			=> 'Vous ne disposez pas de PDO ou d’un des drivers supportés (pdo_mysql, pdo_sqlite)',
 	'dom_is_ok'			=> 'Vous disposez du nécessaire pour parcourir le DOM',
 	'dom_is_nok'			=> 'Il manque une librairie pour parcourir le DOM (paquet php-xml)',
 	'pcre_is_ok'			=> 'Vous disposez du nécessaire pour les expressions régulières (PCRE)',

+ 18 - 10
app/install.php

@@ -249,11 +249,11 @@ function saveStep3 () {
 				'base_url' => '',
 				'title' => $_SESSION['title'],
 				'default_user' => $_SESSION['default_user'],
-				'auth_type' => $_SESSION['auth_type'],
 				'allow_anonymous' => isset($_SESSION['allow_anonymous']) ? $_SESSION['allow_anonymous'] : false,
-				'allow_anonymous_refresh' => false,
-				'unsafe_autologin_enabled' => false,
-				'api_enabled' => false,
+				'allow_anonymous_refresh' => isset($_SESSION['allow_anonymous_refresh']) ? $_SESSION['allow_anonymous_refresh'] : false,
+				'auth_type' => $_SESSION['auth_type'],
+				'api_enabled' => isset($_SESSION['api_enabled']) ? $_SESSION['api_enabled'] : false,
+				'unsafe_autologin_enabled' => isset($_SESSION['unsafe_autologin_enabled']) ? $_SESSION['unsafe_autologin_enabled'] : false,
 			),
 			'db' => array(
 				'type' => $_SESSION['bd_type'],
@@ -499,7 +499,7 @@ function checkStep0 () {
 	if ($ini_array) {
 		$ini_general = isset($ini_array['general']) ? $ini_array['general'] : null;
 		if ($ini_general) {
-			$keys = array('environment', 'salt', 'title', 'default_user', 'allow_anonymous', 'auth_type');
+			$keys = array('environment', 'salt', 'title', 'default_user', 'allow_anonymous', 'allow_anonymous_refresh', 'auth_type', 'api_enabled', 'unsafe_autologin_enabled');
 			foreach ($keys as $key) {
 				if ((empty($_SESSION[$key])) && isset($ini_general[$key])) {
 					$_SESSION[$key] = $ini_general[$key];
@@ -574,7 +574,9 @@ function checkStep1 () {
 	$php = version_compare (PHP_VERSION, '5.2.1') >= 0;
 	$minz = file_exists (LIB_PATH . '/Minz');
 	$curl = extension_loaded ('curl');
-	$pdo = extension_loaded ('pdo_mysql');
+	$pdo_mysql = extension_loaded ('pdo_mysql');
+	$pdo_sqlite = extension_loaded ('pdo_sqlite');
+	$pdo = $pdo_mysql || $pdo_sqlite;
 	$pcre = extension_loaded ('pcre');
 	$ctype = extension_loaded ('ctype');
 	$dom = class_exists('DOMDocument');
@@ -588,7 +590,9 @@ function checkStep1 () {
 		'php' => $php ? 'ok' : 'ko',
 		'minz' => $minz ? 'ok' : 'ko',
 		'curl' => $curl ? 'ok' : 'ko',
-		'pdo-mysql' => $pdo ? 'ok' : 'ko',
+		'pdo-mysql' => $pdo_mysql ? 'ok' : 'ko',
+		'pdo-sqlite' => $pdo_sqlite ? 'ok' : 'ko',
+		'pdo' => $pdo ? 'ok' : 'ko',
 		'pcre' => $pcre ? 'ok' : 'ko',
 		'ctype' => $ctype ? 'ok' : 'ko',
 		'dom' => $dom ? 'ok' : 'ko',
@@ -766,10 +770,10 @@ function printStep1 () {
 	<p class="alert alert-error"><span class="alert-head"><?php echo _t ('damn'); ?></span> <?php echo _t ('minz_is_nok', LIB_PATH . '/Minz'); ?></p>
 	<?php } ?>
 
-	<?php if ($res['pdo-mysql'] == 'ok') { ?>
-	<p class="alert alert-success"><span class="alert-head"><?php echo _t ('ok'); ?></span> <?php echo _t ('pdomysql_is_ok'); ?></p>
+	<?php if ($res['pdo'] == 'ok') { ?>
+	<p class="alert alert-success"><span class="alert-head"><?php echo _t ('ok'); ?></span> <?php echo _t ('pdo_is_ok'); ?></p>
 	<?php } else { ?>
-	<p class="alert alert-error"><span class="alert-head"><?php echo _t ('damn'); ?></span> <?php echo _t ('pdomysql_is_nok'); ?></p>
+	<p class="alert alert-error"><span class="alert-head"><?php echo _t ('damn'); ?></span> <?php echo _t ('pdo_is_nok'); ?></p>
 	<?php } ?>
 
 	<?php if ($res['curl'] == 'ok') { ?>
@@ -923,14 +927,18 @@ function printStep3 () {
 			<label class="group-name" for="type"><?php echo _t ('bdd_type'); ?></label>
 			<div class="group-controls">
 				<select name="type" id="type" onchange="mySqlShowHide()">
+				<?php if (extension_loaded('pdo_mysql')) {?>
 				<option value="mysql"
 					<?php echo (isset($_SESSION['bd_type']) && $_SESSION['bd_type'] === 'mysql') ? 'selected="selected"' : ''; ?>>
 					MySQL
 				</option>
+				<?php }?>
+				<?php if (extension_loaded('pdo_sqlite')) {?>
 				<option value="sqlite"
 					<?php echo (isset($_SESSION['bd_type']) && $_SESSION['bd_type'] === 'sqlite') ? 'selected="selected"' : ''; ?>>
 					SQLite
 				</option>
+				<?php }?>
 				</select>
 			</div>
 		</div>

+ 12 - 5
app/layout/aside_flux.phtml

@@ -1,4 +1,4 @@
-<div class="aside aside_flux" id="aside_flux">
+<div class="aside aside_flux<?php if ($this->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 FreshRSS_Themes::icon('close'); ?></a>
 
 	<ul class="categories">
@@ -41,11 +41,17 @@
 		foreach ($this->cat_aside as $cat) {
 			$feeds = $cat->feeds ();
 			if (!empty ($feeds)) {
-				?><li><?php
 				$c_active = false;
-				if ($this->get_c == $cat->id ()) {
-					$c_active = true;
+				if ($this->conf->display_categories) {
+					if ($this->get_c == $cat->id () && $this->get_f) {
+						$c_active = true;
+					}
+				} else {
+					if ($this->get_c == $cat->id ()) {
+						$c_active = 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 FreshRSS_Themes::icon($c_active ? 'up' : 'down'); ?></a><?php
@@ -55,7 +61,7 @@
 					$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' : ''; ?>"><?php
+					?><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 FreshRSS_Themes::icon('configure'); ?></a><?php
@@ -77,6 +83,7 @@
 	<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 Minz_Translate::t ('filter'); ?></a></li>
+		<li class="item"><a href="<?php echo _url ('stats', 'repartition', 'id', '!!!!!!'); ?>"><?php echo Minz_Translate::t ('stats'); ?></a></li>
 		<li class="item"><a target="_blank" href="http://example.net/"><?php echo Minz_Translate::t ('see_website'); ?></a></li>
 		<?php if ($this->loginOk) { ?>
 		<li class="separator"></li>

+ 3 - 0
app/layout/aside_stats.phtml

@@ -6,4 +6,7 @@
 	<li class="item<?php echo Minz_Request::actionName () == 'idle' ? ' active' : ''; ?>">
 		<a href="<?php echo _url ('stats', 'idle'); ?>"><?php echo Minz_Translate::t ('stats_idle'); ?></a>
 	</li>
+	<li class="item<?php echo Minz_Request::actionName () == 'repartition' ? ' active' : ''; ?>">
+		<a href="<?php echo _url ('stats', 'repartition'); ?>"><?php echo Minz_Translate::t ('stats_repartition'); ?></a>
+	</li>
 </ul>

+ 1 - 1
app/layout/layout.phtml

@@ -16,7 +16,7 @@
 ?>
 		<link id="prefetch" rel="next prefetch" href="<?php echo Minz_Url::display(array('c' => Minz_Request::controllerName(), 'a' => Minz_Request::actionName(), 'params' => $params)); ?>" />
 <?php } ?>
-		<link rel="shortcut icon" type="image/x-icon" sizes="16x16 64x64" href="<?php echo Minz_Url::display('/favicon.ico'); ?>" />
+		<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)) {

+ 12 - 6
app/layout/nav_menu.phtml

@@ -164,11 +164,15 @@
 					break;
 			}
 		}
-		if ($this->order === 'ASC') {
-			$idMax = 0;
-		} else {
-			$p = isset($this->entries[0]) ? $this->entries[0] : null;
-			$idMax = $p === null ? '0' : $p->id();
+
+		$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));
@@ -221,7 +225,9 @@
 
 		<?php
 			$url_output['params']['output'] = 'rss';
-			$url_output['params']['token'] = $this->conf->token;
+			if ($this->conf->token) {
+				$url_output['params']['token'] = $this->conf->token;
+			}
 		?>
 		<a class="view_rss btn" target="_blank" title="<?php echo Minz_Translate::t ('rss_view'); ?>" href="<?php echo Minz_Url::display($url_output); ?>">
 			<?php echo FreshRSS_Themes::icon('rss'); ?>

+ 27 - 8
app/views/configure/reading.phtml

@@ -9,7 +9,7 @@
 		<div class="form-group">
 			<label class="group-name" for="posts_per_page"><?php echo Minz_Translate::t ('articles_per_page'); ?></label>
 			<div class="group-controls">
-				<input type="number" id="posts_per_page" name="posts_per_page" value="<?php echo $this->conf->posts_per_page; ?>" />
+				<input type="number" id="posts_per_page" name="posts_per_page" value="<?php echo $this->conf->posts_per_page; ?>" min="5" max="50" />
 			</div>
 		</div>
 
@@ -44,10 +44,9 @@
 
 		<div class="form-group">
 			<div class="group-controls">
-				<label class="checkbox" for="auto_load_more">
-					<input type="checkbox" name="auto_load_more" id="auto_load_more" value="1"<?php echo $this->conf->auto_load_more ? ' checked="checked"' : ''; ?> />
-					<?php echo Minz_Translate::t ('auto_load_more'); ?>
-					<noscript> — <strong><?php echo Minz_Translate::t ('javascript_should_be_activated'); ?></strong></noscript>
+				<label class="checkbox" for="hide_read_feeds">
+					<input type="checkbox" name="hide_read_feeds" id="hide_read_feeds" value="1"<?php echo $this->conf->hide_read_feeds ? ' checked="checked"' : ''; ?> />
+					<?php echo Minz_Translate::t('hide_read_feeds'); ?>
 				</label>
 			</div>
 		</div>
@@ -64,9 +63,9 @@
 
 		<div class="form-group">
 			<div class="group-controls">
-				<label class="checkbox" for="lazyload">
-					<input type="checkbox" name="lazyload" id="lazyload" value="1"<?php echo $this->conf->lazyload ? ' checked="checked"' : ''; ?> />
-					<?php echo Minz_Translate::t ('img_with_lazyload'); ?>
+				<label class="checkbox" for="display_categories">
+					<input type="checkbox" name="display_categories" id="display_categories" value="1"<?php echo $this->conf->display_categories ? ' checked="checked"' : ''; ?> />
+					<?php echo Minz_Translate::t ('display_categories_unfolded'); ?>
 					<noscript> — <strong><?php echo Minz_Translate::t ('javascript_should_be_activated'); ?></strong></noscript>
 				</label>
 			</div>
@@ -82,6 +81,26 @@
 			</div>
 		</div>
 
+		<div class="form-group">
+			<div class="group-controls">
+				<label class="checkbox" for="auto_load_more">
+					<input type="checkbox" name="auto_load_more" id="auto_load_more" value="1"<?php echo $this->conf->auto_load_more ? ' checked="checked"' : ''; ?> />
+					<?php echo Minz_Translate::t ('auto_load_more'); ?>
+					<noscript> — <strong><?php echo Minz_Translate::t ('javascript_should_be_activated'); ?></strong></noscript>
+				</label>
+			</div>
+		</div>
+
+		<div class="form-group">
+			<div class="group-controls">
+				<label class="checkbox" for="lazyload">
+					<input type="checkbox" name="lazyload" id="lazyload" value="1"<?php echo $this->conf->lazyload ? ' checked="checked"' : ''; ?> />
+					<?php echo Minz_Translate::t ('img_with_lazyload'); ?>
+					<noscript> — <strong><?php echo Minz_Translate::t ('javascript_should_be_activated'); ?></strong></noscript>
+				</label>
+			</div>
+		</div>
+
 		<div class="form-group">
 			<div class="group-controls">
 				<label class="checkbox" for="reading_confirm">

+ 2 - 11
app/views/error/index.phtml

@@ -1,18 +1,9 @@
 <div class="post">
 	<div class="alert alert-error">
 		<h1 class="alert-head"><?php echo $this->code; ?></h1>
-
 		<p>
-			<?php
-			switch(Minz_Request::param ('code')) {
-			case 403:
-				echo Minz_Translate::t ('forbidden_access');
-				break;
-			case 404:
-			default:
-				echo Minz_Translate::t ('page_not_found');
-			} ?><br />
-			<a href="<?php echo _url ('index', 'index'); ?>"><?php echo Minz_Translate::t ('back_to_rss_feeds'); ?></a>
+			<?php echo $this->errorMessage; ?><br />
+			<a href="<?php echo _url('index', 'index'); ?>"><?php echo Minz_Translate::t('back_to_rss_feeds'); ?></a>
 		</p>
 	</div>
 </div>

+ 3 - 3
app/views/helpers/export/opml.phtml

@@ -18,9 +18,9 @@ foreach ($this->categories as $key => $cat) {
 		$opml_array['body'][$key]['@outlines'][] = array(
 			'text' => htmlspecialchars_decode($feed->name()),
 			'type' => 'rss',
-			'xmlUrl' => $feed->url(),
-			'htmlUrl' => $feed->website(),
-			'description' => $feed->description()
+			'xmlUrl' => htmlspecialchars_decode($feed->url()),
+			'htmlUrl' => htmlspecialchars_decode($feed->website()),
+			'description' => htmlspecialchars_decode($feed->description()),
 		);
 	}
 }

+ 2 - 1
app/views/helpers/javascript_vars.phtml

@@ -10,7 +10,6 @@ echo 'var ',
 	',auto_mark_site=', $mark['site'] ? 'true' : 'false',
 	',auto_mark_scroll=', $mark['scroll'] ? 'true' : 'false',
 	',auto_load_more=', $this->conf->auto_load_more ? 'true' : 'false',
-	',full_lazyload=', $this->conf->lazyload && ($this->conf->display_posts || Minz_Request::param('output') === 'reader') ? 'true' : 'false',
 	',does_lazyload=', $this->conf->lazyload ? 'true' : 'false',
 	',sticky_post=', $this->conf->sticky_post ? 'true' : 'false';
 
@@ -50,6 +49,8 @@ echo 'authType="', $authType, '",',
 	'url_logout="', _url ('index', 'logout'), '",';
 
 echo 'str_confirmation="', Minz_Translate::t('confirm_action'), '"', ",\n";
+echo 'str_notif_title_articles="', Minz_Translate::t('notif_title_new_articles'), '"', ",\n";
+echo 'str_notif_body_articles="', Minz_Translate::t('notif_body_new_articles'), '"', ",\n";
 
 $autoActualise = Minz_Session::param('actualize_feeds', false);
 echo 'auto_actualize_feeds=', $autoActualise ? 'true' : 'false', ";\n";

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

@@ -14,7 +14,7 @@
 	<?php } elseif ($markReadUrl) { ?>
 	<a id="bigMarkAsRead" href="<?php echo $markReadUrl; ?>"<?php if ($this->conf->reading_confirm) { echo ' class="confirm"';} ?>>
 		<?php echo Minz_Translate::t ('nothing_to_load'); ?><br />
-		<span class="bigTick"></span><br />
+		<span class="bigTick"></span><br />
 		<?php echo Minz_Translate::t ('mark_all_read'); ?>
 	</a>
 	<?php } else { ?>

+ 9 - 8
app/views/helpers/view/normal_view.phtml

@@ -81,7 +81,12 @@ if (!empty($this->entries)) {
 				}
 			}
 			$feed = FreshRSS_CategoryDAO::findFeed($this->cat_aside, $item->feed ());	//We most likely already have the feed object in cache
-			if (empty($feed)) $feed = $item->feed (true);
+			if ($feed == null) {
+				$feed = $item->feed(true);
+				if ($feed == null) {
+					$feed = FreshRSS_Feed::example();
+				}
+			}
 			?><li class="item website"><a href="<?php echo _url ('index', 'index', 'get', 'f_' . $feed->id ()); ?>"><img class="favicon" src="<?php echo $feed->favicon (); ?>" alt="✇" /> <span><?php echo $feed->name(); ?></span></a></li>
 			<li class="item title"><a target="_blank" href="<?php echo $item->link (); ?>"><?php echo $item->title (); ?></a></li>
 			<?php if ($topline_date) { ?><li class="item date"><?php echo $item->date (); ?> </li><?php } ?>
@@ -92,13 +97,9 @@ if (!empty($this->entries)) {
 			<div class="content <?php echo $content_width; ?>">
 				<h1 class="title"><a target="_blank" href="<?php echo $item->link (); ?>"><?php echo $item->title (); ?></a></h1>
 				<?php
-					$author = $item->author ();
-					echo $author != '' ? '<div class="author">' . Minz_Translate::t ('by_author', $author) . '</div>' : '';
-					if ($lazyload) {
-						echo $hidePosts ? lazyIframe(lazyimg($item->content())) : lazyimg($item->content());
-					} else {
-						echo $item->content();
-					}
+					$author = $item->author();
+					echo $author != '' ? '<div class="author">' . Minz_Translate::t('by_author', $author) . '</div>' : '',
+						$lazyload && $hidePosts ? lazyimg($item->content()) : $item->content();
 				?>
 			</div>
 			<ul class="horizontal-list bottom"><?php

+ 6 - 12
app/views/helpers/view/reader_view.phtml

@@ -21,19 +21,13 @@ if (!empty($this->entries)) {
 				</a>
 				<h1 class="title"><?php echo $item->title (); ?></h1>
 
-				<div class="author">
-					<?php $author = $item->author (); ?>
-					<?php echo $author != '' ? Minz_Translate::t ('by_author', $author) . ' — ' : ''; ?>
-					<?php echo $item->date (); ?>
-				</div>
+				<div class="author"><?php
+					$author = $item->author();
+					echo $author != '' ? Minz_Translate::t('by_author', $author) . ' — ' : '',
+						$item->date();
+				?></div>
 
-				<?php
-					if ($lazyload) {
-						echo lazyimg($item->content ());
-					} else {
-						echo $item->content();
-					}
-				?>
+				<?php echo $item->content(); ?>
 			</div>
 		</div>
 	</div>

+ 0 - 0
app/views/importExport/export.phtml


+ 20 - 11
app/views/importExport/index.phtml

@@ -1,12 +1,14 @@
-<?php $this->partial ('aside_feed'); ?>
+<?php $this->partial('aside_feed'); ?>
 
 <div class="post ">
-	<a href="<?php echo _url ('index', 'index'); ?>"><?php echo Minz_Translate::t ('back_to_rss_feeds'); ?></a>
+	<a href="<?php echo _url('index', 'index'); ?>"><?php echo _t('back_to_rss_feeds'); ?></a>
 
 	<form method="post" action="<?php echo _url('importExport', 'import'); ?>" enctype="multipart/form-data">
-		<legend><?php echo Minz_Translate::t ('import'); ?></legend>
+		<legend><?php echo _t('import'); ?></legend>
 		<div class="form-group">
-			<label class="group-name" for="file"><?php echo Minz_Translate::t ('file_to_import'); ?></label>
+			<label class="group-name" for="file">
+				<?php echo extension_loaded('zip') ? _t('file_to_import') : _t('file_to_import_no_zip'); ?>
+			</label>
 			<div class="group-controls">
 				<input type="file" name="file" id="file" />
 			</div>
@@ -14,27 +16,34 @@
 
 		<div class="form-group form-actions">
 			<div class="group-controls">
-				<button type="submit" class="btn btn-important"><?php echo Minz_Translate::t ('import'); ?></button>
+				<button type="submit" class="btn btn-important"><?php echo _t('import'); ?></button>
 			</div>
 		</div>
 	</form>
 
 	<?php if (count($this->feeds) > 0) { ?>
 	<form method="post" action="<?php echo _url('importExport', 'export'); ?>">
-		<legend><?php echo Minz_Translate::t ('export'); ?></legend>
+		<legend><?php echo _t('export'); ?></legend>
 		<div class="form-group">
 			<div class="group-controls">
 				<label class="checkbox" for="export_opml">
 					<input type="checkbox" name="export_opml" id="export_opml" value="1" checked="checked" />
-					<?php echo Minz_Translate::t ('export_opml'); ?>
+					<?php echo _t('export_opml'); ?>
 				</label>
 
 				<label class="checkbox" for="export_starred">
-					<input type="checkbox" name="export_starred" id="export_starred" value="1" checked="checked" />
-					<?php echo Minz_Translate::t ('export_starred'); ?>
+					<input type="checkbox" name="export_starred" id="export_starred" value="1" <?php echo extension_loaded('zip') ? 'checked="checked"' : ''; ?> />
+					<?php echo _t('export_starred'); ?>
 				</label>
 
-				<select name="export_feeds[]" size="<?php echo min(10, count($this->feeds)); ?>" multiple="multiple">
+				<?php
+					$select_args = '';
+					if (extension_loaded('zip')) {
+						$select_args = ' size="' . min(10, count($this->feeds)) .'" multiple="multiple"';
+					}
+				?>
+				<select name="export_feeds[]"<?php echo $select_args; ?>>
+					<?php echo extension_loaded('zip') ? '' : '<option></option>'; ?>
 					<?php foreach ($this->feeds as $feed) { ?>
 					<option value="<?php echo $feed->id(); ?>"><?php echo $feed->name(); ?></option>
 					<?php } ?>
@@ -44,7 +53,7 @@
 
 		<div class="form-group form-actions">
 			<div class="group-controls">
-				<button type="submit" class="btn btn-important"><?php echo Minz_Translate::t ('export'); ?></button>
+				<button type="submit" class="btn btn-important"><?php echo _t('export'); ?></button>
 			</div>
 		</div>
 	</form>

+ 15 - 8
app/views/index/formLogin.phtml

@@ -1,32 +1,39 @@
 <div class="prompt">
-	<h1><?php echo Minz_Translate::t('login'); ?></h1><?php
+	<h1><?php echo _t('login'); ?></h1><?php
 
 	switch (Minz_Configuration::authType()) {
 	case 'form':
 	?><form id="loginForm" method="post" action="<?php echo _url('index', 'formLogin'); ?>">
 		<div>
-			<label for="username"><?php echo Minz_Translate::t('username'); ?></label>
+			<label for="username"><?php echo _t('username'); ?></label>
 			<input type="text" id="username" name="username" size="16" required="required" maxlength="16" pattern="[0-9a-zA-Z]{1,16}" autofocus="autofocus" />
 		</div>
 		<div>
-			<label for="passwordPlain"><?php echo Minz_Translate::t('password'); ?></label>
+			<label for="passwordPlain"><?php echo _t('password'); ?></label>
 				<input type="password" id="passwordPlain" required="required" />
 				<input type="hidden" id="challenge" name="challenge" /><br />
-				<noscript><strong><?php echo Minz_Translate::t('javascript_should_be_activated'); ?></strong></noscript>
+				<noscript><strong><?php echo _t('javascript_should_be_activated'); ?></strong></noscript>
 		</div>
 		<div>
-			<button id="loginButton" type="submit" class="btn btn-important"><?php echo Minz_Translate::t('login'); ?></button>
+			<label class="checkbox" for="keep_logged_in">
+				<input type="checkbox" name="keep_logged_in" id="keep_logged_in" value="1" />
+				<?php echo _t('keep_logged_in'); ?>
+			</label>
+			<br />
+		</div>
+		<div>
+			<button id="loginButton" type="submit" class="btn btn-important"><?php echo _t('login'); ?></button>
 		</div>
 	</form><?php
 		break;
 
 	case 'persona':
 		?><p>
-			<?php echo FreshRSS_Themes::icon('login'); ?>
-			<a class="signin" href="#"><?php echo Minz_Translate::t('login_with_persona'); ?></a>
+			<?php echo _i('login'); ?>
+			<a class="signin" href="#"><?php echo _t('login_with_persona'); ?></a>
 		</p><?php
 		break;
 	} ?>
 
-	<p><a href="<?php echo _url('index', 'about'); ?>"><?php echo Minz_Translate::t('about_freshrss'); ?></a></p>
+	<p><a href="<?php echo _url('index', 'about'); ?>"><?php echo _t('about_freshrss'); ?></a></p>
 </div>

+ 10 - 11
app/views/javascript/actualize.phtml

@@ -1,25 +1,24 @@
 "use strict";
-var feeds = [<?php
-	foreach ($this->feeds as $feed) {
-		echo "'", Minz_Url::display(array('c' => 'feed', 'a' => 'actualize', 'params' => array('id' => $feed->id(), 'ajax' => '1')), 'php'), "',\n";
-	}
-	?>],
+var feeds = [<?php foreach ($this->feeds as $feed) { ?>{<?php
+	?>url: "<?php echo Minz_Url::display(array('c' => 'feed', 'a' => 'actualize', 'params' => array('id' => $feed->id(), 'ajax' => '1')), 'php'); ?>",<?php
+	?>title: "<?php echo $feed->name(); ?>"<?php
+?>},<?php } ?>],
 	feed_processed = 0,
 	feed_count = feeds.length;
 
 function initProgressBar(init) {
 	if (init) {
 		$("body").after("\<div id=\"actualizeProgress\" class=\"notification good\">\
-			<?php echo _t('refresh'); ?> <span class=\"progress\">0 / " + feed_count + "</span><br />\
-			<progress id=\"actualizeProgressBar\" value=\"0\" max=\"" + feed_count + "\"></progress>\
+			<?php echo _t('refresh'); ?><br /><span class=\"title\">/</span><br />\
+			<span class=\"progress\">0 / " + feed_count + "</span>\
 		</div>");
 	} else {
 		window.location.reload();
 	}
 }
-function updateProgressBar(i) {
-	$("#actualizeProgressBar").val(i);
+function updateProgressBar(i, title_feed) {
 	$("#actualizeProgress .progress").html(i + " / " + feed_count);
+	$("#actualizeProgress .title").html(title_feed);
 }
 
 function updateFeeds() {
@@ -43,10 +42,10 @@ function updateFeed() {
 
 	$.ajax({
 		type: 'POST',
-		url: feed,
+		url: feed['url'],
 	}).complete(function (data) {
 		feed_processed++;
-		updateProgressBar(feed_processed);
+		updateProgressBar(feed_processed, feed['title']);
 
 		if (feed_processed === feed_count) {
 			initProgressBar(false);

+ 13 - 7
app/views/stats/idle.phtml

@@ -1,19 +1,25 @@
 <?php $this->partial('aside_stats'); ?>
 
 <div class="post content">
-	<a href="<?php echo _url ('index', 'index'); ?>"><?php echo _t ('back_to_rss_feeds'); ?></a>
+	<a href="<?php echo _url('index', 'index'); ?>"><?php echo _t('back_to_rss_feeds'); ?></a>
 
-	<h1><?php echo _t ('stats_idle'); ?></h1>
+	<h1><?php echo _t('stats_idle'); ?></h1>
 
-	<?php foreach ($this->idleFeeds as $period => $feeds){ ?>
+	<?php
+		foreach ($this->idleFeeds as $period => $feeds) {
+			if (!empty($feeds)) {
+	?>
 		<div class="stat">
-			<h2><?php echo _t ($period); ?></h2>
+			<h2><?php echo _t($period); ?></h2>
 
 			<ul>
-				<?php foreach ($feeds as $feed){ ?>
-					<li><?php echo $feed; ?></li>
+				<?php foreach ($feeds as $feed) { ?>
+					<li><a href="<?php echo _url('configure', 'feed', 'id', $feed['id']); ?>" title="<?php echo date('Y-m-d', $feed['last_date']); ?>"><?php echo $feed['name']; ?></a></li>
 				<?php } ?>
 			</ul>
 		</div>
-	<?php } ?>
+	<?php
+			}
+		}
+	?>
 </div>

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

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

+ 114 - 0
app/views/stats/repartition.phtml

@@ -0,0 +1,114 @@
+<?php $this->partial('aside_stats'); ?>
+
+<div class="post content">
+	<a href="<?php echo _url('index', 'index'); ?>"><?php echo _t('back_to_rss_feeds'); ?></a>
+
+	<h1><?php echo _t('stats_repartition'); ?></h1>
+
+	<select id="feed_select">
+		<option data-url="<?php echo _url('stats', 'repartition')?>"><?php echo _t('all_feeds')?></option>
+	<?php foreach ($this->categories as $category) {
+		$feeds = $category->feeds();
+		if (!empty($feeds)) {
+			echo '<optgroup label=', $category->name(), '>';
+			foreach ($feeds as $feed) {
+				if ($this->feed && $feed->id() == $this->feed->id()){
+					echo '<option value ="', $feed->id(), '" selected data-url="', _url('stats', 'repartition', 'id', $feed->id()), '">', $feed->name(), '</option>';
+				} else {
+					echo '<option value ="', $feed->id(), '" data-url="', _url('stats', 'repartition', 'id', $feed->id()), '">', $feed->name(), '</option>';
+				}
+			}
+			echo '</optgroup>';
+		}
+	}?>
+	</select>
+
+	<?php if ($this->feed) {?>
+		<a href="<?php echo _url('configure', 'feed', 'id', $this->feed->id()); ?>">
+			<?php echo _t('administration'); ?>
+		</a>
+	<?php }?>
+
+	<div class="stat">
+		<h2><?php echo _t('stats_entry_per_hour'); ?></h2>
+		<div id="statsEntryPerHour" style="height: 300px"></div>
+	</div>
+
+	<div class="stat">
+		<h2><?php echo _t('stats_entry_per_day_of_week'); ?></h2>
+		<div id="statsEntryPerDayOfWeek" style="height: 300px"></div>
+	</div>
+
+	<div class="stat">
+		<h2><?php echo _t('stats_entry_per_month'); ?></h2>
+		<div id="statsEntryPerMonth" style="height: 300px"></div>
+	</div>
+</div>
+
+<script>
+"use strict";
+function initStats() {
+	if (!window.Flotr) {
+		if (window.console) {
+			console.log('FreshRSS waiting for Flotr…');
+		}
+		window.setTimeout(initStats, 50);
+		return;
+	}
+	// Entry per hour
+	Flotr.draw(document.getElementById('statsEntryPerHour'),
+		[<?php echo $this->repartitionHour ?>],
+		{
+			grid: {verticalLines: false},
+			bars: {horizontal: false, show: true},
+			xaxis: {noTicks: 23,
+				tickFormatter: function(x) {
+					var x = parseInt(x);
+					return x + 1;
+				},
+				min: -0.9,
+				max: 23.9,
+				tickDecimals: 0},
+			yaxis: {min: 0},
+			mouse: {relative: true, track: true, trackDecimals: 0, trackFormatter: function(obj) {return numberFormat(obj.y);}}
+		});
+	// Entry per day of week
+	Flotr.draw(document.getElementById('statsEntryPerDayOfWeek'),
+		[<?php echo $this->repartitionDayOfWeek ?>],
+		{
+			grid: {verticalLines: false},
+			bars: {horizontal: false, show: true},
+			xaxis: {noTicks: 6,
+				tickFormatter: function(x) {
+					var x = parseInt(x),
+					    days = <?php echo $this->days?>;
+					return days[x];
+				},
+				min: -0.9,
+				max: 6.9,
+				tickDecimals: 0},
+			yaxis: {min: 0},
+			mouse: {relative: true, track: true, trackDecimals: 0, trackFormatter: function(obj) {return numberFormat(obj.y);}}
+		});
+	// Entry per month
+	Flotr.draw(document.getElementById('statsEntryPerMonth'),
+		[<?php echo $this->repartitionMonth ?>],
+		{
+			grid: {verticalLines: false},
+			bars: {horizontal: false, show: true},
+			xaxis: {noTicks: 12,
+				tickFormatter: function(x) {
+					var x = parseInt(x),
+					    months = <?php echo $this->months?>;
+					return months[(x - 1)];
+				},
+				min: 0.1,
+				max: 12.9,
+				tickDecimals: 0},
+			yaxis: {min: 0},
+			mouse: {relative: true, track: true, trackDecimals: 0, trackFormatter: function(obj) {return numberFormat(obj.y);}}
+		});
+
+}
+initStats();
+</script>

+ 1 - 0
data/tokens/.gitignore

@@ -0,0 +1 @@
+*.txt

+ 13 - 0
data/tokens/index.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-GB" lang="en-GB">
+<head>
+<meta charset="UTF-8" />
+<meta http-equiv="Refresh" content="0; url=/" />
+<title>Redirection</title>
+<meta name="robots" content="noindex" />
+</head>
+
+<body>
+<p><a href="/">Redirection</a></p>
+</body>
+</html>

+ 14 - 3
lib/Minz/Helper.php

@@ -12,11 +12,22 @@ class Minz_Helper {
 	 * Annule les effets des magic_quotes pour une variable donnée
 	 * @param $var variable à traiter (tableau ou simple variable)
 	 */
-	public static function stripslashes_r ($var) {
-		if (is_array ($var)){
-			return array_map (array ('Helper', 'stripslashes_r'), $var);
+	public static function stripslashes_r($var) {
+		if (is_array($var)){
+			return array_map(array('Minz_Helper', 'stripslashes_r'), $var);
 		} else {
 			return stripslashes($var);
 		}
 	}
+
+	/**
+	 * Wrapper for htmlspecialchars.
+	 * Force UTf-8 value and can be used on array too.
+	 */
+	public static function htmlspecialchars_utf8($var) {
+		if (is_array($var)) {
+			return array_map(array('Minz_Helper', 'htmlspecialchars_utf8'), $var);
+		}
+		return htmlspecialchars($var, ENT_COMPAT, 'UTF-8');
+	}
 }

+ 12 - 8
lib/Minz/ModelPdo.php

@@ -33,8 +33,8 @@ class Minz_ModelPdo {
 	 * Créé la connexion à la base de données à l'aide des variables
 	 * HOST, BASE, USER et PASS définies dans le fichier de configuration
 	 */
-	public function __construct() {
-		if (self::$useSharedBd && self::$sharedBd != null) {
+	public function __construct($currentUser = null) {
+		if (self::$useSharedBd && self::$sharedBd != null && $currentUser === null) {
 			$this->bd = self::$sharedBd;
 			$this->prefix = self::$sharedPrefix;
 			return;
@@ -42,6 +42,10 @@ class Minz_ModelPdo {
 
 		$db = Minz_Configuration::dataBase();
 
+		if ($currentUser === null) {
+			$currentUser = Minz_Session::param('currentUser', '_');
+		}
+
 		try {
 			$type = $db['type'];
 			if ($type === 'mysql') {
@@ -51,9 +55,9 @@ class Minz_ModelPdo {
 				$driver_options = array(
 					PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
 				);
-				$this->prefix = $db['prefix'] . Minz_Session::param('currentUser', '_') . '_';
+				$this->prefix = $db['prefix'] . $currentUser . '_';
 			} elseif ($type === 'sqlite') {
-				$string = 'sqlite:' . DATA_PATH . '/' . Minz_Session::param('currentUser', '_') . '.sqlite';
+				$string = 'sqlite:' . DATA_PATH . '/' . $currentUser . '.sqlite';
 				$driver_options = array(
 					//PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
 				);
@@ -67,7 +71,7 @@ class Minz_ModelPdo {
 			self::$sharedDbType = $type;
 			self::$sharedPrefix = $this->prefix;
 
-			$this->bd = new FreshPDO(
+			$this->bd = new MinzPDO(
 				$string,
 				$db['user'],
 				$db['password'],
@@ -98,7 +102,7 @@ class Minz_ModelPdo {
 	}
 }
 
-class FreshPDO extends PDO {
+class MinzPDO extends PDO {
 	private static function check($statement) {
 		if (preg_match('/^(?:UPDATE|INSERT|DELETE)/i', $statement)) {
 			invalidateHttpCache();
@@ -106,12 +110,12 @@ class FreshPDO extends PDO {
 	}
 
 	public function prepare($statement, $driver_options = array()) {
-		FreshPDO::check($statement);
+		MinzPDO::check($statement);
 		return parent::prepare($statement, $driver_options);
 	}
 
 	public function exec($statement) {
-		FreshPDO::check($statement);
+		MinzPDO::check($statement);
 		return parent::exec($statement);
 	}
 }

+ 69 - 49
lib/Minz/Request.php

@@ -10,7 +10,7 @@
 class Minz_Request {
 	private static $controller_name = '';
 	private static $action_name = '';
-	private static $params = array ();
+	private static $params = array();
 
 	private static $default_controller_name = 'index';
 	private static $default_action_name = 'index';
@@ -18,59 +18,53 @@ class Minz_Request {
 	/**
 	 * Getteurs
 	 */
-	public static function controllerName () {
+	public static function controllerName() {
 		return self::$controller_name;
 	}
-	public static function actionName () {
+	public static function actionName() {
 		return self::$action_name;
 	}
-	public static function params () {
+	public static function params() {
 		return self::$params;
 	}
-	static function htmlspecialchars_utf8 ($p) {
-		if (is_array($p)) {
-			return array_map('self::htmlspecialchars_utf8', $p);
-		}
-		return htmlspecialchars($p, ENT_COMPAT, 'UTF-8');
-	}
-	public static function param ($key, $default = false, $specialchars = false) {
-		if (isset (self::$params[$key])) {
+	public static function param($key, $default = false, $specialchars = false) {
+		if (isset(self::$params[$key])) {
 			$p = self::$params[$key];
-			if(is_object($p) || $specialchars) {
+			if (is_object($p) || $specialchars) {
 				return $p;
 			} else {
-				return self::htmlspecialchars_utf8($p);
+				return Minz_Helper::htmlspecialchars_utf8($p);
 			}
 		} else {
 			return $default;
 		}
 	}
-	public static function defaultControllerName () {
+	public static function defaultControllerName() {
 		return self::$default_controller_name;
 	}
-	public static function defaultActionName () {
+	public static function defaultActionName() {
 		return self::$default_action_name;
 	}
 
 	/**
 	 * Setteurs
 	 */
-	public static function _controllerName ($controller_name) {
+	public static function _controllerName($controller_name) {
 		self::$controller_name = $controller_name;
 	}
-	public static function _actionName ($action_name) {
+	public static function _actionName($action_name) {
 		self::$action_name = $action_name;
 	}
-	public static function _params ($params) {
+	public static function _params($params) {
 		if (!is_array($params)) {
-			$params = array ($params);
+			$params = array($params);
 		}
 
 		self::$params = $params;
 	}
-	public static function _param ($key, $value = false) {
+	public static function _param($key, $value = false) {
 		if ($value === false) {
-			unset (self::$params[$key]);
+			unset(self::$params[$key]);
 		} else {
 			self::$params[$key] = $value;
 		}
@@ -79,14 +73,14 @@ class Minz_Request {
 	/**
 	 * Initialise la Request
 	 */
-	public static function init () {
-		self::magicQuotesOff ();
+	public static function init() {
+		self::magicQuotesOff();
 	}
 
 	/**
 	 * Retourn le nom de domaine du site
 	 */
-	public static function getDomainName () {
+	public static function getDomainName() {
 		return $_SERVER['HTTP_HOST'];
 	}
 
@@ -94,7 +88,7 @@ class Minz_Request {
 	 * Détermine la base de l'url
 	 * @return la base de l'url
 	 */
-	public static function getBaseUrl () {
+	public static function getBaseUrl() {
 		$defaultBaseUrl = Minz_Configuration::baseUrl();
 		if (!empty($defaultBaseUrl)) {
 			return $defaultBaseUrl;
@@ -109,13 +103,13 @@ class Minz_Request {
 	 * Récupère l'URI de la requête
 	 * @return l'URI
 	 */
-	public static function getURI () {
-		if (isset ($_SERVER['REQUEST_URI'])) {
-			$base_url = self::getBaseUrl ();
+	public static function getURI() {
+		if (isset($_SERVER['REQUEST_URI'])) {
+			$base_url = self::getBaseUrl();
 			$uri = $_SERVER['REQUEST_URI'];
 
-			$len_base_url = strlen ($base_url);
-			$real_uri = substr ($uri, $len_base_url);
+			$len_base_url = strlen($base_url);
+			$real_uri = substr($uri, $len_base_url);
 		} else {
 			$real_uri = '';
 		}
@@ -129,16 +123,16 @@ class Minz_Request {
 	 * @param $redirect si vrai, force la redirection http
 	 *                > sinon, le dispatcher recharge en interne
 	 */
-	public static function forward ($url = array (), $redirect = false) {
-		$url = Minz_Url::checkUrl ($url);
+	public static function forward($url = array(), $redirect = false) {
+		$url = Minz_Url::checkUrl($url);
 
 		if ($redirect) {
-			header ('Location: ' . Minz_Url::display ($url, 'php'));
-			exit ();
+			header('Location: ' . Minz_Url::display($url, 'php'));
+			exit();
 		} else {
-			self::_controllerName ($url['c']);
-			self::_actionName ($url['a']);
-			self::_params (array_merge (
+			self::_controllerName($url['c']);
+			self::_actionName($url['a']);
+			self::_params(array_merge(
 				self::$params,
 				$url['params']
 			));
@@ -146,6 +140,31 @@ class Minz_Request {
 		}
 	}
 
+
+	/**
+	 * Wrappers good notifications + redirection
+	 * @param $msg notification content
+	 * @param $url url array to where we should be forwarded
+	 */
+	public static function good($msg, $url = array()) {
+		Minz_Session::_param('notification', array(
+			'type' => 'good',
+			'content' => $msg
+		));
+
+		Minz_Request::forward($url, true);
+	}
+
+	public static function bad($msg, $url = array()) {
+		Minz_Session::_param('notification', array(
+			'type' => 'bad',
+			'content' => $msg
+		));
+
+		Minz_Request::forward($url, true);
+	}
+
+
 	/**
 	 * Permet de récupérer une variable de type $_GET
 	 * @param $param nom de la variable
@@ -154,10 +173,10 @@ class Minz_Request {
 	 *         $_GET si $param = false
 	 *         $default si $_GET[$param] n'existe pas
 	 */
-	public static function fetchGET ($param = false, $default = false) {
+	public static function fetchGET($param = false, $default = false) {
 		if ($param === false) {
 			return $_GET;
-		} elseif (isset ($_GET[$param])) {
+		} elseif (isset($_GET[$param])) {
 			return $_GET[$param];
 		} else {
 			return $default;
@@ -172,10 +191,10 @@ class Minz_Request {
 	 *         $_POST si $param = false
 	 *         $default si $_POST[$param] n'existe pas
 	 */
-	public static function fetchPOST ($param = false, $default = false) {
+	public static function fetchPOST($param = false, $default = false) {
 		if ($param === false) {
 			return $_POST;
-		} elseif (isset ($_POST[$param])) {
+		} elseif (isset($_POST[$param])) {
 			return $_POST[$param];
 		} else {
 			return $default;
@@ -188,15 +207,16 @@ class Minz_Request {
 	 *   $_POST
 	 *   $_COOKIE
 	 */
-	private static function magicQuotesOff () {
-		if (get_magic_quotes_gpc ()) {
-			$_GET = Minz_Helper::stripslashes_r ($_GET);
-			$_POST = Minz_Helper::stripslashes_r ($_POST);
-			$_COOKIE = Minz_Helper::stripslashes_r ($_COOKIE);
+	private static function magicQuotesOff() {
+		if (get_magic_quotes_gpc()) {
+			$_GET = Minz_Helper::stripslashes_r($_GET);
+			$_POST = Minz_Helper::stripslashes_r($_POST);
+			$_COOKIE = Minz_Helper::stripslashes_r($_COOKIE);
 		}
 	}
 
-	public static function isPost () {
-		return $_SERVER['REQUEST_METHOD'] === 'POST';
+	public static function isPost() {
+		return isset($_SERVER['REQUEST_METHOD']) &&
+			$_SERVER['REQUEST_METHOD'] === 'POST';
 	}
 }

+ 48 - 26
lib/Minz/Session.php

@@ -2,28 +2,20 @@
 
 /**
  * La classe Session gère la session utilisateur
- * C'est un singleton
  */
 class Minz_Session {
-	/**
-	 * $session stocke les variables de session
-	 */
-	private static $session = array ();	//TODO: Try to avoid having another local copy
-
 	/**
 	 * Initialise la session, avec un nom
-	 * Le nom de session est utilisé comme nom pour les cookies et les URLs (i.e. PHPSESSID).
+	 * Le nom de session est utilisé comme nom pour les cookies et les URLs(i.e. PHPSESSID).
 	 * Il ne doit contenir que des caractères alphanumériques ; il doit être court et descriptif
 	 */
-	public static function init ($name) {
-		// démarre la session
-		session_name ($name);
-		session_set_cookie_params (0, dirname(empty($_SERVER['REQUEST_URI']) ? '/' : dirname($_SERVER['REQUEST_URI'])), null, false, true);
-		session_start ();
+	public static function init($name) {
+		$cookie = session_get_cookie_params();
+		self::keepCookie($cookie['lifetime']);
 
-		if (isset ($_SESSION)) {
-			self::$session = $_SESSION;
-		}
+		// démarre la session
+		session_name($name);
+		session_start();
 	}
 
 
@@ -32,8 +24,8 @@ class Minz_Session {
 	 * @param $p le paramètre à récupérer
 	 * @return la valeur de la variable de session, false si n'existe pas
 	 */
-	public static function param ($p, $default = false) {
-		return isset(self::$session[$p]) ? self::$session[$p] : $default;
+	public static function param($p, $default = false) {
+		return isset($_SESSION[$p]) ? $_SESSION[$p] : $default;
 	}
 
 
@@ -42,13 +34,11 @@ class Minz_Session {
 	 * @param $p le paramètre à créer ou modifier
 	 * @param $v la valeur à attribuer, false pour supprimer
 	 */
-	public static function _param ($p, $v = false) {
+	public static function _param($p, $v = false) {
 		if ($v === false) {
-			unset ($_SESSION[$p]);
-			unset (self::$session[$p]);
+			unset($_SESSION[$p]);
 		} else {
 			$_SESSION[$p] = $v;
-			self::$session[$p] = $v;
 		}
 	}
 
@@ -57,15 +47,47 @@ class Minz_Session {
 	 * Permet d'effacer une session
 	 * @param $force si à false, n'efface pas le paramètre de langue
 	 */
-	public static function unset_session ($force = false) {
-		$language = self::param ('language');
+	public static function unset_session($force = false) {
+		$language = self::param('language');
 
 		session_destroy();
-		self::$session = array ();
+		$_SESSION = array();
 
 		if (!$force) {
-			self::_param ('language', $language);
-			Minz_Translate::reset ();
+			self::_param('language', $language);
+			Minz_Translate::reset();
 		}
 	}
+
+
+	/**
+	 * Spécifie la durée de vie des cookies
+	 * @param $l la durée de vie
+	 */
+	public static function keepCookie($l) {
+		$cookie_dir = empty($_SERVER['REQUEST_URI']) ? '' : $_SERVER['REQUEST_URI'];
+		session_set_cookie_params($l, $cookie_dir, '', false, true);
+	}
+
+
+	/**
+	 * Régénère un id de session.
+	 * Utile pour appeler session_set_cookie_params après session_start()
+	 */
+	public static function regenerateID() {
+		session_regenerate_id(true);
+	}
+
+	public static function deleteLongTermCookie($name) {
+		setcookie($name, '', 1, '', '', false, true);
+	}
+
+	public static function setLongTermCookie($name, $value, $expire) {
+		setcookie($name, $value, $expire, '', '', false, true);
+	}
+
+	public static function getLongTermCookie($name) {
+		return isset($_COOKIE[$name]) ? $_COOKIE[$name] : null;
+	}
+
 }

+ 1 - 1
lib/Minz/Translate.php

@@ -75,5 +75,5 @@ function _t($key) {
 	unset($args[0]);
 	array_unshift($args, $key);
 
-	return call_user_func_array("Minz_Translate::t", $args);
+	return call_user_func_array('Minz_Translate::t', $args);
 }

+ 6 - 18
lib/lib_rss.php

@@ -121,10 +121,10 @@ function customSimplePie() {
 		'onmouseover', 'onmousemove', 'onmouseout', 'onfocus', 'onblur',
 		'onkeypress', 'onkeydown', 'onkeyup', 'onselect', 'onchange', 'seamless')));
 	$simplePie->add_attributes(array(
-		'img' => array('lazyload' => ''),	//http://www.w3.org/TR/resource-priorities/
-		'audio' => array('preload' => 'none'),
-		'iframe' => array('postpone' => '', 'sandbox' => 'allow-scripts allow-same-origin'),
-		'video' => array('postpone' => '', 'preload' => 'none'),
+		'img' => array('lazyload' => '', 'postpone' => ''),	//http://www.w3.org/TR/resource-priorities/
+		'audio' => array('lazyload' => '', 'postpone' => '', 'preload' => 'none'),
+		'iframe' => array('lazyload' => '', 'postpone' => '', 'sandbox' => 'allow-scripts allow-same-origin'),
+		'video' => array('lazyload' => '', 'postpone' => '', 'preload' => 'none'),
 	));
 	$simplePie->set_url_replacements(array(
 		'a' => 'href',
@@ -183,16 +183,8 @@ function get_content_by_parsing ($url, $path) {
  */
 function lazyimg($content) {
 	return preg_replace(
-		'/<img([^>]+?)src=[\'"]([^"\']+)[\'"]([^>]*)>/i',
-		'<img$1src="' . Minz_Url::display('/themes/icons/grey.gif') . '" data-original="$2"$3>',
-		$content
-	);
-}
-
-function lazyIframe($content) {
-	return preg_replace(
-		'/<iframe([^>]+?)src=[\'"]([^"\']+)[\'"]([^>]*)>/i',
-		'<iframe$1src="about:blank" data-original="$2"$3>',
+		'/<((?:img|iframe)[^>]+?)src=[\'"]([^"\']+)[\'"]([^>]*)>/i',
+		'<$1src="' . Minz_Url::display('/themes/icons/grey.gif') . '" data-original="$2"$3>',
 		$content
 	);
 }
@@ -238,7 +230,3 @@ function cryptAvailable() {
 	}
 	return false;
 }
-
-function html_chars_utf8($str) {
-	return htmlspecialchars($str, ENT_COMPAT, 'UTF-8');
-}

+ 1 - 0
p/api/greader.php

@@ -135,6 +135,7 @@ function checkCompatibility() {
 	}
 	if ((!array_key_exists('HTTP_AUTHORIZATION', $_SERVER)) &&	//Apache mod_rewrite trick should be fine
 		(empty($_SERVER['SERVER_SOFTWARE']) || (stripos($_SERVER['SERVER_SOFTWARE'], 'nginx') === false)) &&	//nginx should be fine
+		(empty($_SERVER['SERVER_SOFTWARE']) || (stripos($_SERVER['SERVER_SOFTWARE'], 'lighttpd') === false)) &&	//lighttpd should be fine
 		((!function_exists('getallheaders')) || (stripos(php_sapi_name(), 'cgi') !== false))) {	//Main problem is Apache/CGI mode
 		die('FAIL getallheaders! (probably)');
 	}

+ 1 - 0
p/i/.gitignore

@@ -0,0 +1 @@
+.htaccess

Fișier diff suprimat deoarece este prea mare
+ 0 - 14
p/scripts/jquery.lazyload.min.js


+ 122 - 24
p/scripts/main.js

@@ -69,6 +69,10 @@ function incUnreadsFeed(article, feed_id, nb) {
 		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);
+		if (elem) {
+			elem.setAttribute('data-unread', feed_unreads + nb);
+		}
 	}
 
 	//Update unread: category
@@ -76,6 +80,10 @@ function incUnreadsFeed(article, feed_id, nb) {
 	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);
+		if (elem) {
+			elem.setAttribute('data-unread', feed_unreads + nb);
+		}
 	}
 
 	//Update unread: all
@@ -98,16 +106,16 @@ function incUnreadsFeed(article, feed_id, nb) {
 
 	var isCurrentView = false;
 	//Update unread: title
-	document.title = document.title.replace(/^((?:\([ 0-9]+\) )?)(.*? · )((?:\([ 0-9]+\) )?)/, function (m, p1, p2, p3) {
+	document.title = document.title.replace(/^((?:\([ 0-9]+\) )?)/, function (m, p1) {
 		var $feed = $('#' + feed_id);
 		if (article || ($feed.closest('.active').length > 0 && $feed.siblings('.active').length === 0)) {
 			isCurrentView = true;
-			return incLabel(p1, nb, true) + p2 + incLabel(p3, feed_priority > 0 ? nb : 0, true);
+			return incLabel(p1, nb, true);
 		} else if ($('.all.active').length > 0) {
 			isCurrentView = feed_priority > 0;
-			return incLabel(p1, feed_priority > 0 ? nb : 0, true) + p2 + incLabel(p3, feed_priority > 0 ? nb : 0, true);
+			return incLabel(p1, feed_priority > 0 ? nb : 0, true);
 		} else {
-			return p1 + p2 + incLabel(p3, feed_priority > 0 ? nb : 0, true);
+			return p1;
 		}
 	});
 	return isCurrentView;
@@ -152,6 +160,7 @@ function mark_read(active, only_not_read) {
 		$r.find('.icon').replaceWith(data.icon);
 
 		incUnreadsFeed(active, feed_id, inc);
+		faviconNbUnread();
 
 		pending_feeds.splice(index_pending, 1);
 	});
@@ -361,7 +370,12 @@ function last_category() {
 
 function collapse_entry() {
 	isCollapsed = !isCollapsed;
-	$(".flux.current").toggleClass("active");
+
+	var flux_current = $(".flux.current");
+	flux_current.toggleClass("active");
+	if (isCollapsed) {
+		mark_read(flux_current, true);
+	}
 }
 
 function auto_share(key) {
@@ -407,21 +421,7 @@ function inMarkViewport(flux, box_to_follow, relative_follow) {
 	return (windowBot >= begin && bot >= windowBot);
 }
 
-function init_lazyload() {
-	if ($.fn.lazyload) {
-		if (is_global_mode()) {
-			$(".flux_content img").lazyload({
-				container: $("#panel")
-			});
-		} else {
-			$(".flux_content img").lazyload();
-		}
-	}
-}
-
 function init_posts() {
-	init_lazyload();
-
 	var box_to_follow = $(window),
 		relative_follow = false;
 	if (is_global_mode()) {
@@ -663,7 +663,7 @@ function init_stream(divStream) {
 
 	if (auto_mark_site) {
 		divStream.on('click', '.flux .link > a', function () {
-			mark_read($(this).parent().parent().parent(), true);
+			mark_read($(this).parents(".flux"), true);
 		});
 	}
 }
@@ -768,18 +768,66 @@ function init_notifications() {
 }
 // </notification>
 
+// <notifs html5>
+var notifs_html5_permission = 'denied';
+
+function notifs_html5_is_supported() {
+	return window.Notification !== undefined;
+}
+
+function notifs_html5_ask_permission() {
+	window.Notification.requestPermission(function () {
+		notifs_html5_permission = window.Notification.permission;
+	});
+}
+
+function notifs_html5_show(nb) {
+	if (notifs_html5_permission !== "granted") {
+		return
+	}
+
+	var notification = new window.Notification(str_notif_title_articles, {
+		icon: "../themes/icons/favicon-256.png",
+		body: str_notif_body_articles.replace("\d", nb)
+	});
+
+	notification.onclick = function() {
+		window.location.reload();
+	}
+}
+
+function init_notifs_html5() {
+	if (!notifs_html5_is_supported()) {
+		return;
+	}
+
+	notifs_html5_permission = notifs_html5_ask_permission();
+}
+// </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),
 				feed_unreads = elem ? str2int(elem.getAttribute('data-unread')) : 0;
+
 			if ((incUnreadsFeed(null, feed_id, nbUnreads - feed_unreads) || isAll) &&	//Update of current view?
 				(nbUnreads - feed_unreads > 0)) {
 				$('#new-article').show();
+				new_articles = true;
 			};
 		});
+
+		var nb_unreads = str2int($('.category.all>a').attr('data-unread'));
+
+		if (nb_unreads > 0 && new_articles) {
+			faviconNbUnread(nb_unreads);
+			notifs_html5_show(nb_unreads);
+		}
 	});
 }
 
@@ -812,7 +860,6 @@ function load_more_posts() {
 		});
 
 		init_load_more(box_load_more);
-		init_lazyload();
 
 		$('#load_more').removeClass('loading');
 		load_more = false;
@@ -826,6 +873,12 @@ function focus_search() {
 function init_load_more(box) {
 	box_load_more = box;
 
+	if (!does_lazyload) {
+		$('img[postpone], audio[postpone], iframe[postpone], video[postpone]').each(function () {
+			this.removeAttribute('postpone');
+		});
+	}
+
 	var $next_link = $("#load_more");
 	if (!$next_link.length) {
 		// no more article to load
@@ -967,7 +1020,7 @@ function init_persona() {
 //</persona>
 
 function init_confirm_action() {
-	$('.confirm').click(function () {
+	$('body').on('click', '.confirm', function () {
 		return confirm(str_confirmation);
 	});
 }
@@ -1010,6 +1063,12 @@ function init_share_observers() {
 	});
 }
 
+function init_stats_observers() {
+	$('#feed_select').on('change', function(e) {
+		redirect($(this).find(':selected').data('url'));
+	});
+}
+
 function init_remove_observers() {
 	$('.post').on('click', 'a.remove', function(e) {
 		var remove_what = $(this).attr('data-remove');
@@ -1052,8 +1111,44 @@ function init_password_observers() {
 	});
 }
 
+function faviconNbUnread(n) {
+	if (typeof n === 'undefined') {
+		n = str2int($('.category.all>a').attr('data-unread'));
+	}
+	//http://remysharp.com/2010/08/24/dynamic-favicons/
+	var canvas = document.createElement('canvas'),
+		link = document.getElementById('favicon').cloneNode(true);
+	if (canvas.getContext && link) {
+		canvas.height = canvas.width = 16;
+		var img = document.createElement('img');
+		img.onload = function () {
+			var ctx = canvas.getContext('2d');
+			ctx.drawImage(this, 0, 0, canvas.width, canvas.height);
+			if (n > 0) {
+				var text = '';
+				if (n < 1000) {
+					text = n;
+				} else if (n < 100000) {
+					text = Math.floor(n / 1000) + 'k';
+				} else {
+					text = 'E' + Math.floor(Math.log10(n));
+				}
+				ctx.font = 'bold 9px "Arial", sans-serif';
+				ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
+				ctx.fillRect(0, 7, ctx.measureText(text).width, 9);
+				ctx.fillStyle = '#F00';
+				ctx.fillText(text, 0, canvas.height - 1);
+			}
+			link.href = canvas.toDataURL('image/png');
+			$('link[rel~=icon]').remove();
+			document.head.appendChild(link);
+		};
+		img.src = '../favicon.ico';
+	}
+}
+
 function init_all() {
-	if (!(window.$ && window.url_freshrss && ((!full_lazyload) || $.fn.lazyload))) {
+	if (!(window.$ && window.url_freshrss)) {
 		if (window.console) {
 			console.log('FreshRSS waiting for JS…');
 		}
@@ -1079,13 +1174,16 @@ function init_all() {
 		init_stream($stream);
 		init_nav_entries();
 		init_shortcuts();
+		faviconNbUnread();
 		init_print_action();
+		init_notifs_html5();
 		window.setInterval(refreshUnreads, 120000);
 	} else {
 		init_share_observers();
 		init_remove_observers();
 		init_feed_observers();
 		init_password_observers();
+		init_stats_observers();
 	}
 
 	if (window.console) {

+ 2 - 4
p/themes/Dark/dark.css

@@ -515,15 +515,13 @@ a.btn {
 .categories .feeds .item.empty.active {
 	background: #c95;
 }
-.categories .feeds .item.empty.active .feed {
-	color: #fff;
-}
 .categories .feeds .item.error .feed {
 	color: #a44;
 }
 .categories .feeds .item.error.active {
 	background: #a44;
 }
+.categories .feeds .item.empty.active .feed,
 .categories .feeds .item.error.active .feed {
 	color: #fff;
 }
@@ -570,7 +568,7 @@ a.btn {
 }
 .prompt form {
 	margin: 10px auto 20px auto;
-	width: 180px;
+	width: 200px;
 }
 .prompt input {
 	margin: 5px auto;

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

@@ -309,6 +309,9 @@ a.btn {
 	list-style: none;
 	margin: 0;
 }
+.state_unread li:not(.active)[data-unread="0"] {
+	display: none;
+}
 .category {
 	display: block;
 	overflow: hidden;

+ 6 - 5
p/themes/Flat/flat.css

@@ -492,10 +492,6 @@ a.btn {
 .categories .feeds .item.active {
 	background: #2980b9;
 }
-.categories .feeds .item.active .feed,
-.categories .feeds .item.empty.active .feed {
-	color: #fff;
-}
 .categories .feeds .item.empty.active {
 	background: #f39c12;
 }
@@ -508,6 +504,11 @@ a.btn {
 .categories .feeds .item.error .feed {
 	color: #bd362f;
 }
+.categories .feeds .item.active .feed,
+.categories .feeds .item.empty.active .feed,
+.categories .feeds .item.error.active .feed {
+	color: #fff;
+}
 .categories .feeds .item .feed {
 	margin: 0;
 	width: 165px;
@@ -551,7 +552,7 @@ a.btn {
 }
 .prompt form {
 	margin: 10px auto 20px auto;
-	width: 180px;
+	width: 200px;
 }
 .prompt input {
 	margin: 5px auto;

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

@@ -309,6 +309,9 @@ a.btn {
 	list-style: none;
 	margin: 0;
 }
+.state_unread li:not(.active)[data-unread="0"] {
+	display: none;
+}
 .category {
 	display: block;
 	overflow: hidden;

+ 11 - 9
p/themes/Origine/origine.css

@@ -540,21 +540,23 @@ a.btn {
 .categories .feeds .item.active {
 	background: #0062BE;
 }
-.categories .feeds .item.active .feed {
-	color: #fff;
-}
-.categories .feeds .item.empty .feed {
-	color: #e67e22;
-}
 .categories .feeds .item.empty.active {
 	background: #e67e22;
 }
-.categories .feeds .item.empty.active .feed {
-	color: #fff;
+.categories .feeds .item.error.active {
+	background: #BD362F;
+}
+.categories .feeds .item.empty .feed {
+	color: #e67e22;
 }
 .categories .feeds .item.error .feed {
 	color: #BD362F;
 }
+.categories .feeds .item.active .feed,
+.categories .feeds .item.empty.active .feed,
+.categories .feeds .item.error.active .feed {
+	color: #fff;
+}
 .categories .feeds .item .feed {
 	margin: 0;
 	width: 165px;
@@ -598,7 +600,7 @@ a.btn {
 }
 .prompt form {
 	margin: 10px auto 20px auto;
-	width: 180px;
+	width: 200px;
 }
 .prompt input {
 	margin: 5px auto;

+ 3 - 0
p/themes/Origine/template.css

@@ -309,6 +309,9 @@ a.btn {
 	list-style: none;
 	margin: 0;
 }
+.state_unread li:not(.active)[data-unread="0"] {
+	display: none;
+}
 .category {
 	display: block;
 	overflow: hidden;

+ 36 - 0
p/themes/Screwdriver/README.md

@@ -0,0 +1,36 @@
+Screwdriver 
+=======
+
+**C'est un cocktail! C'est chaud mais "fresh" à la fois. C'est... c'est... un thème pour l'agrégateur de flux RSS<a href="https://github.com/marienfressinaud/FreshRSS/" target="blank">FreshRSS</a>!!**
+En toute modestie, ce thème tue du chaton.
+
+![screenshot](https://github.com/misterair/Screwdriver/blob/master/screenshot.png)
+
+
+Installation
+-----------------
+1. Placez le dossier du thème dans ledossier /FreshRSS/p/themes/Screwdriver de votre FreshRSS;
+2. Allez dans les paramètres d'Affichage et changez de thème;
+3. Profitez de votre Screwdriver!
+4. Remontez les problèmes sur Github (facultatif mais fortement apprécié)
+
+
+
+Screwdriver est distribué sous license BeerWare:
+-----------------
+
+« LICENCE BEERWARE » (Révision 42):
+
+mister.air@gmail.com a créé ce fichier. Tant que vous conservez cet avertissement,
+
+vous pouvez faire ce que vous voulez de ce truc. Si on se rencontre un jour et
+
+que vous pensez que ce truc vaut le coup, vous pouvez me payer une bière en retour.
+
+*Mister aiR*
+
+
+
+
+
+

+ 5 - 0
p/themes/Screwdriver/icons/add.svg

@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16">
+<g transform="translate(-60.0002,-726)">
+<path style="color:#666666;" fill="#666" d="m67,729,0,4-4,0,0,2,4,0,0,4,2,0,0-4,4,0,0-2-4,0,0-4-2,0z"/>
+</g>
+</svg>

+ 7 - 0
p/themes/Screwdriver/icons/all.svg

@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16">
+<g transform="translate(-40.0002,-746)" fill="#bebebe">
+<rect style="color:#bebebe;" height="2.0002" width="9.9996" y="749" x="43"/>
+<rect style="color:#bebebe;" height="2.0002" width="9.9996" y="753" x="43"/>
+<rect style="color:#bebebe;" height="2.0002" width="9.9996" y="757" x="43"/>
+</g>
+</svg>

BIN
p/themes/Screwdriver/icons/apple-touch-icon.png


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

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

+ 60 - 0
p/themes/Screwdriver/icons/bookmark.svg

@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   height="16"
+   width="16"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.48.4 r9939"
+   sodipodi:docname="bookmark.svg">
+  <metadata
+     id="metadata12">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs10" />
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="1366"
+     inkscape:window-height="745"
+     id="namedview8"
+     showgrid="false"
+     inkscape:zoom="14.75"
+     inkscape:cx="-2.2033898"
+     inkscape:cy="8"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg2" />
+  <g
+     transform="translate(-41.000202,-397)"
+     id="g4">
+    <path
+       style="enable-background:accumulate;color:#000000;fill:#d18104;fill-opacity:1"
+       d="m530.95,186.71c-0.77941,0.55189-3.1576-1.906-4.1125-1.9179-0.95532-0.0119-3.3949,2.3858-4.161,1.8149-0.76573-0.57072,0.83698-3.592,0.55319-4.5039-0.2839-0.91223-3.3182-2.4915-3.0119-3.3965,0.30617-0.90461,3.6749-0.31399,4.4544-0.86567,0.77986-0.5519,1.3442-3.9257,2.2995-3.914,0.95494,0.0116,1.4342,3.398,2.1998,3.9689,0.76588,0.57114,4.1489,0.0653,4.4331,0.97746,0.28402,0.9118-2.7885,2.414-3.0949,3.3186-0.30652,0.90489,1.22,3.966,0.44027,4.5182z"
+       fill-rule="nonzero"
+       transform="matrix(1.0472113,-0.00871584,0.00871584,1.0472113,-504.35434,220.15425)"
+       fill="#f1c40f"
+       id="path6" />
+  </g>
+</svg>

+ 7 - 0
p/themes/Screwdriver/icons/category-white.svg

@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16">
+<g transform="translate(-442,-176)">
+<g transform="translate(234.0002,-820)">
+<path d="m208.53,997c-0.28913,0-0.53125,0.24212-0.53125,0.53125v13.938c0,0.2985,0.23264,0.5312,0.53125,0.5312h14.938c0.2986,0,0.53125-0.2326,0.53125-0.5312v-8.9376c0-0.2891-0.24212-0.5312-0.53125-0.5312h-12.469v7.5c0,0.277-0.223,0.5-0.5,0.5s-0.5-0.223-0.5-0.5v-8c0-0.277,0.223-0.5,0.5-0.5h2.9688,8.5312v-1.4062c0-0.3272-0.26666-0.5938-0.59375-0.5938h-7.4062v-1.4688c0-0.39-0.24-0.63-0.53-0.63z" fill="#FFF"/>
+</g>
+</g>
+</svg>

+ 7 - 0
p/themes/Screwdriver/icons/category.svg

@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16">
+<g transform="translate(-442,-176)">
+<g transform="translate(234.0002,-820)">
+<path d="m208.53,997c-0.28913,0-0.53125,0.24212-0.53125,0.53125v13.938c0,0.2985,0.23264,0.5312,0.53125,0.5312h14.938c0.2986,0,0.53125-0.2326,0.53125-0.5312v-8.9376c0-0.2891-0.24212-0.5312-0.53125-0.5312h-12.469v7.5c0,0.277-0.223,0.5-0.5,0.5s-0.5-0.223-0.5-0.5v-8c0-0.277,0.223-0.5,0.5-0.5h2.9688,8.5312v-1.4062c0-0.3272-0.26666-0.5938-0.59375-0.5938h-7.4062v-1.4688c0-0.39-0.24-0.63-0.53-0.63z" fill="#666"/>
+</g>
+</g>
+</svg>

+ 7 - 0
p/themes/Screwdriver/icons/close.svg

@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16">
+<g transform="translate(-60,-518)">
+<g transform="translate(19,-242)">
+<path style="block-progression:tb;color:#bebebe;direction:ltr;text-indent:0;text-align:start;enable-background:new;text-transform:none;" d="m45,764,1,0c0.01037-0.00012,0.02079-0.00046,0.03125,0,0.25495,0.0112,0.50987,0.12858,0.6875,0.3125l2.282,2.28,2.312-2.28c0.266-0.23,0.447-0.3,0.688-0.31h1v1c0,0.28647-0.03434,0.55065-0.25,0.75l-2.2812,2.2812,2.25,2.25c0.188,0.19,0.281,0.45,0.281,0.72v1h-1c-0.2653-0.00001-0.53059-0.0931-0.71875-0.28125l-2.281-2.28-2.281,2.28c-0.188,0.19-0.454,0.28-0.719,0.28h-1v-1c-0.000003-0.26529,0.09306-0.53058,0.28125-0.71875l2.2812-2.25-2.281-2.28c-0.21-0.19-0.303-0.47-0.281-0.75v-1z" fill-rule="nonzero" fill="#bebebe"/>
+</g>
+</g>
+</svg>

+ 5 - 0
p/themes/Screwdriver/icons/configure.svg

@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16">
+<g transform="translate(-441.0002,-400.99999)">
+<path style="color:#666666;enable-background:accumulate;" d="m449,402c-0.22065,0-0.44081,0.0113-0.65625,0.0312l-0.40625,2.0938c-0.33446,0.0733-0.66305,0.17589-0.96875,0.3125l-1.5312-1.4688c-0.38863,0.23011-0.72695,0.51408-1.0625,0.8125l0.90625,1.9062c-0.22242,0.24899-0.42425,0.5225-0.59375,0.8125l-2.0938-0.28125c-0.17772,0.40877-0.30872,0.83637-0.40625,1.2812l1.8438,1c-0.0171,0.16809-0.0312,0.3274-0.0312,0.5s0.0142,0.33191,0.0312,0.5l-1.8438,1c0.0975,0.44488,0.22853,0.87248,0.40625,1.2812l2.0938-0.28125c0.1695,0.29,0.37133,0.56351,0.59375,0.8125l-0.90625,1.9062c0.33555,0.29842,0.67387,0.58239,1.0625,0.8125l1.5312-1.4688c0.3057,0.13661,0.63429,0.23916,0.96875,0.3125l0.40625,2.0938c0.21544,0.02,0.4356,0.0312,0.65625,0.0312s0.44081-0.0113,0.65625-0.0312l0.40625-2.0938c0.33446-0.0733,0.66305-0.17589,0.96875-0.3125l1.5312,1.4688c0.38863-0.23011,0.72695-0.51408,1.0625-0.8125l-0.90625-1.9062c0.22242-0.24899,0.42425-0.5225,0.59375-0.8125l2.0938,0.28125c0.17772-0.40877,0.30872-0.83637,0.40625-1.2812l-1.8438-1c0.0171-0.16809,0.0312-0.3274,0.0312-0.5s-0.0142-0.33191-0.0312-0.5l1.8438-1c-0.0975-0.44488-0.22853-0.87248-0.40625-1.2812l-2.0938,0.28125c-0.1695-0.29-0.37133-0.56351-0.59375-0.8125l0.90625-1.9062c-0.33555-0.29842-0.67387-0.58239-1.0625-0.8125l-1.5312,1.4688c-0.3057-0.13661-0.63429-0.23916-0.96875-0.3125l-0.40625-2.0938c-0.21544-0.02-0.4356-0.0312-0.65625-0.0312zm0,4c1.6568,0,3,1.3432,3,3s-1.3432,3-3,3-3-1.3432-3-3,1.3432-3,3-3z" fill-rule="nonzero" fill="#666"/>
+</g>
+</svg>

+ 5 - 0
p/themes/Screwdriver/icons/down.svg

@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16">
+<g transform="translate(-181.0002,-747)">
+<path style="block-progression:tb;color:#bebebe;direction:ltr;text-indent:0;text-align:start;enable-background:new;text-transform:none;" d="m195.03,751,0,1c-0.00091,0.0111,0.00059,0.021-0.00009,0.0312-0.0112,0.25496-0.12835,0.50994-0.31251,0.6875l-5.7188,6.2977-5.7188-6.2977c-0.18821-0.1881-0.28121-0.45346-0.28122-0.71875v-1h1c0.26531,0.00007,0.53059,0.0931,0.71873,0.28131l4.2812,4.829,4.2813-4.829c0.19464-0.21073,0.46925-0.30315,0.74998-0.2813z" fill-rule="nonzero" fill="#bebebe"/>
+</g>
+</svg>

BIN
p/themes/Screwdriver/icons/favicon-16-32-48-64.ico


BIN
p/themes/Screwdriver/icons/favicon-256.png


+ 13 - 0
p/themes/Screwdriver/icons/favicon.svg

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

BIN
p/themes/Screwdriver/icons/grey.gif


+ 7 - 0
p/themes/Screwdriver/icons/help.svg

@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16">
+<g transform="translate(-182,-490)" fill="#bebebe">
+<path style="block-progression:tb;color:#bebebe;direction:ltr;text-indent:0;text-align:start;enable-background:accumulate;text-transform:none;" d="m190,490c-4.4147,0-8,3.5853-8,8s3.5853,8,8,8,8-3.5853,8-8-3.5853-8-8-8zm0,2c3.3413,0,6,2.6587,6,6s-2.6587,6-6,6-6-2.6587-6-6,2.6587-6,6-6z"/>
+<path style="block-progression:tb;color:#bebebe;direction:ltr;text-indent:0;text-align:start;enable-background:accumulate;text-transform:none;" d="M189.34,495c-1.28,0-2.34,1.06-2.34,2.34v1.3125c0,1.2861,1.0576,2.3438,2.3438,2.3438h1.3125c1.29,0.01,2.35-1.05,2.35-2.33v-1.3125c0-1.29-1.06-2.35-2.34-2.35h-1.3125zm0,1,1.3125,0c0.74942,0,1.3438,0.59433,1.3438,1.3438v1.3125c0.01,0.76-0.58,1.35-1.33,1.35h-1.3125c-0.76,0-1.35-0.59-1.35-1.34v-1.3125c0-0.76,0.59-1.35,1.34-1.35z"/>
+<path d="m186.72,491.44c-1.5103,0.6073-2.6811,1.7985-3.2812,3.3125l3.75,1.875c0.25196-0.64029,0.74249-1.1706,1.375-1.4375l-1.8438-3.75zm6.5625,0-1.8438,3.75c0.63251,0.26694,1.123,0.79721,1.375,1.4375l3.75-1.875c-0.60015-1.514-1.7709-2.7052-3.2812-3.3125zm-6.0938,8-3.75,1.875c0.60709,1.4886,1.789,2.65,3.2812,3.25l1.875-3.75c-0.62682-0.25556-1.1433-0.75203-1.4062-1.375zm5.625,0c-0.26291,0.62297-0.77943,1.1194-1.4062,1.375l1.875,3.75c1.4923-0.60005,2.6742-1.7614,3.2812-3.25l-3.75-1.875z"/>
+</g>
+</svg>

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

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

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

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

Fișier diff suprimat deoarece este prea mare
+ 4 - 0
p/themes/Screwdriver/icons/link.svg


+ 6 - 0
p/themes/Screwdriver/icons/login.svg

@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16">
+<g transform="translate(-181.0002,-237)" fill="#bebebe">
+<path style="color:#bebebe;" d="m184,244c-0.554,0-1,0.446-1,1v0.53125,5.4688h12v-5.4688-0.53c0-0.554-0.446-1-1-1h-10z" fill-rule="nonzero"/>
+<path style="baseline-shift:baseline;block-progression:tb;color:#bebebe;direction:ltr;text-indent:0;text-align:start;enable-background:accumulate;text-transform:none;" d="m188,238c-1.6447,0-3,1.3553-3,3v7c0,1.6447,1.3553,3,3,3h2c1.6447,0,3-1.3553,3-3v-7c0-1.6447-1.3553-3-3-3h-2zm0,2,2,0c0.5713,0,1,0.4287,1,1v7c0,0.5713-0.4287,1-1,1h-2c-0.5713,0-1-0.4287-1-1v-7c0-0.5713,0.4287-1,1-1z"/>
+</g>
+</svg>

+ 6 - 0
p/themes/Screwdriver/icons/logout.svg

@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16">
+<g transform="translate(-201.0002,-237)" fill="#bebebe">
+<path style="color:#bebebe;" d="m204,246c-0.554,0-1,0.446-1,1v0.53125,5.4688h12v-5.4688-0.53c0-0.554-0.446-1-1-1h-10z" fill-rule="nonzero"/>
+<path style="baseline-shift:baseline;block-progression:tb;color:#bebebe;direction:ltr;text-indent:0;text-align:start;enable-background:accumulate;text-transform:none;" d="m208,237c-1.6447,0-3,1.3553-3,3v3h2v-3c0-0.57129,0.42873-1,1-1h2c0.57127,0,1,0.42871,1,1v7h2v-7c0-1.6447-1.3553-3-3-3h-2z"/>
+</g>
+</svg>

+ 5 - 0
p/themes/Screwdriver/icons/next.svg

@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16">
+<g transform="translate(-121.0002,-747)">
+<path style="block-progression:tb;color:#bebebe;direction:ltr;text-indent:0;text-align:start;enable-background:new;text-transform:none;" d="m125,749,1,0c0.0104-0.00012,0.0208-0.00046,0.0313,0,0.25495,0.0112,0.50987,0.12858,0.6875,0.3125l6.2977,5.7188-6.2977,5.7188c-0.18816,0.18819-0.45346,0.28125-0.71875,0.28125h-1v-1c0-0.26529,0.0931-0.53058,0.28125-0.71875l4.829-4.2812-4.829-4.2812c-0.21074-0.19463-0.30316-0.46925-0.28125-0.75z" fill-rule="nonzero" fill="#bebebe"/>
+</g>
+</svg>

Fișier diff suprimat deoarece este prea mare
+ 2 - 0
p/themes/Screwdriver/icons/non-starred.svg


+ 5 - 0
p/themes/Screwdriver/icons/prev.svg

@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16">
+<g transform="translate(-301.0002,-747)">
+<path style="block-progression:tb;color:#bebebe;direction:ltr;text-indent:0;text-align:start;enable-background:new;text-transform:none;" d="m313.01,749-1,0c-0.0104-0.00012-0.0208-0.00046-0.0313,0-0.25495,0.0112-0.50987,0.12858-0.6875,0.3125l-6.2977,5.7188,6.2977,5.7188c0.18816,0.18819,0.45346,0.28125,0.71875,0.28125h1v-1c0-0.26529-0.0931-0.53058-0.28125-0.71875l-4.829-4.2812,4.829-4.2812c0.21074-0.19463,0.30316-0.46925,0.28125-0.75z" fill-rule="nonzero" fill="#bebebe"/>
+</g>
+</svg>

+ 57 - 0
p/themes/Screwdriver/icons/read.svg

@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   height="16.001"
+   width="16"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.48.4 r9939"
+   sodipodi:docname="read.svg">
+  <metadata
+     id="metadata12">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs10" />
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="1366"
+     inkscape:window-height="745"
+     id="namedview8"
+     showgrid="false"
+     inkscape:zoom="14.749079"
+     inkscape:cx="-2.2040272"
+     inkscape:cy="8.0004997"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg2" />
+  <g
+     transform="translate(-60.99995,-296.9989)"
+     id="g4" />
+  <path
+     style="fill:#cccccc;fill-opacity:1"
+     inkscape:connector-curvature="0"
+     d="m 8.0004996,3.2833392 c -3.433907,0 -6.410294,1.9996259 -7.87290101,4.9205634 1.46260701,2.9209364 4.43899401,4.9205624 7.87290101,4.9205624 3.4338474,0 6.4102344,-1.999626 7.8729014,-4.9205624 C 14.410824,5.2829651 11.434347,3.2833392 8.0004996,3.2833392 z m 3.8818634,2.6094965 c 0.925096,0.590068 1.709004,1.3804357 2.29781,2.3110669 -0.588806,0.9306312 -1.372744,1.7209988 -2.29784,2.3110964 -1.162392,0.741404 -2.5047194,1.133295 -3.8818334,1.133295 -1.377143,0 -2.719472,-0.391891 -3.881863,-1.133326 -0.925066,-0.5900366 -1.708974,-1.3804016 -2.29781,-2.3110654 0.588806,-0.9306638 1.372744,-1.7210288 2.29781,-2.3110669 0.06025,-0.038442 0.121108,-0.075682 0.182338,-0.1122479 -0.153123,0.4202145 -0.236925,0.873738 -0.236925,1.3469419 0,2.1740274 1.762423,3.9364493 3.93645,3.9364493 2.1740274,0 3.9364514,-1.7624219 3.9364514,-3.9364493 0,-0.4732039 -0.0838,-0.9267274 -0.236925,-1.3469745 0.0612,0.036566 0.122061,0.073839 0.182337,0.1122805 z M 8.0004996,6.6354719 c 0,0.8152761 -0.660894,1.4761705 -1.476168,1.4761705 -0.815275,0 -1.476169,-0.6608944 -1.476169,-1.4761705 0,-0.8152759 0.660894,-1.4761676 1.476169,-1.4761676 0.815274,0 1.476168,0.6608917 1.476168,1.4761676 z"
+     id="path3167" />
+</svg>

+ 5 - 0
p/themes/Screwdriver/icons/refresh.svg

@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16">
+<g transform="translate(-241.0002,-627)">
+<path style="baseline-shift:baseline;block-progression:tb;color:#666666;direction:ltr;text-indent:0;text-align:start;enable-background:accumulate;text-transform:none;" fill="#666" d="m253.91,628.97a1.0001,1.0001,0,0,0,-0.125,0.0312,1.0001,1.0001,0,0,0,-0.78125,1v1.6875c-0.38225-0.57796-0.84927-1.0822-1.4062-1.5-1.1556-0.86677-2.532-1.2523-3.875-1.1875-0.19186,0.009-0.37223,0.0353-0.5625,0.0625-1.5222,0.21741-2.9782,1.023-3.9688,2.3438-1.9812,2.6414-1.4227,6.425,1.2188,8.4062s6.425,1.4227,8.4062-1.2188a1.0063,1.0063,0,0,0,0.18,-0.59,1.0063,1.0063,0,0,0,0,-0.15625v-0.84375h-0.8125-0.0937a1.0063,1.0063,0,0,0,-0.0937,0,1.0063,1.0063,0,0,0,-0.8125,0.40625c-1.3326,1.7767-3.817,2.1139-5.5938,0.78125-1.7767-1.3326-2.1139-3.817-0.78125-5.5938,1.3326-1.7767,3.817-2.1139,5.5938-0.78125,0.42946,0.32212,0.76954,0.73295,1.0312,1.1875h-1.4375a1.0001,1.0001,0,0,0,-1,1,1.0001,1.0001,0,0,0,0,0.21875v0.78125h0.84375,0.15625,4,1v-1-4a1.0001,1.0001,0,0,0,0,-0.1875v-0.8125h-0.8125a1.0001,1.0001,0,0,0,-0.28125,-0.0312z"/>
+</g>
+</svg>

+ 6 - 0
p/themes/Screwdriver/icons/rss.svg

@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16">
+<g fill-rule="nonzero" transform="translate(-561,-301.00012)" fill="#666">
+<path style="enable-background:new;color:#000000;" d="m325.06,97.188c0,1.7872-0.89543,3.2361-2,3.2361s-2-1.4488-2-3.2361c0-1.7872,0.89543-3.2361,2-3.2361s2,1.4488,2,3.2361z" transform="matrix(1.0000007,0,0,0.61803426,241.93747,252.93479)"/>
+<path style="enable-background:new;color:#000000;" d="m563,303,0,1c0,0.55016,0.45347,1,1,1,4.9706,0,9,4.0294,9,9,0,0.55016,0.45347,1,1,1h1v-1c0-6.0751-4.9249-11-11-11h-1zm0,4,0,1c0,0.55016,0.45347,1,1,1,2.7614,0,5,2.2386,5,5,0,0.55016,0.45347,1,1,1h1v-1c0-3.866-3.134-7-7-7h-1z"/>
+</g>
+</svg>

+ 6 - 0
p/themes/Screwdriver/icons/search.svg

@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16">
+<g fill="#666" transform="translate(-441.0004,-195)">
+<path style="baseline-shift:baseline;block-progression:tb;color:#666666;direction:ltr;text-indent:0;text-align:start;enable-background:new;text-transform:none;" d="m447.51,196c-3.0289,0-5.5107,2.479-5.5107,5.5045,0,3.0254,2.4819,5.5045,5.5107,5.5045s5.5107-2.479,5.5107-5.5045c0-3.0254-2.4819-5.5045-5.5107-5.5045zm0,2.0089c1.9474,0,3.4995,1.5504,3.4995,3.4955s-1.5522,3.4955-3.4995,3.4955c-1.9474,0-3.4995-1.5504-3.4995-3.4955,0-1.9452,1.5522-3.4955,3.4995-3.4955z"/>
+<path style="baseline-shift:baseline;block-progression:tb;color:#666666;direction:ltr;text-indent:0;text-align:start;enable-background:new;text-transform:none;" d="m450.81,204a1.0001,1.0001,0,0,0,-0.5,1.7188l4,4a1.0055,1.0055,0,1,0,1.4062,-1.4375l-4-4a1.0001,1.0001,0,0,0,-0.91,-0.28z"/>
+</g>
+</svg>

+ 8 - 0
p/themes/Screwdriver/icons/share.svg

@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16">
+<g fill="#bebebe" transform="translate(-581.0002,-196)">
+<path style="enable-background:new;color:#000000;" d="m291,178.03c0,1.0873-0.88144,1.9688-1.9688,1.9688-1.0873,0-1.9688-0.88144-1.9688-1.9688,0-1.0873,0.88144-1.9688,1.9688-1.9688,1.0873,0,1.9688,0.88144,1.9688,1.9688z" fill-rule="nonzero" transform="matrix(1.5079365,0,0,1.5079365,148.15963,-64.49107)"/>
+<path style="enable-background:new;color:#000000;" d="m291,178.03c0,1.0873-0.88144,1.9688-1.9688,1.9688-1.0873,0-1.9688-0.88144-1.9688-1.9688,0-1.0873,0.88144-1.9688,1.9688-1.9688,1.0873,0,1.9688,0.88144,1.9688,1.9688z" fill-rule="nonzero" transform="matrix(1.5079365,0,0,1.5079365,158.12818,-59.49107)"/>
+<path style="enable-background:new;color:#000000;" d="m291,178.03c0,1.0873-0.88144,1.9688-1.9688,1.9688-1.0873,0-1.9688-0.88144-1.9688-1.9688,0-1.0873,0.88144-1.9688,1.9688-1.9688,1.0873,0,1.9688,0.88144,1.9688,1.9688z" fill-rule="nonzero" transform="matrix(1.5079365,0,0,1.5079365,158.12818,-69.49107)"/>
+<path style="baseline-shift:baseline;block-progression:tb;color:#000000;direction:ltr;text-indent:0;text-align:start;enable-background:accumulate;text-transform:none;" d="m593.62,198.16-10.062,4.875-1.8125,0.90625,1.8125,0.90625,10.031,5.0625,0.90625-1.8125-8.2188-4.1562,8.2188-4-0.875-1.7812z"/>
+</g>
+</svg>

+ 60 - 0
p/themes/Screwdriver/icons/starred.svg

@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   height="16"
+   width="16"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.48.4 r9939"
+   sodipodi:docname="starred.svg">
+  <metadata
+     id="metadata12">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs10" />
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="685"
+     inkscape:window-height="480"
+     id="namedview8"
+     showgrid="false"
+     inkscape:zoom="14.75"
+     inkscape:cx="8"
+     inkscape:cy="8"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="svg2" />
+  <g
+     transform="translate(-41.000202,-397)"
+     id="g4">
+    <path
+       style="enable-background:accumulate;color:#000000;fill:#eaa904;fill-opacity:1"
+       d="m530.95,186.71c-0.77941,0.55189-3.1576-1.906-4.1125-1.9179-0.95532-0.0119-3.3949,2.3858-4.161,1.8149-0.76573-0.57072,0.83698-3.592,0.55319-4.5039-0.2839-0.91223-3.3182-2.4915-3.0119-3.3965,0.30617-0.90461,3.6749-0.31399,4.4544-0.86567,0.77986-0.5519,1.3442-3.9257,2.2995-3.914,0.95494,0.0116,1.4342,3.398,2.1998,3.9689,0.76588,0.57114,4.1489,0.0653,4.4331,0.97746,0.28402,0.9118-2.7885,2.414-3.0949,3.3186-0.30652,0.90489,1.22,3.966,0.44027,4.5182z"
+       fill-rule="nonzero"
+       transform="matrix(1.0472113,-0.00871584,0.00871584,1.0472113,-504.35434,220.15425)"
+       fill="#f1c40f"
+       id="path6" />
+  </g>
+</svg>

+ 5 - 0
p/themes/Screwdriver/icons/tag.svg

@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16">
+<g transform="translate(-141.0002,-807)">
+<path d="m149,809,0,13,4-4,4,4c0.0525-6.8494-0.0285-10.584,0-13z" fill="#bebebe"/>
+</g>
+</svg>

+ 65 - 0
p/themes/Screwdriver/icons/unread.svg

@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   height="16"
+   width="16"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.48.4 r9939"
+   sodipodi:docname="unread.svg">
+  <metadata
+     id="metadata14">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs12" />
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="1366"
+     inkscape:window-height="745"
+     id="namedview10"
+     showgrid="false"
+     showguides="true"
+     inkscape:guide-bbox="true"
+     inkscape:zoom="20.85965"
+     inkscape:cx="3.2842788"
+     inkscape:cy="5.738225"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg2">
+    <sodipodi:guide
+       orientation="0,1"
+       position="18.02523,13.039528"
+       id="guide4011" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="10.738435,2.1093355"
+       id="guide4013" />
+  </sodipodi:namedview>
+  <path
+     style="fill:#666666;fill-opacity:1"
+     inkscape:connector-curvature="0"
+     d="m 7.9824408,3.5290339 c -3.4339072,0 -6.4102945,1.9996258 -7.87290106,4.9205633 C 1.5721463,11.370534 4.5485336,13.37016 7.9824408,13.37016 c 3.4338472,0 6.4102342,-1.999626 7.8729012,-4.9205628 C 14.392765,5.5286597 11.416288,3.5290339 7.9824408,3.5290339 z m 3.8818632,2.6094964 c 0.925096,0.590068 1.709004,1.3804357 2.29781,2.3110669 -0.588806,0.9306312 -1.372744,1.7209988 -2.29784,2.3110968 -1.162392,0.741404 -2.5047196,1.133295 -3.8818332,1.133295 -1.377143,0 -2.7194718,-0.391891 -3.8818628,-1.133326 C 3.1755118,10.170626 2.3916036,9.380261 1.8027674,8.4495972 2.3915738,7.5189334 3.1755118,6.7285684 4.100578,6.1385303 4.160827,6.1000883 4.221686,6.0628483 4.2829153,6.0262824 c -0.1531228,0.4202145 -0.236925,0.873738 -0.236925,1.3469419 0,2.1740274 1.7624231,3.9364497 3.9364505,3.9364497 2.1740272,0 3.9364512,-1.7624223 3.9364512,-3.9364497 0,-0.4732039 -0.0838,-0.9267274 -0.236925,-1.3469745 0.0612,0.036566 0.122061,0.073839 0.182337,0.1122805 z M 7.9824408,6.8811665 c 0,0.8152761 -0.660894,1.4761705 -1.476168,1.4761705 -0.8152757,0 -1.4761697,-0.6608944 -1.4761697,-1.4761705 0,-0.8152759 0.660894,-1.4761676 1.4761697,-1.4761676 0.815274,0 1.476168,0.6608917 1.476168,1.4761676 z"
+     id="path3167" />
+</svg>

+ 5 - 0
p/themes/Screwdriver/icons/up.svg

@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16">
+<g transform="translate(-201.0002,-747)">
+<path style="block-progression:tb;color:#bebebe;direction:ltr;text-indent:0;text-align:start;enable-background:new;text-transform:none;" d="m215.03,759,0-1c-0.00091-0.0111,0.00059-0.021-0.00009-0.0312-0.0112-0.25496-0.12835-0.50994-0.31251-0.6875l-5.7188-6.2977-5.7188,6.2977c-0.18821,0.1881-0.28121,0.45346-0.28122,0.71875v1h1c0.26531-0.00007,0.53059-0.0931,0.71873-0.28131l4.2812-4.829,4.2813,4.829c0.19464,0.21073,0.46925,0.30315,0.74998,0.2813z" fill-rule="nonzero" fill="#bebebe"/>
+</g>
+</svg>

+ 1 - 0
p/themes/Screwdriver/icons/view-global.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g transform="translate(-61-867)" fill="#666" color="#000"><rect height="2" rx=".385" ry=".379" width="2" x="64" y="870"/><rect height="2" rx=".385" ry=".379" width="2" x="68" y="870"/><rect height="2" rx=".385" ry=".379" width="2" x="72" y="870"/><rect height="2" rx=".385" ry=".379" width="2" x="64" y="874.02"/><rect height="2" rx=".385" ry=".379" width="2" x="68" y="874.02"/><rect height="2" rx=".385" ry=".379" width="2" x="72" y="874.02"/><rect height="2" rx=".385" ry=".379" width="2" x="64" y="878"/><rect height="2" rx=".385" ry=".379" width="2" x="68" y="878"/><rect height="2" rx=".385" ry=".379" width="2" x="72" y="878"/></g></svg>

+ 1 - 0
p/themes/Screwdriver/icons/view-normal.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g transform="translate(-40-746)" fill="#666" color="#666"><path d="m43 749h10v2h-10z"/><path d="m43 753h10v2h-10z"/><path d="m43 757h10v2h-10z"/></g></svg>

+ 1 - 0
p/themes/Screwdriver/icons/view-reader.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g transform="translate(-181-867)" fill="#666" color="#666"><path d="m 181,868 0,1 0,11 0,1 1,0 5,0 c 0.1754,0 0.52538,0.15166 0.8125,0.34375 0.28712,0.19209 0.46875,0.375 0.46875,0.375 L 189,882.4375 l 0.71875,-0.75 c 0,0 0.8963,-0.6875 1.28125,-0.6875 l 5,0 1,0 0,-1 0,-11 0,-1 -1,0 -5,0 c -0.87652,0 -1.56017,0.34756 -2.03125,0.6875 -0.0301,-0.0207 -0.031,-0.0105 -0.0625,-0.0312 C 188.44557,868.35254 187.82811,868 187,868 l -5,0 -1,0 z m 2,2 4,0 c 0.13821,0 0.51476,0.14746 0.8125,0.34375 0.29774,0.19629 0.5,0.375 0.5,0.375 l 0.71875,0.6875 0.6875,-0.71875 c 0,0 0.89975,-0.6875 1.28125,-0.6875 l 4,0 0,9 -4,0 c -0.87693,0 -1.56008,0.34735 -2.03125,0.6875 -0.0196,-0.0135 -0.011,-0.0177 -0.0312,-0.0312 C 188.47725,879.34834 187.83512,879 187,879 l -4,0 0,-9 z"/><g transform="scale(-1 1)"><rect height="2" rx=".375" width="3" x="-187" y="872"/><rect height="2" rx=".375" width="3" x="-187" y="875"/><rect height="2" rx=".375" width="3" x="-194" y="872"/><rect height="2" rx=".375" width="3" x="-194" y="875"/></g></g></svg>

BIN
p/themes/Screwdriver/loader.gif


+ 7 - 0
p/themes/Screwdriver/metadata.json

@@ -0,0 +1,7 @@
+{
+  "name": "Screwdriver",
+  "author": "Mister aiR",
+  "description": "C'est un cocktail ! C'est chaud mais « fresh » à la fois. Ce thème tue du chaton.",
+  "version": 1.1,
+  "files": ["template.css","screwdriver.css"]
+}

+ 1170 - 0
p/themes/Screwdriver/screwdriver.css

@@ -0,0 +1,1170 @@
+@charset "UTF-8";
+
+/*=== FONTS */
+@font-face {
+	font-family: "OpenSans";
+	src: url("../fonts/openSans.woff") format("woff");
+}
+
+/*=== GENERAL */
+/*============*/
+html, body {
+	height: 100%;
+	font-family: "OpenSans", "Cantarell", "Helvetica", "Arial", sans-serif;
+	background: #fafafa;
+	font-size: 92%;
+}
+
+/*=== Links */
+a {
+	color: #D18114;
+	outline: none;
+}
+
+/*=== Forms */
+.form-group{
+	width: 100%;
+	float: left;
+	height: auto;
+	display: inline-block;
+}
+legend {
+	margin: 20px 0 5px;
+	padding: 5px 0;
+	border-bottom: 1px solid #ddd;
+	font-size: 1.4em;
+}
+label {
+	min-height: 25px;
+	padding: 5px 0;
+	cursor: pointer;
+}
+textarea {
+	width: 360px;
+	height: 100px;
+}
+input, select, textarea {
+	min-height: 25px;
+	padding: 5px;
+	background: #fff;
+	border: 1px solid #ccc;
+	border-radius: 3px;
+	color: #222;
+	line-height: 25px;
+	vertical-align: middle;
+	box-shadow: 0 2px 2px #eee inset, 0 1px #fff;
+}
+option {
+	padding: 0 .5em;
+}
+input:focus, select:focus, textarea:focus {
+	color: #0F0F0F;
+	box-shadow: 0 0 3px #E7AB34;
+	border: solid 1px #E7AB34;
+}
+input:invalid, select:invalid {
+	border-color: #f00;
+	box-shadow: 0 0 2px 2px #fdd inset;
+}
+input:disabled, select:disabled {
+	background: #eee;
+}
+input.extend {
+	transition: width 200ms linear;
+	-moz-transition: width 200ms linear;
+	-webkit-transition: width 200ms linear;
+	-o-transition: width 200ms linear;
+	-ms-transition: width 200ms linear;
+}
+
+/*=== Tables */
+table {
+	border-collapse: collapse;
+}
+
+tr, th, td {
+	padding: 0.5em;
+	border: 1px solid #ddd;
+}
+th {
+	background: #f6f6f6;
+}
+form td,
+form th {
+	font-weight: normal;
+	text-align: center;
+}
+
+/*=== COMPONENTS */
+/*===============*/
+/*=== Forms */
+.form-group.form-actions {
+	padding: 5px 0;
+	background: #f4f4f4;
+	border-top: 1px solid #ddd;
+}
+.form-group.form-actions .btn {
+	margin: 0 10px;
+	border-radius: 4px;
+	box-shadow:0 1px rgba(255,255,255,0.08) inset;
+}
+.form-group .group-name {
+	padding: 10px 0;
+	text-align: right;
+}
+.form-group .group-controls {
+	min-height: 25px;
+	padding: 5px 0;
+}
+.form-group table {
+	margin: 10px 0 0 220px;
+}
+
+/*=== Buttons */
+form#add_rss .stick input, .dropdown-menu select{
+    background:#393939;
+    box-shadow: 0 2px  2px #171717 inset,0 1px rgba(255,255,255,0.08);
+    border-left:solid 1px #171717;
+    border-top:solid 1px #171717;
+    border-bottom:solid 1px #171717;
+    border-right:none;
+    color:#fff;
+}
+form#add_rss .stick .btn{
+    background:linear-gradient(180deg, #222 0%, #171717 100%) #171717;
+    background: -webkit-linear-gradient(top, #222 0%, #171717 100%);
+    box-shadow:0 1px rgba(255,255,255,0.08), 0px 1px rgba(255, 255, 255, 0.08) inset;
+    border-right:solid 1px #171717;
+    border-top:solid 1px #171717;
+    border-bottom:solid 1px #171717;
+    border-left:none;
+}
+form#add_rss .stick .btn.dropdown-toggle{
+    border-right:solid 1px #171717;
+    border-top:solid 1px #171717;
+    border-bottom:solid 1px #171717;
+    border-left:solid 1px #171717;
+}
+.stick {
+	vertical-align: middle;
+	font-size: 0;
+}
+.stick input,
+.stick .btn {
+	border-radius: 0;
+}
+.stick .btn:first-child,.stick input:first-child {
+	border-radius: 6px 0 0 6px;
+}
+.stick .btn-important:first-child {
+}
+.stick .btn:last-child, .stick input:last-child {
+	border-radius: 0 6px 6px 0;
+}
+.stick .btn + .btn,
+.stick .btn + input,
+.stick .btn + .dropdown > .btn,
+.stick input + .btn,
+.stick input + input,
+.stick input + .dropdown > .btn,
+.stick .dropdown + .btn,
+.stick .dropdown + input,
+.stick .dropdown + .dropdown > .btn {
+	border-left: none;
+}
+.stick .btn + .dropdown > .btn {
+	border-left: none;
+	border-radius: 0 3px 3px 0;
+}
+
+.btn {
+	display: inline-block;
+	min-height: 37px;
+	min-width: 15px;
+	margin: 0;
+	padding: 5px 10px;
+	color:#222;
+	border: solid 1px #ccc;
+	border-radius: 4px;
+    background: linear-gradient(0deg, #EDE7DE 0%, #FFF 100%) #EDE7DE;
+    background: -webkit-linear-gradient(bottom, #EDE7DE 0%, #FFF 100%);
+	text-shadow: 0px -1px rgba(255,255,255,0.08);
+	font-size: 0.9rem;
+	vertical-align: middle;
+	cursor: pointer;
+	overflow: hidden;
+}
+a.btn {
+	min-height: 25px;
+	line-height: 25px;
+}
+.btn:hover {
+	text-shadow: 0 0 2px #fff;
+	text-decoration:none;
+}
+.btn.active,.btn:active,.dropdown-target:target ~ .btn.dropdown-toggle {
+	background: linear-gradient(180deg, #EDE7DE 0%, #FFF 100%) #EDE7DE;
+	background: -webkit-linear-gradient(top, #EDE7DE 0%, #FFF 100%);
+}
+#loginButton.btn{
+	border:none;
+	box-shadow: 0px 1px rgba(255, 255, 255, 0.08) inset,0 -1px #171717,0px 1px rgba(255, 255, 255, 0.08);
+}
+.nav_menu .btn.active, .nav_menu .btn:active, .nav_menu .dropdown-target:target ~ .btn.dropdown-toggle{
+	box-shadow: 0px 2px #E2972A;
+	border-radius: 0;
+	background:transparent;
+}
+.nav_menu .btn {
+	border: 0;
+	background:transparent;
+}
+
+.read_all {
+	color:#222;
+}
+.btn.dropdown-toggle[href="#dropdown-configure"]{
+	background: linear-gradient(0deg, #EDE7DE 0%, #FFF 100%) #EDE7DE;
+	background: -webkit-linear-gradient(bottom, #EDE7DE 0%, #FFF 100%);
+	border-radius: 4px;
+	border: solid 1px #ccc;
+	box-shadow: 0 1px #fff;
+}
+.btn.dropdown-toggle:active {
+	background:transparent;
+}
+.btn-important {
+	background: linear-gradient(180deg, #E4992C 0%, #D18114 100%) #E4992C;
+	background: -webkit-linear-gradient(top, #E4992C 0%, #D18114 100%);
+	color: #FFF;
+	box-shadow: 0 1px rgba(255,255,255,0.08) inset;
+	border-radius: 4px;
+	text-shadow: 0px -1px rgba(255,255,255,0.08);
+	font-weight: normal;
+}
+.btn-important:hover {
+}
+.btn-important:active {
+	background: linear-gradient(0deg, #E4992C 0%, #D18114 100%) #E4992C;
+	background: -webkit-linear-gradient(bottom, #E4992C 0%, #D18114 100%);
+}
+
+.btn-attention {
+	background: #E95B57;
+	background: linear-gradient(to bottom, #E95B57, #BD362F);
+	background: -webkit-linear-gradient(top, #E95B57 0%, #BD362F 100%);
+	color: #fff;
+	border: 1px solid #C44742;
+	text-shadow: 0px -1px 0px #666;
+}
+.btn-attention:hover {
+	background: linear-gradient(to bottom, #D14641, #BD362F);
+	background: -webkit-linear-gradient(top, #D14641 0%, #BD362F 100%);
+}
+.btn-attention:active {
+	background: #BD362F;
+	box-shadow: none;
+}
+.btn[type="reset"]{
+	color: #fff;
+    background:linear-gradient(180deg, #222 0%, #171717 100%) #171717;
+    background: -webkit-linear-gradient(top, #222 0%, #171717 100%);
+    box-shadow:0 -1px rgba(255,255,255,0.08) inset;
+}
+/*=== Navigation */
+.nav-list .nav-header,
+.nav-list .item {
+	height: 2.5em;
+	line-height: 2.5em;
+	font-size: 0.9rem;
+}
+.nav-list .item:hover {
+	text-shadow: 0 0 2px rgba(255,255,255,0.28);
+	color:#fff;
+}
+
+.nav-list .item.active {
+	background: linear-gradient(180deg, #222 0%, #171717 100%) repeat scroll 0% 0% #171717;
+	background: -webkit-linear-gradient(180deg, #222 0%, #171717 100%);
+	border-width: medium medium 1px;
+	border-style: none none solid;
+	border-color: -moz-use-text-color -moz-use-text-color #171717;
+	box-shadow: -1px 2px 2px #171717, 0px 1px rgba(255, 255, 255, 0.08) inset;
+	margin: 0;
+}
+.nav-list .item.active a {
+	color: #D18114;
+}
+.nav-list .disable {
+	color: #aaa;
+	background: #fafafa;
+	text-align: center;
+}
+.nav-list .item > a {
+	padding: 0 10px;
+	color:#ccc;
+}
+.nav-list a:hover {
+	text-decoration: none;
+}
+.nav-list .item.empty a {
+	color: #f39c12;
+}
+.nav-list .item.active.empty a {
+	color: #fff;
+	background: linear-gradient(180deg, #E4992C 0%, #D18114 100%) #E4992C;
+	background: -webkit-linear-gradient(180deg, #E4992C 0%, #D18114 100%);
+}
+.nav-list .item.error a {
+	color: #BD362F;
+}
+.nav-list .item.active.error a {
+	color: #fff;
+	background: #BD362F;
+}
+
+.nav-list .nav-header {
+	padding: 0 10px;
+	color: #222;
+	background: transparent;
+}
+
+.nav-list .nav-form {
+	padding: 3px;
+	text-align: center;
+}
+
+.nav-head {
+	margin: 0;
+	background: linear-gradient(0deg, #EDE7DE 0%, #FFF 100%) #EDE7DE;
+	background: -webkit-linear-gradient(bottom, #EDE7DE 0%, #FFF 100%);
+	text-align: right;
+}
+.nav-head .item {
+	padding: 5px 10px;
+	font-size: 0.9rem;
+	line-height: 1.5rem;
+}
+
+/*=== Horizontal-list */
+.horizontal-list {
+	margin: 0;
+	padding: 0;
+}
+.horizontal-list .item {
+	vertical-align: middle;
+}
+
+/*=== Dropdown */
+.dropdown-menu {
+	margin: 5px 0 0;
+	padding: 5px 0;
+	border: 1px solid #171717;
+	border-radius: 4px;
+	box-shadow: 0 0 3px #000;
+	font-size: 0.8rem;
+	text-align: left;
+	background: #222;
+}
+.dropdown-menu:after {
+	content: "";
+	position: absolute;
+	top: -6px;
+	right: 13px;
+	width: 10px;
+	height: 10px;
+	background: #222;
+	border-top: 1px solid #171717;
+	border-left: 1px solid #171717;
+	z-index: -10;
+	transform: rotate(45deg);
+	-moz-transform: rotate(45deg);
+	-webkit-transform: rotate(45deg);
+	-ms-transform: rotate(45deg);
+}
+.dropdown-header {
+	display:none;
+}
+.dropdown-menu > .item {
+}
+.dropdown-menu > .item > a {
+	padding: 0 25px;
+	line-height: 2.5em;
+	color:#ccc;
+}
+.dropdown-menu > .item > span {
+	padding: 0 25px;
+	line-height: 2em;
+}
+.dropdown-menu > .item:hover {
+	background: #171717;
+	color: #fff;
+}
+.dropdown-menu > .item[aria-checked="true"] > a:before {
+	font-weight: bold;
+	margin: 0 0 0 -14px;
+}
+.dropdown-menu > .item:hover > a {
+	color: #fff;
+	text-decoration: none;
+}
+.dropdown-menu .input select,
+.dropdown-menu .input input {
+	margin: 0 auto 5px;
+	padding: 2px 5px;
+	border-radius: 3px;
+}
+
+.separator {
+	margin: 5px 0;
+	border-bottom: 1px solid #171717;
+	box-shadow: 0 1px rgba(255,255,255,0.08);
+}
+
+/*=== Alerts */
+.alert {
+	margin: 15px auto;
+	padding: 10px 15px;
+	background: #f4f4f4;
+	border: 1px solid #ccc;
+	border-right: 1px solid #aaa;
+	border-bottom: 1px solid #aaa;
+	border-radius: 5px;
+	color: #aaa;
+	text-shadow: 0 0 1px #eee;
+	font-size: 0.9em;
+}
+.alert-head {
+	font-size: 1.15em;
+}
+.alert > a {
+	color: inherit;
+	text-decoration: underline;
+}
+.alert-warn {
+	background: #ffe;
+	border: 1px solid #eeb;
+	color: #c95;
+}
+.alert-success {
+	background: #dfd;
+	border: 1px solid #cec;
+	color: #484;
+}
+.alert-error {
+	background: #fdd;
+	border: 1px solid #ecc;
+	color: #844;
+}
+
+/*=== Pagination */
+.pagination {
+	background: #fafafa;
+	text-align: center;
+	color: #333;
+	font-size: 0.8em;
+}
+.content .pagination {
+	margin: 0;
+	padding: 0;
+}
+.pagination .item.pager-current {
+	font-weight: bold;
+	font-size: 1.5em;
+}
+.pagination .item a {
+	display: block;
+	color: #333;
+	font-style: italic;
+	line-height: 3em;
+	text-decoration: none;
+}
+.pagination .item a:hover {
+	background: #ddd;
+}
+.pagination:first-child .item {
+	border-bottom: 1px solid #aaa;
+}
+.pagination:last-child .item {
+	border-top: 1px solid #ddd;
+}
+
+.pagination .loading,
+.pagination a:hover.loading {
+	background: url("loader.gif") center center no-repeat #fff;
+	font-size: 0;
+}
+
+/*=== STRUCTURE */
+/*===============*/
+/*=== Header */
+.header {
+	height: 55px;
+	background: linear-gradient(0deg, #EDE7DE 0%, #FFF 100%) #EDE7DE;
+	background: -webkit-linear-gradient(bottom, #EDE7DE 0%, #FFF 100%);
+}
+.header > .item {
+	padding: 0;
+	vertical-align: middle;
+	text-align: center;
+}
+.header > .item.title .logo {
+	display: none;
+}
+.header > .item.title{
+	width: 250px;
+}
+.header > .item.title h1 {
+	margin: 0.5em 0;
+}
+.header > .item.title h1 a {
+	text-decoration: none;
+	font-size: 38px;
+	color:#ccc;
+	text-shadow: 0 1px #fff, 0 -1px rgba(162, 162, 162, 1);
+}
+.header > .item.search input {
+	width: 230px;
+}
+.header .item.search input:focus {
+	width: 350px;
+}
+
+/*=== Body */
+#global {
+	background:#EDE7DE;
+	height: calc(100% - 85px);
+}
+.aside {
+	border-radius: 0px 12px 0px 0px;
+	box-shadow: 0px -1px #FFF, 0 2px 2px #171717 inset;
+	border-top: 1px solid #CCC;
+	background: #222;
+	width: 235px;
+}
+.aside.aside_flux {
+	padding: 10px 0 50px;
+	background: #222;
+}
+
+/*=== Aside main page (categories) */
+.categories {
+	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;
+}
+.category .btn:first-child:not([data-unread="0"]):after {
+	position: absolute;
+	top: 3px; right: 3px;
+	padding: 1px 5px;
+	background: transparent;
+	color: #fff;
+	text-shadow: 0 1px rgba(255,255,255,0.08);
+}
+
+/*=== 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 {
+	color: #e67e22;
+}
+.categories .feeds .item.empty.active {
+	background: #e67e22;
+}
+.categories .feeds .item.empty.active .feed {
+	color: #fff;
+}
+.categories .feeds .item.error .feed {
+	color: #BD362F;
+}
+.categories .feeds .item .feed {
+	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 {
+	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;
+	border-radius: 3px;
+	vertical-align: middle;
+}
+
+/*=== Configuration pages */
+.post {
+	padding: 10px 50px;
+	font-size: 0.9em;
+}
+.post form {
+	margin: 10px 0;
+}
+.post.content {
+	max-width: 550px;
+}
+
+/*=== Prompt (centered) */
+.prompt {
+	text-align: center;
+	color: #FFF;
+	background: #222;
+	padding: 14px 0px;
+	box-shadow: 0px -1px #FFF, 0px 1px #FFF, 0px 2px 2px #171717 inset, 0px -2px 2px #171717 inset;
+	text-shadow: 0 -1px #171717, 0 1px rgba(255,255,255,0.08);
+}
+.prompt label {
+	text-align: left;
+}
+.prompt form {
+	margin: 10px auto 20px auto;
+	width: 180px;
+}
+.prompt input {
+	margin: 5px auto;
+	width: 100%;
+}
+.prompt p {
+	margin: 20px 0;
+}
+.prompt input#username,.prompt input#passwordPlain{
+	border:none;
+	box-shadow: 0px 1px rgba(255, 255, 255, 0.08) inset,0 -1px #171717,0px 1px rgba(255, 255, 255, 0.08);
+	background:#EDE7DE;
+}
+.prompt input#username:focus,.prompt input#passwordPlain:focus{
+	border: solid 1px #E7AB34;
+	box-shadow: 0 0 3px #E7AB34;
+}
+
+/*=== New article notification */
+#new-article {
+	background: #0084CC;
+	text-align: center;
+	font-size: 0.9em;
+}
+#new-article:hover {
+	background: #0066CC;
+}
+#new-article > a {
+	line-height: 3em;
+	color: #fff;
+	font-weight: bold;
+}
+#new-article > a:hover {
+	text-decoration: none;
+}
+
+/*=== Day indication */
+.day {
+	padding: 0 10px;
+	font-style:italic;
+	line-height: 3em;
+	background: #fff;
+	text-align: center;
+}
+#new-article + .day {
+	border-top: none;
+}
+.day .name {
+	display: none;
+}
+
+/*=== Index menu */
+.nav_menu {
+	background: #EDE7DE;
+	border-bottom: 1px solid #ccc;
+	box-shadow:0 -1px #fff inset;
+	text-align: center;
+	padding: 5px 0;
+}
+
+/*=== Feed articles */
+.flux_content {
+    background: #FFF;
+    border-radius: 10px;
+}
+.flux {
+	background: #EDE7DE;
+}
+.flux:hover {
+	background: #F9F7F4;
+}
+.flux:not(.current):hover .item.title {
+	background: #F9F7F4;
+}
+.flux.current .flux .item.title a {
+	text-shadow:0 0 2px #ccc;
+}
+.flux.not_read:not(.current):hover .item.title {
+	opacity:0.85;
+}
+.flux.favorite {
+	background: #FFF6DA;
+}
+.flux.favorite:not(.current):hover{
+	background: #F9F7F4;
+}
+.flux.favorite:not(.current):hover .item.title {
+	background: #F9F7F4;
+}
+.flux.current {
+    background: linear-gradient(0deg, #EDE7DE 0%, #FFF 100%) #EDE7DE;
+	background: -webkit-linear-gradient(bottom, #EDE7DE 0%, #FFF 100%);
+	box-shadow: 0 -1px #fff inset, 0 2px #ccc;
+	border-radius: 10px;
+	margin: 3px 6px;
+}
+
+.flux .item.title {
+opacity: 0.35;
+}
+.flux.favorite .item.title {
+opacity: 1;
+}
+.flux.not_read .item.title {
+opacity: 1;
+}
+.flux.current .item.title a {
+	color: #0f0f0f;
+}
+.flux .item.title a {
+	color: #333;
+}
+
+.flux_header {
+	border-top: 1px solid #ddd;
+	font-size: 0.8rem;
+	cursor: pointer;
+}
+.flux_header .title {
+	font-size: 0.9rem;
+}
+.flux .website .favicon {
+	padding: 5px;
+}
+.flux .date {
+	color: #666;
+	font-size: 0.7rem;
+}
+
+.flux .bottom {
+	font-size: 0.8rem;
+	text-align: center;
+}
+
+/*=== Content of feed articles */
+.content {
+	padding: 20px 10px;
+}
+.content > h1.title > a {
+	color: #000;
+}
+
+.content hr {
+	margin: 30px 10px;
+	height: 1px;
+	background: #ddd;
+	border: 0;
+	box-shadow: 0 2px 5px #ccc;
+}
+
+.content pre {
+	margin: 10px auto;
+	padding: 10px 20px;
+	overflow: auto;
+	background: #222;
+	color: #fff;
+	font-size: 0.9rem;
+	border-radius: 3px;
+}
+.content code {
+	padding: 2px 5px;
+	color: #dd1144;
+	background: #fafafa;
+	border: 1px solid #eee;
+	border-radius: 3px;
+}
+.content pre code {
+	background: transparent;
+	color: #fff;
+	border: none;
+}
+
+.content blockquote {
+	display: block;
+	margin: 0;
+	padding: 5px 20px;
+	border-top: 1px solid #ddd;
+	border-bottom: 1px solid #ddd;
+	background: #fafafa;
+	color: #333;
+}
+.content blockquote p {
+	margin: 0;
+}
+
+/*=== Notification and actualize notification */
+.notification {
+	padding: 0 0 0 5px;
+	text-align: center;
+	background:#222;
+	border: none;
+	border-radius: 0 0 12px 12px;
+	box-shadow: 0px 0px 4px rgba(0,0,0,0.45), 0 -1px rgba(255,255,255,0.08) inset, 0 2px 2px #171717 inset;
+	color:#fff;
+	font-weight: bold;
+	font-size: 0.9em;
+	line-height: 3em;
+	position:absolute;
+	top:0;
+	z-index: 10;
+	vertical-align: middle;
+}
+.notification.good {
+	color: #c95;
+}
+.notification.bad {
+	background: #fdd;
+	color: #844;
+}
+.notification a.close {
+	padding: 0 15px;
+	line-height: 3em;
+}
+.notification#actualizeProgress {
+	line-height: 2em;
+}
+
+/*=== "Load more" part */
+#bigMarkAsRead {
+	text-align: center;
+	text-decoration: none;
+	text-shadow: 0 -1px 0 #aaa;
+	color: #666;
+	background: #EDE7DE;
+}
+#bigMarkAsRead:hover {
+	color: #000;
+	background: #EDE7DE;
+	background: radial-gradient(circle at 50% -25% , #ccc 0%, #EDE7DE 50%);
+}
+#bigMarkAsRead:hover .bigTick {
+	text-shadow: 0 0 10px #666;
+}
+
+/*=== Navigation menu (for articles) */
+#nav_entries {
+	background: linear-gradient(180deg, #222 0%, #171717 100%) #222;
+	background: -webkit-linear-gradient(top, #222 0%, #171717 100%);
+	border-top: 1px solid #171717;
+	text-align: center;
+	line-height: 3em;
+	table-layout: fixed;
+	box-shadow: 0 1px rgba(255,255,255,0.08) inset, 0 -2px 2px #171717;
+	width:235px;
+}
+
+/*=== READER VIEW */
+/*================*/
+#stream.reader .flux {
+	padding: 0 0 50px;
+	border: none;
+	background: #f0f0f0;
+	color: #333;
+}
+#stream.reader .flux .author {
+	margin: 0 0 10px;
+	font-size: 90%;
+	color: #666;
+}
+
+/*=== GLOBAL VIEW */
+/*================*/
+#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;
+	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"]) {
+	color: #222;
+	font-weight: bold;
+}
+#stream.global .btn:first-child:not([data-unread="0"]):after {
+	top: 0;
+	right: 5px;
+	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 {
+	padding: 2px 10px;
+	font-size: 0.9rem;
+	overflow:hidden;
+}
+#stream.global .box-category .feed {
+	color:#222;
+}
+/*=== PANEL */
+/*===========*/
+#panel {
+	box-shadow: 0px 0px 4px #000;
+	border-radius: 8px;
+}
+/*=== DIVERS */
+/*===========*/
+.aside.aside_feed .nav-form input,.aside.aside_feed .nav-form select {
+	width: 130px;
+}
+.aside.aside_feed .nav-form .dropdown .dropdown-menu {
+	right: -20px;
+}
+.aside.aside_feed .nav-form .dropdown .dropdown-menu:after {
+	right: 33px;
+}
+
+/*=== STATISTICS */
+/*===============*/
+.stat {
+	margin: 10px 0 20px;
+}
+
+.stat th,
+.stat td,
+.stat tr {
+	border: none;
+}
+.stat > table td,
+.stat > table th {
+	border-bottom: 1px solid #ccc;
+	background: rgba(255,255,255,0.38);
+	box-shadow: 0 1px #fff;
+	text-align: center;
+}
+
+/*=== LOGS */
+/*=========*/
+.logs {
+	border: 1px solid #aaa;
+	border-radius: 5px;
+	overflow: hidden;
+}
+.log {
+	padding: 5px 10px;
+	background: #fafafa;
+	color: #333;
+	font-size: 0.8rem;
+}
+.log+.log {
+	border-top: 1px solid #aaa;
+}
+.log .date {
+	display: block;
+	font-weight: bold;
+}
+.log.error {
+	background: #fdd;
+	color: #844;
+}
+.log.warning {
+	background: #ffe;
+	color: #c95;
+}
+.log.notice {
+	background: #f4f4f4;
+	color: #aaa;
+}
+.log.debug {
+	background: #333;
+	color: #eee;
+}
+
+/*=== MOBILE */
+/*===========*/
+@media screen and (max-width: 840px) {
+	.header {
+		display: table;
+	}
+	.nav-login {
+		display: none;
+	}
+
+	.aside {
+		width: 0;
+		border-top: none;
+		box-shadow: 3px 0 3px #000;
+		transition: width 200ms linear;
+		-moz-transition: width 200ms linear;
+		-webkit-transition: width 200ms linear;
+		-o-transition: width 200ms linear;
+		-ms-transition: width 200ms linear;
+	}
+	.aside:target {
+		width: 235px;
+	}
+	.aside .toggle_aside,
+	#panel .close {
+		position: absolute;
+		display: block;
+		top: 0; right: 0;
+		width: 30px;
+		height: 30px;
+		line-height: 30px;
+		text-align: center;
+		background: #171717;
+		box-shadow: 0 1px rgba(255,255,255,0.08);
+		border-radius: 0 8px 0 8px;
+	}
+	.aside .btn-important {
+		display: inline-block;
+		margin: 20px 0 0;
+	}
+
+	.nav_menu .btn {
+		margin: 5px 10px;
+	}
+	.nav_menu .stick {
+		margin: 0 10px;
+	}
+	.nav_menu .stick .btn {
+		margin: 5px 0;
+	}
+	.nav_menu .search {
+		display: inline-block;
+		max-width: 97%;
+	}
+	.nav_menu .search input {
+		max-width: 97%;
+		width: 90px;
+	}
+	.nav_menu .search input:focus {
+		width: 400px;
+	}
+
+	.day .name {
+		display: none;
+	}
+
+	.pagination {
+		margin: 0 0 3.5em;
+	}
+
+	.notification a.close {
+		display: block;
+		left: 0;
+		background: transparent;
+	}
+	.notification a.close:hover {
+		opacity: 0.5;
+	}
+	.notification a.close .icon {
+		display: none;
+	}
+	.nav_menu .search {
+		display: none;
+	}
+
+	#nav_entries {
+		width: 100%;
+	}
+}
+
+@media (max-width: 700px) {
+	.header{
+		display: none;
+	}
+	.nav-login {
+		display: inline-block;
+		width: 100%;
+	}
+	.nav_menu .search {
+		display: inline-block;
+	}
+	.aside .btn-important {
+		display: none;
+	}
+}

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

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

+ 7 - 5
p/themes/base-theme/base.css

@@ -390,16 +390,18 @@ a.btn {
 /*=== Aside main page (feeds) */
 .categories .feeds .item.active {
 }
-.categories .feeds .item.active .feed {
-}
-.categories .feeds .item.empty .feed {
-}
 .categories .feeds .item.empty.active {
 }
-.categories .feeds .item.empty.active .feed {
+.categories .feeds .item.error.active {
+}
+.categories .feeds .item.empty .feed {
 }
 .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;

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff