diff options
| author | 2013-07-04 19:38:29 +0200 | |
|---|---|---|
| committer | 2013-07-04 19:38:29 +0200 | |
| commit | eb5f05304c253df90873b94ba52d7093115f3850 (patch) | |
| tree | 1152ab618aa5cf884a1f2f2e2d1926da4167a6be | |
| parent | 8dd5fd51f74a47e5c80052f27a74cdcd5dd044b9 (diff) | |
| parent | b5f233f6d524ca9f74e9d33bf5692a1a678d7fec (diff) | |
Merge branch 'dev'0.4.0
71 files changed, 3670 insertions, 626 deletions
diff --git a/.gitignore b/.gitignore index c9c8cacf4..e69de29bb 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +0,0 @@ -lib/minz diff --git a/README b/README deleted file mode 100644 index 2339c72e9..000000000 --- a/README +++ /dev/null @@ -1,92 +0,0 @@ -Un simple agrégateur de flux rss relativement léger et rapide par rapport aux -mastodontes que sont RSSLounge et TinyTinyRSS. - -@name FreshRSS -@url http://marienfressinaud.github.io/FreshRSS/ -@demo http://marienfressinaud.fr/projets/freshrss/ -@author Marien Fressinaud <dev@marienfressinaud.fr> -@version 0.3.0 -@date 2013-05-05 -@license AGPL3 - -DISCLAIMER -========== -Cette application a été développée pour s'adapter à mes besoins personnels. -Je ne garantis en aucun cas la sécurité de celle-ci, ni son bon fonctionnement -sur un autre serveur que le mien. 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 - -PRE-REQUIS -========== -- Serveur Apache (non testé sur aucun autre) -- PHP 5.3 (il me faudrait des retours sur d'autres versions antérieures) -- libxml pour PHP -- cURL -- PDO et MySQL - -INSTALLATION -============ - 1. Récupérez l'application FreshRSS via la commande git ou en - téléchargeant l'archive - 2. Exécutez le script ./build.sh ou en récupérant à la main la librairie - Minz (https://github.com/marienfressinaud/MINZ) et en copiant la lib - dans ./lib/minz - 3. Déplacez l'application où vous voulez sur votre serveur (attention, - la partie accessible se trouve dans le répertoire `./public`) - 4. Accédez à FreshRSS à travers votre navigateur web et suivez les - instructions - 5. Tout devrait fonctionner :) En cas de problème, n'hésitez pas à me - contacter. - -SÉCURITÉ ET CONSEILS -==================== - 1. Si possible, faites pointer un sous-domaine sur le répertoire - `./public` - 2. Le fichier de log peut être utile à lire si vous avez des soucis - 3. Le fichier `./public/index.php` défini les chemins d'accès aux - répertoires clés de l'application. Si vous les bougez, tout se passe - ici. - 4. Vous pouvez ajouter une tâche CRON sur le script d'actualisation des - flux. Il s'agit d'un script PHP à exécuter avec la commande `php`. - Par exemple, pour exécuter le script toutes les heures : - 0 * * * * php /chemin/vers/freshrss/actualize_script.php >/dev/null 2>&1 - Veuillez cependant vérifier que le fichier ./public/data/Configuration.array.php - soit accessible en lecture / écriture par l'exécuteur du script - -CHANGELOG -========= -2013-05-05 changes with FreshRSS 0.3.0 - *) Fallback pour les icônes SVG (utilisation de PNG à la place) - *) Fallback pour les propriétés CSS3 (utilisation de préfixes) - *) Affichage des tags associés aux articles - *) Internationalisation de l'application (gestion des langues anglaise - et française) - *) Gestion des flux protégés par authentification HTTP - *) Mise en cache des favicons - *) Création d'un logo *temporaire* - *) Affichage des vidéos dans les articles - *) Gestion de la recherche et filtre par tags pleinement fonctionnels - *) Création d'un vrai script CRON permettant de mettre tous les flux à - jour - *) Correction bugs divers - -2013-04-17 changes with FreshRSS 0.2.0 - *) Création d'un installateur - *) Actualisation des flux en Ajax - *) Partage par mail et Shaarli ajouté - *) Export par flux RSS - *) Possibilité de vider une catégorie - *) Possibilité de sélectionner les catégories en vue mobile - *) Les flux peuvent être sortis du flux principal (système de priorité) - *) Amélioration ajout / import / export des flux - *) Amélioration actualisation (meilleure gestion des erreurs) - *) Améliorations CSS - *) Changements dans la base de données - *) Màj de la librairie SimplePie - *) Flux sans auteurs gérés normalement - *) Correction bugs divers - -2013-04-08 changes with FreshRSS 0.1.0 - *) "Première" version diff --git a/README.md b/README.md new file mode 100644 index 000000000..215e11c45 --- /dev/null +++ b/README.md @@ -0,0 +1,101 @@ +# FreshRSS +FreshRSS est un agrégateur de flux RSS à auto-héberger à l'image de [RSSLounge](http://rsslounge.aditu.de/), [TinyTinyRSS](http://tt-rss.org/redmine/projects/tt-rss/wiki) ou [Leed](http://projet.idleman.fr/leed/). Il se veut léger et facile à prendre en main tout en étant un outil puissant et paramétrable. L'objectif étant d'offrir une alternative sérieuse à Google Reader. + +* Site officiel : http://marienfressinaud.github.io/FreshRSS/ +* Démo : http://marienfressinaud.fr/projets/freshrss/ +* Développeur : Marien Fressinaud <dev@marienfressinaud.fr> +* Version actuelle : 0.4.0 +* Date de publication 2013-07-02 +* License AGPL3 + + + +# Disclaimer +Cette application a été développée pour s'adapter à mes besoins personnels. +Je ne garantis en aucun cas la sécurité de celle-ci, ni son bon fonctionnement +sur un autre serveur que le mien. 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 Apache ou Nginx (non testé sur les autres) +* PHP 5.3 (il me faudrait des retours sur d'autres versions antérieures) +* libxml pour PHP +* cURL +* PDO et MySQL + +# 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. Déplacez l'application où vous voulez sur votre serveur (attention, la partie accessible se trouve dans le répertoire `./public`) +3. Accédez à FreshRSS à travers votre navigateur web et suivez les instructions d'installation +4. Tout devrait fonctionner :) En cas de problème, n'hésitez pas à me contacter. + +# Sécurité et conseils +1. Si possible, faites pointer un sous-domaine sur le répertoire `./public` +2. Le fichier de log peut être utile à lire si vous avez des soucis +3. Le fichier `./public/index.php` défini les chemins d'accès aux répertoires clés de l'application. Si vous les bougez, tout se passe ici. +4. Vous pouvez ajouter une tâche CRON sur le script d'actualisation des flux. Il s'agit d'un script PHP à exécuter avec la commande `php`. Par exemple, pour exécuter le script toutes les heures : +``` +0 * * * * php /chemin/vers/freshrss/actualize_script.php >/dev/null 2>&1 +``` + +# Changelog +## 2013-07-02 changes with FreshRSS 0.4.0 + +* Correction bug et ajout notification lors de la phase d'installation +* Affichage d'erreur si fichier OPML invalide +* Les tags sont maintenant cliquables pour filtrer dessus +* Amélioration vue mobile (boutons plus gros et ajout d'une barre de navigation) +* Possibilité d'ajouter directement un flux dans une catégorie dès son ajout +* Affichage des flux en erreur (injoignable par exemple) en rouge pour les différencier +* Possiblité de changer les noms des flux +* Ajout d'une option (désactivable donc) pour charger les images en lazyload permettant de ne pas charger toutes les images d'un coup +* Le framework Minz est maintenant directement inclus dans l'archive (plus besoin de passer par ./build.sh) +* Amélioration des performances pour la récupération des flux tronqués +* Possibilité d'importer des flux sans catégorie lors de l'import OPML +* Suppression de "l'API" (qui était de toutes façons très basique) et de la fonctionnalité de "notes" +* Amélioration de la recherche (garde en mémoire si l'on a sélectionné une catégorie) par exemple +* Modification apparence des balises hr et pre +* Meilleure vérification des champs de formulaire +* Remise en place du mode "endless" (permettant de simplement charger les articles qui suivent plutôt que de charger une nouvelle page) +* Ajout d'une page de visualisation des logs +* Ajout d'une option pour optimiser la BDD (diminue sa taille) +* Ajout des vues lecture et globale (assez basique) +* Les vidéos Youtube ne débordent plus du cadre sur les petits écrans +* Ajout d'une option pour marquer les articles comme lus lors du défilement (et suppression de celle au chargement de la page) + +## 2013-05-05 changes with FreshRSS 0.3.0 + +* Fallback pour les icônes SVG (utilisation de PNG à la place) +* Fallback pour les propriétés CSS3 (utilisation de préfixes) +* Affichage des tags associés aux articles +* Internationalisation de l'application (gestion des langues anglaise et française) +* Gestion des flux protégés par authentification HTTP +* Mise en cache des favicons +* Création d'un logo *temporaire* +* Affichage des vidéos dans les articles +* Gestion de la recherche et filtre par tags pleinement fonctionnels +* Création d'un vrai script CRON permettant de mettre tous les flux à jour +* Correction bugs divers + +## 2013-04-17 changes with FreshRSS 0.2.0 + +* Création d'un installateur +* Actualisation des flux en Ajax +* Partage par mail et Shaarli ajouté +* Export par flux RSS +* Possibilité de vider une catégorie +* Possibilité de sélectionner les catégories en vue mobile +* Les flux peuvent être sortis du flux principal (système de priorité) +* Amélioration ajout / import / export des flux +* Amélioration actualisation (meilleure gestion des erreurs) +* Améliorations CSS +* Changements dans la base de données +* Màj de la librairie SimplePie +* Flux sans auteurs gérés normalement +* Correction bugs divers + +## 2013-04-08 changes with FreshRSS 0.1.0 + +* "Première" version diff --git a/actualize_script.php b/actualize_script.php index 76bbe2e4f..7f72e419e 100755 --- a/actualize_script.php +++ b/actualize_script.php @@ -24,4 +24,5 @@ require (APP_PATH . '/App_FrontController.php'); $front_controller = new App_FrontController (); $front_controller->init (); +Session::_param('mail', true); // permet de se passer de la phase de connexion $front_controller->run (); diff --git a/app/App_FrontController.php b/app/App_FrontController.php index 77261280e..637d61206 100644 --- a/app/App_FrontController.php +++ b/app/App_FrontController.php @@ -36,6 +36,7 @@ class App_FrontController extends FrontController { include (APP_PATH . '/models/Entry.php'); include (APP_PATH . '/models/EntriesGetter.php'); include (APP_PATH . '/models/RSSPaginator.php'); + include (APP_PATH . '/models/Log.php'); } private function loadParamsView () { @@ -56,6 +57,7 @@ class App_FrontController extends FrontController { View::appendScript ('https://login.persona.org/include.js'); } View::appendScript (Url::display ('/scripts/jquery.js')); + View::appendScript (Url::display ('/scripts/jquery.lazyload.min.js')); View::appendScript (Url::display ('/scripts/notification.js')); } diff --git a/app/controllers/apiController.php b/app/controllers/apiController.php deleted file mode 100755 index 025908f3e..000000000 --- a/app/controllers/apiController.php +++ /dev/null @@ -1,46 +0,0 @@ -<?php - -class apiController extends ActionController { - public function firstAction() { - header('Content-type: application/json'); - - $this->view->_useLayout (false); - } - - public function getPublicFeedAction () { - $entryDAO = new EntryDAO (); - $entryDAO->_nbItemsPerPage (-1); - - $entries_tmp = $entryDAO->listPublic ('low_to_high'); - - $entries = array (); - foreach ($entries_tmp as $e) { - $author = $e->author (); - - $notes = $e->notes (); - if ($notes == '') { - $feed = $e->feed (true); - if($author != '') { - $notes = Translate::t ('article_published_on_author', $feed->website (), $feed->name (), $author); - } else { - $notes = Translate::t ('article_published_on', $feed->website (), $feed->name ()); - } - } - - $id = $e->id (); - $entries[$id] = array (); - $entries[$id]['title'] = $e->title (); - $entries[$id]['content'] = $notes; - $entries[$id]['date'] = $e->date (true); - $entries[$id]['lastUpdate'] = $e->lastUpdate (true); - $entries[$id]['tags'] = $e->tags (); - $entries[$id]['url'] = $e->link (); - $entries[$id]['type'] = 'url'; - } - - $this->view->entries = $entries; - } - - public function getNbNotReadAction() { - } -} diff --git a/app/controllers/configureController.php b/app/controllers/configureController.php index 2f56da177..ce53e1040 100755 --- a/app/controllers/configureController.php +++ b/app/controllers/configureController.php @@ -91,6 +91,7 @@ class configureController extends ActionController { $this->view->categories = $catDAO->listCategories (); if (Request::isPost () && $this->view->flux) { + $name = Request::param ('name', ''); $cat = Request::param ('category', 0); $path = Request::param ('path_entries', ''); $priority = Request::param ('priority', 0); @@ -103,6 +104,7 @@ class configureController extends ActionController { } $values = array ( + 'name' => $name, 'category' => $cat, 'pathEntries' => $path, 'priority' => $priority, @@ -138,35 +140,41 @@ class configureController extends ActionController { if (Request::isPost ()) { $language = Request::param ('language', 'en'); $nb = Request::param ('posts_per_page', 10); + $mode = Request::param ('view_mode', 'normal'); $view = Request::param ('default_view', 'all'); $display = Request::param ('display_posts', 'no'); + $lazyload = Request::param ('lazyload', 'yes'); $sort = Request::param ('sort_order', 'low_to_high'); $old = Request::param ('old_entries', 3); $mail = Request::param ('mail_login', false); $openArticle = Request::param ('mark_open_article', 'no'); $openSite = Request::param ('mark_open_site', 'no'); - $openPage = Request::param ('mark_open_page', 'no'); + $scroll = Request::param ('mark_scroll', 'no'); $urlShaarli = Request::param ('shaarli', ''); $this->view->conf->_language ($language); $this->view->conf->_postsPerPage (intval ($nb)); + $this->view->conf->_viewMode ($mode); $this->view->conf->_defaultView ($view); $this->view->conf->_displayPosts ($display); + $this->view->conf->_lazyload ($lazyload); $this->view->conf->_sortOrder ($sort); $this->view->conf->_oldEntries ($old); $this->view->conf->_mailLogin ($mail); $this->view->conf->_markWhen (array ( 'article' => $openArticle, 'site' => $openSite, - 'page' => $openPage, + 'scroll' => $scroll, )); $this->view->conf->_urlShaarli ($urlShaarli); $values = array ( 'language' => $this->view->conf->language (), 'posts_per_page' => $this->view->conf->postsPerPage (), + 'view_mode' => $this->view->conf->viewMode (), 'default_view' => $this->view->conf->defaultView (), 'display_posts' => $this->view->conf->displayPosts (), + 'lazyload' => $this->view->conf->lazyload (), 'sort_order' => $this->view->conf->sortOrder (), 'old_entries' => $this->view->conf->oldEntries (), 'mail_login' => $this->view->conf->mailLogin (), @@ -196,6 +204,9 @@ class configureController extends ActionController { } public function importExportAction () { + $catDAO = new CategoryDAO (); + $this->view->categories = $catDAO->listCategories (); + $this->view->req = Request::param ('q'); if ($this->view->req == 'export') { @@ -218,14 +229,31 @@ class configureController extends ActionController { } elseif ($this->view->req == 'import' && Request::isPost ()) { if ($_FILES['file']['error'] == 0) { // on parse le fichier OPML pour récupérer les catégories et les flux associés - list ($categories, $feeds) = opml_import (file_get_contents ($_FILES['file']['tmp_name'])); - - // On redirige vers le controller feed qui va se charger d'insérer les flux en BDD - // les flux sont mis au préalable dans des variables de Request - Request::_param ('q', 'null'); - Request::_param ('categories', $categories); - Request::_param ('feeds', $feeds); - Request::forward (array ('c' => 'feed', 'a' => 'massiveImport')); + try { + list ($categories, $feeds) = opml_import ( + file_get_contents ($_FILES['file']['tmp_name']) + ); + + // On redirige vers le controller feed qui va se charger d'insérer les flux en BDD + // les flux sont mis au préalable dans des variables de Request + Request::_param ('q', 'null'); + Request::_param ('categories', $categories); + Request::_param ('feeds', $feeds); + Request::forward (array ('c' => 'feed', 'a' => 'massiveImport')); + } catch (OpmlException $e) { + Log::record ($e->getMessage (), Log::ERROR); + + $notif = array ( + 'type' => 'bad', + 'content' => Translate::t ('bad_opml_file') + ); + Session::_param ('notification', $notif); + + Request::forward (array ( + 'c' => 'configure', + 'a' => 'importExport' + ), true); + } } } diff --git a/app/controllers/entryController.php b/app/controllers/entryController.php index 35f3150ea..c7e13f471 100755 --- a/app/controllers/entryController.php +++ b/app/controllers/entryController.php @@ -98,74 +98,22 @@ class entryController extends ActionController { } } - public function noteAction () { - View::appendScript (Url::display (array ('c' => 'javascript', 'a' => 'main'))); - - $not_found = false; - $entryDAO = new EntryDAO (); - $catDAO = new CategoryDAO (); - - $id = Request::param ('id'); - if ($id) { - $entry = $entryDAO->searchById ($id); - - if ($entry) { - $feed = $entry->feed (true); - - if (Request::isPost ()) { - $note = htmlspecialchars (Request::param ('note', '')); - $public = Request::param ('public', 'no'); - if ($public == 'yes') { - $public = true; - } else { - $public = false; - } - - $values = array ( - 'annotation' => $note, - 'is_public' => $public, - 'lastUpdate' => time () - ); - - if ($entryDAO->updateEntry ($id, $values)) { - $notif = array ( - 'type' => 'good', - 'content' => Translate::t ('updated') - ); - } else { - $notif = array ( - 'type' => 'bad', - 'content' => Translate::t ('error_occured') - ); - } - Session::_param ('notification', $notif); - Request::forward (array ( - 'c' => 'entry', - 'a' => 'note', - 'params' => array ( - 'id' => $id - ) - ), true); - } - } else { - $not_found = true; - } - } else { - $not_found = true; - } - - if ($not_found) { - Error::error ( - 404, - array ('error' => array (Translate::t ('page_not_found'))) - ); - } else { - $this->view->entry = $entry; - $this->view->cat_aside = $catDAO->listCategories (); - $this->view->nb_favorites = $entryDAO->countFavorites (); - $this->view->nb_total = $entryDAO->count (); - $this->view->get_c = $feed->category (); - $this->view->get_f = $feed->id (); - } + public function optimizeAction() { + // La table des entrées a tendance à grossir énormément + // Cette action permet d'optimiser cette table permettant de grapiller un peu de place + // Cette fonctionnalité n'est à appeler qu'occasionnellement + $entryDAO = new EntryDAO(); + $entryDAO->optimizeTable(); + + $notif = array ( + 'type' => 'good', + 'content' => Translate::t ('optimization_complete') + ); + Session::_param ('notification', $notif); + + Request::forward(array( + 'c' => 'configure', + 'a' => 'display' + ), true); } } diff --git a/app/controllers/feedController.php b/app/controllers/feedController.php index 77f1787d0..76da41c58 100755 --- a/app/controllers/feedController.php +++ b/app/controllers/feedController.php @@ -159,8 +159,7 @@ class feedController extends ActionController { $feedDAO->updateLastUpdate ($feed->id ()); } catch (FeedException $e) { Log::record ($e->getMessage (), Log::ERROR); - // TODO si on a une erreur ici, il faut mettre - // le flux à jour en BDD (error = 1) (issue #70) + $feedDAO->isInError ($feed->id ()); } // On arrête à 10 flux pour ne pas surcharger le serveur @@ -220,8 +219,8 @@ class feedController extends ActionController { $entryDAO = new EntryDAO (); $feedDAO = new FeedDAO (); - $categories = Request::param ('categories', array ()); - $feeds = Request::param ('feeds', array ()); + $categories = Request::param ('categories', array (), true); + $feeds = Request::param ('feeds', array (), true); // on ajoute les catégories en masse dans une fonction à part $this->addCategories ($categories); diff --git a/app/controllers/indexController.php b/app/controllers/indexController.php index f4f0b98b3..594d379fb 100755 --- a/app/controllers/indexController.php +++ b/app/controllers/indexController.php @@ -6,12 +6,19 @@ class indexController extends ActionController { private $mode = 'all'; public function indexAction () { - if (Request::param ('output') == 'rss') { + $output = Request::param ('output'); + + if ($output == 'rss') { $this->view->_useLayout (false); } else { View::appendScript (Url::display ('/scripts/shortcut.js')); View::appendScript (Url::display (array ('c' => 'javascript', 'a' => 'main'))); View::appendScript (Url::display (array ('c' => 'javascript', 'a' => 'actualize'))); + View::appendScript (Url::display ('/scripts/endless_mode.js')); + + if(!$output) { + Request::_param ('output', $this->view->conf->viewMode()); + } } $entryDAO = new EntryDAO (); @@ -138,6 +145,32 @@ class indexController extends ActionController { View::prependTitle (Translate::t ('about') . ' - '); } + public function logsAction () { + if (login_is_conf ($this->view->conf) && !is_logged ()) { + Error::error ( + 403, + array ('error' => array (Translate::t ('access_denied'))) + ); + } + + View::prependTitle (Translate::t ('logs') . ' - '); + + $logs = array(); + try { + $logDAO = new LogDAO (); + $logs = $logDAO->lister (); + $logs = array_reverse ($logs); + } catch(FileNotExistException $e) { + + } + + //gestion pagination + $page = Request::param ('page', 1); + $this->view->logsPaginator = new Paginator ($logs); + $this->view->logsPaginator->_nbItemsPerPage (50); + $this->view->logsPaginator->_currentPage ($page); + } + public function loginAction () { $this->view->_useLayout (false); diff --git a/app/i18n/en.php b/app/i18n/en.php index a78604c3e..648d70caf 100644 --- a/app/i18n/en.php +++ b/app/i18n/en.php @@ -37,6 +37,9 @@ return array ( 'before_one_day' => 'Before one day', 'before_one_week' => 'Before one week', 'display' => 'Display', + 'normal_view' => 'Normal view', + 'reader_view' => 'Reading view', + 'global_view' => 'Global view', 'show_all_articles' => 'Show all articles', 'show_not_reads' => 'Show only unread', 'older_first' => 'Oldest first', @@ -58,6 +61,7 @@ return array ( 'rss_feed_management' => 'RSS feeds management', 'configuration_updated' => 'Configuration has been updated', 'general_and_reading_management'=> 'General and reading management', + 'bad_opml_file' => 'Your OPML file is invalid', 'shortcuts_updated' => 'Shortcuts have been updated', 'shortcuts_management' => 'Shortcuts management', 'feeds_marked_read' => 'Feeds have been marked as read', @@ -77,6 +81,8 @@ return array ( 'category_emptied' => 'Category has been emptied', 'feed_deleted' => 'Feed has been deleted', + 'optimization_complete' => 'Optimization complete', + 'your_rss_feeds' => 'Your RSS feeds', 'your_favorites' => 'Your favorites', 'public' => 'Public', @@ -113,6 +119,7 @@ return array ( 'or' => 'or', 'informations' => 'Informations', + 'feed_in_error' => 'This feed has encountered a problem. Please verify that it is always reachable then actualize it.', 'website_url' => 'Website URL', 'feed_url' => 'Feed URL', 'number_articles' => 'Number of articles', @@ -142,20 +149,19 @@ return array ( 'default_view' => 'Default view', 'sort_order' => 'Sort order', 'display_articles_unfolded' => 'Show articles unfolded by default', + 'img_with_lazyload' => 'Use "lazy load" mode to load pictures', 'auto_read_when' => 'Mark automatically as read when', 'article_selected' => 'Article is selected', 'article_open_on_website' => 'Article is opened on its original website', - 'page_loaded' => 'Page is loaded', + 'scroll' => 'Page scrolls', 'your_shaarli' => 'Your Shaarli', 'sharing' => 'Sharing', 'share' => 'Share', 'by_email' => 'By mail', 'on_shaarli' => 'On your Shaarli', + 'optimize_bdd' => 'Optimize database', + 'optimize_todo_sometimes' => 'To do occasionally to reduce size of database', - 'note' => 'Note', - 'add_note' => 'Add a note', - 'update_note' => 'Update your note', - 'ask_public_article' => 'Public article?', 'article' => 'Article', 'title' => 'Title', 'author' => 'Author', @@ -187,6 +193,9 @@ return array ( 'credits' => 'Credits', 'credits_content' => 'Some design elements come from <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> although FreshRSS doesn\'t use this framework. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">Icons</a> come from <a href="https://www.gnome.org/">GNOME project</a>. <em>Open Sans</em> font police used has been created by <a href="https://www.google.com/webfonts/specimen/Open+Sans">Steve Matteson</a>. Favicons are collected with <a href="https://getfavicon.appspot.com/">getFavicon API</a>. FreshRSS is based on <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, a PHP framework.', + 'logs' => 'Logs', + 'logs_empty' => 'Log file is empty', + // DATE 'january' => 'january', 'february' => 'february', @@ -259,6 +268,7 @@ return array ( 'do_not_change_if_doubt' => 'Don\'t change if you doubt about it', 'bdd_conf_is_ok' => 'Database configuration has been saved.', + 'bdd_conf_is_ko' => 'Verify your database information.', 'host' => 'Host', 'username' => 'Username', 'password' => 'Password', diff --git a/app/i18n/fr.php b/app/i18n/fr.php index 33f094c21..714443f38 100644 --- a/app/i18n/fr.php +++ b/app/i18n/fr.php @@ -37,6 +37,9 @@ return array ( 'before_one_day' => 'Antérieurs à 1 jour', 'before_one_week' => 'Antérieurs à 1 semaine', 'display' => 'Affichage', + 'normal_view' => 'Vue normale', + 'reader_view' => 'Vue lecture', + 'global_view' => 'Vue globale', 'show_all_articles' => 'Afficher tous les articles', 'show_not_reads' => 'Afficher les non lus', 'older_first' => 'Plus anciens en premier', @@ -58,6 +61,7 @@ return array ( 'rss_feed_management' => 'Gestion des flux RSS', 'configuration_updated' => 'La configuration a été mise à jour', 'general_and_reading_management'=> 'Gestion générale et affichage', + 'bad_opml_file' => 'Votre fichier OPML n\'est pas valide', 'shortcuts_updated' => 'Les raccourcis ont été mis à jour', 'shortcuts_management' => 'Gestion des raccourcis', 'feeds_marked_read' => 'Les flux ont été marqués comme lu', @@ -77,6 +81,8 @@ return array ( 'category_emptied' => 'La catégorie a été vidée', 'feed_deleted' => 'Le flux a été supprimé', + 'optimization_complete' => 'Optimisation terminée', + 'your_rss_feeds' => 'Vos flux RSS', 'your_favorites' => 'Vos favoris', 'public' => 'Public', @@ -113,6 +119,7 @@ return array ( 'or' => 'ou', 'informations' => 'Informations', + 'feed_in_error' => 'Ce flux a rencontré un problème. Veuillez vérifier qu\'il est toujours accessible puis actualisez-le.', 'website_url' => 'URL du site', 'feed_url' => 'URL du flux', 'number_articles' => 'Nombre d\'articles', @@ -142,20 +149,19 @@ return array ( 'default_view' => 'Vue par défaut', 'sort_order' => 'Ordre de tri', 'display_articles_unfolded' => 'Afficher les articles dépliés par défaut', + 'img_with_lazyload' => 'Utiliser le mode "lazy load" pour charger les images', 'auto_read_when' => 'Marquer automatiquement comme lu lorsque', 'article_selected' => 'L\'article est sélectionné', 'article_open_on_website' => 'L\'article est ouvert sur le site d\'origine', - 'page_loaded' => 'La page est chargée', + 'scroll' => 'Au défilement de la page', 'your_shaarli' => 'Votre Shaarli', 'sharing' => 'Partage', 'share' => 'Partager', 'by_email' => 'Par mail', 'on_shaarli' => 'Sur votre Shaarli', + 'optimize_bdd' => 'Optimiser la base de données', + 'optimize_todo_sometimes' => 'À faire de temps en temps pour réduire la taille de la BDD', - 'note' => 'Note', - 'add_note' => 'Ajouter une note', - 'update_note' => 'Modifier votre note', - 'ask_public_article' => 'Article public ?', 'article' => 'Article', 'title' => 'Titre', 'author' => 'Auteur', @@ -187,6 +193,9 @@ return array ( 'credits' => 'Crédits', 'credits_content' => 'Des éléments de design sont issus du <a href="http://twitter.github.io/bootstrap/">projet Bootstrap</a> bien que FreshRSS n\'utilise pas ce framework. Les <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">icônes</a> sont issues du <a href="https://www.gnome.org/">projet GNOME</a>. La police <em>Open Sans</em> utilisée a été créée par <a href="https://www.google.com/webfonts/specimen/Open+Sans">Steve Matteson</a>. Les favicons sont récupérés grâce au site <a href="https://getfavicon.appspot.com/">getFavicon</a>. FreshRSS repose sur <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, un framework PHP.', + 'logs' => 'Logs', + 'logs_empty' => 'Les logs sont vides', + // DATE 'january' => 'janvier', 'february' => 'février', @@ -259,6 +268,7 @@ return array ( 'do_not_change_if_doubt' => 'Laissez tel quel dans le doute', 'bdd_conf_is_ok' => 'La configuration de la base de données a été enregistrée.', + 'bdd_conf_is_ko' => 'Vérifiez les informations d\'accès à la base de données.', 'host' => 'Hôte', 'username' => 'Nom utilisateur', 'password' => 'Mot de passe', diff --git a/app/layout/aside_feed.phtml b/app/layout/aside_feed.phtml index 158f012d0..4c56d12fc 100644 --- a/app/layout/aside_feed.phtml +++ b/app/layout/aside_feed.phtml @@ -43,7 +43,7 @@ <?php if (!empty ($this->feeds)) { ?> <?php foreach ($this->feeds as $feed) { ?> - <li class="item<?php echo ($this->flux && $this->flux->id () == $feed->id ()) ? ' active' : ''; ?>"> + <li class="item<?php echo ($this->flux && $this->flux->id () == $feed->id ()) ? ' active' : ''; ?><?php echo $feed->inError () ? ' error' : ''; ?>"> <a href="<?php echo _url ('configure', 'feed', 'id', $feed->id ()); ?>"> <img class="favicon" src="<?php echo $feed->favicon (); ?>" alt="" /> <?php echo $feed->name (); ?> diff --git a/app/layout/aside_flux.phtml b/app/layout/aside_flux.phtml index d1fe6b759..449ffaff4 100644 --- a/app/layout/aside_flux.phtml +++ b/app/layout/aside_flux.phtml @@ -62,7 +62,7 @@ <ul class="feeds<?php echo $c_active ? ' active' : ''; ?>"> <?php foreach ($feeds as $feed) { ?> <?php $f_active = false; if ($this->get_f == $feed->id ()) { $f_active = true; } ?> - <li class="item<?php echo $f_active ? ' active' : ''; ?>"> + <li class="item<?php echo $f_active ? ' active' : ''; ?><?php echo $feed->inError () ? ' error' : ''; ?>"> <div class="dropdown"> <div id="dropdown-<?php echo $feed->id(); ?>" class="dropdown-target"></div> <a class="dropdown-toggle" href="#dropdown-<?php echo $feed->id(); ?>"><i class="icon i_configure"></i></a> diff --git a/app/layout/header.phtml b/app/layout/header.phtml index 628019c97..4131f8687 100644 --- a/app/layout/header.phtml +++ b/app/layout/header.phtml @@ -15,21 +15,26 @@ </div> <div class="item search"> - <?php - $params = Request::params (); - if (isset ($params['search'])) { - unset ($params['search']); - } - $url = array ( - 'c' => 'index', - 'a' => 'index', - 'params' => $params - ); - ?> - <form action="<?php echo Url::display ($url); ?>" method="get"> + <form action="<?php echo _url ('index', 'index'); ?>" method="get"> <div class="stick"> - <?php $s = Request::param ('search', ''); ?> - <input type="text" name="search" id="search" value="<?php echo $s; ?>" placeholder="<?php echo Translate::t ('search'); ?>" /> + <?php $search = Request::param ('search', ''); ?> + <input type="text" name="search" id="search" value="<?php echo $search; ?>" placeholder="<?php echo Translate::t ('search'); ?>" /> + + <?php $get = Request::param ('get', ''); ?> + <?php if($get != '') { ?> + <input type="hidden" name="get" value="<?php echo $get; ?>" /> + <?php } ?> + + <?php $order = Request::param ('order', ''); ?> + <?php if($order != '') { ?> + <input type="hidden" name="order" value="<?php echo $order; ?>" /> + <?php } ?> + + <?php $state = Request::param ('state', ''); ?> + <?php if($state != '') { ?> + <input type="hidden" name="state" value="<?php echo $state; ?>" /> + <?php } ?> + <button class="btn" type="submit"><i class="icon i_search"></i></button> </div> </form> @@ -49,6 +54,7 @@ <li class="item"><a href="<?php echo _url ('configure', 'shortcut'); ?>"><?php echo Translate::t ('shortcuts'); ?></a></li> <li class="separator"></li> <li class="item"><a href="<?php echo _url ('index', 'about'); ?>"><?php echo Translate::t ('about'); ?></a></li> + <li class="item"><a href="<?php echo _url ('index', 'logs'); ?>"><?php echo Translate::t ('logs'); ?></a></li> </ul> </div> </div> diff --git a/app/layout/nav_entries.phtml b/app/layout/nav_entries.phtml new file mode 100644 index 000000000..101e7443e --- /dev/null +++ b/app/layout/nav_entries.phtml @@ -0,0 +1,5 @@ +<ul class="nav_entries"> + <li class="item"><a class="previous_entry" href="#"><i class="icon i_prev"></i></a></li> + <li class="item"><a href="#"><i class="icon i_up"></i></a></li> + <li class="item"><a class="next_entry" href="#"><i class="icon i_next"></i></a></li> +</ul>
\ No newline at end of file diff --git a/app/layout/nav_menu.phtml b/app/layout/nav_menu.phtml index 3411f344b..d36814bee 100644 --- a/app/layout/nav_menu.phtml +++ b/app/layout/nav_menu.phtml @@ -59,43 +59,72 @@ <ul class="dropdown-menu"> <li class="dropdown-close"><a href="#close"><i class="icon i_close"></i></a></li> + <?php + $url_output = $url; + $actual_view = Request::param('output', 'normal'); + ?> + <?php if($actual_view != 'normal') { ?> + <li class="item"> + <?php $url_output['params']['output'] = 'normal'; ?> + <a class="view_normal" href="<?php echo Url::display ($url_output); ?>"> + <?php echo Translate::t ('normal_view'); ?> + </a> + </li> + <?php } if($actual_view != 'reader') { ?> + <li class="item"> + <?php $url_output['params']['output'] = 'reader'; ?> + <a class="view_normal" href="<?php echo Url::display ($url_output); ?>"> + <?php echo Translate::t ('reader_view'); ?> + </a> + </li> + <?php } if($actual_view != 'global') { ?> + <li class="item"> + <?php $url_output['params']['output'] = 'global'; ?> + <a class="view_normal" href="<?php echo Url::display ($url_output); ?>"> + <?php echo Translate::t ('global_view'); ?> + </a> + </li> + <?php } ?> + + <li class="separator"></li> + <li class="item"> <?php + $url_state = $url; if ($this->state == 'not_read') { - $url['params']['state'] = 'all'; + $url_state['params']['state'] = 'all'; ?> - <a class="print_all" href="<?php echo Url::display ($url); ?>"><?php echo Translate::t ('show_all_articles'); ?></a> + <a class="print_all" href="<?php echo Url::display ($url_state); ?>"> + <?php echo Translate::t ('show_all_articles'); ?> + </a> <?php } else { - $url['params']['state'] = 'not_read'; + $url_state['params']['state'] = 'not_read'; ?> - <a class="print_non_read" href="<?php echo Url::display ($url); ?>"><?php echo Translate::t ('show_not_reads'); ?></a> + <a class="print_non_read" href="<?php echo Url::display ($url_state); ?>"> + <?php echo Translate::t ('show_not_reads'); ?> + </a> <?php } ?> </li> + <li class="separator"></li> - <?php - $params = Request::params (); - if (isset ($params['search'])) { - $params['search'] = urlencode ($params['search']); - } - $url = array ( - 'c' => 'index', - 'a' => 'index', - 'params' => $params - ); - ?> <li class="item"> <?php + $url_order = $url; if ($this->order == 'low_to_high') { - $url['params']['order'] = 'high_to_low'; + $url_order['params']['order'] = 'high_to_low'; ?> - <a href="<?php echo Url::display ($url); ?>"><?php echo Translate::t ('older_first'); ?></a> + <a href="<?php echo Url::display ($url_order); ?>"> + <?php echo Translate::t ('older_first'); ?> + </a> <?php } else { - $url['params']['order'] = 'low_to_high'; + $url_order['params']['order'] = 'low_to_high'; ?> - <a href="<?php echo Url::display ($url); ?>"><?php echo Translate::t ('newer_first'); ?></a> + <a href="<?php echo Url::display ($url_order); ?>"> + <?php echo Translate::t ('newer_first'); ?> + </a> <?php } ?> </li> </ul> diff --git a/app/models/Category.php b/app/models/Category.php index 273559b1e..0c991588d 100755 --- a/app/models/Category.php +++ b/app/models/Category.php @@ -35,10 +35,10 @@ class Category extends Model { public function feeds () { if (is_null ($this->feeds)) { $feedDAO = new FeedDAO (); - return $feedDAO->listByCategory ($this->id ()); - } else { - return $this->feeds; + $this->feeds = $feedDAO->listByCategory ($this->id ()); } + + return $this->feeds; } public function _id ($value) { diff --git a/app/models/Entry.php b/app/models/Entry.php index f49e74239..6af3178ee 100755 --- a/app/models/Entry.php +++ b/app/models/Entry.php @@ -194,6 +194,29 @@ class Entry extends Model { } } + public function loadCompleteContent($pathEntries) { + // Gestion du contenu + // On cherche à récupérer les articles en entier... même si le flux ne le propose pas + if ($pathEntries) { + $entryDAO = new EntryDAO(); + $entry = $entryDAO->searchByGuid($this->feed, $this->guid); + + if($entry) { + // l'article existe déjà en BDD, en se contente de recharger ce contenu + $this->content = $entry->content(); + } else { + try { + // l'article n'est pas en BDD, on va le chercher sur le site + $this->content = get_content_by_parsing( + $this->link(), $pathEntries + ); + } catch (Exception $e) { + // rien à faire, on garde l'ancien contenu (requête a échoué) + } + } + } + } + public function toArray () { return array ( 'id' => $this->id (), @@ -239,7 +262,7 @@ class EntryDAO extends Model_pdo { return true; } else { $info = $stm->errorInfo(); - Log::record ('SQL error : ' . $info[2], Log::ERROR); + Log::record ('SQL error : ' . $info[2], Log::NOTICE); return false; } } @@ -360,6 +383,27 @@ class EntryDAO extends Model_pdo { } } + public function searchByGuid ($feed_id, $id) { + // un guid est unique pour un flux donné + $sql = 'SELECT * FROM entry WHERE id_feed=? AND guid=?'; + $stm = $this->bd->prepare ($sql); + + $values = array ( + $feed_id, + $id + ); + + $stm->execute ($values); + $res = $stm->fetchAll (PDO::FETCH_ASSOC); + list ($entry, $next) = HelperEntry::daoToEntry ($res); + + if (isset ($entry[0])) { + return $entry[0]; + } else { + return false; + } + } + public function searchById ($id) { $sql = 'SELECT * FROM entry WHERE id=?'; $stm = $this->bd->prepare ($sql); @@ -465,6 +509,12 @@ class EntryDAO extends Model_pdo { return $res[0]['count']; } + + public function optimizeTable() { + $sql = 'OPTIMIZE TABLE entry'; + $stm = $this->bd->prepare ($sql); + $stm->execute (); + } } class HelperEntry { diff --git a/app/models/Exception/FeedException.php b/app/models/Exception/FeedException.php index bc61e1736..bff297eb9 100644 --- a/app/models/Exception/FeedException.php +++ b/app/models/Exception/FeedException.php @@ -11,3 +11,9 @@ class BadUrlException extends FeedException { parent::__construct ('`' . $url . '` is not a valid URL'); } } + +class OpmlException extends FeedException { + public function __construct ($name_file) { + parent::__construct ('OPML file is invalid'); + } +} diff --git a/app/models/Feed.php b/app/models/Feed.php index 51c409b69..4c6a3d229 100644 --- a/app/models/Feed.php +++ b/app/models/Feed.php @@ -12,6 +12,7 @@ class Feed extends Model { private $priority = 10; private $pathEntries = ''; private $httpAuth = ''; + private $error = false; public function __construct ($url) { $this->_url ($url); @@ -69,6 +70,9 @@ class Feed extends Model { ); } } + public function inError () { + return $this->error; + } public function nbEntries () { $feedDAO = new FeedDAO (); return $feedDAO->countEntries ($this->id ()); @@ -138,6 +142,14 @@ class Feed extends Model { public function _httpAuth ($value) { $this->httpAuth = $value; } + public function _error ($value) { + if ($value) { + $value = true; + } else { + $value = false; + } + $this->error = $value; + } public function load () { if (!is_null ($this->url)) { @@ -204,18 +216,7 @@ class Feed extends Model { } } - // Gestion du contenu - // On cherche à récupérer les articles en entier... même si le flux ne le propose pas - $path = $this->pathEntries (); - if ($path) { - try { - $content = get_content_by_parsing ($item->get_permalink (), $path); - } catch (Exception $e) { - $content = $item->get_content (); - } - } else { - $content = $item->get_content (); - } + $content = $item->get_content (); $entry = new Entry ( $this->id (), @@ -227,6 +228,8 @@ class Feed extends Model { $date ? $date : time () ); $entry->_tags ($tags); + // permet de récupérer le contenu des flux tronqués + $entry->loadCompleteContent($this->pathEntries()); $entries[$entry->id ()] = $entry; } @@ -289,7 +292,7 @@ class FeedDAO extends Model_pdo { } public function updateLastUpdate ($id) { - $sql = 'UPDATE feed SET lastUpdate=? WHERE id=?'; + $sql = 'UPDATE feed SET lastUpdate=?, error=0 WHERE id=?'; $stm = $this->bd->prepare ($sql); $values = array ( @@ -306,6 +309,23 @@ class FeedDAO extends Model_pdo { } } + public function isInError ($id) { + $sql = 'UPDATE feed SET error=1 WHERE id=?'; + $stm = $this->bd->prepare ($sql); + + $values = array ( + $id + ); + + if ($stm && $stm->execute ($values)) { + return true; + } else { + $info = $stm->errorInfo(); + Log::record ('SQL error : ' . $info[2], Log::ERROR); + return false; + } + } + public function changeCategory ($idOldCat, $idNewCat) { $catDAO = new CategoryDAO (); $newCat = $catDAO->searchById ($idNewCat); @@ -470,6 +490,7 @@ class HelperFeed { $list[$key]->_priority ($dao['priority']); $list[$key]->_pathEntries ($dao['pathEntries']); $list[$key]->_httpAuth (base64_decode ($dao['httpAuth'])); + $list[$key]->_error ($dao['error']); if (isset ($dao['id'])) { $list[$key]->_id ($dao['id']); diff --git a/app/models/Log.php b/app/models/Log.php new file mode 100644 index 000000000..5c280fa7a --- /dev/null +++ b/app/models/Log.php @@ -0,0 +1,47 @@ +<?php + +class Log_Model extends Model { + private $date; + private $level; + private $information; + + public function date () { + return $this->date; + } + public function level () { + return $this->level; + } + public function info () { + return $this->information; + } + public function _date ($date) { + $this->date = $date; + } + public function _level ($level) { + $this->level = $level; + } + public function _info ($information) { + $this->information = $information; + } +} + +class LogDAO extends Model_txt { + public function __construct () { + parent::__construct (LOG_PATH . '/application.log', 'r+'); + } + + public function lister () { + $logs = array (); + + $i = 0; + while (($line = $this->readLine ()) !== false) { + $logs[$i] = new Log_Model (); + $logs[$i]->_date (preg_replace ("'\[(.*?)\] \[(.*?)\] --- (.*?)'U", "\\1", $line)); + $logs[$i]->_level (preg_replace ("'\[(.*?)\] \[(.*?)\] --- (.*?)'U", "\\2", $line)); + $logs[$i]->_info (preg_replace ("'\[(.*?)\] \[(.*?)\] --- (.*?)'U", "\\3", $line)); + $i++; + } + + return $logs; + } +}
\ No newline at end of file diff --git a/app/models/RSSConfiguration.php b/app/models/RSSConfiguration.php index 00fe3fe52..dde120e4a 100755 --- a/app/models/RSSConfiguration.php +++ b/app/models/RSSConfiguration.php @@ -7,8 +7,10 @@ class RSSConfiguration extends Model { ); private $language; private $posts_per_page; + private $view_mode; private $default_view; private $display_posts; + private $lazyload; private $sort_order; private $old_entries; private $shortcuts = array (); @@ -20,8 +22,10 @@ class RSSConfiguration extends Model { $confDAO = new RSSConfigurationDAO (); $this->_language ($confDAO->language); $this->_postsPerPage ($confDAO->posts_per_page); + $this->_viewMode ($confDAO->view_mode); $this->_defaultView ($confDAO->default_view); $this->_displayPosts ($confDAO->display_posts); + $this->_lazyload ($confDAO->lazyload); $this->_sortOrder ($confDAO->sort_order); $this->_oldEntries ($confDAO->old_entries); $this->_shortcuts ($confDAO->shortcuts); @@ -39,12 +43,18 @@ class RSSConfiguration extends Model { public function postsPerPage () { return $this->posts_per_page; } + public function viewMode () { + return $this->view_mode; + } public function defaultView () { return $this->default_view; } public function displayPosts () { return $this->display_posts; } + public function lazyload () { + return $this->lazyload; + } public function sortOrder () { return $this->sort_order; } @@ -66,8 +76,8 @@ class RSSConfiguration extends Model { public function markWhenSite () { return $this->mark_when['site']; } - public function markWhenPage () { - return $this->mark_when['page']; + public function markWhenScroll () { + return $this->mark_when['scroll']; } public function urlShaarli () { return $this->url_shaarli; @@ -80,12 +90,19 @@ class RSSConfiguration extends Model { $this->language = $value; } public function _postsPerPage ($value) { - if (is_int (intval ($value))) { + if (is_int (intval ($value)) && $value > 0) { $this->posts_per_page = $value; } else { $this->posts_per_page = 10; } } + public function _viewMode ($value) { + if ($value == 'global' || $value == 'reader') { + $this->view_mode = $value; + } else { + $this->view_mode = 'normal'; + } + } public function _defaultView ($value) { if ($value == 'not_read') { $this->default_view = 'not_read'; @@ -100,6 +117,13 @@ class RSSConfiguration extends Model { $this->display_posts = 'no'; } } + public function _lazyload ($value) { + if ($value == 'no') { + $this->lazyload = 'no'; + } else { + $this->lazyload = 'yes'; + } + } public function _sortOrder ($value) { if ($value == 'high_to_low') { $this->sort_order = 'high_to_low'; @@ -108,7 +132,7 @@ class RSSConfiguration extends Model { } } public function _oldEntries ($value) { - if (is_int (intval ($value))) { + if (is_int (intval ($value)) && $value > 0) { $this->old_entries = $value; } else { $this->old_entries = 3; @@ -127,9 +151,19 @@ class RSSConfiguration extends Model { } } public function _markWhen ($values) { + if(!isset($values['article'])) { + $values['article'] = 'yes'; + } + if(!isset($values['site'])) { + $values['site'] = 'yes'; + } + if(!isset($values['scroll'])) { + $values['scroll'] = 'yes'; + } + $this->mark_when['article'] = $values['article']; $this->mark_when['site'] = $values['site']; - $this->mark_when['page'] = $values['page']; + $this->mark_when['scroll'] = $values['scroll']; } public function _urlShaarli ($value) { $this->url_shaarli = ''; @@ -142,8 +176,10 @@ class RSSConfiguration extends Model { class RSSConfigurationDAO extends Model_array { public $language = 'en'; public $posts_per_page = 20; + public $view_mode = 'normal'; public $default_view = 'not_read'; public $display_posts = 'no'; + public $lazyload = 'yes'; public $sort_order = 'low_to_high'; public $old_entries = 3; public $shortcuts = array ( @@ -159,7 +195,7 @@ class RSSConfigurationDAO extends Model_array { public $mark_when = array ( 'article' => 'yes', 'site' => 'yes', - 'page' => 'no' + 'scroll' => 'no' ); public $url_shaarli = ''; @@ -172,12 +208,18 @@ class RSSConfigurationDAO extends Model_array { if (isset ($this->array['posts_per_page'])) { $this->posts_per_page = $this->array['posts_per_page']; } + if (isset ($this->array['view_mode'])) { + $this->view_mode = $this->array['view_mode']; + } if (isset ($this->array['default_view'])) { $this->default_view = $this->array['default_view']; } if (isset ($this->array['display_posts'])) { $this->display_posts = $this->array['display_posts']; } + if (isset ($this->array['lazyload'])) { + $this->lazyload = $this->array['lazyload']; + } if (isset ($this->array['sort_order'])) { $this->sort_order = $this->array['sort_order']; } diff --git a/app/views/api/getNbNotRead.phtml b/app/views/api/getNbNotRead.phtml deleted file mode 100644 index 31c58f6ca..000000000 --- a/app/views/api/getNbNotRead.phtml +++ /dev/null @@ -1,3 +0,0 @@ -<?php -echo json_encode($this->nb_not_read); - diff --git a/app/views/api/getPublicFeed.phtml b/app/views/api/getPublicFeed.phtml deleted file mode 100644 index 8eb0774f2..000000000 --- a/app/views/api/getPublicFeed.phtml +++ /dev/null @@ -1,3 +0,0 @@ -<?php -echo json_encode ($this->entries); -?> diff --git a/app/views/configure/display.phtml b/app/views/configure/display.phtml index be67896dc..7da5b2947 100644 --- a/app/views/configure/display.phtml +++ b/app/views/configure/display.phtml @@ -46,6 +46,11 @@ <div class="form-group"> <label class="group-name"><?php echo Translate::t ('default_view'); ?></label> <div class="group-controls"> + <select name="view_mode" id="view_mode"> + <option value="normal"<?php echo $this->conf->viewMode () == 'normal' ? ' selected="selected"' : ''; ?>><?php echo Translate::t ('normal_view'); ?></option> + <option value="reader"<?php echo $this->conf->viewMode () == 'reader' ? ' selected="selected"' : ''; ?>><?php echo Translate::t ('reader_view'); ?></option> + <option value="global"<?php echo $this->conf->viewMode () == 'global' ? ' selected="selected"' : ''; ?>><?php echo Translate::t ('global_view'); ?></option> + </select> <label class="radio" for="radio_all"> <input type="radio" name="default_view" id="radio_all" value="all"<?php echo $this->conf->defaultView () == 'all' ? ' checked="checked"' : ''; ?> /> <?php echo Translate::t ('show_all_articles'); ?> @@ -68,6 +73,20 @@ </div> <div class="form-group"> + <label class="group-name"><?php echo Translate::t ('img_with_lazyload'); ?></label> + <div class="group-controls"> + <label class="radio" for="lazyload_yes"> + <input type="radio" name="lazyload" id="lazyload_yes" value="yes"<?php echo $this->conf->lazyload () == 'yes' ? ' checked="checked"' : ''; ?> /> + <?php echo Translate::t ('yes'); ?><noscript> - <b><?php echo Translate::t ('javascript_should_be_activated'); ?></b></noscript> + </label> + <label class="radio" for="lazyload_no"> + <input type="radio" name="lazyload" id="lazyload_no" value="no"<?php echo $this->conf->lazyload () == 'no' ? ' checked="checked"' : ''; ?> /> + <?php echo Translate::t ('no'); ?> + </label> + </div> + </div> + + <div class="form-group"> <label class="group-name"><?php echo Translate::t ('display_articles_unfolded'); ?></label> <div class="group-controls"> <label class="radio" for="radio_yes"> @@ -92,9 +111,9 @@ <input type="checkbox" name="mark_open_site" id="check_open_site" value="yes"<?php echo $this->conf->markWhenSite () == 'yes' ? ' checked="checked"' : ''; ?> /> <?php echo Translate::t ('article_open_on_website'); ?> </label> - <label class="checkbox" for="check_open_page"> - <input type="checkbox" name="mark_open_page" id="check_open_page" value="yes"<?php echo $this->conf->markWhenPage () == 'yes' ? ' checked="checked"' : ''; ?> /> - <?php echo Translate::t ('page_loaded'); ?> + <label class="checkbox" for="check_scroll"> + <input type="checkbox" name="mark_scroll" id="check_scroll" value="yes"<?php echo $this->conf->markWhenScroll () == 'yes' ? ' checked="checked"' : ''; ?> /> + <?php echo Translate::t ('scroll'); ?> </label> </div> </div> @@ -107,6 +126,17 @@ </div> </div> + <legend><?php echo Translate::t ('advanced'); ?></legend> + <div class="form-group"> + <label class="group-name"></label> + <div class="group-controls"> + <a class="btn" href="<?php echo _url('entry', 'optimize'); ?>"> + <?php echo Translate::t('optimize_bdd'); ?> + </a> + <i class="icon i_help"></i> <?php echo Translate::t('optimize_todo_sometimes'); ?> + </div> + </div> + <div class="form-group form-actions"> <div class="group-controls"> <button type="submit" class="btn btn-important"><?php echo Translate::t ('save'); ?></button> diff --git a/app/views/configure/feed.phtml b/app/views/configure/feed.phtml index 33483f72d..ec2ff3bdb 100644 --- a/app/views/configure/feed.phtml +++ b/app/views/configure/feed.phtml @@ -7,9 +7,19 @@ <h1><?php echo $this->flux->name (); ?></h1> <?php echo $this->flux->description (); ?> + <?php if ($this->flux->inError ()) { ?> + <p class="alert alert-error"><span class="alert-head"><?php echo Translate::t ('damn'); ?></span> <?php echo Translate::t ('feed_in_error'); ?></p> + <?php } ?> + <form method="post" action="<?php echo _url ('configure', 'feed', 'id', $this->flux->id ()); ?>"> <legend><?php echo Translate::t ('informations'); ?></legend> <div class="form-group"> + <label class="group-name" for="name"><?php echo Translate::t ('title'); ?></label> + <div class="group-controls"> + <input type="text" name="name" id="name" value="<?php echo $this->flux->name () ; ?>" /> + </div> + </div> + <div class="form-group"> <label class="group-name"><?php echo Translate::t ('website_url'); ?></label> <div class="group-controls"> <span class="control"><a target="_blank" href="<?php echo $this->flux->website (); ?>"><?php echo $this->flux->website (); ?></a></span> @@ -22,6 +32,14 @@ </div> </div> <div class="form-group"> + <label class="group-name"></label> + <div class="group-controls"> + <a class="btn" href="<?php echo _url ('feed', 'actualize', 'id', $this->flux->id ()); ?>"> + <i class="icon i_refresh"></i> <?php echo Translate::t('actualize'); ?> + </a> + </div> + </div> + <div class="form-group"> <label class="group-name"><?php echo Translate::t ('number_articles'); ?></label> <div class="group-controls"> <span class="control"><?php echo $this->flux->nbEntries (); ?></span> diff --git a/app/views/entry/note.phtml b/app/views/entry/note.phtml deleted file mode 100644 index 061060106..000000000 --- a/app/views/entry/note.phtml +++ /dev/null @@ -1,65 +0,0 @@ -<?php $this->partial ('aside_flux'); ?> - -<div class="post"> - <a href="<?php echo _url ('index', 'index'); ?>"><?php echo Translate::t ('back_to_rss_feeds'); ?></a> - - <form method="post" action="<?php echo _url ('entry', 'note', 'id', $this->entry->id ()); ?>"> - <legend><?php echo Translate::t ('note'); ?></legend> - - <div class="form-group"> - <label class="group-name" for="note"><?php echo Translate::t ('add_note'); ?></label> - <div class="group-controls"> - <textarea rows="5" cols="80" name="note" id="note"><?php echo $this->entry->notes (); ?></textarea> - </div> - </div> - <div class="form-group"> - <label class="group-name" for="public_note"><?php echo Translate::t ('ask_public_article'); ?></label> - <div class="group-controls"> - <label class="checkbox" for="public"> - <input type="checkbox" name="public" id="public" value="yes"<?php echo $this->entry->isPublic () ? ' checked="checked"' : ''; ?> /> <?php echo Translate::t ('yes'); ?> - </label> - </div> - </div> - - <div class="form-group form-actions"> - <div class="group-controls"> - <button type="submit" class="btn btn-important"><?php echo Translate::t ('save'); ?></button> - <button type="reset" class="btn"><?php echo Translate::t ('cancel'); ?></button> - </div> - </div> - - <legend><?php echo Translate::t ('article'); ?></legend> - - <div class="form-group"> - <label class="group-name"><?php echo Translate::t ('title'); ?></label> - <div class="group-controls"> - <span class="control"><a href="<?php echo $this->entry->link (); ?>"><?php echo $this->entry->title (); ?></a></span> - </div> - </div> - - <?php - $author = $this->entry->author (); - if ($author) { ?> - <div class="form-group"> - <label class="group-name"><?php echo Translate::t ('author'); ?></label> - <div class="group-controls"> - <span class="control"><?php echo $author; ?></span> - </div> - </div> - <?php } ?> - - <div class="form-group"> - <label class="group-name"><?php echo Translate::t ('publication_date'); ?></label> - <div class="group-controls"> - <span class="control"><?php echo $this->entry->date (); ?></span> - </div> - </div> - - <div class="form-group"> - <label class="group-name"><?php echo Translate::t ('article'); ?></label> - <div class="group-controls"> - <span class="control"><?php echo $this->entry->content (); ?></span> - </div> - </div> - </form> -</div> diff --git a/app/views/helpers/global_view.phtml b/app/views/helpers/global_view.phtml new file mode 100644 index 000000000..fff391cba --- /dev/null +++ b/app/views/helpers/global_view.phtml @@ -0,0 +1,37 @@ +<?php $this->partial ('nav_menu'); ?> + +<div id="stream" class="global"> +<?php + foreach ($this->cat_aside as $cat) { + $feeds = $cat->feeds (); + $catNotRead = $cat->nbNotRead (); + if (!empty ($feeds)) { +?> + <div class="category"> + <div class="cat_header"> + <a href="<?php echo _url ('index', 'index', 'get', 'c_' . $cat->id ()); ?>"> + <?php echo $cat->name(); ?><?php echo $catNotRead > 0 ? ' (' . $catNotRead . ')' : ''; ?> + </a> + </div> + + <ul class="feeds"> + <?php foreach ($feeds as $feed) { ?> + <?php $not_read = $feed->nbNotRead (); ?> + <li class="item"> + <img class="favicon" src="<?php echo $feed->favicon (); ?>" alt="" /> + + <a href="<?php echo _url ('index', 'index', 'get', 'f_' . $feed->id ()); ?>"> + <?php echo $not_read > 0 ? '<b>' : ''; ?> + <?php echo $feed->name(); ?> + <?php echo $not_read > 0 ? ' (' . $not_read . ')' : ''; ?> + <?php echo $not_read > 0 ? '</b>' : ''; ?> + </a> + </li> + <?php } ?> + </ul> + </div> +<?php + } + } +?> +</div>
\ No newline at end of file diff --git a/app/views/helpers/logs_pagination.phtml b/app/views/helpers/logs_pagination.phtml new file mode 100755 index 000000000..0088dabc6 --- /dev/null +++ b/app/views/helpers/logs_pagination.phtml @@ -0,0 +1,47 @@ +<?php + $c = Request::controllerName (); + $a = Request::actionName (); + $params = Request::params (); +?> + +<?php if ($this->nbPage > 1) { ?> +<ul class="pagination"> + <?php $params[$getteur] = 1; ?> + <li class="item pager-first"> + <?php if ($this->currentPage > 1) { ?> + <a href="<?php echo Url::display (array ('c' => $c, 'a' => $a, 'params' => $params)); ?>">« Début</a> + <?php } ?> + </li> + + <?php $params[$getteur] = $this->currentPage - 1; ?> + <li class="item pager-previous"> + <?php if ($this->currentPage > 1) { ?> + <a href="<?php echo Url::display (array ('c' => $c, 'a' => $a, 'params' => $params)); ?>">‹ Précédent</a> + <?php } ?> + </li> + + <?php for ($i = $this->currentPage - 2; $i <= $this->currentPage + 2; $i++) { ?> + <?php if($i > 0 && $i <= $this->nbPage) { ?> + <?php if ($i != $this->currentPage) { ?> + <?php $params[$getteur] = $i; ?> + <li class="item pager-item"><a href="<?php echo Url::display (array ('c' => $c, 'a' => $a, 'params' => $params)); ?>"><?php echo $i; ?></a></li> + <?php } else { ?> + <li class="item pager-current"><?php echo $i; ?></li> + <?php } ?> + <?php } ?> + <?php } ?> + + <?php $params[$getteur] = $this->currentPage + 1; ?> + <li class="item pager-next"> + <?php if ($this->currentPage < $this->nbPage) { ?> + <a href="<?php echo Url::display (array ('c' => $c, 'a' => $a, 'params' => $params)); ?>">Suivant ›</a> + <?php } ?> + </li> + <?php $params[$getteur] = $this->nbPage; ?> + <li class="item pager-last"> + <?php if ($this->currentPage < $this->nbPage) { ?> + <a href="<?php echo Url::display (array ('c' => $c, 'a' => $a, 'params' => $params)); ?>">Fin »</a> + <?php } ?> + </li> +</ul> +<?php } ?> diff --git a/app/views/helpers/normal_view.phtml b/app/views/helpers/normal_view.phtml new file mode 100644 index 000000000..126fa5a78 --- /dev/null +++ b/app/views/helpers/normal_view.phtml @@ -0,0 +1,131 @@ +<?php + +$this->partial ('aside_flux'); +$this->partial ('nav_menu'); + +if (isset ($this->entryPaginator) && !$this->entryPaginator->isEmpty ()) { + $items = $this->entryPaginator->items (); +?> + +<div id="stream" class="normal"> + <?php + $display_today = true; + $display_yesterday = true; + $display_others = true; + ?> + <?php foreach ($items as $item) { ?> + + <?php if ($display_today && $item->isDay (Days::TODAY)) { ?> + <div class="day"><?php echo Translate::t ('today'); ?> - <?php echo timestamptodate (time (), false); ?></div> + <?php $display_today = false; } ?> + <?php if ($display_yesterday && $item->isDay (Days::YESTERDAY)) { ?> + <div class="day"><?php echo Translate::t ('yesterday'); ?> - <?php echo timestamptodate (time () - 86400, false); ?></div> + <?php $display_yesterday = false; } ?> + <?php if ($display_others && $item->isDay (Days::BEFORE_YESTERDAY)) { ?> + <div class="day"><?php echo Translate::t ('before_yesterday'); ?></div> + <?php $display_others = false; } ?> + + <div class="flux<?php echo !$item->isRead () ? ' not_read' : ''; ?><?php echo $item->isFavorite () ? ' favorite' : ''; ?>" id="flux_<?php echo $item->id (); ?>"> + <ul class="horizontal-list flux_header"> + <?php if (!login_is_conf ($this->conf) || is_logged ()) { ?> + <li class="item manage"> + <?php if (!$item->isRead ()) { ?> + <a class="read" href="<?php echo _url ('entry', 'read', 'id', $item->id (), 'is_read', 1); ?>"> </a> + <?php } else { ?> + <a class="read" href="<?php echo _url ('entry', 'read', 'id', $item->id (), 'is_read', 0); ?>"> </a> + <?php } ?> + + <?php if (!$item->isFavorite ()) { ?> + <a class="bookmark" href="<?php echo _url ('entry', 'bookmark', 'id', $item->id (), 'is_favorite', 1); ?>"> </a> + <?php } else { ?> + <a class="bookmark" href="<?php echo _url ('entry', 'bookmark', 'id', $item->id (), 'is_favorite', 0); ?>"> </a> + <?php } ?> + </li> + <?php } ?> + <?php $feed = $item->feed (true); ?> + <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"><?php echo $item->title (); ?></li> + <li class="item date"><?php echo $item->date (); ?></li> + <li class="item link"><a target="_blank" href="<?php echo $item->link (); ?>"> </a></li> + </ul> + + <div class="flux_content"> + <div class="content"> + <h1 class="title"><?php echo $item->title (); ?></h1> + <?php $author = $item->author (); ?> + <?php echo $author != '' ? '<div class="author">' . Translate::t ('by_author', $author) . '</div>' : ''; ?> + <?php + if($this->conf->lazyload() == 'yes') { + echo lazyimg($item->content ()); + } else { + echo $item->content(); + } + ?> + </div> + + <ul class="horizontal-list bottom"> + <?php if (!login_is_conf ($this->conf) || is_logged ()) { ?> + <li class="item manage"> + <?php if (!$item->isRead ()) { ?> + <a class="read" href="<?php echo _url ('entry', 'read', 'id', $item->id (), 'is_read', 1); ?>"> </a> + <?php } else { ?> + <a class="read" href="<?php echo _url ('entry', 'read', 'id', $item->id (), 'is_read', 0); ?>"> </a> + <?php } ?> + + <?php if (!$item->isFavorite ()) { ?> + <a class="bookmark" href="<?php echo _url ('entry', 'bookmark', 'id', $item->id (), 'is_favorite', 1); ?>"> </a> + <?php } else { ?> + <a class="bookmark" href="<?php echo _url ('entry', 'bookmark', 'id', $item->id (), 'is_favorite', 0); ?>"> </a> + <?php } ?> + </li> + <?php } ?> + <li class="item"> + <div class="dropdown"> + <div id="dropdown-share-<?php echo $item->id ();?>" class="dropdown-target"></div> + <i class="icon i_share"></i> <a class="dropdown-toggle" href="#dropdown-share-<?php echo $item->id ();?>"><?php echo Translate::t ('share'); ?></a> + + <ul class="dropdown-menu"> + <li class="dropdown-close"><a href="#close"><i class="icon i_close"></i></a></li> + + <li class="item"><a href="mailto:?subject=<?php echo $item->title (); ?>&body=J'ai trouvé cet article intéressant, tu peux le lire à cette adresse : <?php echo urlencode($item->link ()); ?>"><?php echo Translate::t ('by_email'); ?></a></li> + <?php + $shaarli = $this->conf->urlShaarli (); + if ($shaarli) { + ?> + <li class="item"><a target="_blank" href="<?php echo $shaarli . '?post=' . urlencode($item->link ()) . '&title=' . urlencode ($item->title ()) . '&source=bookmarklet'; ?>"><?php echo Translate::t ('on_shaarli'); ?></a></li> + <?php } ?> + </ul> + </div> + </li> + <?php $tags = $item->tags(); ?> + <?php if(!empty($tags)) { ?> + <li class="item"> + <div class="dropdown"> + <div id="dropdown-tags-<?php echo $item->id ();?>" class="dropdown-target"></div> + <i class="icon i_tag"></i> <a class="dropdown-toggle" href="#dropdown-tags-<?php echo $item->id ();?>"><?php echo Translate::t ('related_tags'); ?></a> + + <ul class="dropdown-menu"> + <li class="dropdown-close"><a href="#close"><i class="icon i_close"></i></a></li> + + <?php foreach($tags as $tag) { ?> + <li class="item"><a href="<?php echo _url ('index', 'index', 'search', urlencode ('#' . $tag)); ?>"><?php echo $tag; ?></a></li> + <?php } ?> + </ul> + </div> + </li> + <?php } ?> + </ul> + </div> + </div> + <?php } ?> + + <?php $this->entryPaginator->render ('pagination.phtml', 'next'); ?> +</div> + +<?php $this->partial ('nav_entries'); ?> + +<?php } else { ?> +<div class="alert alert-warn"> + <span class="alert-head"><?php echo Translate::t ('no_feed_to_display'); ?></span> +</div> +<?php } ?>
\ No newline at end of file diff --git a/app/views/helpers/pagination.phtml b/app/views/helpers/pagination.phtml index f029f281a..80c0976ad 100755 --- a/app/views/helpers/pagination.phtml +++ b/app/views/helpers/pagination.phtml @@ -8,7 +8,7 @@ <li class="item pager-next"> <?php if ($this->next != '') { ?> <?php $params[$getteur] = $this->next; ?> - <a href="<?php echo Url::display (array ('c' => $c, 'a' => $a, 'params' => $params)); ?>"><?php echo Translate::t ('load_more'); ?></a> + <a id="load_more" href="<?php echo Url::display (array ('c' => $c, 'a' => $a, 'params' => $params)); ?>"><?php echo Translate::t ('load_more'); ?></a> <?php } else { ?> <?php echo Translate::t ('nothing_to_load'); ?> <?php } ?> diff --git a/app/views/helpers/reader_view.phtml b/app/views/helpers/reader_view.phtml new file mode 100644 index 000000000..cdca393b9 --- /dev/null +++ b/app/views/helpers/reader_view.phtml @@ -0,0 +1,45 @@ +<?php +$this->partial ('nav_menu'); + +if (isset ($this->entryPaginator) && !$this->entryPaginator->isEmpty ()) { + $items = $this->entryPaginator->items (); +?> + +<div id="stream" class="reader"> + <?php foreach ($items as $item) { ?> + + <div class="flux<?php echo !$item->isRead () ? ' not_read' : ''; ?><?php echo $item->isFavorite () ? ' favorite' : ''; ?>" id="flux_<?php echo $item->id (); ?>"> + <div class="flux_content"> + <div class="content"> + <?php $feed = $item->feed (true); ?> + <a href="<?php echo $item->link (); ?>"> + <img class="favicon" src="<?php echo $feed->favicon (); ?>" alt="" /> <span><?php echo $feed->name (); ?></span> + </a> + <h1 class="title"><?php echo $item->title (); ?></h1> + + <div class="author"> + <?php $author = $item->author (); ?> + <?php echo $author != '' ? Translate::t ('by_author', $author) . ' - ' : ''; ?> + <?php echo $item->date (); ?> + </div> + + <?php + if($this->conf->lazyload() == 'yes') { + echo lazyimg($item->content ()); + } else { + echo $item->content(); + } + ?> + </div> + </div> + </div> + <?php } ?> + + <?php $this->entryPaginator->render ('pagination.phtml', 'next'); ?> +</div> + +<?php } else { ?> +<div class="alert alert-warn"> + <span class="alert-head"><?php echo Translate::t ('no_feed_to_display'); ?></span> +</div> +<?php } ?>
\ No newline at end of file diff --git a/app/views/helpers/rss.phtml b/app/views/helpers/rss_view.phtml index 83de6de2e..83de6de2e 100755 --- a/app/views/helpers/rss.phtml +++ b/app/views/helpers/rss_view.phtml diff --git a/app/views/index/index.phtml b/app/views/index/index.phtml index 0e09f84df..46ff33b3a 100644 --- a/app/views/index/index.phtml +++ b/app/views/index/index.phtml @@ -1,120 +1,13 @@ <?php + $output = Request::param ('output', 'normal'); if ($output == 'rss') { - $this->renderHelper ('rss'); + $this->renderHelper ('rss_view'); +} elseif($output == 'reader') { + $this->renderHelper ('reader_view'); +} elseif($output == 'global') { + $this->renderHelper ('global_view'); } else { - $this->partial ('aside_flux'); - $this->partial ('nav_menu'); - - if (isset ($this->entryPaginator) && !$this->entryPaginator->isEmpty ()) { - $items = $this->entryPaginator->items (); -?> - -<div id="stream"> - <?php - $display_today = true; - $display_yesterday = true; - $display_others = true; - ?> - <?php foreach ($items as $item) { ?> - - <?php if ($display_today && $item->isDay (Days::TODAY)) { ?> - <div class="day"><?php echo Translate::t ('today'); ?> - <?php echo timestamptodate (time (), false); ?></div> - <?php $display_today = false; } ?> - <?php if ($display_yesterday && $item->isDay (Days::YESTERDAY)) { ?> - <div class="day"><?php echo Translate::t ('yesterday'); ?> - <?php echo timestamptodate (time () - 86400, false); ?></div> - <?php $display_yesterday = false; } ?> - <?php if ($display_others && $item->isDay (Days::BEFORE_YESTERDAY)) { ?> - <div class="day"><?php echo Translate::t ('before_yesterday'); ?></div> - <?php $display_others = false; } ?> - - <div class="flux<?php echo !$item->isRead () ? ' not_read' : ''; ?><?php echo $item->isFavorite () ? ' favorite' : ''; ?>" id="flux_<?php echo $item->id (); ?>"> - <ul class="horizontal-list flux_header"> - <?php if (!login_is_conf ($this->conf) || is_logged ()) { ?> - <li class="item manage"> - <?php if (!$item->isRead ()) { ?> - <a class="read" href="<?php echo _url ('entry', 'read', 'id', $item->id (), 'is_read', 1); ?>"> </a> - <?php } else { ?> - <a class="read" href="<?php echo _url ('entry', 'read', 'id', $item->id (), 'is_read', 0); ?>"> </a> - <?php } ?> - - <?php if (!$item->isFavorite ()) { ?> - <a class="bookmark" href="<?php echo _url ('entry', 'bookmark', 'id', $item->id (), 'is_favorite', 1); ?>"> </a> - <?php } else { ?> - <a class="bookmark" href="<?php echo _url ('entry', 'bookmark', 'id', $item->id (), 'is_favorite', 0); ?>"> </a> - <?php } ?> - </li> - <?php } ?> - <?php $feed = $item->feed (true); ?> - <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"><?php echo $item->title (); ?></li> - <li class="item date"><?php echo $item->date (); ?></li> - <li class="item link"><a target="_blank" href="<?php echo $item->link (); ?>"> </a></li> - </ul> - - <div class="flux_content"> - <div class="content"> - <h1 class="title"><?php echo $item->title (); ?></h1> - <?php $author = $item->author (); ?> - <?php echo $author != '' ? '<div class="author">' . Translate::t ('by_author', $author) . '</div>' : ''; ?> - <?php echo $item->content (); ?> - </div> - - <ul class="horizontal-list bottom"> - <li class="item"> - <?php if ($item->notes () != '') { ?> - <i class="icon i_note"></i> <a class="note" href="<?php echo _url ('entry', 'note', 'id', $item->id ()); ?>"><?php echo Translate::t ('update_note'); ?></a> - <?php } else { ?> - <i class="icon i_note_empty"></i> <a class="note" href="<?php echo _url ('entry', 'note', 'id', $item->id ()); ?>"><?php echo Translate::t ('add_note'); ?></a> - <?php } ?> - </li> - <li class="item"> - <div class="dropdown"> - <div id="dropdown-share-<?php echo $item->id ();?>" class="dropdown-target"></div> - <i class="icon i_share"></i> <a class="dropdown-toggle" href="#dropdown-share-<?php echo $item->id ();?>"><?php echo Translate::t ('share'); ?></a> - - <ul class="dropdown-menu"> - <li class="dropdown-close"><a href="#close"><i class="icon i_close"></i></a></li> - - <li class="item"><a href="mailto:?subject=<?php echo $item->title (); ?>&body=J'ai trouvé cet article intéressant, tu peux le lire à cette adresse : <?php echo urlencode($item->link ()); ?>"><?php echo Translate::t ('by_email'); ?></a></li> - <?php - $shaarli = $this->conf->urlShaarli (); - if ($shaarli) { - ?> - <li class="item"><a target="_blank" href="<?php echo $shaarli . '?post=' . urlencode($item->link ()) . '&title=' . urlencode ($item->title ()) . '&source=bookmarklet'; ?>"><?php echo Translate::t ('on_shaarli'); ?></a></li> - <?php } ?> - </ul> - </div> - </li> - <?php $tags = $item->tags(); ?> - <?php if(!empty($tags)) { ?> - <li class="item"> - <div class="dropdown"> - <div id="dropdown-tags-<?php echo $item->id ();?>" class="dropdown-target"></div> - <i class="icon i_tag"></i> <a class="dropdown-toggle" href="#dropdown-tags-<?php echo $item->id ();?>"><?php echo Translate::t ('related_tags'); ?></a> - - <ul class="dropdown-menu"> - <li class="dropdown-close"><a href="#close"><i class="icon i_close"></i></a></li> - - <?php foreach($tags as $tag) { ?> - <li class="item"><span><?php echo $tag; ?></span></li> - <?php } ?> - </ul> - </div> - </li> - <?php } ?> - </ul> - </div> - </div> - <?php } ?> - - <?php $this->entryPaginator->render ('pagination.phtml', 'next'); ?> -</div> - - <?php } else { ?> -<div class="alert alert-warn"> - <span class="alert-head"><?php echo Translate::t ('no_feed_to_display'); ?></span> -</div> - <?php } ?> -<?php } ?> + $this->renderHelper ('normal_view'); +}
\ No newline at end of file diff --git a/app/views/index/logs.phtml b/app/views/index/logs.phtml new file mode 100644 index 000000000..c72a84c86 --- /dev/null +++ b/app/views/index/logs.phtml @@ -0,0 +1,21 @@ +<div class="post content"> + <a href="<?php echo _url ('index', 'index'); ?>"><?php echo Translate::t ('back_to_rss_feeds'); ?></a> + + <h1><?php echo Translate::t ('logs'); ?></h1> + + <?php $items = $this->logsPaginator->items (); ?> + + <?php if (!empty ($items)) { ?> + <div class="logs"> + <?php $this->logsPaginator->render ('logs_pagination.phtml', 'page'); ?> + + <?php foreach ($items as $log) { ?> + <div class="log <?php echo $log->level (); ?>"><span class="date"><?php echo date ('d/m/Y - H:i:s', strtotime ($log->date ())); ?></span><?php echo $log->info (); ?></div> + <?php } ?> + + <?php $this->logsPaginator->render ('logs_pagination.phtml','page'); ?> + </div> + <?php } else { ?> + <p class="alert alert-warn"><?php echo Translate::t ('logs_empty'); ?></p> + <?php } ?> +</div>
\ No newline at end of file diff --git a/app/views/javascript/main.phtml b/app/views/javascript/main.phtml index e6c882333..9226efe63 100644 --- a/app/views/javascript/main.phtml +++ b/app/views/javascript/main.phtml @@ -9,6 +9,16 @@ var hide_posts = false; $mark = $this->conf->markWhen (); ?> +function is_reader_mode() { + var stream = $("#stream.reader"); + return stream.html() != null; +} + +function is_normal_mode() { + var stream = $("#stream.normal"); + return stream.html() != null; +} + function redirect (url, new_tab) { if (url) { if (new_tab) { @@ -42,19 +52,20 @@ function toggleContent (new_active, old_active) { <?php } ?> } -var load = false; function mark_read (active, only_not_read) { if (active[0] === undefined || ( - only_not_read === true && !active.hasClass("not_read")) || - load === true) { + only_not_read === true && !active.hasClass("not_read"))) { return false; } - load = true; + if (active.hasClass ("not_read")) { + active.removeClass ("not_read"); + } else { + active.addClass ("not_read"); + } url = active.find ("a.read").attr ("href"); if (url === undefined) { - load = false; return false; } @@ -66,27 +77,16 @@ function mark_read (active, only_not_read) { res = jQuery.parseJSON(data); active.find ("a.read").attr ("href", res.url); - if (active.hasClass ("not_read")) { - active.removeClass ("not_read"); - } else { - active.addClass ("not_read"); - } - - load = false; }); } function mark_favorite (active) { - if (active[0] === undefined || - load === true) { + if (active[0] === undefined) { return false; } - load = true; - url = active.find ("a.bookmark").attr ("href"); if (url === undefined) { - load = false; return false; } @@ -103,34 +103,69 @@ function mark_favorite (active) { } else { active.addClass ("favorite"); } - - load = false; }); } +function prev_entry() { + old_active = $(".flux.active"); + last_active = $(".flux:last"); + new_active = old_active.prevAll (".flux:first"); + + if (new_active.hasClass("flux")) { + toggleContent (new_active, old_active); + } else if (old_active[0] === undefined && + new_active[0] === undefined) { + toggleContent (last_active, old_active); + } +} + +function next_entry() { + old_active = $(".flux.active"); + first_active = $(".flux:first"); + new_active = old_active.nextAll (".flux:first"); + + if (new_active.hasClass("flux")) { + toggleContent (new_active, old_active); + } else if (old_active[0] === undefined && + new_active[0] === undefined) { + toggleContent (first_active, old_active); + } +} + function init_img () { - $(".flux .content img").each (function () { - if ($(this).width () > ($("#stream .content").width()) / 2) { + $(".flux_content .content img").each (function () { + if ($(this).width () > ($(".flux_content .content").width()) / 2) { $(this).addClass("big"); } }); } -function init_posts () { - <?php if ($mark['page'] == 'yes') { ?> - if ($(".flux.not_read")[0] != undefined) { - url = $(".nav_menu a.read_all").attr ("href"); - redirect (url, false); - } - <?php } ?> +function inMarkViewport(flux) { + var top = flux.position().top; + var height = flux.height(); + var begin = top + 3 * height / 4; + var bot = top + height; + var windowTop = $(window).scrollTop(); + var windowBot = windowTop + $(window).height(); + + return (windowBot >= begin && windowBot <= bot); +} + +var lastScroll = 0; +function init_posts () { init_img (); + <?php if($this->conf->lazyload() == 'yes') { ?> + $(".flux .content img").lazyload(); + <?php } ?> if (hide_posts) { $(".flux:not(.active) .flux_content").hide (); } - $(".flux_header .item.title, .flux_header .item.date").click (function () { + var flux_header_toggle = $(".flux_header .item.title, .flux_header .item.date"); + flux_header_toggle.unbind('click'); // évite d'associer 2 fois le toggle + flux_header_toggle.click (function () { old_active = $(".flux.active"); new_active = $(this).parent ().parent (); @@ -160,9 +195,32 @@ function init_posts () { mark_read($(this).parent().parent().parent(), true); }); <?php } ?> + + <?php if ($mark['scroll'] == 'yes') { ?> + var flux = $('.flux'); + $(window).scroll(function() { + var windowTop = $(this).scrollTop(); + if(Math.abs(windowTop - lastScroll) <= 50) { + return; + } + lastScroll = windowTop; + + flux.each(function() { + if($(this).hasClass('not_read') && + $(this).children(".flux_content").is(':visible') && + inMarkViewport($(this))) { + mark_read($(this), true); + } + }); + }); + <?php } ?> } function init_column_categories () { + if(!is_normal_mode()) { + return; + } + $(".category").addClass ("stick"); $(".categories .category .btn:first-child").width ("160px"); $(".category").append ("<a class=\"btn dropdown-toggle\" href=\"#\"><i class=\"icon i_down\"></i></a>"); @@ -202,18 +260,7 @@ function init_shortcuts () { }); // Touches de navigation - shortcut.add("<?php echo $s['prev_entry']; ?>", function () { - old_active = $(".flux.active"); - last_active = $(".flux:last"); - new_active = old_active.prevAll (".flux:first"); - - if (new_active.hasClass("flux")) { - toggleContent (new_active, old_active); - } else if (old_active[0] === undefined && - new_active[0] === undefined) { - toggleContent (last_active, old_active); - } - }, { + shortcut.add("<?php echo $s['prev_entry']; ?>", prev_entry, { 'disable_in_input':true }); shortcut.add("shift+<?php echo $s['prev_entry']; ?>", function () { @@ -226,18 +273,7 @@ function init_shortcuts () { }, { 'disable_in_input':true }); - shortcut.add("<?php echo $s['next_entry']; ?>", function () { - old_active = $(".flux.active"); - first_active = $(".flux:first"); - new_active = old_active.nextAll (".flux:first"); - - if (new_active.hasClass("flux")) { - toggleContent (new_active, old_active); - } else if (old_active[0] === undefined && - new_active[0] === undefined) { - toggleContent (first_active, old_active); - } - }, { + shortcut.add("<?php echo $s['next_entry']; ?>", next_entry, { 'disable_in_input':true }); shortcut.add("shift+<?php echo $s['next_entry']; ?>", function () { @@ -277,8 +313,23 @@ function init_shortcuts () { }); } +function init_nav_entries() { + $('.nav_entries a.previous_entry').click(function() { + prev_entry(); + return false; + }); + $('.nav_entries a.next_entry').click(function() { + next_entry(); + return false; + }); +} + $(document).ready (function () { + if(is_reader_mode()) { + hide_posts = false; + } init_posts (); init_column_categories (); init_shortcuts (); + init_nav_entries(); }); diff --git a/build.sh b/build.sh deleted file mode 100755 index e6c563b83..000000000 --- a/build.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash - -MINZ_REPO_URL="https://github.com/marienfressinaud/MINZ.git" -MINZ_CLONE_PATH="./minz_tmp" -LIB_MINZ_PATH="./minz_tmp/lib/*" -LIB_PATH="./lib/minz" -LOG_PATH="./log" -CACHE_PATH="./cache" - -git_check() { - printf "Vérification de la présence de git... " - - EXE_PATH=$(which "git" 2>/dev/null) - if [ $? -ne 0 ]; then - printf "git n'est pas présent sur votre système. Veuillez l'installer avant de continuer\n"; - exit 1 - else - printf "git a été trouvé\n" - fi -} - -dir_check() { - test -d $LOG_PATH - if [ $? -ne 0 ]; then - mkdir $LOG_PATH - fi - - test -d $CACHE_PATH - if [ $? -ne 0 ]; then - mkdir $CACHE_PATH - fi -} - -clone_minz() { - printf "Récupération de Minz...\n" - - git clone $MINZ_REPO_URL $MINZ_CLONE_PATH - test -d $LIB_PATH - if [ $? -ne 0 ]; then - mkdir -p $LIB_PATH - fi - mv $LIB_MINZ_PATH $LIB_PATH - rm -rf $MINZ_CLONE_PATH - - printf "Récupération de Minz terminée...\n" -} - -git_check -dir_check -clone_minz diff --git a/lib/lib_rss.php b/lib/lib_rss.php index c574cd3fd..772003089 100644 --- a/lib/lib_rss.php +++ b/lib/lib_rss.php @@ -65,9 +65,12 @@ function opml_import ($xml) { $opml = @simplexml_load_string ($xml); if (!$opml) { - return array (array (), array ()); + throw new OpmlException (); } + $catDAO = new CategoryDAO(); + $defCat = $catDAO->getDefault(); + $categories = array (); $feeds = array (); @@ -99,8 +102,8 @@ function opml_import ($xml) { $feeds = array_merge ($feeds, getFeedsOutline ($outline, $cat->id ())); } } else { - // Flux rss - $feeds[] = getFeed ($outline, ''); + // Flux rss sans catégorie, on récupère l'ajoute dans la catégorie par défaut + $feeds[] = getFeed ($outline, $defCat->id()); } } diff --git a/lib/lib_text.php b/lib/lib_text.php index 6e8f7b2bf..9792e191e 100644 --- a/lib/lib_text.php +++ b/lib/lib_text.php @@ -86,3 +86,11 @@ function parse_tags ($desc) { return $desc_parse; } + +function lazyimg($content) { + return preg_replace( + '/<img([^<]+)src=([\'"])([^"\']*)([\'"])([^<]*)>/i', + '<img$1src="' . Url::display('/data/grey.gif') . '" data-original="$3"$5>', + $content + ); +}
\ No newline at end of file diff --git a/lib/minz/ActionController.php b/lib/minz/ActionController.php new file mode 100755 index 000000000..ab9389dbd --- /dev/null +++ b/lib/minz/ActionController.php @@ -0,0 +1,42 @@ +<?php +/** + * MINZ - Copyright 2011 Marien Fressinaud + * Sous licence AGPL3 <http://www.gnu.org/licenses/> +*/ + +/** + * La classe ActionController représente le contrôleur de l'application + */ +class ActionController { + protected $router; + protected $view; + + /** + * Constructeur + * @param $controller nom du controller + * @param $action nom de l'action à lancer + */ + public function __construct ($router) { + $this->router = $router; + $this->view = new View (); + $this->view->attributeParams (); + } + + /** + * Getteur + */ + public function view () { + return $this->view; + } + + /** + * Méthodes à redéfinir (ou non) par héritage + * firstAction est la première méthode exécutée par le Dispatcher + * lastAction est la dernière + */ + public function init () { } + public function firstAction () { } + public function lastAction () { } +} + + diff --git a/lib/minz/Cache.php b/lib/minz/Cache.php new file mode 100644 index 000000000..69143a70c --- /dev/null +++ b/lib/minz/Cache.php @@ -0,0 +1,116 @@ +<?php +/** + * MINZ - Copyright 2011 Marien Fressinaud + * Sous licence AGPL3 <http://www.gnu.org/licenses/> +*/ + +/** + * La classe Cache permet de gérer facilement les pages en cache + */ +class Cache { + /** + * $expire timestamp auquel expire le cache de $url + */ + private $expire = 0; + + /** + * $file est le nom du fichier de cache + */ + private $file = ''; + + /** + * $enabled permet de déterminer si le cache est activé + */ + private static $enabled = true; + + /** + * Constructeur + */ + public function __construct () { + $this->_fileName (); + $this->_expire (); + } + + /** + * Setteurs + */ + public function _fileName () { + $file = md5 (Request::getURI ()); + + $this->file = CACHE_PATH . '/'.$file; + } + + public function _expire () { + if ($this->exist ()) { + $this->expire = filemtime ($this->file) + + Configuration::delayCache (); + } + } + + /** + * Permet de savoir si le cache est activé + * @return true si activé, false sinon + */ + public static function isEnabled () { + return Configuration::cacheEnabled () && self::$enabled; + } + + /** + * Active / désactive le cache + */ + public static function switchOn () { + self::$enabled = true; + } + public static function switchOff () { + self::$enabled = false; + } + + /** + * Détermine si le cache de $url a expiré ou non + * @return true si il a expiré, false sinon + */ + public function expired () { + return time () > $this->expire; + } + + /** + * Affiche le contenu du cache + * @print le code html du cache + */ + public function render () { + if ($this->exist ()) { + include ($this->file); + } + } + + /** + * Enregistre $html en cache + * @param $html le html à mettre en cache + */ + public function cache ($html) { + file_put_contents ($this->file, $html); + } + + /** + * Permet de savoir si le cache existe + * @return true si il existe, false sinon + */ + public function exist () { + return file_exists ($this->file); + } + + /** + * Nettoie le cache en supprimant tous les fichiers + */ + public static function clean () { + $files = opendir (CACHE_PATH); + + while ($fic = readdir ($files)) { + if ($fic != '.' && $fic != '..') { + unlink (CACHE_PATH.'/'.$fic); + } + } + + closedir ($files); + } +} diff --git a/lib/minz/Configuration.php b/lib/minz/Configuration.php new file mode 100755 index 000000000..7cb7ea207 --- /dev/null +++ b/lib/minz/Configuration.php @@ -0,0 +1,240 @@ +<?php +/** + * MINZ - Copyright 2011 Marien Fressinaud + * Sous licence AGPL3 <http://www.gnu.org/licenses/> +*/ + +/** + * La classe Configuration permet de gérer la configuration de l'application + */ +class Configuration { + const CONF_PATH_NAME = '/configuration/application.ini'; + + /** + * VERSION est la version actuelle de MINZ + */ + const VERSION = '1.3.1'; + + /** + * valeurs possibles pour l'"environment" + * SILENT rend l'application muette (pas de log) + * PRODUCTION est recommandée pour une appli en production + * (log les erreurs critiques) + * DEVELOPMENT log toutes les erreurs + */ + const SILENT = 0; + const PRODUCTION = 1; + const DEVELOPMENT = 2; + + /** + * définition des variables de configuration + * $sel_application une chaîne de caractères aléatoires (obligatoire) + * $environment gère le niveau d'affichage pour log et erreurs + * $use_url_rewriting indique si on utilise l'url_rewriting + * $base_url le chemin de base pour accéder à l'application + * $title le nom de l'application + * $language la langue par défaut de l'application + * $cacheEnabled permet de savoir si le cache doit être activé + * $delayCache la limite de cache + * $db paramètres pour la base de données (tableau) + * - host le serveur de la base + * - user nom d'utilisateur + * - password mot de passe de l'utilisateur + * - base le nom de la base de données + */ + private static $sel_application = ''; + private static $environment = Configuration::PRODUCTION; + private static $base_url = ''; + private static $use_url_rewriting = false; + private static $title = ''; + private static $language = 'en'; + private static $cache_enabled = false; + private static $delay_cache = 3600; + + private static $db = array ( + 'host' => false, + 'user' => false, + 'password' => false, + 'base' => false + ); + + /* + * Getteurs + */ + public static function selApplication () { + return self::$sel_application; + } + public static function environment () { + return self::$environment; + } + public static function baseUrl () { + return self::$base_url; + } + public static function useUrlRewriting () { + return self::$use_url_rewriting; + } + public static function title () { + return self::$title; + } + public static function language () { + return self::$language; + } + public static function cacheEnabled () { + return self::$cache_enabled; + } + public static function delayCache () { + return self::$delay_cache; + } + public static function dataBase () { + return self::$db; + } + + /** + * Initialise les variables de configuration + * @exception FileNotExistException si le CONF_PATH_NAME n'existe pas + * @exception BadConfigurationException si CONF_PATH_NAME mal formaté + */ + public static function init () { + try { + self::parseFile (); + self::setReporting (); + } catch (BadConfigurationException $e) { + throw $e; + } catch (FileNotExistException $e) { + throw $e; + } + } + + /** + * Parse un fichier de configuration de type ".ini" + * @exception FileNotExistException si le CONF_PATH_NAME n'existe pas + * @exception BadConfigurationException si CONF_PATH_NAME mal formaté + */ + private static function parseFile () { + if (!file_exists (APP_PATH . self::CONF_PATH_NAME)) { + throw new FileNotExistException ( + APP_PATH . self::CONF_PATH_NAME, + MinzException::ERROR + ); + } + $ini_array = parse_ini_file ( + APP_PATH . self::CONF_PATH_NAME, + true + ); + + // [general] est obligatoire + if (!isset ($ini_array['general'])) { + throw new BadConfigurationException ( + '[general]', + MinzException::ERROR + ); + } + $general = $ini_array['general']; + + + // sel_application est obligatoire + if (!isset ($general['sel_application'])) { + throw new BadConfigurationException ( + 'sel_application', + MinzException::ERROR + ); + } + self::$sel_application = $general['sel_application']; + + if (isset ($general['environment'])) { + switch ($general['environment']) { + case 'silent': + self::$environment = Configuration::SILENT; + break; + case 'development': + self::$environment = Configuration::DEVELOPMENT; + break; + case 'production': + self::$environment = Configuration::PRODUCTION; + break; + default: + throw new BadConfigurationException ( + 'environment', + MinzException::ERROR + ); + } + + } + if (isset ($general['base_url'])) { + self::$base_url = $general['base_url']; + } + if (isset ($general['use_url_rewriting'])) { + self::$use_url_rewriting = $general['use_url_rewriting']; + } + + if (isset ($general['title'])) { + self::$title = $general['title']; + } + if (isset ($general['language'])) { + self::$language = $general['language']; + } + if (isset ($general['cache_enabled'])) { + self::$cache_enabled = $general['cache_enabled']; + if (CACHE_PATH === false && self::$cache_enabled) { + throw new FileNotExistException ( + 'CACHE_PATH', + MinzException::ERROR + ); + } + } + if (isset ($general['delay_cache'])) { + self::$delay_cache = $general['delay_cache']; + } + + // Base de données + $db = false; + if (isset ($ini_array['db'])) { + $db = $ini_array['db']; + } + if ($db) { + if (!isset ($db['host'])) { + throw new BadConfigurationException ( + 'host', + MinzException::ERROR + ); + } + if (!isset ($db['user'])) { + throw new BadConfigurationException ( + 'user', + MinzException::ERROR + ); + } + if (!isset ($db['password'])) { + throw new BadConfigurationException ( + 'password', + MinzException::ERROR + ); + } + if (!isset ($db['base'])) { + throw new BadConfigurationException ( + 'base', + MinzException::ERROR + ); + } + + self::$db['host'] = $db['host']; + self::$db['user'] = $db['user']; + self::$db['password'] = $db['password']; + self::$db['base'] = $db['base']; + } + } + + private static function setReporting () { + if (self::environment () == self::DEVELOPMENT) { + error_reporting (E_ALL); + ini_set ('display_errors','On'); + ini_set('log_errors', 'On'); + } elseif (self::environment () == self::PRODUCTION) { + error_reporting(E_ALL); + ini_set('display_errors','Off'); + ini_set('log_errors', 'On'); + } else { + error_reporting(0); + } + } +} diff --git a/lib/minz/Dispatcher.php b/lib/minz/Dispatcher.php new file mode 100644 index 000000000..9d08c142b --- /dev/null +++ b/lib/minz/Dispatcher.php @@ -0,0 +1,152 @@ +<?php +/** + * MINZ - Copyright 2011 Marien Fressinaud + * Sous licence AGPL3 <http://www.gnu.org/licenses/> +*/ + +/** + * Le Dispatcher s'occupe d'initialiser le Controller et d'executer l'action + * déterminée dans la Request + * C'est un singleton + */ +class Dispatcher { + const CONTROLLERS_PATH_NAME = '/controllers'; + + /* singleton */ + private static $instance = null; + + private $router; + private $controller; + + /** + * Récupère l'instance du Dispatcher + */ + public static function getInstance ($router) { + if (is_null (self::$instance)) { + self::$instance = new Dispatcher ($router); + } + return self::$instance; + } + + /** + * Constructeur + */ + private function __construct ($router) { + $this->router = $router; + } + + /** + * Lance le controller indiqué dans Request + * Remplit le body de Response à partir de la Vue + * @exception MinzException + */ + public function run () { + $cache = new Cache(); + // Le ob_start est dupliqué : sans ça il y a un bug sous Firefox + // ici on l'appelle avec 'ob_gzhandler', après sans. + // Vraisemblablement la compression fonctionne mais c'est sale + // J'ignore les effets de bord :( + ob_start ('ob_gzhandler'); + + if (Cache::isEnabled () && !$cache->expired ()) { + ob_start (); + $cache->render (); + $text = ob_get_clean(); + } else { + while (Request::$reseted) { + Request::$reseted = false; + + try { + $this->createController ( + Request::controllerName () + . 'Controller' + ); + + $this->controller->init (); + $this->controller->firstAction (); + $this->launchAction ( + Request::actionName () + . 'Action' + ); + $this->controller->lastAction (); + + if (!Request::$reseted) { + ob_start (); + $this->controller->view ()->build (); + $text = ob_get_clean(); + } + } catch (MinzException $e) { + throw $e; + } + } + + if (Cache::isEnabled ()) { + $cache->cache ($text); + } + } + + Response::setBody ($text); + } + + /** + * Instancie le Controller + * @param $controller_name le nom du controller à instancier + * @exception FileNotExistException le fichier correspondant au + * > controller n'existe pas + * @exception ControllerNotExistException le controller n'existe pas + * @exception ControllerNotActionControllerException controller n'est + * > pas une instance de ActionController + */ + private function createController ($controller_name) { + $filename = APP_PATH . self::CONTROLLERS_PATH_NAME . '/' + . $controller_name . '.php'; + + if (!file_exists ($filename)) { + throw new FileNotExistException ( + $filename, + MinzException::ERROR + ); + } + require_once ($filename); + + if (!class_exists ($controller_name)) { + throw new ControllerNotExistException ( + $controller_name, + MinzException::ERROR + ); + } + $this->controller = new $controller_name ($this->router); + + if (! ($this->controller instanceof ActionController)) { + throw new ControllerNotActionControllerException ( + $controller_name, + MinzException::ERROR + ); + } + } + + /** + * Lance l'action sur le controller du dispatcher + * @param $action_name le nom de l'action + * @exception ActionException si on ne peut pas exécuter l'action sur + * > le controller + */ + private function launchAction ($action_name) { + if (!Request::$reseted) { + if (!is_callable (array ( + $this->controller, + $action_name + ))) { + throw new ActionException ( + get_class ($this->controller), + $action_name, + MinzException::ERROR + ); + } + call_user_func (array ( + $this->controller, + $action_name + )); + } + } +} diff --git a/lib/minz/Error.php b/lib/minz/Error.php new file mode 100755 index 000000000..0e8c2f60b --- /dev/null +++ b/lib/minz/Error.php @@ -0,0 +1,94 @@ +<?php +/** + * MINZ - Copyright 2011 Marien Fressinaud + * Sous licence AGPL3 <http://www.gnu.org/licenses/> +*/ + +/** + * La classe Error permet de lancer des erreurs HTTP + */ +class Error { + public function __construct () { } + + /** + * Permet de lancer une erreur + * @param $code le type de l'erreur, par défaut 404 (page not found) + * @param $logs logs d'erreurs découpés de la forme + * > $logs['error'] + * > $logs['warning'] + * > $logs['notice'] + * @param $redirect indique s'il faut forcer la redirection (les logs ne seront pas transmis) + */ + public static function error ($code = 404, $logs = array (), $redirect = false) { + $logs = self::processLogs ($logs); + $error_filename = APP_PATH . '/controllers/errorController.php'; + + if (file_exists ($error_filename)) { + $params = array ( + 'code' => $code, + 'logs' => $logs + ); + + Response::setHeader ($code); + if ($redirect) { + Request::forward (array ( + 'c' => 'error' + ), true); + } else { + Request::forward (array ( + 'c' => 'error', + 'params' => $params + ), false); + } + } else { + $text = '<h1>An error occured</h1>'."\n"; + + if (!empty ($logs)) { + $text .= '<ul>'."\n"; + foreach ($logs as $log) { + $text .= '<li>' . $log . '</li>'."\n"; + } + $text .= '</ul>'."\n"; + } + + Response::setHeader ($code); + Response::setBody ($text); + Response::send (); + exit (); + } + } + + /** + * Permet de retourner les logs de façon à n'avoir que + * ceux que l'on veut réellement + * @param $logs les logs rangés par catégories (error, warning, notice) + * @return la liste des logs, sans catégorie, + * > en fonction de l'environment + */ + private static function processLogs ($logs) { + $env = Configuration::environment (); + $logs_ok = array (); + $error = array (); + $warning = array (); + $notice = array (); + + if (isset ($logs['error'])) { + $error = $logs['error']; + } + if (isset ($logs['warning'])) { + $warning = $logs['warning']; + } + if (isset ($logs['notice'])) { + $notice = $logs['notice']; + } + + if ($env == Configuration::PRODUCTION) { + $logs_ok = $error; + } + if ($env == Configuration::DEVELOPMENT) { + $logs_ok = array_merge ($error, $warning, $notice); + } + + return $logs_ok; + } +} diff --git a/lib/minz/FrontController.php b/lib/minz/FrontController.php new file mode 100755 index 000000000..ab951263d --- /dev/null +++ b/lib/minz/FrontController.php @@ -0,0 +1,123 @@ +<?php +# ***** BEGIN LICENSE BLOCK ***** +# MINZ - a free PHP Framework like Zend Framework +# Copyright (C) 2011 Marien Fressinaud +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# ***** END LICENSE BLOCK ***** + +/** + * La classe FrontController est le noyau du framework, elle lance l'application + * Elle est appelée en général dans le fichier index.php à la racine du serveur + */ +class FrontController { + protected $dispatcher; + protected $router; + + /** + * Constructeur + * Initialise le router et le dispatcher + */ + public function __construct () { + $this->loadLib (); + + if (LOG_PATH === false) { + $this->killApp ('Path doesn\'t exist : LOG_PATH'); + } + + try { + Configuration::init (); + + Request::init (); + + $this->router = new Router (); + $this->router->init (); + } catch (RouteNotFoundException $e) { + Log::record ($e->getMessage (), Log::ERROR); + Error::error ( + 404, + array ('error' => array ($e->getMessage ())) + ); + } catch (MinzException $e) { + Log::record ($e->getMessage (), Log::ERROR); + $this->killApp (); + } + + $this->dispatcher = Dispatcher::getInstance ($this->router); + } + + /** + * Inclue les fichiers de la librairie + */ + private function loadLib () { + require ('ActionController.php'); + require ('Cache.php'); + require ('Configuration.php'); + require ('Dispatcher.php'); + require ('Error.php'); + require ('Helper.php'); + require ('Log.php'); + require ('Model.php'); + require ('Paginator.php'); + require ('Request.php'); + require ('Response.php'); + require ('Router.php'); + require ('Session.php'); + require ('Translate.php'); + require ('Url.php'); + require ('View.php'); + + require ('dao/Model_pdo.php'); + require ('dao/Model_txt.php'); + require ('dao/Model_array.php'); + + require ('exceptions/MinzException.php'); + } + + /** + * Démarre l'application (lance le dispatcher et renvoie la réponse + */ + public function run () { + try { + $this->dispatcher->run (); + Response::send (); + } catch (MinzException $e) { + Log::record ($e->getMessage (), Log::ERROR); + + if ($e instanceof FileNotExistException || + $e instanceof ControllerNotExistException || + $e instanceof ControllerNotActionControllerException || + $e instanceof ActionException) { + Error::error ( + 404, + array ('error' => array ($e->getMessage ())), + true + ); + } else { + $this->killApp (); + } + } + } + + /** + * Permet d'arrêter le programme en urgence + */ + private function killApp ($txt = '') { + if ($txt == '') { + $txt = 'See logs files'; + } + exit ('### Application problem ###'."\n".$txt); + } +} diff --git a/lib/minz/Helper.php b/lib/minz/Helper.php new file mode 100755 index 000000000..4f64ba218 --- /dev/null +++ b/lib/minz/Helper.php @@ -0,0 +1,22 @@ +<?php +/** + * MINZ - Copyright 2011 Marien Fressinaud + * Sous licence AGPL3 <http://www.gnu.org/licenses/> +*/ + +/** + * La classe Helper représente une aide pour des tâches récurrentes + */ +class 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); + } else { + return stripslashes($var); + } + } +} diff --git a/lib/minz/Log.php b/lib/minz/Log.php new file mode 100755 index 000000000..30e4eb6a4 --- /dev/null +++ b/lib/minz/Log.php @@ -0,0 +1,92 @@ +<?php +/** + * MINZ - Copyright 2011 Marien Fressinaud + * Sous licence AGPL3 <http://www.gnu.org/licenses/> +*/ + +/** + * La classe Log permet de logger des erreurs + */ +class Log { + /** + * Les différents niveau de log + * ERROR erreurs bloquantes de l'application + * WARNING erreurs pouvant géner le bon fonctionnement, mais non bloquantes + * NOTICE messages d'informations, affichés pour le déboggage + */ + const ERROR = 0; + const WARNING = 10; + const NOTICE = 20; + + /** + * Enregistre un message dans un fichier de log spécifique + * Message non loggué si + * - environment = SILENT + * - level = WARNING et environment = PRODUCTION + * - level = NOTICE et environment = PRODUCTION + * @param $information message d'erreur / information à enregistrer + * @param $level niveau d'erreur + * @param $file_name fichier de log, par défaut LOG_PATH/application.log + */ + public static function record ($information, $level, $file_name = null) { + $env = Configuration::environment (); + + if (! ($env == Configuration::SILENT + || ($env == Configuration::PRODUCTION + && ($level == Log::WARNING || $level == Log::NOTICE)))) { + if (is_null ($file_name)) { + $file_name = LOG_PATH . '/application.log'; + } + + switch ($level) { + case Log::ERROR : + $level_label = 'error'; + break; + case Log::WARNING : + $level_label = 'warning'; + break; + case Log::NOTICE : + $level_label = 'notice'; + break; + default : + $level_label = 'unknown'; + } + + if ($env == Configuration::PRODUCTION) { + $file = @fopen ($file_name, 'a'); + } else { + $file = fopen ($file_name, 'a'); + } + + if ($file !== false) { + $log = '[' . date('r') . ']'; + $log .= ' [' . $level_label . ']'; + $log .= ' --- ' . $information . "\n"; + fwrite ($file, $log); + fclose ($file); + } else { + Error::error ( + 500, + array ('error' => array ( + 'Permission is denied for `' + . $file_name . '`') + ) + ); + } + } + } + + /** + * Automatise le log des variables globales $_GET et $_POST + * Fait appel à la fonction record(...) + * Ne fonctionne qu'en environnement "development" + * @param $file_name fichier de log, par défaut LOG_PATH/application.log + */ + public static function recordRequest($file_name = null) { + $msg_get = str_replace("\n", '', '$_GET content : ' . print_r($_GET, true)); + $msg_post = str_replace("\n", '', '$_POST content : ' . print_r($_POST, true)); + + self::record($msg_get, Log::NOTICE, $file_name); + self::record($msg_post, Log::NOTICE, $file_name); + } +} diff --git a/lib/minz/Model.php b/lib/minz/Model.php new file mode 100755 index 000000000..37fc19ed1 --- /dev/null +++ b/lib/minz/Model.php @@ -0,0 +1,12 @@ +<?php +/** + * MINZ - Copyright 2011 Marien Fressinaud + * Sous licence AGPL3 <http://www.gnu.org/licenses/> +*/ + +/** + * La classe Model représente un modèle de l'application (représentation MVC) + */ +class Model { + +} diff --git a/lib/minz/Paginator.php b/lib/minz/Paginator.php new file mode 100755 index 000000000..1a8376e75 --- /dev/null +++ b/lib/minz/Paginator.php @@ -0,0 +1,196 @@ +<?php +/** + * MINZ - Copyright 2011 Marien Fressinaud + * Sous licence AGPL3 <http://www.gnu.org/licenses/> +*/ + +/** + * La classe Paginator permet de gérer la pagination de l'application facilement + */ +class Paginator { + /** + * $items tableau des éléments à afficher/gérer + */ + private $items = array (); + + /** + * $nbItemsPerPage le nombre d'éléments par page + */ + private $nbItemsPerPage = 10; + + /** + * $currentPage page actuelle à gérer + */ + private $currentPage = 1; + + /** + * $nbPage le nombre de pages de pagination + */ + private $nbPage = 1; + + /** + * $nbItems le nombre d'éléments + */ + private $nbItems = 0; + + /** + * Constructeur + * @param $items les éléments à gérer + */ + public function __construct ($items) { + $this->_items ($items); + $this->_nbItems (count ($this->items (true))); + $this->_nbItemsPerPage ($this->nbItemsPerPage); + $this->_currentPage ($this->currentPage); + } + + /** + * Permet d'afficher la pagination + * @param $view nom du fichier de vue situé dans /app/views/helpers/ + * @param $getteur variable de type $_GET[] permettant de retrouver la page + */ + public function render ($view, $getteur) { + $view = APP_PATH . '/views/helpers/'.$view; + + if (file_exists ($view)) { + include ($view); + } + } + + /** + * Permet de retrouver la page d'un élément donné + * @param $item l'élément à retrouver + * @return la page à laquelle se trouve l'élément (false si non trouvé) + */ + public function pageByItem ($item) { + $page = false; + $i = 0; + + do { + if ($item == $this->items[$i]) { + $page = ceil (($i + 1) / $this->nbItemsPerPage); + } + + $i++; + } while (!$page && $i < $this->nbItems ()); + + return $page; + } + + /** + * Permet de retrouver la position d'un élément donné (à partir de 0) + * @param $item l'élément à retrouver + * @return la position à laquelle se trouve l'élément (false si non trouvé) + */ + public function positionByItem ($item) { + $find = false; + $i = 0; + + do { + if ($item == $this->items[$i]) { + $find = true; + } else { + $i++; + } + } while (!$find && $i < $this->nbItems ()); + + return $i; + } + + /** + * Permet de récupérer un item par sa position + * @param $pos la position de l'élément + * @return l'item situé à $pos (dernier item si $pos<0, 1er si $pos>=count($items)) + */ + public function itemByPosition ($pos) { + if ($pos < 0) { + $pos = $this->nbItems () - 1; + } + if ($pos >= count($this->items)) { + $pos = 0; + } + + return $this->items[$pos]; + } + + /** + * GETTEURS + */ + /** + * @param $all si à true, retourne tous les éléments sans prendre en compte la pagination + */ + public function items ($all = false) { + $array = array (); + $nbItems = $this->nbItems (); + + if ($nbItems <= $this->nbItemsPerPage || $all) { + $array = $this->items; + } else { + $begin = ($this->currentPage - 1) * $this->nbItemsPerPage; + $counter = 0; + $i = 0; + + foreach ($this->items as $key => $item) { + if ($i >= $begin) { + $array[$key] = $item; + $counter++; + } + if ($counter >= $this->nbItemsPerPage) { + break; + } + $i++; + } + } + + return $array; + } + public function nbItemsPerPage () { + return $this->nbItemsPerPage; + } + public function currentPage () { + return $this->currentPage; + } + public function nbPage () { + return $this->nbPage; + } + public function nbItems () { + return $this->nbItems; + } + + /** + * SETTEURS + */ + public function _items ($items) { + if (is_array ($items)) { + $this->items = $items; + } + + $this->_nbPage (); + } + public function _nbItemsPerPage ($nbItemsPerPage) { + if ($nbItemsPerPage > $this->nbItems ()) { + $nbItemsPerPage = $this->nbItems (); + } + if ($nbItemsPerPage < 0) { + $nbItemsPerPage = 0; + } + + $this->nbItemsPerPage = $nbItemsPerPage; + $this->_nbPage (); + } + public function _currentPage ($page) { + if($page < 1 || ($page > $this->nbPage && $this->nbPage > 0)) { + throw new CurrentPagePaginationException ($page); + } + + $this->currentPage = $page; + } + private function _nbPage () { + if ($this->nbItemsPerPage > 0) { + $this->nbPage = ceil ($this->nbItems () / $this->nbItemsPerPage); + } + } + public function _nbItems ($value) { + $this->nbItems = $value; + } +} diff --git a/lib/minz/Request.php b/lib/minz/Request.php new file mode 100644 index 000000000..bd5fcb95e --- /dev/null +++ b/lib/minz/Request.php @@ -0,0 +1,196 @@ +<?php +/** + * MINZ - Copyright 2011 Marien Fressinaud + * Sous licence AGPL3 <http://www.gnu.org/licenses/> +*/ + +/** + * Request représente la requête http + */ +class Request { + private static $controller_name = ''; + private static $action_name = ''; + private static $params = array (); + + private static $default_controller_name = 'index'; + private static $default_action_name = 'index'; + + public static $reseted = true; + + /** + * Getteurs + */ + public static function controllerName () { + return self::$controller_name; + } + public static function actionName () { + return self::$action_name; + } + public static function params () { + return self::$params; + } + public static function param ($key, $default = false, $specialchars = false) { + if (isset (self::$params[$key])) { + $p = self::$params[$key]; + if(is_object($p) || $specialchars) { + return $p; + } elseif(is_array($p)) { + return array_map('htmlspecialchars', $p); + } else { + return htmlspecialchars($p); + } + } else { + return $default; + } + } + public static function defaultControllerName () { + return self::$default_controller_name; + } + public static function defaultActionName () { + return self::$default_action_name; + } + + /** + * Setteurs + */ + public static function _controllerName ($controller_name) { + self::$controller_name = $controller_name; + } + public static function _actionName ($action_name) { + self::$action_name = $action_name; + } + public static function _params ($params) { + if (!is_array($params)) { + $params = array ($params); + } + + self::$params = $params; + } + public static function _param ($key, $value = false) { + if ($value === false) { + unset (self::$params[$key]); + } else { + self::$params[$key] = $value; + } + } + + /** + * Initialise la Request + */ + public static function init () { + self::magicQuotesOff (); + } + + /** + * Retourn le nom de domaine du site + */ + public static function getDomainName () { + return $_SERVER['HTTP_HOST']; + } + + /** + * Détermine la base de l'url + * @return la base de l'url + */ + public static function getBaseUrl () { + return Configuration::baseUrl (); + } + + /** + * 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 (); + $uri = $_SERVER['REQUEST_URI']; + + $len_base_url = strlen ($base_url); + $real_uri = substr ($uri, $len_base_url); + } else { + $real_uri = ''; + } + + return $real_uri; + } + + /** + * Relance une requête + * @param $url l'url vers laquelle est relancée la requête + * @param $redirect si vrai, force la redirection http + * > sinon, le dispatcher recharge en interne + */ + public static function forward ($url = array (), $redirect = false) { + $url = Url::checkUrl ($url); + + if ($redirect) { + header ('Location: ' . Url::display ($url, 'php')); + exit (); + } else { + self::$reseted = true; + + self::_controllerName ($url['c']); + self::_actionName ($url['a']); + self::_params (array_merge ( + self::$params, + $url['params'] + )); + } + } + + /** + * Permet de récupérer une variable de type $_GET + * @param $param nom de la variable + * @param $default valeur par défaut à attribuer à la variable + * @return $_GET[$param] + * $_GET si $param = false + * $default si $_GET[$param] n'existe pas + */ + public static function fetchGET ($param = false, $default = false) { + if ($param === false) { + return $_GET; + } elseif (isset ($_GET[$param])) { + return $_GET[$param]; + } else { + return $default; + } + } + + /** + * Permet de récupérer une variable de type $_POST + * @param $param nom de la variable + * @param $default valeur par défaut à attribuer à la variable + * @return $_POST[$param] + * $_POST si $param = false + * $default si $_POST[$param] n'existe pas + */ + public static function fetchPOST ($param = false, $default = false) { + if ($param === false) { + return $_POST; + } elseif (isset ($_POST[$param])) { + return $_POST[$param]; + } else { + return $default; + } + } + + /** + * Méthode désactivant les magic_quotes pour les variables + * $_GET + * $_POST + * $_COOKIE + */ + private static function magicQuotesOff () { + if (get_magic_quotes_gpc ()) { + $_GET = Helper::stripslashes_r ($_GET); + $_POST = Helper::stripslashes_r ($_POST); + $_COOKIE = Helper::stripslashes_r ($_COOKIE); + } + } + + public static function isPost () { + return !empty ($_POST) || !empty ($_FILES); + } +} + + diff --git a/lib/minz/Response.php b/lib/minz/Response.php new file mode 100644 index 000000000..fcf53c5b1 --- /dev/null +++ b/lib/minz/Response.php @@ -0,0 +1,60 @@ +<?php +/** + * MINZ - Copyright 2011 Marien Fressinaud + * Sous licence AGPL3 <http://www.gnu.org/licenses/> +*/ + +/** + * Response représente la requête http renvoyée à l'utilisateur + */ +class Response { + private static $header = 'HTTP/1.0 200 OK'; + private static $body = ''; + + /** + * Mets à jour le body de la Response + * @param $text le texte à incorporer dans le body + */ + public static function setBody ($text) { + self::$body = $text; + } + + /** + * Mets à jour le header de la Response + * @param $code le code HTTP, valeurs possibles + * - 200 (OK) + * - 403 (Forbidden) + * - 404 (Forbidden) + * - 500 (Forbidden) -> par défaut si $code erroné + * - 503 (Forbidden) + */ + public static function setHeader ($code) { + switch ($code) { + case 200 : + self::$header = 'HTTP/1.0 200 OK'; + break; + case 403 : + self::$header = 'HTTP/1.0 403 Forbidden'; + break; + case 404 : + self::$header = 'HTTP/1.0 404 Not Found'; + break; + case 500 : + self::$header = 'HTTP/1.0 500 Internal Server Error'; + break; + case 503 : + self::$header = 'HTTP/1.0 503 Service Unavailable'; + break; + default : + self::$header = 'HTTP/1.0 500 Internal Server Error'; + } + } + + /** + * Envoie la Response à l'utilisateur + */ + public static function send () { + header (self::$header); + echo self::$body; + } +} diff --git a/lib/minz/Router.php b/lib/minz/Router.php new file mode 100755 index 000000000..c5d6f5baa --- /dev/null +++ b/lib/minz/Router.php @@ -0,0 +1,209 @@ +<?php +/** + * MINZ - Copyright 2011 Marien Fressinaud + * Sous licence AGPL3 <http://www.gnu.org/licenses/> +*/ + +/** + * La classe Router gère le routage de l'application + * Les routes sont définies dans APP_PATH.'/configuration/routes.php' + */ +class Router { + const ROUTES_PATH_NAME = '/configuration/routes.php'; + + private $routes = array (); + + /** + * Constructeur + * @exception FileNotExistException si ROUTES_PATH_NAME n'existe pas + * et que l'on utilise l'url rewriting + */ + public function __construct () { + if (Configuration::useUrlRewriting ()) { + if (file_exists (APP_PATH . self::ROUTES_PATH_NAME)) { + $routes = include ( + APP_PATH . self::ROUTES_PATH_NAME + ); + + if (!is_array ($routes)) { + $routes = array (); + } + + $this->routes = array_map ( + array ('Url', 'checkUrl'), + $routes + ); + } else { + throw new FileNotExistException ( + self::ROUTES_PATH_NAME, + MinzException::ERROR + ); + } + } + } + + /** + * Initialise le Router en déterminant le couple Controller / Action + * Mets à jour la Request + * @exception RouteNotFoundException si l'uri n'est pas présente dans + * > la table de routage + */ + public function init () { + $url = array (); + + if (Configuration::useUrlRewriting ()) { + try { + $url = $this->buildWithRewriting (); + } catch (RouteNotFoundException $e) { + throw $e; + } + } else { + $url = $this->buildWithoutRewriting (); + } + + $url['params'] = array_merge ( + $url['params'], + Request::fetchPOST () + ); + + Request::forward ($url); + } + + /** + * Retourne un tableau représentant l'url passée par la barre d'adresses + * Ne se base PAS sur la table de routage + * @return tableau représentant l'url + */ + public function buildWithoutRewriting () { + $url = array (); + + $url['c'] = Request::fetchGET ( + 'c', + Request::defaultControllerName () + ); + $url['a'] = Request::fetchGET ( + 'a', + Request::defaultActionName () + ); + $url['params'] = Request::fetchGET (); + + // post-traitement + unset ($url['params']['c']); + unset ($url['params']['a']); + + return $url; + } + + /** + * Retourne un tableau représentant l'url passée par la barre d'adresses + * Se base sur la table de routage + * @return tableau représentant l'url + * @exception RouteNotFoundException si l'uri n'est pas présente dans + * > la table de routage + */ + public function buildWithRewriting () { + $url = array (); + $uri = Request::getURI (); + $find = false; + + foreach ($this->routes as $route) { + $regex = '*^' . $route['route'] . '$*'; + if (preg_match ($regex, $uri, $matches)) { + $url['c'] = $route['controller']; + $url['a'] = $route['action']; + $url['params'] = $this->getParams ( + $route['params'], + $matches + ); + $find = true; + break; + } + } + + if (!$find && $uri != '/') { + throw new RouteNotFoundException ( + $uri, + MinzException::ERROR + ); + } + + // post-traitement + $url = Url::checkUrl ($url); + + return $url; + } + + /** + * Retourne l'uri d'une url en se basant sur la table de routage + * @param l'url sous forme de tableau + * @return l'uri formatée (string) selon une route trouvée + */ + public function printUriRewrited ($url) { + $route = $this->searchRoute ($url); + + if ($route !== false) { + return $this->replaceParams ($route, $url['params']); + } + + return ''; + } + + /** + * Recherche la route correspondante à une url + * @param l'url sous forme de tableau + * @return la route telle que spécifiée dans la table de routage, + * false si pas trouvée + */ + public function searchRoute ($url) { + foreach ($this->routes as $route) { + if ($route['controller'] == $url['c'] + && $route['action'] == $url['a']) { + // calcule la différence des tableaux de params + $params = array_flip ($route['params']); + $difference_params = array_diff_key ( + $params, + $url['params'] + ); + + // vérifie que pas de différence + // et le cas où $params est vide et pas $url['params'] + if (empty ($difference_params) + && (!empty ($params) || empty ($url['params']))) { + return $route; + } + } + } + + return false; + } + + /** + * Récupère un tableau dont + * - les clés sont définies dans $params_route + * - les valeurs sont situées dans $matches + * Le tableau $matches est décalé de +1 par rapport à $params_route + */ + private function getParams($params_route, $matches) { + $params = array (); + + for ($i = 0; $i < count ($params_route); $i++) { + $param = $params_route[$i]; + $params[$param] = $matches[$i + 1]; + } + + return $params; + } + + /** + * Remplace les éléments de la route par les valeurs contenues dans $params + */ + private function replaceParams ($route, $params_replace) { + $uri = $route['route']; + $params = array(); + foreach($route['params'] as $param) { + $uri = preg_replace('#\((.+)\)#U', $params_replace[$param], $uri, 1); + } + + return stripslashes($uri); + } +} diff --git a/lib/minz/Session.php b/lib/minz/Session.php new file mode 100755 index 000000000..f9c9c6754 --- /dev/null +++ b/lib/minz/Session.php @@ -0,0 +1,78 @@ +<?php + +/** + * La classe Session gère la session utilisateur + * C'est un singleton + */ +class Session { + /** + * $session stocke les variables de session + */ + private static $session = array (); + + /** + * Initialise la session + */ + public static function init () { + // démarre la session + session_name (md5 (Configuration::selApplication ())); + session_start (); + + if (isset ($_SESSION)) { + self::$session = $_SESSION; + } + } + + + /** + * Permet de récupérer une variable de 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) { + if (isset (self::$session[$p])) { + $return = self::$session[$p]; + } else { + $return = $default; + } + + return $return; + } + + + /** + * Permet de créer ou mettre à jour une variable de 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) { + if ($v === false) { + unset ($_SESSION[$p]); + unset (self::$session[$p]); + } else { + $_SESSION[$p] = $v; + self::$session[$p] = $v; + + if($p == 'language') { + // reset pour remettre à jour le fichier de langue à utiliser + Translate::reset (); + } + } + } + + + /** + * 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'); + + session_unset (); + self::$session = array (); + + if (!$force) { + self::_param ('language', $language); + } + } +} diff --git a/lib/minz/Translate.php b/lib/minz/Translate.php new file mode 100644 index 000000000..e8cbe4852 --- /dev/null +++ b/lib/minz/Translate.php @@ -0,0 +1,71 @@ +<?php +/** + * MINZ - Copyright 2011 Marien Fressinaud + * Sous licence AGPL3 <http://www.gnu.org/licenses/> + */ + +/** + * La classe Translate se charge de la traduction + * Utilise les fichiers du répertoire /app/i18n/ + */ +class Translate { + /** + * $language est la langue à afficher + */ + private static $language; + + /** + * $translates est le tableau de correspondance + * $key => $traduction + */ + private static $translates = array (); + + /** + * Inclus le fichier de langue qui va bien + * l'enregistre dans $translates + */ + public static function init () { + $l = Configuration::language (); + self::$language = Session::param ('language', $l); + + $l_path = APP_PATH . '/i18n/' . self::$language . '.php'; + + if (file_exists ($l_path)) { + self::$translates = include ($l_path); + } + } + + /** + * Alias de init + */ + public static function reset () { + self::init (); + } + + /** + * Traduit une clé en sa valeur du tableau $translates + * @param $key la clé à traduire + * @return la valeur correspondante à la clé + * > si non présente dans le tableau, on retourne la clé elle-même + */ + public static function t ($key) { + $translate = $key; + + if (isset (self::$translates[$key])) { + $translate = self::$translates[$key]; + } + + $args = func_get_args (); + unset($args[0]); + + return vsprintf ($translate, $args); + } + + /** + * Retourne la langue utilisée actuellement + * @return la langue + */ + public static function language () { + return self::$language; + } +} diff --git a/lib/minz/Url.php b/lib/minz/Url.php new file mode 100755 index 000000000..c1c3e9a0f --- /dev/null +++ b/lib/minz/Url.php @@ -0,0 +1,130 @@ +<?php + +/** + * La classe Url permet de gérer les URL à travers MINZ + */ +class Url { + /** + * Affiche une Url formatée selon que l'on utilise l'url_rewriting ou non + * si oui, on cherche dans la table de routage la correspondance pour formater + * @param $url l'url à formater définie comme un tableau : + * $url['c'] = controller + * $url['a'] = action + * $url['params'] = tableau des paramètres supplémentaires + * $url['protocol'] = protocole à utiliser (http par défaut) + * ou comme une chaîne de caractère + * @param $encodage pour indiquer comment encoder les & (& ou & pour html) + * @return l'url formatée + */ + public static function display ($url = array (), $encodage = 'html') { + $url = self::checkUrl ($url); + + $url_string = ''; + + if (is_array ($url) && isset ($url['protocol'])) { + $protocol = $url['protocol']; + } else { + if(isset($_SERVER['HTTPS']) && $_SERVER["HTTPS"] == 'on') { + $protocol = 'https'; + } else { + $protocol = 'http'; + } + } + $url_string .= $protocol . '://'; + + $url_string .= Request::getDomainName (); + + $url_string .= Request::getBaseUrl (); + + if (is_array ($url)) { + $router = new Router (); + + if (Configuration::useUrlRewriting ()) { + $url_string .= $router->printUriRewrited ($url); + } else { + $url_string .= self::printUri ($url, $encodage); + } + } else { + $url_string .= $url; + } + + return $url_string; + } + + /** + * Construit l'URI d'une URL sans url rewriting + * @param l'url sous forme de tableau + * @param $encodage pour indiquer comment encoder les & (& ou & pour html) + * @return l'uri sous la forme ?key=value&key2=value2 + */ + private static function printUri ($url, $encodage) { + $uri = ''; + $separator = '/?'; + + if($encodage == 'html') { + $and = '&'; + } else { + $and = '&'; + } + + if (isset ($url['c']) + && $url['c'] != Request::defaultControllerName ()) { + $uri .= $separator . 'c=' . $url['c']; + $separator = $and; + } + + if (isset ($url['a']) + && $url['a'] != Request::defaultActionName ()) { + $uri .= $separator . 'a=' . $url['a']; + $separator = $and; + } + + if (isset ($url['params'])) { + foreach ($url['params'] as $key => $param) { + $uri .= $separator . $key . '=' . $param; + $separator = $and; + } + } + + return $uri; + } + + /** + * Vérifie que les éléments du tableau représentant une url soit ok + * @param l'url sous forme de tableau (sinon renverra directement $url) + * @return l'url vérifié + */ + public static function checkUrl ($url) { + $url_checked = $url; + + if (is_array ($url)) { + if (!isset ($url['c'])) { + $url_checked['c'] = Request::defaultControllerName (); + } + if (!isset ($url['a'])) { + $url_checked['a'] = Request::defaultActionName (); + } + if (!isset ($url['params'])) { + $url_checked['params'] = array (); + } + } + + return $url_checked; + } +} + +function _url ($controller, $action) { + $nb_args = func_num_args (); + + if($nb_args < 2 || $nb_args % 2 != 0) { + return false; + } + + $args = func_get_args (); + $params = array (); + for($i = 2; $i < $nb_args; $i = $i + 2) { + $params[$args[$i]] = $args[$i + 1]; + } + + return Url::display (array ('c' => $controller, 'a' => $action, 'params' => $params)); +} diff --git a/lib/minz/View.php b/lib/minz/View.php new file mode 100755 index 000000000..2bb747aa9 --- /dev/null +++ b/lib/minz/View.php @@ -0,0 +1,232 @@ +<?php +/** + * MINZ - Copyright 2011 Marien Fressinaud + * Sous licence AGPL3 <http://www.gnu.org/licenses/> +*/ + +/** + * La classe View représente la vue de l'application + */ +class View { + const VIEWS_PATH_NAME = '/views'; + const LAYOUT_PATH_NAME = '/layout'; + const LAYOUT_FILENAME = '/layout.phtml'; + + private $view_filename = ''; + private $use_layout = false; + + private static $title = ''; + private static $styles = array (); + private static $scripts = array (); + + private static $params = array (); + + /** + * Constructeur + * Détermine si on utilise un layout ou non + */ + public function __construct () { + $this->view_filename = APP_PATH + . self::VIEWS_PATH_NAME . '/' + . Request::controllerName () . '/' + . Request::actionName () . '.phtml'; + + if (file_exists (APP_PATH + . self::LAYOUT_PATH_NAME + . self::LAYOUT_FILENAME)) { + $this->use_layout = true; + } + + self::$title = Configuration::title (); + } + + /** + * Construit la vue + */ + public function build () { + if ($this->use_layout) { + $this->buildLayout (); + } else { + $this->render (); + } + } + + /** + * Construit le layout + */ + public function buildLayout () { + include ( + APP_PATH + . self::LAYOUT_PATH_NAME + . self::LAYOUT_FILENAME + ); + } + + /** + * Affiche la Vue en elle-même + */ + public function render () { + if (file_exists ($this->view_filename)) { + include ($this->view_filename); + } else { + Log::record ('File doesn\'t exist : `' + . $this->view_filename . '`', + Log::NOTICE); + } + } + + /** + * Ajoute un élément du layout + * @param $part l'élément partial à ajouter + */ + public function partial ($part) { + $fic_partial = APP_PATH + . self::LAYOUT_PATH_NAME . '/' + . $part . '.phtml'; + + if (file_exists ($fic_partial)) { + include ($fic_partial); + } else { + Log::record ('File doesn\'t exist : `' + . $fic_partial . '`', + Log::WARNING); + } + } + + /** + * Affiche un élément graphique situé dans APP./views/helpers/ + * @param $helper l'élément à afficher + */ + public function renderHelper ($helper) { + $fic_helper = APP_PATH + . '/views/helpers/' + . $helper . '.phtml'; + + if (file_exists ($fic_helper)) { + include ($fic_helper); + } else { + Log::record ('File doesn\'t exist : `' + . $fic_helper . '`', + Log::WARNING); + } + } + + /** + * Permet de choisir si on souhaite utiliser le layout + * @param $use true si on souhaite utiliser le layout, false sinon + */ + public function _useLayout ($use) { + $this->use_layout = $use; + } + + /** + * Gestion du titre + */ + public static function title () { + return self::$title; + } + public static function headTitle () { + return '<title>' . self::$title . '</title>' . "\n"; + } + public static function _title ($title) { + self::$title = $title; + } + public static function prependTitle ($title) { + self::$title = $title . self::$title; + } + public static function appendTitle ($title) { + self::$title = self::$title . $title; + } + + /** + * Gestion des feuilles de style + */ + public static function headStyle () { + $styles = ''; + + foreach(self::$styles as $style) { + $cond = $style['cond']; + if ($cond) { + $styles .= '<!--[if ' . $cond . ']>'; + } + + $styles .= '<link rel="stylesheet" type="text/css"'; + $styles .= ' media="' . $style['media'] . '"'; + $styles .= ' href="' . $style['url'] . '" />'; + + if ($cond) { + $styles .= '<![endif]-->'; + } + + $styles .= "\n"; + } + + return $styles; + } + public static function prependStyle ($url, $media = 'all', $cond = false) { + array_unshift (self::$styles, array ( + 'url' => $url, + 'media' => $media, + 'cond' => $cond + )); + } + public static function appendStyle ($url, $media = 'all', $cond = false) { + self::$styles[] = array ( + 'url' => $url, + 'media' => $media, + 'cond' => $cond + ); + } + + /** + * Gestion des scripts JS + */ + public static function headScript () { + $scripts = ''; + + foreach (self::$scripts as $script) { + $cond = $script['cond']; + if ($cond) { + $scripts .= '<!--[if ' . $cond . ']>'; + } + + $scripts .= '<script type="text/javascript"'; + $scripts .= ' src="' . $script['url'] . '">'; + $scripts .= '</script>'; + + if ($cond) { + $scripts .= '<![endif]-->'; + } + + $scripts .= "\n"; + } + + return $scripts; + } + public static function prependScript ($url, $cond = false) { + array_unshift(self::$scripts, array ( + 'url' => $url, + 'cond' => $cond + )); + } + public static function appendScript ($url, $cond = false) { + self::$scripts[] = array ( + 'url' => $url, + 'cond' => $cond + ); + } + + /** + * Gestion des paramètres ajoutés à la vue + */ + public static function _param ($key, $value) { + self::$params[$key] = $value; + } + public function attributeParams () { + foreach (View::$params as $key => $value) { + $this->$key = $value; + } + } +} + + diff --git a/lib/minz/dao/Model_array.php b/lib/minz/dao/Model_array.php new file mode 100755 index 000000000..0b9ccf071 --- /dev/null +++ b/lib/minz/dao/Model_array.php @@ -0,0 +1,122 @@ +<?php +/** + * MINZ - Copyright 2011 Marien Fressinaud + * Sous licence AGPL3 <http://www.gnu.org/licenses/> +*/ + +/** + * La classe Model_array représente le modèle interragissant avec les fichiers de type texte gérant des tableaux php + */ +class Model_array extends Model_txt { + /** + * $array Le tableau php contenu dans le fichier $nameFile + */ + protected $array = array (); + + /** + * Ouvre le fichier indiqué, charge le tableau dans $array et le $nameFile + * @param $nameFile le nom du fichier à ouvrir contenant un tableau + * Remarque : $array sera obligatoirement un tableau + */ + public function __construct ($nameFile) { + parent::__construct ($nameFile); + + if (!$this->getLock ('read')) { + throw new PermissionDeniedException ($this->filename); + } else { + $this->array = include ($this->filename); + $this->releaseLock (); + + if (!is_array ($this->array)) { + $this->array = array (); + } + + $this->array = $this->decodeArray ($this->array); + } + } + + /** + * Écrit un tableau dans le fichier $nameFile + * @param $array le tableau php à enregistrer + **/ + public function writeFile ($array) { + if (!$this->getLock ('write')) { + throw new PermissionDeniedException ($this->namefile); + } else { + $this->erase (); + + $this->writeLine ('<?php'); + $this->writeLine ('return ', false); + $this->writeArray ($array); + $this->writeLine (';'); + + $this->releaseLock (); + } + } + + private function writeArray ($array, $profondeur = 0) { + $tab = ''; + for ($i = 0; $i < $profondeur; $i++) { + $tab .= "\t"; + } + $this->writeLine ('array ('); + + foreach ($array as $key => $value) { + if (is_int ($key)) { + $this->writeLine ($tab . "\t" . $key . ' => ', false); + } else { + $this->writeLine ($tab . "\t" . '\'' . $key . '\'' . ' => ', false); + } + + if (is_array ($value)) { + $this->writeArray ($value, $profondeur + 1); + $this->writeLine (','); + } else { + if (is_numeric ($value)) { + $this->writeLine ($value . ','); + } else { + $this->writeLine ('\'' . addslashes ($value) . '\','); + } + } + } + + $this->writeLine ($tab . ')', false); + } + + private function decodeArray ($array) { + $new_array = array (); + + foreach ($array as $key => $value) { + if (is_array ($value)) { + $new_array[$key] = $this->decodeArray ($value); + } else { + $new_array[$key] = stripslashes ($value); + } + } + + return $new_array; + } + + private function getLock ($type) { + if ($type == 'write') { + $lock = LOCK_EX; + } else { + $lock = LOCK_SH; + } + + $count = 1; + while (!flock ($this->file, $lock) && $count <= 50) { + $count++; + } + + if ($count >= 50) { + return false; + } else { + return true; + } + } + + private function releaseLock () { + flock ($this->file, LOCK_UN); + } +} diff --git a/lib/minz/dao/Model_pdo.php b/lib/minz/dao/Model_pdo.php new file mode 100755 index 000000000..6114de127 --- /dev/null +++ b/lib/minz/dao/Model_pdo.php @@ -0,0 +1,39 @@ +<?php +/** + * MINZ - Copyright 2011 Marien Fressinaud + * Sous licence AGPL3 <http://www.gnu.org/licenses/> +*/ + +/** + * La classe Model_sql représente le modèle interragissant avec les bases de données + * Seul la connexion MySQL est prise en charge pour le moment + */ +class Model_pdo { + /** + * $bd variable représentant la base de données + */ + protected $bd; + + /** + * 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 ($type = 'mysql') { + $db = Configuration::dataBase (); + try { + $string = $type + . ':host=' . $db['host'] + . ';dbname=' . $db['base']; + $this->bd = new PDO ( + $string, + $db['user'], + $db['password'] + ); + } catch (Exception $e) { + throw new PDOConnectionException ( + $string, + $db['user'], MinzException::WARNING + ); + } + } +} diff --git a/lib/minz/dao/Model_txt.php b/lib/minz/dao/Model_txt.php new file mode 100755 index 000000000..c9d5cfe77 --- /dev/null +++ b/lib/minz/dao/Model_txt.php @@ -0,0 +1,77 @@ +<?php +/** + * MINZ - Copyright 2011 Marien Fressinaud + * Sous licence AGPL3 <http://www.gnu.org/licenses/> +*/ + +/** + * La classe Model_txt représente le modèle interragissant avec les fichiers de type texte + */ +class Model_txt { + /** + * $file représente le fichier à ouvrir + */ + protected $file; + + /** + * $filename est le nom du fichier + */ + protected $filename; + + /** + * Ouvre un fichier dans $file + * @param $nameFile nom du fichier à ouvrir + * @param $mode mode d'ouverture du fichier ('a+' par défaut) + * @exception FileNotExistException si le fichier n'existe pas + * > ou ne peux pas être ouvert + */ + public function __construct ($nameFile, $mode = 'a+') { + $this->filename = $nameFile; + $this->file = @fopen ($this->filename, $mode); + + if (!$this->file) { + throw new FileNotExistException ( + $this->filename, + MinzException::WARNING + ); + } + } + + /** + * Lit une ligne de $file + * @return une ligne du fichier + */ + public function readLine () { + return fgets ($this->file); + } + + /** + * Écrit une ligne dans $file + * @param $line la ligne à écrire + */ + public function writeLine ($line, $newLine = true) { + $char = ''; + if ($newLine) { + $char = "\n"; + } + + fwrite ($this->file, $line . $char); + } + + /** + * Efface le fichier $file + * @return true en cas de succès, false sinon + */ + public function erase () { + return ftruncate ($this->file, 0); + } + + /** + * Ferme $file + */ + public function __destruct () { + if (isset ($this->file)) { + fclose ($this->file); + } + } +} diff --git a/lib/minz/exceptions/MinzException.php b/lib/minz/exceptions/MinzException.php new file mode 100644 index 000000000..8fca5ec16 --- /dev/null +++ b/lib/minz/exceptions/MinzException.php @@ -0,0 +1,94 @@ +<?php + +class MinzException extends Exception { + const ERROR = 0; + const WARNING = 10; + const NOTICE = 20; + + public function __construct ($message, $code = self::ERROR) { + if ($code != MinzException::ERROR + && $code != MinzException::WARNING + && $code != MinzException::NOTICE) { + $code = MinzException::ERROR; + } + + parent::__construct ($message, $code); + } +} + +class PermissionDeniedException extends MinzException { + public function __construct ($file_name, $code = self::ERROR) { + $message = 'Permission is denied for `' . $file_name.'`'; + + parent::__construct ($message, $code); + } +} +class FileNotExistException extends MinzException { + public function __construct ($file_name, $code = self::ERROR) { + $message = 'File doesn\'t exist : `' . $file_name.'`'; + + parent::__construct ($message, $code); + } +} +class BadConfigurationException extends MinzException { + public function __construct ($part_missing, $code = self::ERROR) { + $message = '`' . $part_missing + . '` in the configuration file is missing'; + + parent::__construct ($message, $code); + } +} +class ControllerNotExistException extends MinzException { + public function __construct ($controller_name, $code = self::ERROR) { + $message = 'Controller `' . $controller_name + . '` doesn\'t exist'; + + parent::__construct ($message, $code); + } +} +class ControllerNotActionControllerException extends MinzException { + public function __construct ($controller_name, $code = self::ERROR) { + $message = 'Controller `' . $controller_name + . '` isn\'t instance of ActionController'; + + parent::__construct ($message, $code); + } +} +class ActionException extends MinzException { + public function __construct ($controller_name, $action_name, $code = self::ERROR) { + $message = '`' . $action_name . '` cannot be invoked on `' + . $controller_name . '`'; + + parent::__construct ($message, $code); + } +} +class RouteNotFoundException extends MinzException { + private $route; + + public function __construct ($route, $code = self::ERROR) { + $this->route = $route; + + $message = 'Route `' . $route . '` not found'; + + parent::__construct ($message, $code); + } + + public function route () { + return $this->route; + } +} +class PDOConnectionException extends MinzException { + public function __construct ($string_connection, $user, $code = self::ERROR) { + $message = 'Access to database is denied for `' . $user . '`' + . ' (`' . $string_connection . '`)'; + + parent::__construct ($message, $code); + } +} +class CurrentPagePaginationException extends MinzException { + public function __construct ($page) { + $message = 'Page number `' . $page . '` doesn\'t exist'; + + parent::__construct ($message, self::ERROR); + } +} diff --git a/public/install.php b/public/install.php index 672bb4418..930241496 100644 --- a/public/install.php +++ b/public/install.php @@ -168,9 +168,8 @@ function saveStep3 () { if (!empty ($_POST)) { if (empty ($_POST['host']) || empty ($_POST['user']) || - empty ($_POST['pass']) || empty ($_POST['base'])) { - return false; + $_SESSION['bd_error'] = true; } $_SESSION['bd_host'] = $_POST['host']; @@ -196,7 +195,10 @@ function saveStep3 () { $res = checkBD (); if ($res) { + $_SESSION['bd_error'] = false; header ('Location: index.php?step=4'); + } else { + $_SESSION['bd_error'] = true; } } } @@ -275,11 +277,13 @@ function checkStep3 () { isset ($_SESSION['bd_user']) && isset ($_SESSION['bd_pass']) && isset ($_SESSION['bd_name']); + $conn = !isset ($_SESSION['bd_error']) || !$_SESSION['bd_error']; return array ( 'bd' => $bd ? 'ok' : 'ko', + 'conn' => $conn ? 'ok' : 'ko', 'conf' => $conf ? 'ok' : 'ko', - 'all' => $bd && $conf ? 'ok' : 'ko' + 'all' => $bd && $conn && $conf ? 'ok' : 'ko' ); } function checkBD () { @@ -358,8 +362,8 @@ 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 $version = curl_version(); ?> <?php if ($res['curl'] == 'ok') { ?> + <?php $version = curl_version(); ?> <p class="alert alert-success"><span class="alert-head"><?php echo _t ('ok'); ?></span> <?php echo _t ('curl_is_ok', $version['version']); ?></p> <?php } else { ?> <p class="alert alert-error"><span class="alert-head"><?php echo _t ('damn'); ?></span> <?php echo _t ('curl_is_nok'); ?></p> @@ -467,6 +471,8 @@ function printStep3 () { ?> <?php $s3 = checkStep3 (); if ($s3['all'] == 'ok') { ?> <p class="alert alert-success"><span class="alert-head"><?php echo _t ('ok'); ?></span> <?php echo _t ('bdd_conf_is_ok'); ?></p> + <?php } elseif ($s3['conn'] == 'ko') { ?> + <p class="alert alert-error"><span class="alert-head"><?php echo _t ('damn'); ?></span> <?php echo _t ('bdd_conf_is_ko'); ?></p> <?php } ?> <form action="index.php?step=3" method="post"> diff --git a/public/scripts/endless_mode.js b/public/scripts/endless_mode.js new file mode 100644 index 000000000..489b69f30 --- /dev/null +++ b/public/scripts/endless_mode.js @@ -0,0 +1,31 @@ +var url_load_more = ""; +var load = false; + +function init_load_more() { + url_load_more = $("a#load_more").attr("href"); + + $("#load_more").click (function () { + load_more_posts (); + + return false; + }); +} + +function load_more_posts () { + load = true; + $("#load_more").addClass("loading"); + $.get (url_load_more, function (data) { + $("#stream .flux:last").after($("#stream .flux", data)); + $(".pagination").html($(".pagination", data).html()); + + init_load_more(); + init_posts(); + + $("#load_more").removeClass("loading"); + load = false; + }); +} + +$(document).ready (function () { + init_load_more(); +});
\ No newline at end of file diff --git a/public/scripts/jquery.lazyload.min.js b/public/scripts/jquery.lazyload.min.js new file mode 100644 index 000000000..79663d7bb --- /dev/null +++ b/public/scripts/jquery.lazyload.min.js @@ -0,0 +1,15 @@ +/* + * 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.8.4 + * + */ +(function(a,b,c,d){var e=a(b);a.fn.lazyload=function(c){function i(){var b=0;f.each(function(){var c=a(this);if(h.skip_invisible&&!c.is(":visible"))return;if(!a.abovethetop(this,h)&&!a.leftofbegin(this,h))if(!a.belowthefold(this,h)&&!a.rightoffold(this,h))c.trigger("appear"),b=0;else if(++b>h.failure_limit)return!1})}var f=this,g,h={threshold:0,failure_limit:0,event:"scroll",effect:"show",container:b,data_attribute:"original",skip_invisible:!0,appear:null,load:null};return c&&(d!==c.failurelimit&&(c.failure_limit=c.failurelimit,delete c.failurelimit),d!==c.effectspeed&&(c.effect_speed=c.effectspeed,delete c.effectspeed),a.extend(h,c)),g=h.container===d||h.container===b?e:a(h.container),0===h.event.indexOf("scroll")&&g.bind(h.event,function(a){return i()}),this.each(function(){var b=this,c=a(b);b.loaded=!1,c.one("appear",function(){if(!this.loaded){if(h.appear){var d=f.length;h.appear.call(b,d,h)}a("<img />").bind("load",function(){c.hide().attr("src",c.data(h.data_attribute))[h.effect](h.effect_speed),b.loaded=!0;var d=a.grep(f,function(a){return!a.loaded});f=a(d);if(h.load){var e=f.length;h.load.call(b,e,h)}}).attr("src",c.data(h.data_attribute))}}),0!==h.event.indexOf("scroll")&&c.bind(h.event,function(a){b.loaded||c.trigger("appear")})}),e.bind("resize",function(a){i()}),/iphone|ipod|ipad.*os 5/gi.test(navigator.appVersion)&&e.bind("pageshow",function(b){b.originalEvent.persisted&&f.each(function(){a(this).trigger("appear")})}),a(b).load(function(){i()}),this},a.belowthefold=function(c,f){var g;return f.container===d||f.container===b?g=e.height()+e.scrollTop():g=a(f.container).offset().top+a(f.container).height(),g<=a(c).offset().top-f.threshold},a.rightoffold=function(c,f){var g;return f.container===d||f.container===b?g=e.width()+e.scrollLeft():g=a(f.container).offset().left+a(f.container).width(),g<=a(c).offset().left-f.threshold},a.abovethetop=function(c,f){var g;return f.container===d||f.container===b?g=e.scrollTop():g=a(f.container).offset().top,g>=a(c).offset().top+f.threshold+a(c).height()},a.leftofbegin=function(c,f){var g;return f.container===d||f.container===b?g=e.scrollLeft():g=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) diff --git a/public/theme/freshrss.css b/public/theme/freshrss.css index 880b45085..93bb32437 100644 --- a/public/theme/freshrss.css +++ b/public/theme/freshrss.css @@ -77,6 +77,7 @@ } .favicon { + height: 16px; width: 16px; } @@ -114,6 +115,9 @@ line-height: 35px; float: right; } + .categories .feeds .item.error .feed { + color: #BD362F; + } .categories .feeds .item .feed { display: inline-block; margin: 0; @@ -197,39 +201,39 @@ line-height: 25px; border-top: 1px solid #ddd; } - .flux_header .item.manage { - width: 60px; + .item.manage { + width: 80px; white-space: nowrap; font-size: 0px; text-align: center; } - .flux_header .item.manage .read { + .item.manage .read { display: inline-block; - width: 30px; + width: 40px; height: 40px; background: url("icons/read.png") center center no-repeat; background: url("icons/read.svg") center center no-repeat; vertical-align: middle; } - .flux_header .item.manage .read:hover { + .item.manage .read:hover { text-decoration: none; } - .flux.not_read .flux_header .item.manage .read { + .flux.not_read .item.manage .read { background: url("icons/unread.png") center center no-repeat; background: url("icons/unread.svg") center center no-repeat; } - .flux_header .item.manage .bookmark { + .item.manage .bookmark { display: inline-block; - width: 30px; + width: 40px; height: 40px; background: url("icons/non-starred.png") center center no-repeat; background: url("icons/non-starred.svg") center center no-repeat; vertical-align: middle; } - .flux_header .item.manage .bookmark:hover { + .item.manage .bookmark:hover { text-decoration: none; } - .flux.favorite .flux_header .item.manage .bookmark { + .flux.favorite .item.manage .bookmark { background: url("icons/starred.png") center center no-repeat; background: url("icons/starred.svg") center center no-repeat; } @@ -240,9 +244,11 @@ text-overflow: ellipsis; line-height: 40px; } + .flux_header .item.website .favicon { + padding: 5px; + } .flux_header .item.website a { display: block; - padding: 0 5px; height: 40px; } .flux_header .item.title { @@ -265,12 +271,12 @@ cursor: pointer; } .flux_header .item.link { - width: 35px; + width: 40px; text-align: center; } .flux_header .item.link a { display: inline-block; - width: 35px; + width: 40px; height: 40px; background: url("icons/link.png") center center no-repeat; background: url("icons/link.svg") center center no-repeat; @@ -280,12 +286,64 @@ text-decoration: none; } +#stream.reader .flux { + padding: 0 0 30px; + border: none; + background: #f0f0f0; + color: #333; +} + #stream.reader .flux .author { + margin: 0 0 10px; + font-size: 90%; + color: #666; + } + +#stream.global { + text-align: center; +} + #stream.global .category { + display: inline-block; + width: 280px; + margin: 20px 10px; + vertical-align: top; + background: #fff; + border: 1px solid #aaa; + border-radius: 5px; + text-align: left; + box-shadow: 0 0 5px #bbb; + } + #stream.global .cat_header { + height: 35px; + padding: 0 10px; + background: #eee; + border-bottom: 1px solid #aaa; + border-radius: 5px 5px 0 0; + line-height: 35px; + font-size: 120%; + } + #stream.global .cat_header a { + color: #333; + text-shadow: 0 -1px 0px #aaa; + } + #stream.global .category .feeds { + max-height: 250px; + margin: 0; + list-style: none; + overflow: auto; + } + #stream.global .category .feeds .item { + padding: 2px 10px; + font-size: 90%; + } + .content { + min-height: 300px; max-width: 550px; margin: 0 auto; padding: 20px 10px; line-height: 170%; font-family: 'OpenSans'; + word-wrap: break-word; } .content .title { margin: 0 0 5px; @@ -301,15 +359,19 @@ display: block; margin: 10px auto; } + .content hr { + margin: 30px 0; + height: 1px; + background: #ddd; + border: 0; + } .content pre { - width: 90%; margin: 10px auto; padding: 10px; overflow: auto; - background: #666; - border: 1px solid #000; - color: #fafafa; - border-radius: 5px; + background: #000; + color: #fff; + font-size: 110%; } .content q, .content blockquote { display: block; @@ -342,20 +404,39 @@ } .pagination .item { display: table-cell; - padding: 5px 10px; - border-top: 1px solid #aaa; + line-height: 40px; } + .pagination .item.pager-current { + font-weight: bold; + font-size: 140%; + } + .pagination .item.pager-first, + .pagination .item.pager-previous, + .pagination .item.pager-next, + .pagination .item.pager-last { + width: 100px; + } .pagination .item a { + display: block; color: #333; font-style: italic; } - .pagination .pager-previous, .pagination .pager-next { - width: 200px; + .pagination:first-child .item { + border-bottom: 1px solid #aaa; } - .pagination .item.pager-current { - font-weight: bold; + .pagination:last-child .item { + border-top: 1px solid #aaa; } +.nav_entries { + display: none; +} + +.loading { + background: url("loader.gif") center center no-repeat; + font-size: 0; +} + /*** NOTIFICATION ***/ .notification { position: fixed; @@ -415,6 +496,33 @@ vertical-align: middle; } +.logs { + border: 1px solid #aaa; +} + .logs .log { + padding: 5px 2%; + overflow: auto; + background: #fafafa; + border-bottom: 1px solid #999; + color: #333; + font-size: 90%; + } + .logs .log .date { + display: block; + } + .logs .log.error { + background: #fdd; + color: #844; + } + .logs .log.warning { + background: #ffe; + color: #c95; + } + .logs .log.notice { + background: #f4f4f4; + color: #aaa; + } + @media(max-width: 840px) { .header, .aside .btn-important, @@ -425,9 +533,20 @@ display: none; } .flux_header .item.website { - width: 30px; + width: 40px; text-align: center; } + .flux_header .item.website .favicon { + padding: 12px; + } + + .content { + font-size: 120%; + } + + .pagination { + margin: 0 0 40px; + } .pagination .pager-previous, .pagination .pager-next { width: 100px; } @@ -440,26 +559,53 @@ top: 0; left: 0; width: 0; overflow: hidden; + border-right: none; z-index: 10; transition: width 200ms linear; } .aside:target { width: 80%; + border-right: 1px solid #aaa; overflow: auto; } .aside .toggle_aside { position: absolute; right: 0; display: inline-block; - width: 20px; - height: 20px; + width: 26px; + height: 26px; margin: 0 10px 0 0; border: 1px solid #ccc; - border-radius: 10px; + border-radius: 20px; text-align: center; - line-height: 20px; + line-height: 26px; } .aside .categories { margin: 30px 0; } + + .nav_entries { + display: table; + width: 100%; + height: 40px; + position: fixed; + bottom: 0; + margin: 0; + background: #fff; + border-top: 1px solid #ddd; + text-align: center; + line-height: 40px; + table-layout: fixed; + } + .nav_entries .item { + display: table-cell; + width: 30%; + } + .nav_entries .item a { + display: block; + } + .nav_entries .item .icon.i_up { + margin: 5px 0 0; + vertical-align: top; + } } diff --git a/public/theme/global.css b/public/theme/global.css index 99b335585..b3d80e52a 100644 --- a/public/theme/global.css +++ b/public/theme/global.css @@ -50,6 +50,11 @@ img { border: none; } +/* VIDEOS */ +iframe, embed, object { + max-width: 100%; +} + /* FORMULAIRES */ legend { display: block; @@ -83,8 +88,8 @@ input, select, textarea { } input[type="radio"], input[type="checkbox"] { - width: 15px; - min-height: 15px; + width: 15px !important; + min-height: 15px !important; } input:focus, select:focus, textarea:focus { color: #0062BE; @@ -183,6 +188,7 @@ input, select, textarea { line-height: 20px; vertical-align: middle; cursor: pointer; + overflow: hidden; } a.btn { min-height: 25px; @@ -261,6 +267,13 @@ input, select, textarea { .nav.nav-list a:hover { text-decoration: none; } + .nav.nav-list .item.error a { + color: #BD362F; + } + .nav.nav-list .item.active.error a { + color: #fff; + background: #BD362F; + } .nav.nav-list .nav-header { padding: 0 10px; @@ -384,15 +397,19 @@ input, select, textarea { display: inline-block; position: absolute; top: -16px; right: -16px; - width: 16px; - height: 16px; - padding: 5px; + width: 26px; + height: 26px; background: #fff; border-radius: 50px; border: 1px solid #ddd; - line-height: 16px; + line-height: 26px; text-align: center; } + .dropdown .dropdown-close a { + display: block; + width: 100%; + height: 100%; + } .dropdown .dropdown-close:hover { background: #f4f4f4; } @@ -501,6 +518,14 @@ input, select, textarea { background-image: url("icons/up.png"); background-image: url("icons/up.svg"); } + .icon.i_next { + background-image: url("icons/next.png"); + background-image: url("icons/next.svg"); + } + .icon.i_prev { + background-image: url("icons/previous.png"); + background-image: url("icons/previous.svg"); + } .icon.i_help { background-image: url("icons/help.png"); background-image: url("icons/help.svg"); diff --git a/public/theme/icons/next.png b/public/theme/icons/next.png Binary files differnew file mode 100644 index 000000000..ab3490c3b --- /dev/null +++ b/public/theme/icons/next.png diff --git a/public/theme/icons/next.svg b/public/theme/icons/next.svg new file mode 100644 index 000000000..72637b4e6 --- /dev/null +++ b/public/theme/icons/next.svg @@ -0,0 +1,31 @@ +<?xml version='1.0' encoding='UTF-8' standalone='no'?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg xmlns:cc='http://creativecommons.org/ns#' xmlns:dc='http://purl.org/dc/elements/1.1/' sodipodi:docname='go-next-symbolic.svg' height='16' id='svg7384' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:svg='http://www.w3.org/2000/svg' inkscape:version='0.48.4 r9939' version='1.1' width='16' xmlns='http://www.w3.org/2000/svg'> + <metadata id='metadata90'> + <rdf:RDF> + <cc:Work rdf:about=''> + <dc:format>image/svg+xml</dc:format> + <dc:type rdf:resource='http://purl.org/dc/dcmitype/StillImage'/> + <dc:title>Gnome Symbolic Icon Theme</dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <sodipodi:namedview inkscape:bbox-nodes='false' inkscape:bbox-paths='true' bordercolor='#666666' borderopacity='1' inkscape:current-layer='layer12' inkscape:cx='78.648774' inkscape:cy='9.99302' gridtolerance='10' inkscape:guide-bbox='true' guidetolerance='10' id='namedview88' inkscape:object-nodes='false' inkscape:object-paths='false' objecttolerance='10' pagecolor='#3a3b39' inkscape:pageopacity='1' inkscape:pageshadow='2' showborder='false' showgrid='false' showguides='true' inkscape:snap-bbox='true' inkscape:snap-bbox-midpoints='false' inkscape:snap-global='true' inkscape:snap-grids='true' inkscape:snap-nodes='false' inkscape:snap-others='false' inkscape:snap-to-guides='true' inkscape:window-height='1408' inkscape:window-maximized='1' inkscape:window-width='2560' inkscape:window-x='0' inkscape:window-y='0' inkscape:zoom='1'> + <inkscape:grid empspacing='2' enabled='true' id='grid4866' originx='120px' originy='530px' snapvisiblegridlinesonly='true' spacingx='1px' spacingy='1px' type='xygrid' visible='true'/> + </sodipodi:namedview> + <title id='title9167'>Gnome Symbolic Icon Theme</title> + <defs id='defs7386'/> + <g inkscape:groupmode='layer' id='layer9' inkscape:label='status' style='display:inline' transform='translate(-121.0002,-747)'/> + <g inkscape:groupmode='layer' id='layer10' inkscape:label='devices' transform='translate(-121.0002,-747)'/> + <g inkscape:groupmode='layer' id='layer11' inkscape:label='apps' transform='translate(-121.0002,-747)'/> + <g inkscape:groupmode='layer' id='layer13' inkscape:label='places' transform='translate(-121.0002,-747)'/> + <g inkscape:groupmode='layer' id='layer14' inkscape:label='mimetypes' transform='translate(-121.0002,-747)'/> + <g inkscape:groupmode='layer' id='layer15' inkscape:label='emblems' style='display:inline' transform='translate(-121.0002,-747)'/> + <g inkscape:groupmode='layer' id='g71291' inkscape:label='emotes' style='display:inline' transform='translate(-121.0002,-747)'/> + <g inkscape:groupmode='layer' id='g4953' inkscape:label='categories' style='display:inline' transform='translate(-121.0002,-747)'/> + <g inkscape:groupmode='layer' id='layer12' inkscape:label='actions' style='display:inline' transform='translate(-121.0002,-747)'> + + <path inkscape:connector-curvature='0' d='m 125.0004,749 1,0 c 0.0104,-1.2e-4 0.0208,-4.6e-4 0.0313,0 0.25495,0.0112 0.50987,0.12858 0.6875,0.3125 l 6.29767,5.71875 -6.29772,5.71875 c -0.18816,0.18819 -0.45346,0.28125 -0.71875,0.28125 l -1,0 0,-1 c 0,-0.26529 0.0931,-0.53058 0.28125,-0.71875 l 4.82897,-4.28125 -4.82897,-4.28125 c -0.21074,-0.19463 -0.30316,-0.46925 -0.28125,-0.75 z' id='path10839-9-9-5-9' sodipodi:nodetypes='ccsccccccccccc' style='font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;color:#bebebe;fill:#bebebe;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.78124988;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new;font-family:Andale Mono;-inkscape-font-specification:Andale Mono'/> + </g> +</svg> diff --git a/public/theme/icons/previous.png b/public/theme/icons/previous.png Binary files differnew file mode 100644 index 000000000..10e40669e --- /dev/null +++ b/public/theme/icons/previous.png diff --git a/public/theme/icons/previous.svg b/public/theme/icons/previous.svg new file mode 100644 index 000000000..67685c50c --- /dev/null +++ b/public/theme/icons/previous.svg @@ -0,0 +1,31 @@ +<?xml version='1.0' encoding='UTF-8' standalone='no'?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg xmlns:cc='http://creativecommons.org/ns#' xmlns:dc='http://purl.org/dc/elements/1.1/' sodipodi:docname='go-next-rtl-symbolic.svg' height='16' id='svg7384' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:svg='http://www.w3.org/2000/svg' inkscape:version='0.48.4 r9939' version='1.1' width='16' xmlns='http://www.w3.org/2000/svg'> + <metadata id='metadata90'> + <rdf:RDF> + <cc:Work rdf:about=''> + <dc:format>image/svg+xml</dc:format> + <dc:type rdf:resource='http://purl.org/dc/dcmitype/StillImage'/> + <dc:title>Gnome Symbolic Icon Theme</dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <sodipodi:namedview inkscape:bbox-nodes='false' inkscape:bbox-paths='true' bordercolor='#666666' borderopacity='1' inkscape:current-layer='layer12' inkscape:cx='-101.35123' inkscape:cy='9.99302' gridtolerance='10' inkscape:guide-bbox='true' guidetolerance='10' id='namedview88' inkscape:object-nodes='false' inkscape:object-paths='false' objecttolerance='10' pagecolor='#3a3b39' inkscape:pageopacity='1' inkscape:pageshadow='2' showborder='false' showgrid='false' showguides='true' inkscape:snap-bbox='true' inkscape:snap-bbox-midpoints='false' inkscape:snap-global='true' inkscape:snap-grids='true' inkscape:snap-nodes='false' inkscape:snap-others='false' inkscape:snap-to-guides='true' inkscape:window-height='1408' inkscape:window-maximized='1' inkscape:window-width='2560' inkscape:window-x='0' inkscape:window-y='0' inkscape:zoom='1'> + <inkscape:grid empspacing='2' enabled='true' id='grid4866' originx='-60px' originy='530px' snapvisiblegridlinesonly='true' spacingx='1px' spacingy='1px' type='xygrid' visible='true'/> + </sodipodi:namedview> + <title id='title9167'>Gnome Symbolic Icon Theme</title> + <defs id='defs7386'/> + <g inkscape:groupmode='layer' id='layer9' inkscape:label='status' style='display:inline' transform='translate(-301.0002,-747)'/> + <g inkscape:groupmode='layer' id='layer10' inkscape:label='devices' transform='translate(-301.0002,-747)'/> + <g inkscape:groupmode='layer' id='layer11' inkscape:label='apps' transform='translate(-301.0002,-747)'/> + <g inkscape:groupmode='layer' id='layer13' inkscape:label='places' transform='translate(-301.0002,-747)'/> + <g inkscape:groupmode='layer' id='layer14' inkscape:label='mimetypes' transform='translate(-301.0002,-747)'/> + <g inkscape:groupmode='layer' id='layer15' inkscape:label='emblems' style='display:inline' transform='translate(-301.0002,-747)'/> + <g inkscape:groupmode='layer' id='g71291' inkscape:label='emotes' style='display:inline' transform='translate(-301.0002,-747)'/> + <g inkscape:groupmode='layer' id='g4953' inkscape:label='categories' style='display:inline' transform='translate(-301.0002,-747)'/> + <g inkscape:groupmode='layer' id='layer12' inkscape:label='actions' style='display:inline' transform='translate(-301.0002,-747)'> + + <path inkscape:connector-curvature='0' d='m 313.01372,749 -1,0 c -0.0104,-1.2e-4 -0.0208,-4.6e-4 -0.0313,0 -0.25495,0.0112 -0.50987,0.12858 -0.6875,0.3125 l -6.29767,5.71875 6.29772,5.71875 c 0.18816,0.18819 0.45346,0.28125 0.71875,0.28125 l 1,0 0,-1 c 0,-0.26529 -0.0931,-0.53058 -0.28125,-0.71875 l -4.82897,-4.28125 4.82897,-4.28125 c 0.21074,-0.19463 0.30316,-0.46925 0.28125,-0.75 z' id='path5441' sodipodi:nodetypes='ccsccccccccccc' style='font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;color:#bebebe;fill:#bebebe;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.78124988;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new;font-family:Andale Mono;-inkscape-font-specification:Andale Mono'/> + </g> +</svg> diff --git a/public/theme/loader.gif b/public/theme/loader.gif Binary files differnew file mode 100644 index 000000000..5ff26f0e3 --- /dev/null +++ b/public/theme/loader.gif |
