2010 Open Source Design Plans

2010 is the year we dive into open source design. We’ve dipped our toes in this pool before (icon contest, graphic design component for Trac tickets, header refresh contest, etc.), but this year we’re going to cannonball and make a big splash. Here’s what you need to know if you want to get involved.

A list for all seasons. Developers have the wp-hackers mailing list to discuss core and plugin code. Sometimes UI/UX stuff comes up and gets discussed there, but there is a whole universe of discussion around navigation labels, gradients, button styling, layouts, alignment, etc. that would be clutter on wp-hackers. Designers need a list to call their own, and now we have one. You can sign up for the wp-ui list to discuss ways to improve the interface or user experience of WordPress, and to discuss progress on design-related projects for the open source project, like the design challenges we’re going to have.

Design Challenges. We learned a lot from the icon design and header refresh contests, and we want to do these kind of open design challenges on a regular basis to give UI/UX designers who want to contribute to the WordPress open source project more opportunities to do so. If we could do one per month, that would be ideal, keeping the challenges relatively bite-sized to allow potential contributors an easy way to get involved at first. As each challenge is posted, people can use the list to bounce ideas off each other and work toward optimal solutions. I’m hoping the design challenges will evolve to be less contest and more collaboration. We’ll announce the first one before the end of January, so if you’re interested, please sign up for the list! (Hint: one will likely be a touch up to the Right Now dashboard module, to improve the information design, and there will be a couple of screen layout challenges coming up as well.)

Distributed Usability Testing. We started to try this out last year, and several dozen usability professionals volunteered to help get the program going, but a combination of scheduling and infrastructure issues combined to stall the progress. Having the “UI/UX contributor team” infrastructure in place, starting with the mailing list, will make it much easier to get this project going again.

Chit-chat. The weekly developer chats in IRC at #wordpress-dev have been very productive. We’ve created an IRC room at #wordpress-ui on irc.freenode.net so that we can have the same kind of “water cooler” for UI/UX contributors as for core code contributors. In addition to being a place where you can drop in and discuss core UI/UX (note: this room will not be a place to discuss the design of blog themes, it’s to discuss the design of the Wordpress application itself), we’ll set up a weekly chat. Choosing a day and time for the chats will probably be the first discussion on the mailing list.

A blog of our own. Once again, taking a page from the code contributor infrastructure, we’ll set up a blog for UI/UX updates, announcements, progress reports, etc. This will be on WordPress.org in the nearish future, and will be announced to the mailing list when it is live.

So, if you want to become a contributor to core WordPress by using your design skills, join the wp-ui mailing list and get ready for a fun year!

Directory Handler

Package:
Summary:
Rename, copy or delete directories
Groups:
Author:
Description:
This class can be used to rename, copy or delete directories.

It can traverse a given directory recursively and copy its files and sub-directories to a given destination directory.

The class may also delete a given directory by deleting its contents first.

It can also rename a directory by copying its contents to a new directory with the final name. Then it deletes the directory with the original name.


PHP Installer

Package:
Summary:
Create PHP application install scripts
Groups:
Author:
Description:
This class can be used to create PHP application install scripts.

It generates a single PHP script that can install an application when it is executed.

The class generates a script that may contain compressed versions of the application scripts and other types of files.

It may also contain a dump of the MySQL application database that is restored when the generated installer script is executed.


PHP script for sending RSS feed to Twitter

I found a small sample PHP script for sending your RSS feed to Twitter. You can view / copy the code here:

<?php
/*
* Script that posts multiple RSS feeds to twitter
*
* Use this with crontab (it works with CLI too)
* to post your feed’s content to twitter
*
*/

$settings [‘twitter-username’] = ‘USERNAME’;// put here the twitter username you want to auto feed
$settings [‘twitter-password’] = ‘PASSWORD’;// the twitter password that belongs to the username
$settings [‘feed-url’] =‘FEED’;// here you place the url of the RSS feed you want to post to twitter

set_time_limit(0);//We need this because otherwise php will probably time out
//Let’s see if we have the curl library
if (!extension_loaded(‘curl’) && !dl(‘curl.’ . (stristr(php_OS, ‘WIN’)?‘dll’:‘so’))) die( ‘Curl is not loaded’ );
//Now we need a file to log the last entry so we wont post it again
$file = dirname(__FILE__) . ‘/feed.log.txt’;
if ( !
file_exists( $file ) ){
$fp = @fopen( $file, ‘w’);
if (! $fp )
die( ‘Could not write to log file. Check your permissions.’ );
fclose( $fp );
} else
if ( !is_writable( $file ) ) die( ‘Could not write to log file. Check your permissions.’ );
//Fetch the RSS feed
$ch = curl_init( $settings[‘feed-url’] );
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$feed = simplexml_load_string ( curl_exec($ch) );
curl_close($ch);
//Parse the feed
$messages = array();//Here we will store all the messages that we are going to post to twitter
foreach( $feed->channel->item as $item ){
$title = $item->title; // If you want to fetch the description instead use $item->description
$url = $item->link;
//Have we already posted that?
if ( $url == file_get_contents($file) ) break;
//Now do the actual posting.
//First we need to shorten the url
$ch = curl_init( ‘http://twt.gs/?url=’.$url.‘&user=’.$settings[‘twitter-username’].‘&pass=’.$settings[‘twitter-password’] );
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$shortURL = curl_exec($ch);
$resp = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ( $resp != ‘200’ )
die(‘Problem with the url shortening service. Check back later. Error: ‘.$shortURL);
//Now Calculate the twit message
$cnt = 140 strlen($shortURL) – 1;
$messages [] = strlen($title) <= $cnt ? $title . ‘ ‘ . $shortURL : substr($title,0,$cnt3).‘… ‘.$shortURL;
$lastURL = empty($lastURL) ? $url : $lastURL;
}
//Post to twitter
while( $message = array_pop($messages) ){
$ch = curl_init(‘http://twitter.com/statuses/update.xml’);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, ‘status=’.urlencode($message));
curl_setopt($ch, CURLOPT_USERPWD, $settings[‘twitter-username’].‘:’.$settings[‘twitter-password’]);

$response = curl_exec($ch);
$resp = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ( $resp != ‘200’ )
die(‘Problem with twitter. We should try later. Twitter reported: ‘.$response);
else
sleep(5);//Sleep 5 seconds before the next update
}
$fp = fopen($file, ‘w’);
fwrite($fp, $lastURL);
fclose($fp);
?>

What the above script is missing is the auto starting. You can use an external cronjob service if your hosting provider does not give you access to CRONjobs.

Get Favicon image using PHP

Some websites use a small Favicon as a logo for their website. In some cases it would be nice if you are able to get this small image (icon) and use it for your PHP output. For example when you have a link to this site or when you are showing the RSS feed from this site.

I found 2 different scripts for getting the favicon from external websites using PHP scripting. Here are the scripts.

function getFavicon($url) {
    $HTTPRequest = @fopen($url, 'r');
    if ($HTTPRequest) {
        stream_set_timeout($HTTPRequest, 0.1);
        $html = fread($HTTPRequest, 4096);
        $HTTPRequestData = stream_get_meta_data($HTTPRequest);
        fclose($HTTPRequest);
        if (!$HTTPRequestData['timed_out']) {
            if (preg_match('/<link[^>]+rel="(?:shortcut )?icon"[^>]+?href="([^"]+?)"/si', $html, $matches)) {
                $linkUrl = html_entity_decode($matches[1]);
                if (substr($linkUrl, 0, 1) == '/') {
                    $urlParts = parse_url($url);
                    $faviconURL = $urlParts['scheme'].'://'.$urlParts['host'].$linkUrl;
                } elseif (substr($linkUrl, 0, 7) == 'http://') {
                    $faviconURL = $linkUrl;
                } elseif (substr($url, -1, 1) == '/') {
                    $faviconURL = $url.$linkUrl;
                } else {
                    $faviconURL = $url.'/'.$linkUrl;
                }
            } else {
                $urlParts = parse_url($url);
                $faviconURL = $urlParts['scheme'].'://'.$urlParts['host'].'/favicon.ico';
            }
            $HTTPRequest = @fopen($faviconURL, 'r');
            if ($HTTPRequest) {
                stream_set_timeout($HTTPRequest, 0.1);
                $favicon = fread($HTTPRequest, 8192);
                $HTTPRequestData = stream_get_meta_data($HTTPRequest);
                fclose($HTTPRequest);
                if (!$HTTPRequestData['timed_out'] && strlen($favicon) < 8192) {
                    return $faviconURL;
                }
            }
        }
    }
	return false;
}

And the other PHP script I found is:
<?php

################################################################################
#   
#    Favicon Class (work with favicons), ver. 1.0, June, 2006.
#   
#    (c) 2006 ControlStyle Company. All rights reserved.
#    Developped by Nikolay I. Yarovoy, Dmitry V. Domojilov.
#
#    http://www.controlstyle.com
#    info@controlstyle.com
#
################################################################################

class favicon
{
    var $ver = '1.1';   
    var $site_url = ''; # url of site
    var $if_modified_since = 0; # cache
    var $is_not_modified = false;
    var $ico_type = 'ico'; # ico, gif or png only
    var $ico_url = ''; # full uri to favicon
    var $ico_exists = 'not checked'; # no comments
    var $ico_data = ''; # ico binary data
    var $output_data = ''; # output image binary data

    # main proc
    function favicon($site_url, $if_modified_since = 0)
    {       
        $site_url = trim(str_replace('http://', '', trim($site_url)), '/');
        $site_url = explode('/', $site_url);
        $site_url = 'http://' . $site_url[0] . '/';
        $this->site_url = $site_url;
        $this->if_modified_since = $if_modified_since;
        $this->get_ico_url();
        $this->is_ico_exists();
        $this->get_ico_data();
    }

    # get uri of favicon
    function get_ico_url()
    {
        if ($this->ico_url == '')
        {
            $this->ico_url = $this->site_url . 'favicon.ico';

            # get html of page
            $h = @fopen($this->site_url, 'r');
            if ($h)
            {
                $html = '';
                while (!feof($h) and !preg_match('/<([s]*)body([^>]*)>/i', $html))
                {
                    $html .= fread($h, 200);
                }
                fclose($h);

                # search need <link> tag
                if (preg_match('/<([^>]*)link([^>]*)(rel="icon"|rel="shortcut icon")([^>]*)>/iU', $html, $out))
                {

                    $link_tag = $out[0];
                    if (preg_match('/href([s]*)=([s]*)"([^"]*)"/iU', $link_tag, $out))
                    {
                        $this->ico_type = (!(strpos($link_tag, 'png')===false)) ? 'png' : 'ico';
                        $ico_href = trim($out[3]);
                        if (strpos($ico_href, 'http://')===false)
                        {
                            $ico_href = rtrim($this->site_url, '/') . '/' . ltrim($ico_href, '/');
                        }
                        $this->ico_url = $ico_href;
                    }
                }
            }           
        }
        return $this->ico_url;
    }

    # check that favicon is exists
    function is_ico_exists()
    {
        if ($this->ico_exists=='not checked')
        {
            $h = @fopen($this->ico_url, 'r');
            $this->ico_exists = ($h) ? true : false;
            if ($h) fclose($h);
        }
        return $this->ico_exists;
    }

    # get ico data
    function get_ico_data()
    {

    if ($this->ico_data=='' && $this->ico_url!=''
&& $this->ico_exists && !$this->is_not_modified)
        {
            # get ico data
            $ico_z = parse_url($this->ico_url);
            $ico_path = $ico_z['path'];
            $ico_host = $ico_z['host'];
            $ico_ims = gmdate('D, d M Y H:i:s', $this->if_modified_since) . ' GMT';

            $fp = @fsockopen($ico_host, 80);
            if ($fp)
            {
                $out = "GET {$ico_path} HTTP/1.1\r\n";
                $out .= "Host: {$ico_host}\r\n";

            $out .= "User-Agent=Mozilla/5.0 (Windows; U; Windows NT
5.1; ru; rv:1.8.0.3) Gecko/20060426 Firefox/1.5.0.3\r\n";
                $out .= "If-Modified-Since: {$ico_ims}\r\n";
                $out .= "Connection: Close\r\n\r\n";
                fwrite($fp, $out);
                $data = '';
                while (!feof($fp)) $data .= fgets($fp, 128);
                fclose($fp);
                $response = substr($data, 0, 15);
                $this->is_not_modified = !(strpos($response, '304')===false);
                if ($this->is_not_modified)
                {
                    $this->ico_data = 'not modified';                   
                }
                else
                {
                    $data = explode("\r\n\r\n", $data);
                    if (count($data)>0)
                    {
                        unset($data[0]);
                        $this->ico_data = implode("\r\n\r\n", $data);
                    }
                }
                $this->output_data = '';
            }
        }
        return $this->ico_data;
    }

    # get output data
    function get_output_data()
    {
        if ($this->output_data=='')
        {
            if ($this->ico_data=='not modified')
            {
                # icon is not modified since defined time
                $this->output_data = 'not modified';
            }
            elseif($this->ico_data=='')
            {
                # error(s) in getting icon data
                $this->output_data = $this->empty_png();
            }
            else
            {
                # convert ico to png, gif & return
                if (substr($this->ico_data, 0, 3)==='GIF') $this->ico_type = 'gif';
                $this->output_data = $this->ico_data;
                if ($this->ico_type==='ico')
                {
                    $this->output_data = $this->ico2png($this->output_data);
                }
                if ($this->ico_type==='gif')
                {
                    $this->output_data = $this->gif2png($this->output_data);
                }
            }
        }
        return $this->output_data;
    }    

    # if error or icon is not found we output empty png image
    function empty_png()
    {
        $res = '';
        $im = imagecreatetruecolor(16, 16);
        $color = imagecolorallocate($im, 255, 255, 255);
        imagefill($im, 1, 1, $color);

        # output png
        ob_start();

        # imagesavealpha($im, true);
        imagepng($im);
        imagedestroy($im);
        $res = ob_get_clean();
        return $res;   
    }

    # Convert gif to png function,
    # support gif-functions by GD is needed
    function gif2png($gif)
    {
        $im2 = imagecreatefromstring($gif); 

        # background alpha is disabled because IE 5.5 + have bug with alpha-channels
        # by default background color is white
        # imagealphablending($im, false);
        # imagefilledrectangle($im, 0, 0, 16, 16, $color);
        # imagealphablending($im, true);
        $im = imagecreatetruecolor(16, 16);
        $color = imagecolorallocate($im, 255, 255, 255);
        imagefill($im, 1, 1, $color);
        imagecopy($im, $im2, 0, 0, 0, 0, 16, 16);        

        # output png
        ob_start();
        # imagesavealpha($im, true);
        imagepng($im);
        imagedestroy($im);
        imagedestroy($im2);
        $res = ob_get_clean();
        return $res;
    }

    # Convert ico to png function,
    # information about ico format is accessible on a site http://kainsk.tomsk.ru/g2003/sys26/oswin.htm,
    function ico2png($ico)
    {
        $res = '';

        while(!isset($tmp))
        {
            $tmp = '';

            # get ICONDIR struct & check that it is correct ico format
            $icondir = unpack('sidReserved/sidType/sidCount', substr($ico, 0, 6));
            if ($icondir['idReserved']!=0 || $icondir['idType']!=1 || $icondir['idCount']<1) break;
            $icondir['idEntries'] = array();
            $entry = array();
            for($i=0; $i<$icondir['idCount']; $i++)
            {

            $entry =
unpack('CbWidth/CbHeight/CbColorCount/CbReserved/swPlanes/swBitCount/LdwBytesInRes/LdwImageOffset',
substr($ico, 6 + $i*16, 16));
                $icondir['idEntries'][] = $entry;
            }

            # select need icon & get it raw data
            $iconres = '';
            $bpx = 1; # bits per pixel
            $idx = 0; # index of need icon
            foreach($icondir['idEntries'] as $k=>$entry)
            {

            if ($entry['bWidth']==16 &&
isset($entry['swBitCount']) && $entry['swBitCount']>$bpx
&& $entry['swBitCount']<33)
                {
                    $idx = $k;
                    $bpx = $entry['swBitCount'];
                }
            }           
            $iconres = substr($ico, $icondir['idEntries'][$idx]['dwImageOffset'], $icondir['idEntries'][$idx]['dwBytesInRes']);
            unset($ico);
            unset($icondir);

            # getting bitmap info
            $bitmap_info = array();

        $bitmap_info['header'] =
unpack('LbiSize/LbiWidth/LbiHeight/SbiPlanes/SbiBitCount/LbiCompression/LbiSizeImage/LbiXPelsPerMeter/LbiYPelsPerMeter/LbiClrUsed/LbiClrImportant',
substr($iconres, 0, 40));

            $bitmap_info['header']['biHeight'] = $bitmap_info['header']['biHeight'] / 2;           
            $number_color = 0;

            if ($bitmap_info['header']['biBitCount'] > 16)
            {
                $number_color = 0;

            $sizecolor =
$bitmap_info['header']['biWidth']*$bitmap_info['header']['biBitCount']
* $bitmap_info['header']['biHeight'] / 8;  
            }
            elseif ( $bitmap_info['header']['biBitCount'] < 16)
            {
                $number_color = (int) pow(2, $bitmap_info['header']['biBitCount']);

            $sizecolor =
$bitmap_info['header']['biWidth']*$bitmap_info['header']['biBitCount']
* $bitmap_info['header']['biHeight'] / 8;  
                if ($bitmap_info['header']['biBitCount']=='1') $sizecolor = $sizecolor * 2;
            }
            else return $res;

            $rgb_table_size =  4 * $number_color;       
            for($i=0; $i<$number_color; $i++)
            {
                $bitmap_info['colors'][] = unpack('CrgbBlue/CrgbGreen/CrgbRed/CrgbReserved', substr($iconres, 40 + $i*4, 4));
            }
            $current_offset = 40 + $number_color * 4;

            $arraycolor = array();

            for($i=0; $i<$sizecolor; $i++)
            {
                $value = unpack('Cvalue', substr($iconres, $current_offset, 1));
                $arraycolor[] = $value['value'];
                $current_offset++;
            }

            # background alpha is disabled because IE 5.5 + have bug with alpha-channels
            # by default background color is white
            # imagealphablending($im, false);
            # imagefilledrectangle($im, 0, 0, 16, 16, $color);
            # imagealphablending($im, true);
            $im = imagecreatetruecolor(16, 16);
            $color = imagecolorallocate($im, 255, 255, 255);
            imagefill($im, 1, 1, $color);

            # getting mask
            $alpha = '';
            for($i=0; $i<16; $i++)
            {
                $z = unpack('Cx/Cy', substr($iconres, $current_offset, 2));
                $z = str_pad(decbin($z['x']), 8, '0', STR_PAD_RIGHT)  . str_pad(decbin($z['y']), 8, '0', STR_PAD_LEFT);
                $alpha .= $z;
                $current_offset = $current_offset + 4;
            }

            # drawing image
            $ico_size = 16;   
            $off = 0; # range (0-255)

            # cases for different color depth
            switch ($bitmap_info['header']['biBitCount'])   
            {        

                ###################### for 32 bit icons ######################
                case 32:
                    for($y=0; $y<$ico_size; $y++)
                    {
                        for($x=0; $x<$ico_size; $x++)
                        {
                            $a = round((255-$arraycolor[$off*4+3])/2);
                            $a = ($a<0) ? 0 : $a;
                            $a = ($a>127) ? 127 : $a;

                        $color = imagecolorallocatealpha($im,
$arraycolor[$off*4+2], $arraycolor[$off*4+1], $arraycolor[$off*4], $a);
                            imagesetpixel($im, $x, $ico_size-1-$y, $color);
                            $off++;
                        }
                    }
                break;

                ###################### for 24 bit icons ######################
                case 24:
                    for($y=0; $y<$ico_size; $y++)
                    {
                        for($x=0; $x<$ico_size; $x++)
                        {
                            $valpha = ($alpha[$off]=='1') ? 127 : 0;

                        $color = imagecolorallocatealpha($im,
$arraycolor[$off*3+2], $arraycolor[$off*3+1], $arraycolor[$off*3],
$valpha);
                            imagesetpixel ($im, $x, $ico_size-1-$y, $color);
                            $off++;
                        }
                    }
                break;

                ###################### for 08 bit icons ######################
                case 8:
                    for($y=0; $y<$ico_size; $y++)
                    {
                        for($x=0; $x<$ico_size; $x++)
                        {
                            $valpha = ($alpha[$off]=='1') ? 127 : 0;
                            $c = $arraycolor[$off];
                            $c = $bitmap_info['colors'][$c];
                            $color = imagecolorallocatealpha($im, $c['rgbRed'], $c['rgbGreen'], $c['rgbBlue'], $valpha);
                            imagesetpixel ($im, $x, $ico_size-1-$y, $color);
                            $off++;
                        }
                    }
                break;

                ###################### for 04 bit icons ######################
                # 318 = 22 (header) + 40 (bitmap_info) + 16 * 4 (colors) + 128 (pixels) + 64 (mask)
                case 4:
                    for($y=0; $y<$ico_size; $y++)
                    {
                        for($x=0; $x<$ico_size; $x++)
                        {
                            $valpha = ($alpha[$off]=='1') ? 127 : 0;
                            $c = ($arraycolor[floor($off/2)]);
                            $c = str_pad(decbin($c), 8, '0', STR_PAD_LEFT);
                            $m =  (fmod($off+1, 2)==0) ? 1 : 0;
                            $c = bindec(substr($c, $m*4, 4));
                            $c = $bitmap_info['colors'][$c];
                            $color = imagecolorallocatealpha($im, $c['rgbRed'], $c['rgbGreen'], $c['rgbBlue'], $valpha);
                            imagesetpixel ($im, $x, $ico_size-1-$y, $color);
                            $off++;
                        }
                    }
                break;

                ###################### for 01 bit icons ######################
                # 198 = 22 (header) + 40 (bitmap_info) + 2 * 4 (colors) + 64 (pixels, but real 32 needed?) + 64 (mask)
                case 1:
                    for($y=0; $y<$ico_size; $y++)
                    {
                        for($x=0; $x<$ico_size; $x++)
                        {
                            $valpha = ($alpha[$off]=='1') ? 127 : 0;
                            $c = ($arraycolor[floor($off/8)]); # меняем байт каждые 8 пикселей
                            $c = str_pad(decbin($c), 8, '0', STR_PAD_LEFT);
                            $m = fmod($off+8, 8) + 1; # bit number
                            $c = (int) substr($c, $m-1, 1);
                            $c = $bitmap_info['colors'][$c];
                            $color = imagecolorallocatealpha($im, $c['rgbRed'], $c['rgbGreen'], $c['rgbBlue'], $valpha);
                            imagesetpixel ($im, $x, $ico_size-1-$y, $color);
                            $off++;
                        }
                        $off = $off + 16;
                    }           
                break;

                ##############################################################

                default:
                return '';
            }

            # output png
            ob_start();
            # imagesavealpha($im, true);
            imagepng($im);
            imagedestroy($im);
            $res = ob_get_clean();
        }
        return $res;
    }
}

?>

And you can use the above PHP script with the following call:
<?php
    require_once('favicon.inc.php');

    $favicon = new favicon('http://www.controlstyle.ru/', 0);
    $fv = $favicon->get_output_data();

    if ($fv!=='')
    {
        header('Content-type: image/png');
        echo $fv;
    }
?>

What would be nice is some PHP code / script that can convert the found favicon to a GIF / JPG or PNG image so you can use it inside the HTML code.

Simple Singleton

Package:
Summary:
Create singleton objects
Groups:
Author:
Description:
This is a simple class that can be used to create singleton objects.

When requested, it can create an instance of the class itself and store it as static variable.

If the class object was already created and stored in the static variable, the class returns that object.


[New] Free Text User Interface Programming Libraries and Source Code

Create fancy user interfaces for your text-based programs in a device independent way with these Free Text User Interface (TUI) Programming Libraries. Now you don't have to worry about supporting the wide variety of video adapters, terminals and whatnot while still adding things like windows, dialog boxes and other widgets to your programs.

Mysql Ajax Table Editor

Mysql Ajax Table Editor is a php script that will allow you to create web pages to edit mysql tables in a matter of minutes. Similar to phpMyEdit but way better. Some of MATE's features are advanced search, order by columns, edit multiple rows, language support and join other tables.
Powered by Gewgley