Simple jQuery slideshows

written by Andrey Khoronko, on Apr 9, 2009 1:31:00 PM.

Recently I’ve got a task to create simple JavaScript slideshow. I’m going to show you how I did 2 variants of slideshow: alternating and synchronous.

Markup

Let’s start with HTML markup:

<div class="slideshow_container">
	<p class="controls">
		<a class="button start_button" href="#" title="Start">Start</a>
		<a class="button stop_button" href="#" title="Stop">Stop</a>
		<span class="activity_indicator">[ Stopped ]</span>
	</p>
	<div class="slide">
		<img src="/uploads/simple-jquery-slideshows/default.gif" alt="slide1">
	</div>
	<div class="slide">
		<img src="/uploads/simple-jquery-slideshows/default.gif" alt="slide2">
	</div>
</div>

Then add some CSS:

.slideshow_container {
        clear: both;
}
.controls {
	width: 100%; height: 30px;
	padding: 0;
	margin: 5px;
}
.activity_indicator {
	padding: 5px;
	margin: 0;
	color: gray;
}
.button {
	padding: 5px;
	margin-right: 10px;
	background-color: black;
	color: white;
	text-decoration: none;
}
.slide {
	float: left;
	width: 200px; height: 200px;
	padding: 0;
	margin: 5px;
}

Alternating slideshow

Now let’s writing JS code. I used the jQuery framework.

For alternating (slide-by-slide) animation we need to track the readiness (slide is loaded and previous animation is finished) of the neighbour slide.

//our slides
var additional_slides_urls = ["/uploads/simple-jquery-slideshows/chinchilla.jpg",
	"/uploads/simple-jquery-slideshows/donkey.jpg",
	"/uploads/simple-jquery-slideshows/penguin.jpg",
	"/uploads/simple-jquery-slideshows/skunk.jpg",
	"/uploads/simple-jquery-slideshows/squirrel.jpg",
	"/uploads/simple-jquery-slideshows/walrus.jpg"];

var slideshow_speed = 3500; //slide changing speed, milliseconds
var animation_speed = 600; //fade in/out speed, milliseconds

var slideshow_container = $(".slideshow_container");
var slideshow_indicator = $("span", slideshow_container); //indicator will show activity status
var slide1_container = $("div:first", slideshow_container);
var slide2_container = $("div:last", slideshow_container);

var slides = [$("img", slide1_container), $("img", slide2_container)]; //init with default images 

//load slides
$.each(additional_slides_urls, function(index, url) {
	var img = $(new Image());
	img.load(function() {
		slides[index + 2] = img;
	}).attr("src", url);
});

var timer;

//start slideshow button 
$(".start_button", slideshow_container).click(function() {
	if (additional_slides_urls.length >= 2) { //if less than 2 additional (not default) slides, it does not make sense to start slideshow
		timer = setInterval(slides_update, slideshow_speed);
		slideshow_indicator.text("[ Running ]"); //change indicator text
	}
	return false; //prevent default click action
});

//stop slideshow button
$(".stop_button", slideshow_container).click(function() {
	clearInterval(timer);
	slideshow_indicator.text("[ Stopped ]");
	return false;
});

var slide_counter = 1; //remember current slide index
var slide_is_ready = true; //slide updating animation finished

function slides_update() {
	if (slide_is_ready) {
		slide_is_ready = false;
		slide_counter++;
		var slide;
		if (slide_counter == additional_slides_urls.length + 2) slide_counter = 0; //if we come to the end of the list - returns to the first slide
		slide = slides[slide_counter];
                
		var change_slides = setInterval(function() {
                        //is next slide loaded?
			if (slide) {
				clearInterval(change_slides); //next slide is ready
                                //slide updating animation start
				if (slide_counter % 2 == 1) animate(slide, slide2_container);
				else animate(slide, slide1_container);
			}
		}, 200);
	} 
        else setTimeout(slides_update, 200);
};

function animate(next_slide, container) {
	var slide_container = container;
	var current_slide = $("img", slide_container);
	var slide = next_slide;

	slide.hide();

	current_slide.fadeOut(animation_speed, function() {
		slide_container.append(slide);
		slide.fadeIn(animation_speed, function() {
			slide_is_ready = true;
		});
		current_slide.remove();
	});
};

Result:

Start Stop [ Stopped ]

slide1
slide2

Synchronous slideshow

For synchronous animation we need to track the readiness (slide is loaded) of the both slides:

var slide_counter = 2;

function slides_update() {
	var counter;
	var slide1;
	var slide2;

	if (slide_counter == additional_slides_urls.length + 1) {
		slide1 = slides[slide_counter];
		slide2 = slides[0];
		slide_counter = -1;
	}
	else {
		if (slide_counter == additional_slides_urls.length + 2) slide_counter = 0;
		slide1 = slides[slide_counter];
		slide2 = slides[slide_counter + 1];
	}

	var change_slides = setInterval(function() {
      		if (slide1 && slide2) {
	        	clearInterval(change_slides);
		        animate(slide1, slide1_container);
       			animate(slide2, slide2_container);
        	}
	}, 200);
};

function animate(next_slide, container) {
	var slide_container = container;
	var current_slide = $("img", slide_container);
	var slide = next_slide;

	slide.hide();

	current_slide.fadeOut(animation_speed, function() {
		slide_container.append(slide);
		slide.fadeIn(animation_speed);
		current_slide.remove();
		slide_counter++;
	});
};

Result:

Start Stop [ Stopped ]

slide1
slide2

Funny animal images was downloaded from turbomilk.com

My first experience with GAE

written by Andrey Khoronko, on Mar 30, 2009 1:46:00 PM.

Few days ago I wrote a couple of web services based on Google App Engine. Both of them powered by webapp framework and Universal Feed Parser library.

Feed to Sitemap converter

The first one (link) is a converter from RSS/Atom/CDF syndicated feeds to Sitemap protocol.

Usage:

http://feed2sitemap.appspot.com/converter?feed=FEED_URL
where FEED_URL is an http or https URL of feed.

You can use Sitemap cross submission for using this service with your website. Just add a single line to your robots.txt like here:

Sitemap: http://feed2sitemap.appspot.com/converter?feed=http%3A%2F%2Fkefafala.com%2Ffeed.atom

Blogger says ‘Hey!’

The second service (link) is a multi pinger for notifying different search engines that your blog has updated.

Service is looking for the latest entry into the submitted feed, then send (only if new entries was found) an XML-RPC signal to ping servers and finally returns feed’s content. For example you can subscribe to your own blog feed via this service and pings will be sent automatically (as your feed aggregator soft will periodically run a scan for updates).

Usage:

http://bloggersayshey.appspot.com/retransmitter?feed=FEED_URL
where FEED_URL is an http or https URL of feed.

It used the following ping servers:

  • http://ping.blogs.yandex.ru/RPC2
  • http://api.my.yahoo.com/RPC2
  • http://rpc.technorati.com/rpc/ping
  • http://blogsearch.google.com/ping/RPC2
  • http://rpc.icerocket.com:10080/

Howto: running Zine with Nginx and WSGI server

written by Andrey Khoronko, on Mar 27, 2009 2:05:00 AM.

This article will look at running Zine with Nginx, Cogen and Supervisor on Ubuntu server.

Zine

Zine is an open-source blog engine written in Python.

Install dependencies:

$ sudo aptitude install python2.5-dev libxml2-dev libxslt-dev
$ sudo easy_install Werkzeug Jinja2 MySQL-python SQLAlchemy simplejson pytz Babel lxml html5lib

Download Zine sources:

$ mkdir ~/temp && cd ~/temp
$ hg clone http://dev.pocoo.org/hg/zine-main zine
$ cd zine
$ ./configure --prefix=/usr/local
$ checkinstall --pkgname="zine" --pkgversion=0.2-dev --maintainer="foo@bar.com" --provides="zine" --strip=yes --stripso=yes --backup=no -y

Create your blog folder and folder for blog media (images, files, …):

$ mkdir ~/zine
$ mkdir ~/zine/uploads

You can put favicon.ico and robots.txt files to ~/zine too.

Nginx

Nginx is a high perfomance and light weight HTTP and reverse proxy server.

Install dependencies:

$ sudo aptitude install libc6 libpcre3 libpcre3-dev libpcrecpp0 libssl0.9.8 libssl-dev zlib1g zlib1g-dev lsb-base

Download Nginx and install it:

$ cd ~/temp
$ wget http://sysoev.ru/nginx/nginx-0.7.42.tar.gz && tar zxvf nginx-0.7.42.tar.gz
$ cd nginx-0.7.42
$ ./configure --with-http_ssl_module && make
$ checkinstall --pkgname="nginx" --pkgversion=0.7.42 --maintainer="foo@bar.com" --provides="nginx" --strip=yes --stripso=yes --backup=no -y

Create init script for nginx daemon:

$ sudo touch /etc/init.d/nginx
$ sudo chmod +x /etc/init.d/nginx
$ sudo update-rc.d nginx defaults

Content of /etc/init.d/nginx:

#! /bin/sh

### BEGIN INIT INFO
# Provides:          nginx
# Required-Start:    $all
# Required-Stop:     $all
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: starts the nginx web server
# Description:       starts nginx using start-stop-daemon
### END INIT INFO

PATH=/usr/local/nginx/sbin:/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
DAEMON=/usr/local/nginx/nginx
PID=/usr/local/nginx/nginx.pid
NAME=nginx
DESC=nginx

test -x $DAEMON || exit 0

# Include nginx defaults if available
if [ -f /etc/default/nginx ] ; then
        . /etc/default/nginx
fi

set -e

case "$1" in
  start)
        echo -n "Starting $DESC: "
        start-stop-daemon --start --quiet --pidfile $PID --exec $DAEMON -- $DAEMON_OPTS
        echo "$NAME."
        ;;
  stop)
        echo -n "Stopping $DESC: "
        start-stop-daemon --stop --quiet --pidfile $PID --exec $DAEMON -- $DAEMON_OPTS
        echo "$NAME."
        ;;
  restart|force-reload)
        echo -n "Restarting $DESC: "
        start-stop-daemon --stop --quiet --pidfile $PID --exec $DAEMON -- $DAEMON_OPTS
        sleep 1
        start-stop-daemon --start --quiet --pidfile $PID --exec $DAEMON -- $DAEMON_OPTS
	echo "$NAME."
        ;;
  reload)
      echo -n "Reloading $DESC configuration: "
      start-stop-daemon --stop --signal HUP --quiet --pidfile $PID --exec $DAEMON -- $DAEMON_OPTS
      echo "$NAME."
      ;;
  *)
      N=/etc/init.d/$NAME
      echo "Usage: $N {start|stop|restart|reload|force-reload}" >&2
      exit 1
      ;;
esac

exit 0

Change Nginx configuration /usr/local/nginx/nginx.conf:

user www-data;
worker_processes 1;

error_log logs/error.log warn;

pid nginx.pid;

events {
    worker_connections 1024;
}

http {
    include mime.types;
    default_type application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] $request '
                      '"$status" $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log logs/access.log  main;

    sendfile on;

    tcp_nopush on;
    tcp_nodelay on;

    server_names_hash_bucket_size 128;
    
    keepalive_timeout 75 20;

    gzip on;
    gzip_min_length 1100;
    gzip_buffers 4 32k;
    gzip_comp_level 2;
    gzip_proxied any;
    gzip_types text/plain text/html text/css application/x-javascript text/xml application/xml application/xml+rss text/javascript;
	
    include sites/yourblogname.com;
}

Create /usr/local/nginx/sites/yourblogname.com:

server {
    listen 80;
    server_name www.yourblogname.com;
    rewrite ^/(.*)$ http://yourblogname.com/$1 redirect; 
}

server {
    listen 80;
    server_name yourblogname.com;

    error_log logs/yourblogname_error.log warn;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header X-Forwarded-Host $server_name;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $remote_addr;

        access_log logs/yourblogname_access.log main;
    }

    location = /favicon.ico {
        root /home/username/zine;
	access_log off;
        expires 30d;
    }

    location = /robots.txt {
        root /home/username/zine;
	access_log off;
        expires 30d;
    }
    
    location ~ ^/_shared/(akismet_spam_filter|core|dark_vessel_colorscheme|eric_the_fish|miniblog_theme|myrtle_theme|vessel_theme)/(.+\.(?:jpe?g|gif|png|ico|bmp|js|css|gz|swf))$ {
	alias /usr/local/share/zine/htdocs/$1/$2;
	expires 30d;
	access_log off;
    }
    
    location ~ ^/_shared/(?!(pygments_support|creole_parser|markdown_parser|typography))([^/]+)/(.+\.(?:jpe?g|gif|png|ico|bmp|js|css|gz|swf))$ {
    	alias /home/username/zine/plugins/$2/shared/$3;
	expires 30d;
	access_log off;
    }
    
    location /uploads/ {
	root /home/username/zine;
	expires 30d;
	access_log off;
	autoindex off;
    }

    location /admin {
        rewrite ^/(.*)$ https://yourblogname.com/$1 redirect;
    }
    
    error_page 500 502 503 504 /50x.html;
    location = /50x.html {
        root html;
    }
    
    error_page 403 /403.html;    
    location = /403.html {
	root html;
    }
}

server {
    listen 443;
    server_name www.yourblogname.com;
    rewrite ^/(.*)$ https://yourblogname.com/$1 redirect;
}

server {
    listen 443;
    server_name yourblogname.com;

    error_log logs/yourblogname_admin_error.log warn;

    ssl on;
    ssl_certificate /etc/ssl/certs/some_ssl_cert.crt;
    ssl_certificate_key /etc/ssl/private/some_ssl_cert.key;
    ssl_session_timeout 5m;
    ssl_protocols SSLv2 SSLv3 TLSv1;
    ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP;
    ssl_prefer_server_ciphers on;

    add_header Front-End-Https on;

    location / {
        rewrite ^/(.*)$ http://yourblogname.com/$1 permanent;
    }

    location /admin {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header X-Forwarded-Host $server_name;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $remote_addr;

        access_log logs/yourblogname_admin_access.log main;
    }

    location = /favicon.ico {
        root /home/username/zine;
	access_log off;
        expires 30d;
    }

    location ~ ^/_shared/(akismet_spam_filter|core|dark_vessel_colorscheme|eric_the_fish|miniblog_theme|myrtle_theme|vessel_theme)/(.+\.(?:jpe?g|gif|png|ico|bmp|js|css|gz|swf))$ {
	alias /usr/local/share/zine/htdocs/$1/$2;
	expires 30d;
	access_log off;
    }

    location ~ ^/_shared/(?!(pygments_support|creole_parser|markdown_parser|typography))([^/]+)/(.+\.(?:jpe?g|gif|png|ico|bmp|js|css|gz|swf))$ {
    	alias /home/username/zine/plugins/$2/shared/$3;
	expires 30d;
	access_log off;
    }
    
    location /uploads/ {
	root /home/username/zine;
	expires 30d;
	access_log off;
	autoindex off;
    }
    
    error_page 403 /403.html;    
    location = /403.html {
	root html;
    }

    error_page 500 502 503 504 /50x.html;
    location = /50x.html {
        root html;
    }
}

Cogen

Cogen is a crossplatform library for network oriented, coroutine based programming. We will use it as WSGI server.

$ sudo easy_install cogen

Put to the blog folder running script for Zine:

$ touch run_zine.py
$ chmod +x run_zine.py

Content of run_zine.py:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

INSTANCE_FOLDER = '/home/username/zine'
PLUGINS_FOLDER = os.path.join(INSTANCE_FOLDER, 'plugins/')
ZINE_LIB = '/usr/local/lib/zine'

POOL_SIZE = None
POOL_RECYCLE = None
POOL_TIMEOUT = None

BEHIND_PROXY = None

import sys
import os
if ZINE_LIB not in sys.path:
    sys.path.insert(0, ZINE_LIB)

if PLUGINS_FOLDER not in sys.path:
    sys.path.append(PLUGINS_FOLDER)

from zine import get_wsgi_app, override_environ_config
override_environ_config(POOL_SIZE, POOL_RECYCLE, POOL_TIMEOUT, BEHIND_PROXY)
application = get_wsgi_app(INSTANCE_FOLDER)

from cogen.web import wsgi
from cogen.web.async import sync_input
from cogen.common import *

m = Scheduler(default_priority=priority.LAST, default_timeout=15)
server = wsgi.WSGIServer(
           ('localhost', 8080),
            sync_input(application), 
            m,
            server_name='localhost')
m.add(server.serve)
try:
    m.run()
except (KeyboardInterrupt, SystemExit):
    pass

Supervisor

Supervisor is a client/server system that allows to monitor and control a number of processes. If you run multiple sites on your server Supervisor is very useful for manage them.

Install Supervisor and create init script:

$ sudo easy_install supervisor
$ sudo echo_supervisord_conf > /etc/supervisord.conf
$ sudo touch /etc/init.d/supervisord
$ sudo chmod +x /etc/init.d/supervisord
$ sudo update-rc.d supervisord defaults

Content of /etc/init.d/supervisord:

#!/bin/sh
### BEGIN INIT INFO
# Provides:          supervisor
# Default-Start:     2 3 4 5
# Default-Stop:      S 0 1 6
# Short-Description: Starts/stops the supervisor daemon
# Description:       This starts and stops the supervisor dameon
#                    which is used to run and monitor arbitrary programs as
#                    services, e.g. application servers etc.
### END INIT INFO

PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
DESC="supervisor daemon"
NAME="supervisor"
DAEMON="/usr/bin/${NAME}d"
SUPERVISORCTL="/usr/bin/${NAME}ctl"
PIDFILE="/var/run/${NAME}d.pid"
SCRIPTNAME="/etc/init.d/$NAME"
CONFFILE="/etc/${NAME}d.conf"

test -x "$DAEMON" || exit 0
test -r "$CONFFILE" || exit 0

if [ -r "/etc/default/$NAME" ]; then
    . "/etc/default/$NAME"
fi

set -e

d_start() {
    start-stop-daemon --start --quiet --pidfile "$PIDFILE" \
        --exec "$DAEMON" \
        || echo -n " already running"
}

d_stop() {
    $SUPERVISORCTL shutdown
}

d_reload() {
    $SUPERVISORCTL reload
}

case "$1" in
  start)
    echo -n "Starting $DESC: $NAME"
    d_start
    echo "."
    ;;
  stop)
    echo -n "Stopping $DESC: $NAME"
    d_stop
    echo "."
    ;;
  reload|force-reload)
    echo -n "Reloading $DESC configuration..."
    d_reload
    echo "done."
  ;;
  restart)
    echo -n "Restarting $DESC: $NAME"
    d_stop
    sleep 1
    d_start
    echo "."
    ;;
  *)
    echo "Usage: "$SCRIPTNAME" {start|stop|restart|force-reload}" >&2
    exit 3
    ;;
esac

exit 0

Modify /etc/supervisord.conf and add program section for running your Zine blog:

[program:zine]
command=/home/username/zine/run_zine.py
user=username

Final

Now you can launch your blog:

$ sudo /etc/init.d/nginx start
$ sudo /etc/init.d/supervisord start

init

written by Andrey Khoronko, on Mar 23, 2009 2:52:00 PM.

hg commit -m 'initial commit'