July
1st 2011
Building an endless image/photo/screenshot stream

Posted under JavaScript & jQuery & Snippets & WoW-Screenshots

For WoW-Screenshots.net I was looking for a sweet Endless Stream script but I only stumbled into some weird scripts that did more then they should or nothing at all. Finally I ran into a simple script giving some good hints.

For this script jQuery is required and the str_replace port from phpjs.org.

The basics of the script is are simple: on the window scroll event check if the user scrolled nearly or to the bottom of the page if so fetch information from the server and add them to the page.

First off we start with the class constructor

1
2
3
function endlessEntityStream(conf) {
    var self = this;
}

We add a few configuration options and match them against the conf object:

1
2
3
4
5
6
7
self.conf = {};
self.conf.loading = false;
self.conf.identifier = conf.identifier || 'EES_%id%';
self.conf.url = conf.url;
self.conf.count = conf.count;
self.conf.prevId = conf.initalId;
self.conf.template = conf.template;

We load the first batch upon constructing the class. Since the backend provides us with information whether or not we should continue checking for more:

1
self.load(conf.startCount);

We hook into the window scroll event. This and the previous 2 blocks are code are all in the constructor:

1
2
3
$(window).scroll(function(){
    self.check();
});

Now we need 2 settings and 2 getters to keep the code a bit cleaner

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
endlessEntityStream.prototype.setLoading = function(bool) {
    var self = this;
    self.conf.loading = bool ? true : false;
};

endlessEntityStream.prototype.getLoading = function() {
    var self = this;
    return self.conf.loading;
};

endlessEntityStream.prototype.setPrevId = function(prevId) {
    var self = this;
    self.conf.prevId = prevId;
};

endlessEntityStream.prototype.getPrevId = function() {
    var self = this;
    return self.conf.prevId;
};

The following functions are the most interesting. First the check function that checks the scroll status. Seccond the load function that does most of the heavy lifting.

The function works very simple. It check if you either scrolled to the end of the page or within the 500 pixels above the end of the page. Then it checks if a load is already in progress and load if not. Note the 500 in the function, change that to your needs if the default isn’t working properly.

1
2
3
4
5
6
7
8
endlessEntityStream.prototype.check = function() {
    var self = this;
    if($(window).scrollTop() == $(document).height() - $(window).height() || ($(window).scrollTop() + 500) >= $(document).height() - $(window).height()) {
        if(!self.getLoading()) {
            self.load(self.count);
        }
    }
};

We start the load function by preventing another load can start. Another imported part of this method is the replacing of 3 keywords with the corresponding information.

  • %id% – the id of the last added item, the backend uses this the determine where to start reading
  • %count% – the amount of items the backend should return
  • %mathrnd% – a random number, it can be used to force updates
1
2
3
4
5
6
7
8
endlessEntityStream.prototype.load = function(count) {
    var self = this;
    self.setLoading(true);
    url = self.conf.url;
    url = url.replace('%id%',self.conf.prevId);
    url = url.replace('%count%',self.conf.count);
    url = url.replace('%mathrnd%',Math.random().toString().replace('0.',''));
};

Now we know where we should fetch the information from we can send the request and parse the returned data:

1
2
3
4
5
6
7
$.get(url,
    function(data){
        if (data != "") {
            var entities = $.parseJSON(data);
        }
    }
);

After the data has been parsed we’ll walk through the data and add the item at the end of the stream. This happens in several steps. First off an identifier is generated for the current item and the identifier (based on the previous id) from the previous item is generated. Second the entity properties are matched against the template. And finally the item is added to the end of the stream and the current id is set as previous id.

1
2
3
4
5
6
7
8
9
10
11
for(var entity in entities.shots) {
    var identifier = self.conf.identifier.replace('%id%', entities.shots[entity].id);
    var previousIdentifier = self.conf.identifier.replace('%id%', self.getPrevId());
    var html = self.conf.template;
    $.each(entities.shots[entity], function(index,value) {
        html = str_replace('%' + index + '%', value , html);
    });
    html = html.replace('%identifier%', identifier);
    $('#' + previousIdentifier).after(html);
    self.setPrevId(entities.shots[entity].id);
}

Once everything has been loaded we’ll check the data if this is the last block or not. If it’s the last block it keeps the loading process locked. If it isn’t the last block it unlocks the loading process and invokes the check method again. This makes sure a user keeps buffered at all times until there is nothing more to fetch.

1
2
3
4
5
6
if(entities.lastShot) {
    self.setLoading(true);
} else {
    self.setLoading(false);
    self.check();
}

We now have all the class code ready to use we need to tell it where to get it’s data from and how to append it where. The following configuration options are needed to make it all work:

url – Url

1
http://domain.tld/path/to/stream/%id%/%count%/

target – The stream container

1
ESSContainer

initalId – The id of the last item in the preloaded stream.

1
123

count – The number of items to be returned by the server. %count% in the url parameter is replaced by this number.

1
25

template – Template for the item placed at the end of the stream.

1
<a href="%href%" id="%identifier%" title="%title%"><img src="%src%" alt="%title%" /></a>

debug – The debug option is optional but can be useful to debug issues with this script.

1
true

Combined that gives you this:

1
2
3
4
5
6
7
ees = new endlessEntityStream({
    url: 'http://domain.tld/path/to/stream/%id%/%count%/',    
    target: 'ESSContainer',
    initalId: 123,
    count: 5,
    template: '<a href="%href%" id="%identifier%" title="%title%"><img src="%src%" alt="%title%" /></a>'
});
1
2
3
4
5
<div id="ESSContainer">
    <a id="EES_121" href="/model/view/121.html" title=""><img src="/img/121.jpg" alt="" /></a>
    <a id="EES_122" href="/model/view/121.html" title=""><img src="/img/122.jpg" alt="" /></a>
    <a id="EES_123" href="/model/view/121.html" title=""><img src="/img/123.jpg" alt="" /></a>
</div>

The script expects a JSON object in the following format:

1
2
3
4
5
6
7
8
9
10
11
12
{
    "lastShot":false,
    "count":5,
    "shots":
    [
        {"id":"124","href":"\/model\/view\/124.html","src":"\/img\/124.jpg","title":""},
        {"id":"125","href":"\/model\/view\/125.html","src":"\/img\/125.jpg","title":""},
        {"id":"126","href":"\/model\/view\/126.html","src":"\/img\/126.jpg","title":""},
        {"id":"127","href":"\/model\/view\/127.html","src":"\/img\/127.jpg","title":""},
        {"id":"128","href":"\/model\/view\/128.html","src":"\/img\/128.jpg","title":""}
    ]
}

As you can see the object holds 2 configuration settings and 1 array with all the items. (Heck while writing this I realize that shots as name for the array might not but the appropriate name for a more general class.) The count property optional. lastShot tells the script if the shots array contains the last item and should stop polling after this.

This code is available on github. As for the future of this project it will evolve for the upcoming time to become an even more powerful script. (Might turn it into a jQuery plugin.) For a live demo see WoW-Screenshots.net.

No Comments »

October
31st 2010
A styling guide for @predominant cakephp tags plugin article

Posted under Cakephp & Snippets

This post assumes you’ve completed reading Graham Weldon (@predominant) article on using the plugin, the plugin pages them self and in specific the the tag cloud helper page. The helper example sets a size attribute  on the li tag. A way to utilize this would be writing a piece of jQuery (or javascript in general) that would take it and apply some styling to it. This would mean a lot more resource usage compared to pure CSS.

The PHP

At the PHP side we need to change a few things currently this is the code:

1
2
3
echo $this->TagCloud->display($tags, array(
    'before' => '<li size="%size%" class="tag">',
    'after' => '</li>'));

We’ll be change that into this:

1
2
3
4
5
echo $this->TagCloud->display($tags, array(
    'before' => '<li class="fs%size% tag">',
    'after' => '</li>',
    'maxSize' => 50,
    'minSize' => 1));

As you can see a few things change. First off we also supply the min and maxSize options. The helper uses these and calculates a number in between those values, we’ll use those numbers for the CSS classes later on. A option that change is before as you can see %size% is now used for a class, this class will contain it’s font-size and any options you might want to add yourself.

Generating the CSS

If your lazy and just want a working example you can skip this bit and skip to The CSS. Since the example requires 50 css class and it takes alot of time to calculate the required values by hand I’ve a little script to do it for me/us.

1
2
3
4
5
6
7
8
9
10
11
<?php
$css = '';
$class = 'fs';
$start_size = 0.75;
$stop_size = 2.5;
$count = 50;
$precision = 4;
for($i=1;$i<=$count;$i++) {
        $css .= '.' . $class . $i . '{font-size:' . round((((($stop_size - $start_size) / ($count - 1)) * ($i - 1)) + $start_size),$precision) . "em;}\r\n";
}
echo $css;

The script has a few configuration values.

  • class – the prefix to the %size% value
  • start_size – the smallest possible tag size in em
  • stop_size – the biggest possible tag size in em
  • count – The number of classes and for now 50 :)
  • precision – the maximum of digest behind the dot

The CSS

1
2
3
.fs1{font-size:0.75em;}
...
.fs50{font-size:2.5em;}

(Click here for the complete list.)

The Result

1 Comment »

May
17th 2010
Flattr Cakephp helper

Posted under Cakephp & Snippets

EDIT: DerEuroMark wrote an excellent replacement for this helper.

After the Flattr MOD for phpBB3 release 2 days ago I wrote this simple flattr cakephp helper (basic cakephp helper knowledge is required) to aid cakephp developers integrate flattr quick and easy into their websites. The helper has only 1 function and is very simple in use.

Basic usage

1
2
3
4
<!--?php echo $flattr--->badge(array(
'uid' =&gt; 4080,
'tle' =&gt; 'test',
)); ?&gt;

As you can see this is the bare minimum in options you need to use to get it to work.

  • uid The Flattr User ID as found on the Flattr dashboard (in the example I used mine).
  • tle The title for the link to be submitted.

Since the helper supports the full range of options below are the other options:

  • dsc A description for the link.
  • cat The category for the link. This can be any of the following: text, images, video, audio, software, rest. The default if this option isn’t specified is text.
  • lng The language of the link. Any of the languages on this list and defaults to en_GB.
  • tags Any tags matching the link. This field must be an array!
  • url The URL of the link.
  • btn The badge to use. Currently the only option is compact but if not specified or set to something else it defaults to the standard larger badge

app/views/helpers/flattr.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<!--?php<br /--> class FlattrHelper extends Helper {
public $helpers = array('Javascript');
function badge($options=array()) {
App::import('Sanitize');
$vars = '';
$vars .= "var flattr_uid = '" . intval($options['uid']) . "';\r\n";
$vars .= "var flattr_tle = '" . $options['tle'] . "';\r\n";
if(!isset($options['dsc']))
{
$options['dsc'] = '';
}
$vars .= "var flattr_dsc = '" . $options['dsc'] . "';\r\n";
if(!isset($options['cat']))
{
$options['cat'] = 'text';
}
$vars .= "var flattr_cat = '" . $options['cat'] . "';\r\n";
if(!isset($options['lng']))
{
$options['lng'] = 'en_GB';
}
$vars .= "var flattr_lng = '" . $options['lng'] . "';\r\n";
if(isset($options['tags']) &amp;&amp; count($options['tags'])&gt;0)
{
array_walk($options['tags'],'Sanitize::paranoid');
$vars .= "var flattr_tag = '" . implode(', ',$options['tags']) . "';\r\n";
}
if(isset($options['url']) &amp;&amp; ((version_compare(phpversion(), '5.2.0', '&gt;=')  &amp;&amp; function_exists('filter_var')) ? filter_var($options['url'], FILTER_VALIDATE_URL) : true))
{
$vars .= "var flattr_url = '" . $options['url'] . "';\r\n";
}
if(isset($options['btn']) &amp;&amp; $options['btn']=='compact')
{
$vars .= "var flattr_btn = 'compact';\r\n";
}
$code = $this-&gt;Javascript-&gt;codeBlock($vars, array('inline' =&gt; true));
$code .= $this-&gt;Javascript-&gt;link('http://api.flattr.com/button/load.js', array('inline' =&gt; true));
return $code;
}
}

4 Comments »

April
28th 2010
Cakephp url shorten behavior (0x.tc, bit.ly, is.gd and u.nu)

Posted under Cakephp & Snippets

A while ago I started working on some twitter services and I needed an automatic urlshortner. This first started as a model that would shorten en url and save the result in a table for caching purposes. After a good chat with a friend (beeman) it turned into a behavior with the model only using the behavior to act like it did before. One of the reasons for a behavior was to create a simple interface for the developer without the need to poll the caching model but at the same time be able to use the cache model when the resulting short url wouldn’t be stored anywhere else (like an autmatic tweet). The model currently supports 4 different services (I want to add more in the future tho so any suggestions are welcome): 0x.tc, is.gd, bit.ly and u.nu.

Configuration

mode has 2 possible options ordered (default) or randomize. ordered checks the order in order while randomize uses shuffle to randomize the order.
Example:

1
'mode' => 'ordered',

fields is an array with fields that the behavior should send to the services to shorten the url (note that the value in the field MUST be an url). This option has no default value!
Example:

1
2
3
'fields' => array(
    'short'
),

order is an array with the services listed in the order to use. This option wil only be used is mode is set to ordered else if mode is set to randomize it will grab the default order array and overwrite what you place in order.
Example:

1
2
3
4
5
6
'order' => array(
                'zero_xtc',
                'bit_ly'
                'u_nu',
                'is_gd',
),

retries number of times it loops through the list of shortners before giving up. Default value is 5.
Example:

1
'retries' => 10,

shortners list with shortners and their options. All the shortners have an enable option. Shortners that require an API key also have key option to store the needed API key.
Example:

1
2
3
4
5
6
7
8
9
10
            'shortners' => array(
                'bit_ly' => array(
                    'enable' => true,
                    // Go to http://bit.ly/account/your_api_key to get yours
                    'key' => array(
                        'login' => 'YOUR_BITLY_API_LOGIN_HERE',
                        'key' => 'YOUR_BITLY_API_KEY_HERE'
                    ),
                ),
            ),

http_config is an array for the Cakephp HttpSocket class. An example an option is the time out, you can find all possible at the 1.2 API (or 1.3 ofcourse ;) ). The default configuration is shown in the example below.
Example:

1
2
3
            'http_config' => array(
                            'timeout' => 10
            ),

http_headers is an array for additional HTTP headers like the User-Agent. The default configuration is shown in the example below.
Example:

1
2
3
            'http_headers' => array(
                            'User-Agent' => 'Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.0.1) Gecko/2008070208 Firefox/3.0.1'
            ),

Files

Following are the mode and behavior. The model also shows the basic configuration for the behavior tell it what field in the database to shorten urls for. It also

app/models/shorturl.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
class Shorturl extends AppModel {
    public $actsAs = array(
        'Shorturl' => array(
            'fields' => array(
                'short'
            ),
        ),
    );
    public function shorten($long_url) {
        $url = $this->findByLong($long_url);
        if(!$url) {
            $data = array();
            $data['long'] = $long_url;
            $data['short'] = $long_url;
            $this->create();
            $this->save($data);
            $url = $this->findByLong($long_url);
        }
        return $url['Shorturl']['short'];
    }
}

The models database table (as a (My)SQL query)

1
2
3
4
5
6
CREATE TABLE IF NOT EXISTS `shorturls` (
  `id` INT(32) NOT NULL AUTO_INCREMENT,
  `long` VARCHAR(1024) NOT NULL,
  `short` VARCHAR(255) NOT NULL,
  PRIMARY KEY  (`id`)
) TYPE=MyISAM  AUTO_INCREMENT=1 ;

app/models/behaviors/shorturl.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
<?php
class ShorturlBehavior extends ModelBehavior {
    private $defaults = array(
            'fields' => array(),
            'shortners' => array(
                            'zero_xtc' => array(
                                            'enable' => true,
                            ),
                            'is_gd' => array(
                                            'enable' => true,
                            ),
                            'bit_ly' => array(
                                            'enable' => true,
                                            // Go to http://bit.ly/account/your_api_key to get yours
                                            'key' => array(
                                                            'login' => '',
                                                            'key' => ''
                                            ),
                            ),
                            'u_nu' => array(
                                            'enable' => true,
                            ),
            ),
            'order' => array(
                            'zero_xtc',
                            'is_gd',
                            'bit_ly',
                            'u_nu',
            ),
            'http_config' => array(
                            'timeout' => 10
            ),
            'http_headers' => array(
                            'User-Agent' => 'Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.0.1) Gecko/2008070208 Firefox/3.0.1'
            ),
            'retries' => 5,
            'mode' => 'ordered',
    );
    public $settings = array();
    public function setup(&$model,$config = array()) {
        $this->settings[$model->alias] = $this->defaults;
        $this->settings[$model->alias] = $this->array_merge_recursive_distinct($this->settings[$model->alias],(array) $config);
    }
    public function beforeSave(&$model) {
        if(isset($this->settings[$model->alias]['fields'])) {
            foreach($this->settings[$model->alias]['fields'] as $void => $field) {
                if(isset($model->data[$model->alias][$field]) && !empty($model->data[$model->alias][$field]) && ((version_compare(phpversion(), '5.2.0', '>=') && function_exists('filter_var')) ? filter_var($model->data[$model->alias][$field], FILTER_VALIDATE_URL) : true)) {
                    $model->data[$model->alias][$field] = $this->shorten($model,$model->data[$model->alias][$field]);
                }
            }
        }
        return true;
    }
    private function shorten(&$model,$long_url) {
        App::import('Core', 'Set');
        App::import('Core', 'Xml');
        App::import('Core', 'HttpSocket');
        if($this->settings[$model->alias]['mode']=='randomize') {
            $this->settings[$model->alias]['order'] = $this->defaults['order'];
            shuffle($this->settings[$model->alias]['order']);
        }
        for($j=0;$j<($this->settings[$model->alias]['retries'] + 1);$j++) {
            foreach($this->settings[$model->alias]['order'] as $shortner) {
                if($this->settings[$model->alias]['shortners'][$shortner]['enable']) {
                    $short_url = $this->{$shortner}($model,$long_url);
                    if($short_url && !empty($short_url) && !is_null($short_url) && ((version_compare(phpversion(), '5.2.0', '>=') && function_exists('filter_var')) ? filter_var($short_url, FILTER_VALIDATE_URL) : true)) {
                        return $short_url;
                    }
                }
            }
        }
        return $long_url;
    }

    private function zero_xtc(&$model,$long_url) {
        $this->Http = new HttpSocket($this->settings[$model->alias]['http_config']);
        $request = 'http://0x.tc/x?go=' . rawurlencode($long_url) . '&t=' . time();
        $response = $this->Http->get(
                $request,
                array(),
                array('header' => $this->settings[$model->alias]['http_headers'])
        );
        $response = Set::reverse(new Xml($response));
        if(is_array($response['Taken']['xUrl'])) {
            return false;
        }
        elseif(substr($response['Taken']['xUrl'], 0, 4) == 'http') {
            return $response['Taken']['xUrl'];
        }
        else {
            return false;
        }
    }

    private function is_gd(&$model,$long_url) {
        $this->Http = new HttpSocket($this->settings[$model->alias]['http_config']);
        $request = 'http://is.gd/api.php?longurl=' . urlencode($long_url);
        $response = $this->Http->get(
                $request,
                array(),
                array('header' => $this->settings[$model->alias]['http_headers'])
        );
        if (substr($request, 0, 4) == 'http') {
            return $response;
        }
        else {
            return false;
        }
    }

    private function bit_ly(&$model,$long_url) {
        if(isset($this->settings[$model->alias]['shortners']['bit_ly']['key']['login']) && !empty($this->settings[$model->alias]['shortners']['bit_ly']['key']['login']) && isset($this->settings[$model->alias]['shortners']['bit_ly']['key']['key']) && !empty($this->settings[$model->alias]['shortners']['bit_ly']['key']['key'])) {
            $this->Http = new HttpSocket($this->settings[$model->alias]['http_config']);
            $request = 'http://api.bit.ly/shorten?version=2.0.1&longUrl=' . urlencode($long_url) . '&login=' . $this->settings[$model->alias]['shortners']['bit_ly']['key']['login'] . '&apiKey=' . $this->settings[$model->alias]['shortners']['bit_ly']['key']['key'];
            $response = $this->Http->get(
                    $request,
                    array(),
                    array('header' => $this->settings[$model->alias]['http_headers'])
            );
            $response = json_decode($response);
            if ($response->errorCode==0 && $response->statusCode=='OK' && isset($response->results->{$long_url}->shortUrl)) {
                return $response->results->{$long_url}->shortUrl;
            }
            else {
                return false;
            }
        }
        else {
            return false;
        }
    }

    private function u_nu(&$model,$long_url) {
        $this->Http = new HttpSocket($this->settings[$model->alias]['http_config']);
        $request = 'http://u.nu/unu-api-simple?url=' . urlencode($long_url);
        $response = $this->Http->get(
                $request,
                array(),
                array('header' => $this->settings[$model->alias]['http_headers'])
        );
        if (substr($request, 0, 4) == 'http') {
            return $response;
        }
        else {
            return false;
        }
    }

    // Taken from: http://www.php.net/manual/en/function.array-merge-recursive.php#96201
    /**
     * Merges any number of arrays / parameters recursively, replacing
     * entries with string keys with values from latter arrays.
     * If the entry or the next value to be assigned is an array, then it
     * automagically treats both arguments as an array.
     * Numeric entries are appended, not replaced, but only if they are
     * unique
     *
     * calling: result = array_merge_recursive_distinct(a1, a2, ... aN)
     **/


    private function array_merge_recursive_distinct () {
        $arrays = func_get_args();
        $base = array_shift($arrays);
        if(!is_array($base)) $base = empty($base) ? array() : array($base);
        foreach($arrays as $append) {
            if(!is_array($append)) $append = array($append);
            foreach($append as $key => $value) {
                if(!array_key_exists($key, $base) && !is_numeric($key)) {
                    $base[$key] = $append[$key];
                    continue;
                }
                if(is_array($value) || (isset($base[$key]) && is_array($base[$key]))) {
                    $base[$key] = $this->array_merge_recursive_distinct($base[$key], $append[$key]);
                } else if(is_numeric($key)) {
                    if(!in_array($value, $base)) $base[] = $value;
                } else {
                    $base[$key] = $value;
                }
            }
        }
        return $base;
    }
}

2 Comments »