Add favicon support

This commit is contained in:
Frédéric Guillot 2014-12-24 17:54:27 -05:00
parent 7d4d4e0193
commit c1d74b8332
28 changed files with 176 additions and 61 deletions

View File

@ -662,6 +662,10 @@ a.icon:hover {
padding-bottom: 10px;
}
.items article .favicon {
height: 16px;
}
/* other pages */
section li {
margin-left: 15px;

View File

@ -42,12 +42,14 @@ Router\get_action('bookmarks', function() {
$offset = Request\int_param('offset', 0);
$nb_items = Model\Item\count_bookmarks();
$items = Model\Item\get_bookmarks($offset, Model\Config\get('items_per_page'));
Response\html(Template\layout('bookmarks', array(
'favicons' => Model\Feed\get_item_favicons($items),
'order' => '',
'direction' => '',
'display_mode' => Model\Config\get('items_display_mode'),
'items' => Model\Item\get_bookmarks($offset, Model\Config\get('items_per_page')),
'items' => $items,
'nb_items' => $nb_items,
'offset' => $offset,
'items_per_page' => Model\Config\get('items_per_page'),

View File

@ -134,7 +134,7 @@ Router\get_action('config', function() {
// Update preferences
Router\post_action('config', function() {
$values = Request\values() + array('nocontent' => 0, 'image_proxy' => 0);
$values = Request\values() + array('nocontent' => 0, 'image_proxy' => 0, 'favicons' => 0);
Model\Config\check_csrf_values($values);
list($valid, $errors) = Model\Config\validate_modification($values);

View File

@ -11,15 +11,17 @@ Router\get_action('history', function() {
$offset = Request\int_param('offset', 0);
$nb_items = Model\Item\count_by_status('read');
Response\html(Template\layout('history', array(
'items' => Model\Item\get_all(
$items = Model\Item\get_all(
'read',
$offset,
Model\Config\get('items_per_page'),
'updated',
Model\Config\get('items_sorting_direction')
),
);
Response\html(Template\layout('history', array(
'favicons' => Model\Feed\get_item_favicons($items),
'items' => $items,
'order' => '',
'direction' => '',
'display_mode' => Model\Config\get('items_display_mode'),

View File

@ -25,6 +25,7 @@ Router\get_action('unread', function() {
}
Response\html(Template\layout('unread_items', array(
'favicons' => Model\Feed\get_item_favicons($items),
'order' => $order,
'direction' => $direction,
'display_mode' => Model\Config\get('items_display_mode'),
@ -88,6 +89,7 @@ Router\get_action('feed-items', function() {
$items = Model\Item\get_all_by_feed($feed_id, $offset, Model\Config\get('items_per_page'), $order, $direction);
Response\html(Template\layout('feed_items', array(
'favicons' => Model\Feed\get_favicons(array($feed['id'])),
'order' => $order,
'direction' => $direction,
'display_mode' => Model\Config\get('items_display_mode'),

View File

@ -2,6 +2,15 @@
namespace Helper;
function favicon(array $favicons, $feed_id)
{
if (isset($favicons[$feed_id])) {
return '<img src="'.$favicons[$feed_id].'" class="favicon"/>';
}
return '';
}
function isRTL(array $item)
{
return ! empty($item['rtl']) || \PicoFeed\Parser\Parser::isLanguageRTL($item['language']);

View File

@ -232,4 +232,5 @@ return array(
// 'Application' => '',
// 'Enable image proxy' => '',
// 'Avoid mixed content warnings with HTTPS' => '',
// 'Download favicons' => '',
);

View File

@ -232,4 +232,5 @@ return array(
// 'Application' => '',
// 'Enable image proxy' => '',
// 'Avoid mixed content warnings with HTTPS' => '',
// 'Download favicons' => '',
);

View File

@ -232,4 +232,5 @@ return array(
// 'Application' => '',
// 'Enable image proxy' => '',
// 'Avoid mixed content warnings with HTTPS' => '',
// 'Download favicons' => '',
);

View File

@ -232,4 +232,5 @@ return array(
'Application' => 'Application',
'Enable image proxy' => 'Activer le proxy pour les images',
'Avoid mixed content warnings with HTTPS' => 'Évite les alertes du navigateur web en HTTPS',
'Download favicons' => 'Télécharger les icônes des sites web',
);

View File

@ -232,4 +232,5 @@ return array(
// 'Application' => '',
// 'Enable image proxy' => '',
// 'Avoid mixed content warnings with HTTPS' => '',
// 'Download favicons' => '',
);

View File

@ -232,4 +232,5 @@ return array(
// 'Application' => '',
// 'Enable image proxy' => '',
// 'Avoid mixed content warnings with HTTPS' => '',
// 'Download favicons' => '',
);

View File

@ -232,4 +232,5 @@ return array(
// 'Application' => '',
// 'Enable image proxy' => '',
// 'Avoid mixed content warnings with HTTPS' => '',
// 'Download favicons' => '',
);

View File

@ -285,34 +285,13 @@ function get($name)
// Get all config parameters
function get_all()
{
return Database::get('db')
$config = Database::get('db')
->table('config')
->columns(
'username',
'language',
'timezone',
'autoflush',
'autoflush_unread',
'nocontent',
'items_per_page',
'theme',
'api_token',
'feed_token',
'fever_token',
'bookmarklet_token',
'items_sorting_direction',
'items_display_mode',
'redirect_nothing_to_read',
'auto_update_url',
'pinboard_enabled',
'pinboard_token',
'pinboard_tags',
'instapaper_enabled',
'instapaper_username',
'instapaper_password',
'image_proxy'
)
->findOne();
unset($config['password']);
return $config;
}
// Validation for edit action

View File

@ -2,18 +2,72 @@
namespace Model\Feed;
use Model\Config;
use Model\Item;
use SimpleValidator\Validator;
use SimpleValidator\Validators;
use PicoDb\Database;
use Model\Config;
use Model\Item;
use PicoFeed\Serialization\Export;
use PicoFeed\Serialization\Import;
use PicoFeed\Reader\Reader;
use PicoFeed\Reader\Favicon;
use PicoFeed\PicoFeedException;
const LIMIT_ALL = -1;
// Download and store the favicon
function fetch_favicon($feed_id, $site_url)
{
$favicon = new Favicon;
return Database::get('db')
->table('favicons')
->save(array(
'feed_id' => $feed_id,
'link' => $favicon->find($site_url),
'icon' => $favicon->getDataUri(),
));
}
// Refresh favicon
function refresh_favicon($feed_id, $site_url)
{
if (Config\get('favicons') == 1 && ! has_favicon($feed_id)) {
fetch_favicon($feed_id, $site_url);
}
}
// Return true if the feed have a favicon
function has_favicon($feed_id)
{
return Database::get('db')->table('favicons')->eq('feed_id', $feed_id)->count() === 1;
}
// Get favicons for those feeds
function get_favicons(array $feed_ids)
{
if (Config\get('favicons') == 0) {
return array();
}
return Database::get('db')
->table('favicons')
->in('feed_id', $feed_ids)
->listing('feed_id', 'icon');
}
// Get all favicons for a list of items
function get_item_favicons(array $items)
{
$feed_ids = array();
foreach ($items as $item) {
$feeds_ids[] = $item['feed_id'];
}
return get_favicons($feed_ids);
}
// Update feed information
function update(array $values)
{
@ -117,6 +171,8 @@ function create($url, $enable_grabber = false, $force_rtl = false)
$feed_id = $db->getConnection()->getLastId();
Item\update_all($feed_id, $feed->getItems());
refresh_favicon($feed_id, $feed->getSiteUrl());
Config\write_debug();
return (int) $feed_id;
@ -187,6 +243,7 @@ function refresh($feed_id)
update_cache($feed_id, $resource->getLastModified(), $resource->getEtag());
Item\update_all($feed_id, $feed->getItems());
refresh_favicon($feed_id, $feed->getSiteUrl());
}
update_parsing_error($feed_id, 0);

View File

@ -5,7 +5,21 @@ namespace Schema;
use PDO;
use Model\Config;
const VERSION = 33;
const VERSION = 34;
function version_34($pdo)
{
$pdo->exec('ALTER TABLE config ADD COLUMN favicons INTEGER DEFAULT 0');
$pdo->exec(
'CREATE TABLE favicons (
feed_id INTEGER UNIQUE,
link TEXT,
icon TEXT,
FOREIGN KEY(feed_id) REFERENCES feeds(id) ON DELETE CASCADE
)'
);
}
function version_33($pdo)
{

View File

@ -12,7 +12,14 @@
<section class="items" id="listing">
<?php foreach ($items as $item): ?>
<?= \PicoFarad\Template\load('item', array('item' => $item, 'menu' => $menu, 'offset' => $offset, 'hide' => false, 'display_mode' => $display_mode)) ?>
<?= \PicoFarad\Template\load('item', array(
'item' => $item,
'menu' => $menu,
'offset' => $offset,
'hide' => false,
'display_mode' => $display_mode,
'favicons' => $favicons,
)) ?>
<?php endforeach ?>
<?= \PicoFarad\Template\load('paging', array('menu' => $menu, 'nb_items' => $nb_items, 'items_per_page' => $items_per_page, 'offset' => $offset, 'order' => $order, 'direction' => $direction)) ?>

View File

@ -12,7 +12,6 @@
<form method="post" action="?action=config" autocomplete="off">
<h3><?= t('Authentication') ?></h3>
<?= Helper\form_hidden('csrf', $values) ?>
<?= Helper\form_label(t('Username'), 'username') ?>
<?= Helper\form_text('username', $values, $errors, array('required')) ?><br/>
@ -62,6 +61,8 @@
<?= Helper\form_checkbox('nocontent', t('Do not fetch the content of articles'), 1, isset($values['nocontent']) && $values['nocontent'] == 1) ?><br />
<?= Helper\form_checkbox('favicons', t('Download favicons'), 1, isset($values['favicons']) && $values['favicons'] == 1) ?><br />
<div class="form-actions">
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
</div>

View File

@ -28,6 +28,7 @@
'offset' => $offset,
'hide' => false,
'display_mode' => $display_mode,
'favicons' => $favicons,
)) ?>
<?php endforeach ?>

View File

@ -15,7 +15,14 @@
<section class="items" id="listing">
<?php foreach ($items as $item): ?>
<?= \PicoFarad\Template\load('item', array('item' => $item, 'menu' => $menu, 'offset' => $offset, 'hide' => true, 'display_mode' => $display_mode)) ?>
<?= \PicoFarad\Template\load('item', array(
'item' => $item,
'menu' => $menu,
'offset' => $offset,
'hide' => true,
'display_mode' => $display_mode,
'favicons' => $favicons,
)) ?>
<?php endforeach ?>
<?= \PicoFarad\Template\load('paging', array('menu' => $menu, 'nb_items' => $nb_items, 'items_per_page' => $items_per_page, 'offset' => $offset, 'order' => $order, 'direction' => $direction)) ?>

View File

@ -10,6 +10,7 @@
<h2 <?= Helper\isRTL($item) ? 'dir="rtl"' : '' ?>>
<span class="bookmark-icon"></span>
<span class="read-icon"></span>
<?= Helper\favicon($favicons, $item['feed_id']) ?>
<a
href="?action=show&amp;menu=<?= $menu ?>&amp;id=<?= $item['id'] ?>"
class="show"

View File

@ -44,13 +44,6 @@
data-action="bookmark"
></a>
</li>
<li>
<a
href="?action=mark-item-unread&amp;id=<?= $item['id'] ?>&amp;redirect=<?= $menu ?>&amp;feed_id=<?= $item['feed_id'] ?>"
title="<?= t('mark as unread') ?>"
class="read-icon icon"
></a>
</li>
<li>
<a href="?action=feed-items&amp;feed_id=<?= $feed['id'] ?>"><?= Helper\escape($feed['title']) ?></a>
</li>

View File

@ -22,6 +22,7 @@
'offset' => $offset,
'hide' => true,
'display_mode' => $display_mode,
'favicons' => $favicons,
)) ?>
<?php endforeach ?>

2
vendor/autoload.php vendored
View File

@ -4,4 +4,4 @@
require_once __DIR__ . '/composer' . '/autoload_real.php';
return ComposerAutoloaderInit2e47cecea56754de3b457018a8eee985::getLoader();
return ComposerAutoloaderInita5bf9ee28a13532106d0068a171b06ab::getLoader();

View File

@ -2,7 +2,7 @@
// autoload_real.php @generated by Composer
class ComposerAutoloaderInit2e47cecea56754de3b457018a8eee985
class ComposerAutoloaderInita5bf9ee28a13532106d0068a171b06ab
{
private static $loader;
@ -19,9 +19,9 @@ class ComposerAutoloaderInit2e47cecea56754de3b457018a8eee985
return self::$loader;
}
spl_autoload_register(array('ComposerAutoloaderInit2e47cecea56754de3b457018a8eee985', 'loadClassLoader'), true, true);
spl_autoload_register(array('ComposerAutoloaderInita5bf9ee28a13532106d0068a171b06ab', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInit2e47cecea56754de3b457018a8eee985', 'loadClassLoader'));
spl_autoload_unregister(array('ComposerAutoloaderInita5bf9ee28a13532106d0068a171b06ab', 'loadClassLoader'));
$map = require __DIR__ . '/autoload_namespaces.php';
foreach ($map as $namespace => $path) {
@ -42,14 +42,14 @@ class ComposerAutoloaderInit2e47cecea56754de3b457018a8eee985
$includeFiles = require __DIR__ . '/autoload_files.php';
foreach ($includeFiles as $file) {
composerRequire2e47cecea56754de3b457018a8eee985($file);
composerRequirea5bf9ee28a13532106d0068a171b06ab($file);
}
return $loader;
}
}
function composerRequire2e47cecea56754de3b457018a8eee985($file)
function composerRequirea5bf9ee28a13532106d0068a171b06ab($file)
{
require $file;
}

View File

@ -162,18 +162,18 @@
"source": {
"type": "git",
"url": "https://github.com/fguillot/picoFeed.git",
"reference": "b3f2202845be5895ce818f1393cdd28b0aa1b8cb"
"reference": "e785e62ee79a02478e9691cc0cc50e689f2bf4a4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/fguillot/picoFeed/zipball/b3f2202845be5895ce818f1393cdd28b0aa1b8cb",
"reference": "b3f2202845be5895ce818f1393cdd28b0aa1b8cb",
"url": "https://api.github.com/repos/fguillot/picoFeed/zipball/e785e62ee79a02478e9691cc0cc50e689f2bf4a4",
"reference": "e785e62ee79a02478e9691cc0cc50e689f2bf4a4",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"time": "2014-12-24 20:46:58",
"time": "2014-12-24 22:35:22",
"type": "library",
"installation-source": "dist",
"autoload": {

View File

@ -20,6 +20,20 @@ use PicoFeed\Parser\XmlParser;
*/
class Favicon
{
/**
* Valid types for favicon (supported by browsers)
*
* @access private
* @var array
*/
private $types = array(
'image/png',
'image/gif',
'image/x-icon',
'image/jpeg',
'image/jpg',
);
/**
* Config class instance
*
@ -74,7 +88,13 @@ class Favicon
*/
public function getType()
{
return $this->content_type;
foreach ($this->types as $type) {
if (strpos($this->content_type, $type) === 0) {
return $type;
}
}
return 'image/x-icon';
}
/**
@ -87,7 +107,7 @@ class Favicon
{
return sprintf(
'data:%s;base64,%s',
$this->content_type,
$this->getType(),
base64_encode($this->content)
);
}

View File

@ -155,4 +155,12 @@ class FaviconTest extends PHPUnit_Framework_TestCase
$this->assertEquals($expected, $favicon->getDataUri());
}
public function testDataUriWithBadContentType()
{
$favicon = new Favicon;
$this->assertNotEmpty($favicon->find('http://www.lemonde.fr/'));
$expected = 'data:image/x-icon;base64,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';
$this->assertEquals($expected, $favicon->getDataUri());
}
}