aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG20
-rw-r--r--README.fr.md101
-rw-r--r--README.md131
-rwxr-xr-xapp/Controllers/configureController.php2
-rw-r--r--app/Controllers/errorController.php52
-rw-r--r--app/Controllers/importExportController.php272
-rwxr-xr-xapp/Controllers/indexController.php55
-rw-r--r--app/Controllers/statsController.php53
-rw-r--r--app/Controllers/usersController.php4
-rw-r--r--app/FreshRSS.php49
-rw-r--r--app/Models/Configuration.php8
-rw-r--r--app/Models/EntryDAO.php14
-rw-r--r--app/Models/EntryDAOSQLite.php4
-rw-r--r--app/Models/Feed.php12
-rw-r--r--app/Models/StatsDAO.php132
-rw-r--r--app/Models/StatsDAOSQLite.php28
-rw-r--r--app/Models/UserDAO.php39
-rw-r--r--app/SQL/install.sql.sqlite.php1
-rw-r--r--app/i18n/en.php57
-rw-r--r--app/i18n/fr.php35
-rw-r--r--app/i18n/install.en.php4
-rw-r--r--app/i18n/install.fr.php4
-rw-r--r--app/install.php28
-rw-r--r--app/layout/aside_flux.phtml17
-rw-r--r--app/layout/aside_stats.phtml3
-rw-r--r--app/layout/layout.phtml2
-rw-r--r--app/layout/nav_menu.phtml18
-rw-r--r--app/views/configure/reading.phtml35
-rw-r--r--app/views/error/index.phtml13
-rw-r--r--app/views/helpers/export/opml.phtml6
-rw-r--r--app/views/helpers/javascript_vars.phtml3
-rwxr-xr-xapp/views/helpers/pagination.phtml2
-rw-r--r--app/views/helpers/view/normal_view.phtml17
-rw-r--r--app/views/helpers/view/reader_view.phtml18
-rw-r--r--app/views/importExport/export.phtml0
-rw-r--r--app/views/importExport/index.phtml31
-rw-r--r--app/views/index/formLogin.phtml23
-rw-r--r--app/views/javascript/actualize.phtml21
-rw-r--r--app/views/stats/idle.phtml20
-rw-r--r--app/views/stats/main.phtml127
-rw-r--r--app/views/stats/repartition.phtml114
-rw-r--r--data/tokens/.gitignore1
-rw-r--r--data/tokens/index.html13
-rw-r--r--lib/Minz/Helper.php17
-rw-r--r--lib/Minz/ModelPdo.php20
-rw-r--r--lib/Minz/Request.php118
-rw-r--r--lib/Minz/Session.php74
-rw-r--r--lib/Minz/Translate.php2
-rw-r--r--lib/lib_rss.php24
-rw-r--r--p/api/greader.php1
-rw-r--r--p/i/.gitignore1
-rw-r--r--p/scripts/jquery.lazyload.min.js15
-rw-r--r--p/scripts/main.js146
-rw-r--r--p/themes/Dark/dark.css6
-rw-r--r--p/themes/Dark/template.css3
-rw-r--r--p/themes/Flat/flat.css11
-rw-r--r--p/themes/Flat/template.css3
-rw-r--r--p/themes/Origine/origine.css20
-rw-r--r--p/themes/Origine/template.css3
-rw-r--r--p/themes/Screwdriver/README.md36
-rw-r--r--p/themes/Screwdriver/icons/add.svg5
-rw-r--r--p/themes/Screwdriver/icons/all.svg7
-rw-r--r--p/themes/Screwdriver/icons/apple-touch-icon.pngbin0 -> 5648 bytes
-rw-r--r--p/themes/Screwdriver/icons/bookmark-add.svg6
-rw-r--r--p/themes/Screwdriver/icons/bookmark.svg60
-rw-r--r--p/themes/Screwdriver/icons/category-white.svg7
-rw-r--r--p/themes/Screwdriver/icons/category.svg7
-rw-r--r--p/themes/Screwdriver/icons/close.svg7
-rw-r--r--p/themes/Screwdriver/icons/configure.svg5
-rw-r--r--p/themes/Screwdriver/icons/down.svg5
-rw-r--r--p/themes/Screwdriver/icons/favicon-16-32-48-64.icobin0 -> 32038 bytes
-rw-r--r--p/themes/Screwdriver/icons/favicon-256.pngbin0 -> 17174 bytes
-rw-r--r--p/themes/Screwdriver/icons/favicon.svg13
-rw-r--r--p/themes/Screwdriver/icons/grey.gifbin0 -> 56 bytes
-rw-r--r--p/themes/Screwdriver/icons/help.svg7
-rw-r--r--p/themes/Screwdriver/icons/icon.svg12
-rw-r--r--p/themes/Screwdriver/icons/key.svg7
-rw-r--r--p/themes/Screwdriver/icons/link.svg7
-rw-r--r--p/themes/Screwdriver/icons/login.svg6
-rw-r--r--p/themes/Screwdriver/icons/logout.svg6
-rw-r--r--p/themes/Screwdriver/icons/next.svg5
-rw-r--r--p/themes/Screwdriver/icons/non-starred.svg5
-rw-r--r--p/themes/Screwdriver/icons/prev.svg5
-rw-r--r--p/themes/Screwdriver/icons/read.svg57
-rw-r--r--p/themes/Screwdriver/icons/refresh.svg5
-rw-r--r--p/themes/Screwdriver/icons/rss.svg6
-rw-r--r--p/themes/Screwdriver/icons/search.svg6
-rw-r--r--p/themes/Screwdriver/icons/share.svg8
-rw-r--r--p/themes/Screwdriver/icons/starred.svg60
-rw-r--r--p/themes/Screwdriver/icons/tag.svg5
-rw-r--r--p/themes/Screwdriver/icons/unread.svg65
-rw-r--r--p/themes/Screwdriver/icons/up.svg5
-rw-r--r--p/themes/Screwdriver/icons/view-global.svg1
-rw-r--r--p/themes/Screwdriver/icons/view-normal.svg1
-rw-r--r--p/themes/Screwdriver/icons/view-reader.svg1
-rw-r--r--p/themes/Screwdriver/loader.gifbin0 -> 4167 bytes
-rw-r--r--p/themes/Screwdriver/metadata.json7
-rw-r--r--p/themes/Screwdriver/screwdriver.css1170
-rw-r--r--p/themes/Screwdriver/template.css695
-rw-r--r--p/themes/base-theme/base.css12
-rw-r--r--p/themes/base-theme/template.css3
101 files changed, 3697 insertions, 681 deletions
diff --git a/CHANGELOG b/CHANGELOG
index cd48765f8..df43fe1d1 100644
--- a/CHANGELOG
+++ b/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
diff --git a/README.fr.md b/README.fr.md
new file mode 100644
index 000000000..df43ef7e8
--- /dev/null
+++ b/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)
diff --git a/README.md b/README.md
index f857bae99..0a22df8f1 100644
--- a/README.md
+++ b/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)
diff --git a/app/Controllers/configureController.php b/app/Controllers/configureController.php
index 79f40b30b..bb96bfae3 100755
--- a/app/Controllers/configureController.php
+++ b/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));
diff --git a/app/Controllers/errorController.php b/app/Controllers/errorController.php
index dc9a2ee25..922650b3d 100644
--- a/app/Controllers/errorController.php
+++ b/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 . ' · ');
}
}
diff --git a/app/Controllers/importExportController.php b/app/Controllers/importExportController.php
index ba172cc6d..5adf3878a 100644
--- a/app/Controllers/importExportController.php
+++ b/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);
+ }
}
diff --git a/app/Controllers/indexController.php b/app/Controllers/indexController.php
index 9a46bde6c..b0b051119 100755
--- a/app/Controllers/indexController.php
+++ b/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);
}
}
diff --git a/app/Controllers/statsController.php b/app/Controllers/statsController.php
index 9009468bc..98f46f0d2 100644
--- a/app/Controllers/statsController.php
+++ b/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(
diff --git a/app/Controllers/usersController.php b/app/Controllers/usersController.php
index 35fa3675f..a9e6c32bc 100644
--- a/app/Controllers/usersController.php
+++ b/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);
diff --git a/app/FreshRSS.php b/app/FreshRSS.php
index 84cf3429b..30f711e20 100644
--- a/app/FreshRSS.php
+++ b/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 () {
diff --git a/app/Models/Configuration.php b/app/Models/Configuration.php
index 7596c54cd..4c804a9fb 100644
--- a/app/Models/Configuration.php
+++ b/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';
}
diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php
index 8c001e73b..75a8aeba4 100644
--- a/app/Models/EntryDAO.php
+++ b/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);
diff --git a/app/Models/EntryDAOSQLite.php b/app/Models/EntryDAOSQLite.php
index 3dabce4b2..9dc395c3c 100644
--- a/app/Models/EntryDAOSQLite.php
+++ b/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` '
diff --git a/app/Models/Feed.php b/app/Models/Feed.php
index 576f37760..2a5ea45ac 100644
--- a/app/Models/Feed.php
+++ b/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" />';
}
}
}
diff --git a/app/Models/StatsDAO.php b/app/Models/StatsDAO.php
index 66f5104b3..89be76a26 100644
--- a/app/Models/StatsDAO.php
+++ b/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);
+ }
+
}
diff --git a/app/Models/StatsDAOSQLite.php b/app/Models/StatsDAOSQLite.php
index dea590c92..6cb54ddf6 100644
--- a/app/Models/StatsDAOSQLite.php
+++ b/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);
+ }
+
}
diff --git a/app/Models/UserDAO.php b/app/Models/UserDAO.php
index dcf847a62..9f64fb4a7 100644
--- a/app/Models/UserDAO.php
+++ b/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;
+ }
}
}
}
diff --git a/app/SQL/install.sql.sqlite.php b/app/SQL/install.sql.sqlite.php
index b90a5ef5e..7988ada04 100644
--- a/app/SQL/install.sql.sqlite.php
+++ b/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,
diff --git a/app/i18n/en.php b/app/i18n/en.php
index 8634f99b5..be0cdc642 100644
--- a/app/i18n/en.php
+++ b/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',
diff --git a/app/i18n/fr.php b/app/i18n/fr.php
index e04078dba..08f12234e 100644
--- a/app/i18n/fr.php
+++ b/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',
diff --git a/app/i18n/install.en.php b/app/i18n/install.en.php
index 553a79921..50208fcef 100644
--- a/app/i18n/install.en.php
+++ b/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)',
diff --git a/app/i18n/install.fr.php b/app/i18n/install.fr.php
index 470d83e1a..9c039f904 100644
--- a/app/i18n/install.fr.php
+++ b/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)',
diff --git a/app/install.php b/app/install.php
index 3767e3d91..eaa1100c1 100644
--- a/app/install.php
+++ b/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>
diff --git a/app/layout/aside_flux.phtml b/app/layout/aside_flux.phtml
index 817dae676..432d6fdb7 100644
--- a/app/layout/aside_flux.phtml
+++ b/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>
diff --git a/app/layout/aside_stats.phtml b/app/layout/aside_stats.phtml
index 32a3f5dee..fbfb9d84d 100644
--- a/app/layout/aside_stats.phtml
+++ b/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>
diff --git a/app/layout/layout.phtml b/app/layout/layout.phtml
index d2e1e4b3b..96a88d245 100644
--- a/app/layout/layout.phtml
+++ b/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)) {
diff --git a/app/layout/nav_menu.phtml b/app/layout/nav_menu.phtml
index 29ea9032c..25833c16d 100644
--- a/app/layout/nav_menu.phtml
+++ b/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'); ?>
diff --git a/app/views/configure/reading.phtml b/app/views/configure/reading.phtml
index 4d439e83d..5a26501a4 100644
--- a/app/views/configure/reading.phtml
+++ b/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>
@@ -84,6 +83,26 @@
<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">
<input type="checkbox" name="reading_confirm" id="reading_confirm" value="1"<?php echo $this->conf->reading_confirm ? ' checked="checked"' : ''; ?> />
<?php echo Minz_Translate::t ('reading_confirm'); ?>
diff --git a/app/views/error/index.phtml b/app/views/error/index.phtml
index 6a09c3aa2..ef4fbd39d 100644
--- a/app/views/error/index.phtml
+++ b/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>
diff --git a/app/views/helpers/export/opml.phtml b/app/views/helpers/export/opml.phtml
index f667d093c..8622d9144 100644
--- a/app/views/helpers/export/opml.phtml
+++ b/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()),
);
}
}
diff --git a/app/views/helpers/javascript_vars.phtml b/app/views/helpers/javascript_vars.phtml
index 6e0a20de3..7144c519a 100644
--- a/app/views/helpers/javascript_vars.phtml
+++ b/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";
diff --git a/app/views/helpers/pagination.phtml b/app/views/helpers/pagination.phtml
index f38913c06..f237e1f3e 100755
--- a/app/views/helpers/pagination.phtml
+++ b/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 { ?>
diff --git a/app/views/helpers/view/normal_view.phtml b/app/views/helpers/view/normal_view.phtml
index 6f172d579..87bf2e22a 100644
--- a/app/views/helpers/view/normal_view.phtml
+++ b/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
diff --git a/app/views/helpers/view/reader_view.phtml b/app/views/helpers/view/reader_view.phtml
index e37c78cb4..665f72849 100644
--- a/app/views/helpers/view/reader_view.phtml
+++ b/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>
diff --git a/app/views/importExport/export.phtml b/app/views/importExport/export.phtml
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/app/views/importExport/export.phtml
diff --git a/app/views/importExport/index.phtml b/app/views/importExport/index.phtml
index 309058959..35371faca 100644
--- a/app/views/importExport/index.phtml
+++ b/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>
diff --git a/app/views/index/formLogin.phtml b/app/views/index/formLogin.phtml
index cc925ea59..b79c1b614 100644
--- a/app/views/index/formLogin.phtml
+++ b/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>
diff --git a/app/views/javascript/actualize.phtml b/app/views/javascript/actualize.phtml
index d08dc47d1..74cef4998 100644
--- a/app/views/javascript/actualize.phtml
+++ b/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);
diff --git a/app/views/stats/idle.phtml b/app/views/stats/idle.phtml
index 356fea20f..2ba5237f7 100644
--- a/app/views/stats/idle.phtml
+++ b/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>
diff --git a/app/views/stats/main.phtml b/app/views/stats/main.phtml
deleted file mode 100644
index fe372e221..000000000
--- a/app/views/stats/main.phtml
+++ /dev/null
@@ -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>
diff --git a/app/views/stats/repartition.phtml b/app/views/stats/repartition.phtml
new file mode 100644
index 000000000..9d2eb28e4
--- /dev/null
+++ b/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>
diff --git a/data/tokens/.gitignore b/data/tokens/.gitignore
new file mode 100644
index 000000000..2211df63d
--- /dev/null
+++ b/data/tokens/.gitignore
@@ -0,0 +1 @@
+*.txt
diff --git a/data/tokens/index.html b/data/tokens/index.html
new file mode 100644
index 000000000..85faaa37e
--- /dev/null
+++ b/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>
diff --git a/lib/Minz/Helper.php b/lib/Minz/Helper.php
index b058211d3..f4a547c4e 100644
--- a/lib/Minz/Helper.php
+++ b/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');
+ }
}
diff --git a/lib/Minz/ModelPdo.php b/lib/Minz/ModelPdo.php
index 1f56f09c2..45a1e9451 100644
--- a/lib/Minz/ModelPdo.php
+++ b/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);
}
}
diff --git a/lib/Minz/Request.php b/lib/Minz/Request.php
index 755784522..52f53012f 100644
--- a/lib/Minz/Request.php
+++ b/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';
}
}
diff --git a/lib/Minz/Session.php b/lib/Minz/Session.php
index ddabc4658..af4de75bb 100644
--- a/lib/Minz/Session.php
+++ b/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;
+ }
+
}
diff --git a/lib/Minz/Translate.php b/lib/Minz/Translate.php
index df48350e9..8c2f90041 100644
--- a/lib/Minz/Translate.php
+++ b/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);
}
diff --git a/lib/lib_rss.php b/lib/lib_rss.php
index 7ca611b04..823f53716 100644
--- a/lib/lib_rss.php
+++ b/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');
-}
diff --git a/p/api/greader.php b/p/api/greader.php
index 7a961225f..5a6fdad7d 100644
--- a/p/api/greader.php
+++ b/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)');
}
diff --git a/p/i/.gitignore b/p/i/.gitignore
new file mode 100644
index 000000000..03c88fd7a
--- /dev/null
+++ b/p/i/.gitignore
@@ -0,0 +1 @@
+.htaccess
diff --git a/p/scripts/jquery.lazyload.min.js b/p/scripts/jquery.lazyload.min.js
deleted file mode 100644
index 8dd097dc3..000000000
--- a/p/scripts/jquery.lazyload.min.js
+++ /dev/null
@@ -1,15 +0,0 @@
-/*
- * Lazy Load - jQuery plugin for lazy loading images
- *
- * Copyright (c) 2007-2013 Mika Tuupola
- *
- * Licensed under the MIT license:
- * http://www.opensource.org/licenses/mit-license.php
- *
- * Project home:
- * http://www.appelsiini.net/projects/lazyload
- *
- * Version: 1.9.0
- *
- */
-!function(a,b,c,d){var e=a(b);a.fn.lazyload=function(f){function g(){var b=0;i.each(function(){var c=a(this);if(!j.skip_invisible||c.is(":visible"))if(a.abovethetop(this,j)||a.leftofbegin(this,j));else if(a.belowthefold(this,j)||a.rightoffold(this,j)){if(++b>j.failure_limit)return!1}else c.trigger("appear"),b=0})}var h,i=this,j={threshold:0,failure_limit:0,event:"scroll",effect:"show",container:b,data_attribute:"original",skip_invisible:!0,appear:null,load:null,placeholder:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC"};return f&&(d!==f.failurelimit&&(f.failure_limit=f.failurelimit,delete f.failurelimit),d!==f.effectspeed&&(f.effect_speed=f.effectspeed,delete f.effectspeed),a.extend(j,f)),h=j.container===d||j.container===b?e:a(j.container),0===j.event.indexOf("scroll")&&h.bind(j.event,function(){return g()}),this.each(function(){var b=this,c=a(b);b.loaded=!1,(c.attr("src")===d||c.attr("src")===!1)&&c.attr("src",j.placeholder),c.one("appear",function(){if(!this.loaded){if(j.appear){var d=i.length;j.appear.call(b,d,j)}a("<img />").bind("load",function(){var d=c.data(j.data_attribute);c.hide(),c.is("img")?c.attr("src",d):c.css("background-image","url('"+d+"')"),c[j.effect](j.effect_speed),b.loaded=!0;var e=a.grep(i,function(a){return!a.loaded});if(i=a(e),j.load){var f=i.length;j.load.call(b,f,j)}}).attr("src",c.data(j.data_attribute))}}),0!==j.event.indexOf("scroll")&&c.bind(j.event,function(){b.loaded||c.trigger("appear")})}),e.bind("resize",function(){g()}),/iphone|ipod|ipad.*os 5/gi.test(navigator.appVersion)&&e.bind("pageshow",function(b){b.originalEvent&&b.originalEvent.persisted&&i.each(function(){a(this).trigger("appear")})}),a(c).ready(function(){g()}),this},a.belowthefold=function(c,f){var g;return g=f.container===d||f.container===b?(b.innerHeight?b.innerHeight:e.height())+e.scrollTop():a(f.container).offset().top+a(f.container).height(),g<=a(c).offset().top-f.threshold},a.rightoffold=function(c,f){var g;return g=f.container===d||f.container===b?e.width()+e.scrollLeft():a(f.container).offset().left+a(f.container).width(),g<=a(c).offset().left-f.threshold},a.abovethetop=function(c,f){var g;return g=f.container===d||f.container===b?e.scrollTop():a(f.container).offset().top,g>=a(c).offset().top+f.threshold+a(c).height()},a.leftofbegin=function(c,f){var g;return g=f.container===d||f.container===b?e.scrollLeft():a(f.container).offset().left,g>=a(c).offset().left+f.threshold+a(c).width()},a.inviewport=function(b,c){return!(a.rightoffold(b,c)||a.leftofbegin(b,c)||a.belowthefold(b,c)||a.abovethetop(b,c))},a.extend(a.expr[":"],{"below-the-fold":function(b){return a.belowthefold(b,{threshold:0})},"above-the-top":function(b){return!a.belowthefold(b,{threshold:0})},"right-of-screen":function(b){return a.rightoffold(b,{threshold:0})},"left-of-screen":function(b){return!a.rightoffold(b,{threshold:0})},"in-viewport":function(b){return a.inviewport(b,{threshold:0})},"above-the-fold":function(b){return!a.belowthefold(b,{threshold:0})},"right-of-fold":function(b){return a.rightoffold(b,{threshold:0})},"left-of-fold":function(b){return!a.rightoffold(b,{threshold:0})}})}(jQuery,window,document); \ No newline at end of file
diff --git a/p/scripts/main.js b/p/scripts/main.js
index d0f3c27e9..c37f9f6f2 100644
--- a/p/scripts/main.js
+++ b/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) {
diff --git a/p/themes/Dark/dark.css b/p/themes/Dark/dark.css
index 5edc09042..2ef48c406 100644
--- a/p/themes/Dark/dark.css
+++ b/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;
diff --git a/p/themes/Dark/template.css b/p/themes/Dark/template.css
index 09ecaf685..466ec4603 100644
--- a/p/themes/Dark/template.css
+++ b/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;
diff --git a/p/themes/Flat/flat.css b/p/themes/Flat/flat.css
index 4ae3e98e9..fcfbb1424 100644
--- a/p/themes/Flat/flat.css
+++ b/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;
diff --git a/p/themes/Flat/template.css b/p/themes/Flat/template.css
index 09ecaf685..466ec4603 100644
--- a/p/themes/Flat/template.css
+++ b/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;
diff --git a/p/themes/Origine/origine.css b/p/themes/Origine/origine.css
index 0b95e2d70..0d1d95bad 100644
--- a/p/themes/Origine/origine.css
+++ b/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;
diff --git a/p/themes/Origine/template.css b/p/themes/Origine/template.css
index 09ecaf685..466ec4603 100644
--- a/p/themes/Origine/template.css
+++ b/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;
diff --git a/p/themes/Screwdriver/README.md b/p/themes/Screwdriver/README.md
new file mode 100644
index 000000000..4b10eb90f
--- /dev/null
+++ b/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*
+
+
+
+
+
+
diff --git a/p/themes/Screwdriver/icons/add.svg b/p/themes/Screwdriver/icons/add.svg
new file mode 100644
index 000000000..652491775
--- /dev/null
+++ b/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> \ No newline at end of file
diff --git a/p/themes/Screwdriver/icons/all.svg b/p/themes/Screwdriver/icons/all.svg
new file mode 100644
index 000000000..aeaa96277
--- /dev/null
+++ b/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> \ No newline at end of file
diff --git a/p/themes/Screwdriver/icons/apple-touch-icon.png b/p/themes/Screwdriver/icons/apple-touch-icon.png
new file mode 100644
index 000000000..0bbab07ea
--- /dev/null
+++ b/p/themes/Screwdriver/icons/apple-touch-icon.png
Binary files differ
diff --git a/p/themes/Screwdriver/icons/bookmark-add.svg b/p/themes/Screwdriver/icons/bookmark-add.svg
new file mode 100644
index 000000000..51db9c498
--- /dev/null
+++ b/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> \ No newline at end of file
diff --git a/p/themes/Screwdriver/icons/bookmark.svg b/p/themes/Screwdriver/icons/bookmark.svg
new file mode 100644
index 000000000..4041c6503
--- /dev/null
+++ b/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>
diff --git a/p/themes/Screwdriver/icons/category-white.svg b/p/themes/Screwdriver/icons/category-white.svg
new file mode 100644
index 000000000..5bb5cecd9
--- /dev/null
+++ b/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> \ No newline at end of file
diff --git a/p/themes/Screwdriver/icons/category.svg b/p/themes/Screwdriver/icons/category.svg
new file mode 100644
index 000000000..b5470d84a
--- /dev/null
+++ b/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> \ No newline at end of file
diff --git a/p/themes/Screwdriver/icons/close.svg b/p/themes/Screwdriver/icons/close.svg
new file mode 100644
index 000000000..20d1577ab
--- /dev/null
+++ b/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> \ No newline at end of file
diff --git a/p/themes/Screwdriver/icons/configure.svg b/p/themes/Screwdriver/icons/configure.svg
new file mode 100644
index 000000000..2957a7abe
--- /dev/null
+++ b/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> \ No newline at end of file
diff --git a/p/themes/Screwdriver/icons/down.svg b/p/themes/Screwdriver/icons/down.svg
new file mode 100644
index 000000000..8a234ee99
--- /dev/null
+++ b/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> \ No newline at end of file
diff --git a/p/themes/Screwdriver/icons/favicon-16-32-48-64.ico b/p/themes/Screwdriver/icons/favicon-16-32-48-64.ico
new file mode 100644
index 000000000..90f7d5118
--- /dev/null
+++ b/p/themes/Screwdriver/icons/favicon-16-32-48-64.ico
Binary files differ
diff --git a/p/themes/Screwdriver/icons/favicon-256.png b/p/themes/Screwdriver/icons/favicon-256.png
new file mode 100644
index 000000000..8b1021e1a
--- /dev/null
+++ b/p/themes/Screwdriver/icons/favicon-256.png
Binary files differ
diff --git a/p/themes/Screwdriver/icons/favicon.svg b/p/themes/Screwdriver/icons/favicon.svg
new file mode 100644
index 000000000..a252050b9
--- /dev/null
+++ b/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>
diff --git a/p/themes/Screwdriver/icons/grey.gif b/p/themes/Screwdriver/icons/grey.gif
new file mode 100644
index 000000000..c7212bc1f
--- /dev/null
+++ b/p/themes/Screwdriver/icons/grey.gif
Binary files differ
diff --git a/p/themes/Screwdriver/icons/help.svg b/p/themes/Screwdriver/icons/help.svg
new file mode 100644
index 000000000..9a0022f76
--- /dev/null
+++ b/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> \ No newline at end of file
diff --git a/p/themes/Screwdriver/icons/icon.svg b/p/themes/Screwdriver/icons/icon.svg
new file mode 100644
index 000000000..caa987dac
--- /dev/null
+++ b/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>
diff --git a/p/themes/Screwdriver/icons/key.svg b/p/themes/Screwdriver/icons/key.svg
new file mode 100644
index 000000000..9193f4552
--- /dev/null
+++ b/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>
diff --git a/p/themes/Screwdriver/icons/link.svg b/p/themes/Screwdriver/icons/link.svg
new file mode 100644
index 000000000..e5290af76
--- /dev/null
+++ b/p/themes/Screwdriver/icons/link.svg
@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="16.004" width="16">
+<g fill="#bebebe" transform="translate(-183,-529)">
+<path style="enable-background:accumulate;color:#000000;" d="m191,533.85,0,10.38-2.344-2.2882-1.3394,2.7346c-0.32808,0.73962-2.0337,0.14492-1.5487-0.84412l1.3255-2.8393h-2.9579l6.8645-7.1436z" fill-rule="nonzero" display="block"/>
+<path d="m190.16,530.06c-3.8266,0.46006-6.5788,3.9578-6.0938,7.7812,0.13127,1.0347,0.29377,1.3818,0.29377,1.3818l1.675-1.6318c-0.33104-2.7534,1.6216-5.2315,4.375-5.5625,2.7534-0.33104,5.2315,1.6216,5.5625,4.375,0.31355,2.608-1.3913,5.0249-3.9688,5.5312l0.0312,2s0.52086-0.1059,0.62354-0.13097c3.4156-0.83385,5.7063-4.1273,5.2827-7.6503-0.46006-3.8266-3.9547-6.5538-7.7812-6.0938z" style="baseline-shift:baseline;block-progression:tb;color:#000000;direction:ltr;text-indent:0;text-align:start;enable-background:accumulate;text-transform:none;"/>
+<path opacity="0.3" style="enable-background:accumulate;color:#000000;" d="m187.11,536.81,0-0.20574-0.19826,0.0186c0.0165-0.13095,0.0329-0.26167,0.0496-0.3926h-0.11578l-0.11556,0.14959-0.11578,0.0559-0.1653-0.0932-0.0165-0.20575,0.0331-0.22438,0.24798-0.18688h0.19826l0.0329-0.11229,0.24786,0.0559,0.18183,0.2246,0.0331-0.37419,0.31401-0.26167,0.11567-0.28055,0.23133-0.0934,0.1322-0.18688,0.29738-0.0564,0.14885-0.22415h-0.44623l0.28094-0.13095h0.19814l0.28106-0.0937,0.0331-0.11186-0.0992-0.0937-0.11567-0.0375,0.0331-0.11208-0.0826-0.16822-0.19837,0.0746,0.0331-0.14947-0.23134-0.13096-0.18171,0.3177,0.0165,0.11229-0.18171,0.075-0.11578,0.24302-0.0495-0.22438-0.31402-0.13095-0.0496-0.16822,0.41315-0.24325,0.18182-0.16822,0.0165-0.20563-0.0991-0.0562-0.13219-0.0188-0.0826,0.20575s-0.1382,0.0271-0.17373,0.0358c-0.45378,0.41804-1.3707,1.3204-1.5837,3.024,0.008,0.0395,0.15441,0.26854,0.15441,0.26854l0.347,0.20552,0.347,0.0937m3.9661-4.3003-0.4298-0.16833-0.49552,0.0561-0.61161,0.16822-0.11567,0.11229,0.38008,0.26167,0,0.14959-0.14875,0.14959,0.19846,0.39294,0.13188-0.075,0.16561-0.26168c0.2553-0.0789,0.4842-0.16833,0.72686-0.28053l0.19846-0.5048m2.5292,0.34192-0.375,0.0937-0.21875,0.15625,0,0.125-0.375,0.25,0.0937,0.34375,0.21875-0.15625,0.125,0.15625,0.15625,0.0937,0.0937-0.28125-0.0625-0.15625,0.0625-0.0937,0.21875-0.1875,0.0937,0-0.0937,0.21875,0,0.1875c0.0892-0.0242,0.1588-0.051,0.25-0.0625l-0.25,0.1875v0.125l-0.3125,0.21875-0.28125-0.0625v-0.15625l-0.125,0.0625,0.0625,0.15625h-0.21875l-0.125,0.21875-0.15625,0.15625-0.0937,0.0312v0.1875l0.0312,0.15625h-0.0312v0.53125l0.0625-0.0312,0.0937-0.21875,0.1875-0.125,0.0312-0.0937,0.28125-0.0625,0.15625,0.1875,0.1875,0.0937-0.0937,0.1875,0.15625-0.0312,0.0625-0.21875-0.1875-0.21875h0.0625l0.21875,0.15625,0.0312,0.21875,0.15625,0.21875,0.0625-0.3125,0.0937-0.0312c0.0959,0.0996,0.1692,0.23163,0.25,0.34375h0.28125l0.1875,0.125-0.0937,0.0937-0.15625,0.15625h-0.25l-0.34375-0.0937h-0.1875l-0.125,0.15625-0.34375-0.375-0.25-0.0625-0.375,0.0625-0.15625,0.0937v2.4062l0.0312,0.0312,0.25-0.15625,0.0937,0.0937h0.28125l0.125,0.15625-0.0937,0.3125,0.1875,0.1875v0.375l0.125,0.25-0.0937,0.25c-0.009,0.16159,0,0.30714,0,0.46875,0.0795,0.21894,0.14355,0.43575,0.21875,0.65625l0.0625,0.34375v0.1875h0.125l0.21875-0.125h0.25l0.375-0.4375-0.0312-0.15625,0.25-0.21875-0.1875-0.1875,0.21875-0.1875,0.21875-0.125,0.0937-0.125-0.0625-0.25v-0.59375l0.1875-0.375,0.1875-0.25,0.25-0.5625v-0.15625c-0.11654,0.0146-0.22972,0.0231-0.34375,0.0312-0.0722,0.005-0.14446,0-0.21875,0-0.12359-0.25961-0.2183-0.50966-0.3125-0.78125l-0.15625-0.1875-0.0937-0.3125,0.0625-0.0625,0.21875,0.25,0.25,0.5625,0.15625,0.15625-0.0625,0.21875,0.15625,0.15625,0.25-0.25,0.3125-0.21875,0.15625-0.1875v-0.21875c-0.0389-0.0732-0.0547-0.14545-0.0937-0.21875l-0.15625,0.1875-0.125-0.15625-0.1875-0.125v-0.28125l0.21875,0.21875,0.21875-0.0312c0.10166,0.0923,0.19205,0.20751,0.28125,0.3125l0.15625-0.1875c0-0.17463-0.19976-1.0204-0.625-1.75-0.42526-0.72932-1.1562-1.4062-1.1562-1.4062l-0.0625,0.0937-0.21875,0.21875-0.25-0.25h0.25l0.125-0.125-0.46875-0.0937-0.25-0.0937z" fill-rule="nonzero"/>
+</g>
+</svg> \ No newline at end of file
diff --git a/p/themes/Screwdriver/icons/login.svg b/p/themes/Screwdriver/icons/login.svg
new file mode 100644
index 000000000..954a0b74b
--- /dev/null
+++ b/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> \ No newline at end of file
diff --git a/p/themes/Screwdriver/icons/logout.svg b/p/themes/Screwdriver/icons/logout.svg
new file mode 100644
index 000000000..8dd4ef08b
--- /dev/null
+++ b/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> \ No newline at end of file
diff --git a/p/themes/Screwdriver/icons/next.svg b/p/themes/Screwdriver/icons/next.svg
new file mode 100644
index 000000000..d504d096c
--- /dev/null
+++ b/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> \ No newline at end of file
diff --git a/p/themes/Screwdriver/icons/non-starred.svg b/p/themes/Screwdriver/icons/non-starred.svg
new file mode 100644
index 000000000..4ebcfccbd
--- /dev/null
+++ b/p/themes/Screwdriver/icons/non-starred.svg
@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16">
+<g transform="translate(-61.000202,-397)">
+<path style="baseline-shift:baseline;block-progression:tb;color:#000000;direction:ltr;text-indent:0;text-align:start;enable-background:accumulate;text-transform:none;" fill="#bebebe" d="m69.003,398.01c-0.2659-0.00099-0.49859,0.1006-0.6647,0.2393-0.16611,0.13869-0.27742,0.32137-0.38968,0.50028-0.22453,0.35782-0.39269,0.76873-0.56546,1.2036-0.17277,0.43483-0.34713,0.88766-0.5046,1.2323-0.15747,0.34465-0.3456,0.57871-0.35862,0.58776-0.01287,0.009-0.30722,0.117-0.69576,0.15181-0.38855,0.0348-0.88122,0.0492-1.3632,0.0694-0.48202,0.0202-0.9703,0.0362-1.393,0.12817-0.21138,0.046-0.4154,0.10148-0.60434,0.20956s-0.36689,0.28631-0.45004,0.53002-0.04901,0.50627,0.03636,0.70157c0.08537,0.19531,0.22656,0.36514,0.3682,0.52344,0.2833,0.31663,0.6327,0.60557,1.0079,0.89849s0.77671,0.55926,1.0678,0.81027c0.29104,0.25101,0.45422,0.50796,0.45912,0.52271,0.0048,0.0146,0.03119,0.33498-0.05456,0.70231-0.08575,0.36732-0.2238,0.81174-0.35282,1.2603-0.12902,0.44861-0.25176,0.90196-0.2917,1.3184-0.01997,0.20819-0.04285,0.40729,0.0053,0.61409,0.04814,0.20679,0.1845,0.43007,0.39902,0.58168,0.21451,0.15161,0.44936,0.1881,0.66823,0.1701s0.43535-0.0703,0.63515-0.15132c0.39961-0.16214,0.80177-0.42851,1.2064-0.68231,0.40465-0.2538,0.80822-0.52155,1.1456-0.71107,0.33734-0.18952,0.6484-0.2686,0.66445-0.26854,0.01586,0.00006,0.30338,0.0951,0.63894,0.28732,0.33556,0.19221,0.72532,0.46503,1.1276,0.72205,0.40229,0.25702,0.81996,0.49752,1.218,0.66284,0.19903,0.0827,0.38893,0.15086,0.60762,0.17059,0.2187,0.0197,0.47978-0.031,0.69551-0.18105,0.21572-0.15001,0.33928-0.35235,0.38918-0.55877s0.04291-0.43517,0.02476-0.64358c-0.03632-0.41683-0.15899-0.86394-0.2841-1.3137-0.12511-0.44978-0.26165-0.88661-0.34421-1.2548-0.08256-0.36813-0.07169-0.68662-0.06666-0.70133,0.005-0.0145,0.18746-0.25247,0.4806-0.50101,0.29313-0.24854,0.67599-0.53755,1.0536-0.82731,0.37764-0.28976,0.76716-0.57158,1.0531-0.88579,0.14298-0.1571,0.2418-0.33444,0.32882-0.52904s0.136-0.42874,0.05481-0.67306-0.27108-0.41314-0.45912-0.52272-0.36679-0.18158-0.57782-0.22931c-0.42206-0.0955-0.91359-0.14009-1.3956-0.16426-0.48198-0.0242-0.94584-0.039-1.3342-0.077s-0.71565-0.13122-0.72859-0.14037c-0.01279-0.009-0.18402-0.23636-0.3384-0.58217s-0.32658-0.78819-0.49548-1.2243c-0.1689-0.4361-0.33477-0.86429-0.55609-1.2238-0.11067-0.17977-0.23335-0.35397-0.39826-0.49396s-0.40309-0.25684-0.66899-0.25783zm0.0086,0.99424c0.0422,0.0358,0.10671,0.13602,0.1841,0.26173,0.15477,0.25141,0.33068,0.6272,0.49397,1.0488,0.16329,0.42163,0.31905,0.88645,0.49598,1.2828,0.17693,0.39633,0.31456,0.73379,0.6753,0.98889,0.36072,0.25509,0.77019,0.29763,1.2152,0.34118,0.44506,0.0435,0.92882,0.0532,1.3948,0.0765,0.46599,0.0234,0.91824,0.0697,1.2135,0.13647,0.14763,0.0334,0.25558,0.0572,0.30381,0.0853-0.02227,0.0498-0.07958,0.15478-0.17956,0.26464-0.19997,0.21973-0.53749,0.48746-0.90261,0.7676-0.36511,0.28015-0.77804,0.57219-1.114,0.85704-0.33595,0.28485-0.63992,0.53221-0.77987,0.94209-0.13995,0.40986-0.05396,0.77681,0.04065,1.1987s0.25406,0.87813,0.37502,1.313c0.12096,0.43486,0.22688,0.84692,0.25228,1.1385,0.01156,0.13264-0.01699,0.23485-0.02778,0.29267-0.05993-0.008-0.1764-0.0324-0.30381-0.0853-0.27836-0.11561-0.64649-0.32808-1.0354-0.57657-0.38894-0.24849-0.8039-0.53053-1.1885-0.75081-0.38457-0.22028-0.70791-0.39837-1.1551-0.40015-0.44722-0.002-0.79205,0.17245-1.1786,0.38965-0.38659,0.21719-0.78438,0.49517-1.1756,0.74055-0.39123,0.24538-0.77636,0.4507-1.0559,0.56412-0.13977,0.0567-0.24616,0.0856-0.3023,0.0902-0.01026-0.0578-0.01522-0.16008-0.0025-0.29243,0.02793-0.29118,0.13818-0.70893,0.26291-1.1426,0.12473-0.43372,0.25404-0.89785,0.35232-1.3188,0.09828-0.42099,0.18672-0.78846,0.05027-1.1994-0.13644-0.41097-0.43218-0.64202-0.76571-0.92967-0.33353-0.28766-0.70551-0.58555-1.0683-0.86876-0.36275-0.2832-0.7178-0.56075-0.91597-0.78224-0.09908-0.11075-0.16221-0.21163-0.1841-0.26173,0.04834-0.0276,0.15448-0.058,0.3023-0.0902,0.29562-0.0643,0.74518-0.10714,1.2112-0.1267,0.46603-0.0196,0.94824-0.0298,1.3935-0.0697,0.44531-0.0399,0.84679-0.0499,1.2097-0.30216,0.36289-0.25221,0.5383-0.60511,0.71876-1.0001,0.18046-0.39497,0.33731-0.84115,0.50435-1.2616,0.16704-0.42041,0.31848-0.83556,0.47556-1.0859,0.07853-0.12515,0.16751-0.20007,0.21012-0.23565z"/>
+</g>
+</svg> \ No newline at end of file
diff --git a/p/themes/Screwdriver/icons/prev.svg b/p/themes/Screwdriver/icons/prev.svg
new file mode 100644
index 000000000..ea4372098
--- /dev/null
+++ b/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> \ No newline at end of file
diff --git a/p/themes/Screwdriver/icons/read.svg b/p/themes/Screwdriver/icons/read.svg
new file mode 100644
index 000000000..ab6fe6422
--- /dev/null
+++ b/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>
diff --git a/p/themes/Screwdriver/icons/refresh.svg b/p/themes/Screwdriver/icons/refresh.svg
new file mode 100644
index 000000000..62fe56c1e
--- /dev/null
+++ b/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> \ No newline at end of file
diff --git a/p/themes/Screwdriver/icons/rss.svg b/p/themes/Screwdriver/icons/rss.svg
new file mode 100644
index 000000000..2a8713be3
--- /dev/null
+++ b/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> \ No newline at end of file
diff --git a/p/themes/Screwdriver/icons/search.svg b/p/themes/Screwdriver/icons/search.svg
new file mode 100644
index 000000000..35a28dfca
--- /dev/null
+++ b/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> \ No newline at end of file
diff --git a/p/themes/Screwdriver/icons/share.svg b/p/themes/Screwdriver/icons/share.svg
new file mode 100644
index 000000000..db7a3649c
--- /dev/null
+++ b/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> \ No newline at end of file
diff --git a/p/themes/Screwdriver/icons/starred.svg b/p/themes/Screwdriver/icons/starred.svg
new file mode 100644
index 000000000..4b4a8f7ed
--- /dev/null
+++ b/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>
diff --git a/p/themes/Screwdriver/icons/tag.svg b/p/themes/Screwdriver/icons/tag.svg
new file mode 100644
index 000000000..0d6db6ed8
--- /dev/null
+++ b/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> \ No newline at end of file
diff --git a/p/themes/Screwdriver/icons/unread.svg b/p/themes/Screwdriver/icons/unread.svg
new file mode 100644
index 000000000..596dec0ac
--- /dev/null
+++ b/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>
diff --git a/p/themes/Screwdriver/icons/up.svg b/p/themes/Screwdriver/icons/up.svg
new file mode 100644
index 000000000..4819c9205
--- /dev/null
+++ b/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> \ No newline at end of file
diff --git a/p/themes/Screwdriver/icons/view-global.svg b/p/themes/Screwdriver/icons/view-global.svg
new file mode 100644
index 000000000..d81e79790
--- /dev/null
+++ b/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> \ No newline at end of file
diff --git a/p/themes/Screwdriver/icons/view-normal.svg b/p/themes/Screwdriver/icons/view-normal.svg
new file mode 100644
index 000000000..c35b101df
--- /dev/null
+++ b/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> \ No newline at end of file
diff --git a/p/themes/Screwdriver/icons/view-reader.svg b/p/themes/Screwdriver/icons/view-reader.svg
new file mode 100644
index 000000000..3243aed6e
--- /dev/null
+++ b/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> \ No newline at end of file
diff --git a/p/themes/Screwdriver/loader.gif b/p/themes/Screwdriver/loader.gif
new file mode 100644
index 000000000..5ff26f0e3
--- /dev/null
+++ b/p/themes/Screwdriver/loader.gif
Binary files differ
diff --git a/p/themes/Screwdriver/metadata.json b/p/themes/Screwdriver/metadata.json
new file mode 100644
index 000000000..f45f1a98a
--- /dev/null
+++ b/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"]
+}
diff --git a/p/themes/Screwdriver/screwdriver.css b/p/themes/Screwdriver/screwdriver.css
new file mode 100644
index 000000000..665f89c71
--- /dev/null
+++ b/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;
+ }
+}
diff --git a/p/themes/Screwdriver/template.css b/p/themes/Screwdriver/template.css
new file mode 100644
index 000000000..bf421e322
--- /dev/null
+++ b/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;
+ }
+}
diff --git a/p/themes/base-theme/base.css b/p/themes/base-theme/base.css
index c45b1812e..76ac37933 100644
--- a/p/themes/base-theme/base.css
+++ b/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;
diff --git a/p/themes/base-theme/template.css b/p/themes/base-theme/template.css
index 09ecaf685..466ec4603 100644
--- a/p/themes/base-theme/template.css
+++ b/p/themes/base-theme/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;