summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md7
-rw-r--r--attentionbar.php10
-rw-r--r--attentionbar.txt1
-rw-r--r--banned.php7
-rw-r--r--inc/api.php123
-rw-r--r--inc/config.php67
-rw-r--r--inc/display.php52
-rw-r--r--inc/functions.php132
-rw-r--r--inc/mod/pages.php105
-rw-r--r--install.php16
-rw-r--r--install.sql12
-rw-r--r--js/attention-bar.js8
-rw-r--r--js/catalog-link.js18
-rw-r--r--js/chrome-board-expanding-fix.js7
-rw-r--r--js/expand.js8
-rw-r--r--js/hide-threads.js17
-rw-r--r--js/inline-expanding-filename.js33
-rw-r--r--js/inline-expanding.js1
-rw-r--r--js/jscolor/arrow.gifbin0 -> 66 bytes
-rw-r--r--js/jscolor/cross.gifbin0 -> 83 bytes
-rw-r--r--js/jscolor/hs.pngbin0 -> 2684 bytes
-rw-r--r--js/jscolor/hv.pngbin0 -> 2865 bytes
-rw-r--r--js/jscolor/jscolor.js995
-rw-r--r--js/mobile-style.js8
-rw-r--r--js/oekaki.js250
l---------js/post-hider.js1
-rw-r--r--js/post-hover.js72
-rw-r--r--js/quick-reply.js3
-rw-r--r--js/smartphone-spoiler.js24
-rw-r--r--js/toggle-locked-threads.js4
-rw-r--r--js/upload-selection.js67
-rw-r--r--mod.php3
-rw-r--r--post.php20
-rw-r--r--search.php170
-rw-r--r--stylesheets/dark.css18
-rw-r--r--stylesheets/futaba+vichan.css16
-rw-r--r--stylesheets/futaba.css9
-rw-r--r--stylesheets/img/arrow.pngbin0 -> 159 bytes
-rw-r--r--stylesheets/miku.css14
-rw-r--r--stylesheets/style.css54
-rw-r--r--stylesheets/testorange.css67
-rw-r--r--stylesheets/wasabi.css11
-rw-r--r--templates/attention_bar.html6
-rw-r--r--templates/boardlist.html1
-rw-r--r--templates/generic_page.html1
-rw-r--r--templates/header.html2
-rw-r--r--templates/index.html26
-rw-r--r--templates/main.js10
-rw-r--r--templates/post_form.html37
-rw-r--r--templates/post_thread.html9
-rw-r--r--templates/search_form.html19
-rw-r--r--templates/themes/ukko/info.php53
-rw-r--r--templates/themes/ukko/theme.php103
-rw-r--r--templates/themes/ukko/thumb.pngbin0 -> 15123 bytes
-rw-r--r--templates/themes/ukko/ukko.js115
-rw-r--r--templates/thread.html12
-rw-r--r--tools/benchmark.php52
-rwxr-xr-xtools/i18n_compile.php44
-rwxr-xr-xtools/i18n_extract.php47
-rw-r--r--tools/inc/cli.php100
-rw-r--r--tools/inc/lib/jsgettext/JSParser.php94
-rw-r--r--tools/inc/lib/jsgettext/PoeditParser.php83
-rw-r--r--tools/inc/lib/jsgettext/PoeditString.php29
-rw-r--r--tools/inc/lib/jsgettext/jsgettext.php63
-rw-r--r--tools/inc/lib/jsgettext/po2json.php42
-rwxr-xr-xtools/rebuild.php105
66 files changed, 3349 insertions, 134 deletions
diff --git a/README.md b/README.md
index 967b1c88..c7d968d9 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,13 @@
Tinyboard - A lightweight PHP imageboard.
==========================================
+Tinyboard + vichan-devel
+------------
+Tinyboard branch taking lightweightness somewhat more liberally. Running live at
+https://pl.vichan.net/ (Polish) and http://vichan.net/ (International; may be outdated).
+
+It contains many changes from original Tinyboard, mainly in frontend area.
+
About
------------
Tinyboard is a free light-weight, fast, highly configurable and user-friendly
diff --git a/attentionbar.php b/attentionbar.php
new file mode 100644
index 00000000..e928daae
--- /dev/null
+++ b/attentionbar.php
@@ -0,0 +1,10 @@
+<?php
+ require_once 'inc/functions.php';
+ checkBan();
+ if(@strlen($_POST["text"])>0) {
+ file_put_contents("attentionbar.txt",$_POST["text"]);
+ if(strlen($_SERVER['HTTP_REFERER'])>0) { header('Location: ' . $_SERVER['HTTP_REFERER']); }
+ else { header('Location: /'); }
+ } else print(file_get_contents("attentionbar.txt"));
+ return;
+?>
diff --git a/attentionbar.txt b/attentionbar.txt
new file mode 100644
index 00000000..50aa355b
--- /dev/null
+++ b/attentionbar.txt
@@ -0,0 +1 @@
+- * ( Pasek Atencji ) * -
diff --git a/banned.php b/banned.php
new file mode 100644
index 00000000..57e4a9bc
--- /dev/null
+++ b/banned.php
@@ -0,0 +1,7 @@
+<?php
+ require_once 'inc/functions.php';
+ checkBan();
+ print "<!doctype html><html><head><meta charset='utf-8'><title>"._("Banned?")."</title></head><body>";
+ print "<h1>"._("You are not banned.")."</h1>";
+ print "</body></html>";
+?>
diff --git a/inc/api.php b/inc/api.php
new file mode 100644
index 00000000..5592d8af
--- /dev/null
+++ b/inc/api.php
@@ -0,0 +1,123 @@
+<?php
+
+/*
+ * Copyright (c) 2010-2013 Tinyboard Development Group
+ */
+
+/**
+ * Class for generating json API compatible with 4chan API
+ */
+class Api {
+
+ /**
+ * Translation from local fields to fields in 4chan-style API
+ */
+ public static $postFields = array(
+ 'id' => 'no',
+ 'thread' => 'resto',
+ 'subject' => 'sub',
+ 'email' => 'email',
+ 'name' => 'name',
+ 'trip' => 'trip',
+ 'capcode' => 'capcode',
+ 'body' => 'com',
+ 'time' => 'time',
+ 'thumb' => 'thumb', // non-compatible field
+ 'thumbx' => 'tn_w',
+ 'thumby' => 'tn_h',
+ 'file' => 'file', // non-compatible field
+ 'filex' => 'w',
+ 'filey' => 'h',
+ 'filesize' => 'fsize',
+ //'filename' => 'filename',
+ 'omitted' => 'omitted_posts',
+ 'omitted_images' => 'omitted_images',
+ //'posts' => 'replies',
+ //'ip' => '',
+ 'sticky' => 'sticky',
+ 'locked' => 'locked',
+ //'bumplocked' => '',
+ //'embed' => '',
+ //'root' => '',
+ //'mod' => '',
+ //'hr' => '',
+ );
+
+ static $ints = array(
+ 'no' => 1,
+ 'resto' => 1,
+ 'time' => 1,
+ 'tn_w' => 1,
+ 'tn_h' => 1,
+ 'w' => 1,
+ 'h' => 1,
+ 'fsize' => 1,
+ 'omitted_posts' => 1,
+ 'omitted_images' => 1,
+ 'sticky' => 1,
+ 'locked' => 1,
+ );
+
+ private function translatePost($post) {
+ $apiPost = array();
+ foreach (self::$postFields as $local => $translated) {
+ if (!isset($post->$local))
+ continue;
+
+ $toInt = isset(self::$ints[$translated]);
+ $val = $post->$local;
+ if ($val !== null && $val !== '') {
+ $apiPost[$translated] = $toInt ? (int) $val : $val;
+ }
+ }
+
+ if (isset($post->filename)) {
+ $dotPos = strrpos($post->filename, '.');
+ $apiPost['filename'] = substr($post->filename, 0, $dotPos);
+ $apiPost['ext'] = substr($post->filename, $dotPos);
+ }
+
+ return $apiPost;
+ }
+
+ function translateThread(Thread $thread) {
+ $apiPosts = array();
+ $op = $this->translatePost($thread);
+ $op['resto'] = 0;
+ $apiPosts['posts'][] = $op;
+
+ foreach ($thread->posts as $p) {
+ $apiPosts['posts'][] = $this->translatePost($p);
+ }
+
+ return $apiPosts;
+ }
+
+ function translatePage(array $threads) {
+ $apiPage = array();
+ foreach ($threads as $thread) {
+ $apiPage['threads'][] = $this->translateThread($thread);
+ }
+ return $apiPage;
+ }
+
+ function translateCatalogPage(array $threads) {
+ $apiPage = array();
+ foreach ($threads as $thread) {
+ $ts = $this->translateThread($thread);
+ $apiPage['threads'][] = current($ts['posts']);
+ }
+ return $apiPage;
+ }
+
+ function translateCatalog($catalog) {
+ $apiCatalog = array();
+ foreach ($catalog as $page => $threads) {
+ $apiPage = $this->translateCatalogPage($threads);
+ $apiPage['page'] = $page;
+ $apiCatalog[] = $apiPage;
+ }
+
+ return $apiCatalog;
+ }
+}
diff --git a/inc/config.php b/inc/config.php
index fbe20d3e..ae3ce0c0 100644
--- a/inc/config.php
+++ b/inc/config.php
@@ -349,8 +349,10 @@
$config['auto_unicode'] = true;
// Whether to turn URLs into functional links.
$config['markup_urls'] = true;
+
// Optional URL prefix for links (eg. "http://anonym.to/?").
$config['link_prefix'] = '';
+ $config['url_ads'] = &$config['link_prefix']; // leave alias
// Allow "uploading" images via URL as well. Users can enter the URL of the image and then Tinyboard will
// download it. Not usually recommended.
@@ -389,6 +391,9 @@
// When true, a blank password will be used for files (not usable for deletion).
$config['field_disable_password'] = false;
+ // When true, users are instead presented a selectbox for email. Contains, blank, noko and sage.
+ $config['field_email_selectbox'] = false;
+
// Attach country flags to posts. Requires the PHP "geoip" extension to be installed:
// http://www.php.net/manual/en/intro.geoip.php. In the future, maybe I will find and include a proper
// pure-PHP geolocation library.
@@ -458,7 +463,7 @@
$config['markup'][] = array("/'''([^<]+?)'''/", "<strong>\$1</strong>");
$config['markup'][] = array("/''([^<]+?)''/", "<em>\$1</em>");
$config['markup'][] = array("/\*\*([^<]+?)\*\*/", "<span class=\"spoiler\">\$1</span>");
- // $config['markup'][] = array("/^[ |\t]*==([^<]+?)==[ |\t]*$/m", "<span class=\"heading\">\$1</span>");
+ $config['markup'][] = array("/^[ |\t]*==([^<]+?)==[ |\t]*$/m", "<span class=\"heading\">\$1</span>");
// Highlight PHP code wrapped in <code> tags (PHP 5.3+)
// $config['markup'][] = array(
@@ -601,7 +606,12 @@
// Display image identification links using regex.info/exif, TinEye and Google Images.
$config['image_identification'] = false;
-
+
+ // Number of posts in a "View Last X Posts" page
+ $config['noko50_count'] = 50;
+ // Number of posts a thread needs before it gets a "View Last X Posts" page.
+ // Set to an arbitrarily large value to disable.
+ $config['noko50_min'] = 100;
/*
* ====================
* Board settings
@@ -627,9 +637,23 @@
// Number of reports you can create at once.
$config['report_limit'] = 3;
- // Allow unfiltered HTML in board subtitles. This is useful for placing icons and links.
+ // Attention Whoring Bar
+ // REMEMBER TO CHMOD attentionbar.txt PROPERLY
+ // Oh, and add jQuery in additional_javascript.
+ $config['attention_bar'] = false;
+
+ // Allow unfiltered HTML in board subtitle. This is useful for placing icons and links.
$config['allow_subtitle_html'] = false;
+ // Oekaki - To enable oekaki, just enable its script with:
+ // $config['additional_javascript'][] = 'js/oekaki.js';
+ // Color selection in oekaki can be greatly improved by including the bundled jscolor script.
+ // $config['additional_javascript'][] = 'js/jscolor/jscolor.js';
+ // $config['oekaki'] holds Oekaki options, which are currently just default canvas size.
+
+ $config['oekaki']['height'] = 250;
+ $config['oekaki']['width'] = 500;
+
/*
* ====================
* Display settings
@@ -712,7 +736,7 @@
// );
// Whether or not to put brackets around the whole board list
- $config['boardlist_wrap_bracket'] = true;
+ $config['boardlist_wrap_bracket'] = false;
// Board categories. Only used in the "Categories" theme.
// $config['categories'] = array(
@@ -731,6 +755,15 @@
// Automatically remove unnecessary whitespace when compiling HTML files from templates.
$config['minify_html'] = true;
+ /*
+ * Advertisement HTML to appear at the top and bottom of board pages.
+ */
+
+ // $config['ad'] = array(
+ // 'top' => '',
+ // 'bottom' => '',
+ // );
+
// Optional HTML to append to "You are banned" pages. For example, you could include instructions and/or
// a link to an email address or IRC chat room to appeal the ban.
$config['ban_page_extra'] = '';
@@ -909,6 +942,7 @@
// Location of files.
$config['file_index'] = 'index.html';
$config['file_page'] = '%d.html';
+ $config['file_page50'] = '%d+50.html';
$config['file_mod'] = 'mod.php';
$config['file_post'] = 'post.php';
$config['file_script'] = 'main.js';
@@ -1016,6 +1050,9 @@
// persistent spammers and ban evaders. Again, a little more database load.
$config['ban_cidr'] = true;
+ // Enable the moving of single replies
+ $config['move_replies'] = false;
+
// How often (minimum) to purge the ban list of expired bans (which have been seen). Only works when
// $config['cache'] is enabled and working.
$config['purge_bans'] = 60 * 60 * 12; // 12 hours
@@ -1266,6 +1303,28 @@
/*
* ====================
+ * Public post search
+ * ====================
+ */
+ $config['search'] = array();
+
+ // Enable the search form
+ $config['search']['enable'] = false;
+
+ // Maximal number of queries per IP address per minutes
+ $config['search']['queries_per_minutes'] = Array(15, 2);
+
+ // Global maximal number of queries per minutes
+ $config['search']['queries_per_minutes_all'] = Array(50, 2);
+
+ // Limit of search results
+ $config['search']['search_limit'] = 100;
+
+ // Boards for searching
+ //$config['search']['boards'] = array('a', 'b', 'c', 'd', 'e');
+
+/*
+ * ====================
* Events (PHP 5.3.0+)
* ====================
*/
diff --git a/inc/display.php b/inc/display.php
index 6fa97f4f..02668e40 100644
--- a/inc/display.php
+++ b/inc/display.php
@@ -19,19 +19,23 @@ function format_bytes($size) {
return round($size, 2).$units[$i];
}
-function doBoardListPart($list, $root) {
+function doBoardListPart($list, $root, &$boards) {
global $config;
$body = '';
- foreach ($list as $board) {
+ foreach ($list as $key => $board) {
if (is_array($board))
- $body .= ' [' . doBoardListPart($board, $root) . '] ';
- // $body .= ' <span class="sub">[' . doBoardListPart($board, $root) . ']</span> ';
+ $body .= ' <span class="sub" data-description="' . $key . '">[' . doBoardListPart($board, $root, $boards) . ']</span> ';
else {
- if (($key = array_search($board, $list)) && gettype($key) == 'string') {
+ if (gettype($key) == 'string') {
$body .= ' <a href="' . $board . '">' . $key . '</a> /';
- } else {
- $body .= ' <a href="' . $root . $board . '/' . $config['file_index'] . '">' . $board . '</a> /';
+ } else {
+ $title = '';
+ if (isset ($boards[$board])) {
+ $title = ' title="'.$boards[$board].'"';
+ }
+
+ $body .= ' <a href="' . $root . $board . '/' . $config['file_index'] . '"'.$title.'>' . $board . '</a> /';
}
}
}
@@ -45,7 +49,14 @@ function createBoardlist($mod=false) {
if (!isset($config['boards'])) return array('top'=>'','bottom'=>'');
- $body = doBoardListPart($config['boards'], $mod?'?/':$config['root']);
+ $xboards = listBoards();
+ $boards = array();
+ foreach ($xboards as $val) {
+ $boards[$val['uri']] = $val['title'];
+ }
+
+ $body = doBoardListPart($config['boards'], $mod?'?/':$config['root'], $boards);
+
if ($config['boardlist_wrap_bracket'] && !preg_match('/\] $/', $body))
$body = '[' . $body . ']';
@@ -73,6 +84,9 @@ function error($message, $priority = true, $debug_stuff = false) {
if ($config['debug'] && isset($db_error)) {
$debug_stuff = array_combine(array('SQLSTATE', 'Error code', 'Error message'), $db_error);
}
+
+ // Return the bad request header, necessary for AJAX posts
+ http_response_code(400);
die(Element('page.html', array(
'config' => $config,
@@ -323,12 +337,17 @@ class Post {
$built .= ' ' . secure_link_confirm($config['mod']['link_deletefile'], _('Delete file'), _('Are you sure you want to delete this file?'), $board['dir'] . 'deletefile/' . $this->id);
// Spoiler file (keep post)
- if (!empty($this->file) && $this->file != 'deleted' && $this->thumb != 'spoiler' && hasPermission($config['mod']['spoilerimage'], $board['uri'], $this->mod) && $config['spoiler_images'])
- $built .= ' ' . secure_link_confirm($config['mod']['link_spoilerimage'], 'Spoiler File', 'Are you sure you want to spoiler this file?', $board['uri'] . '/spoiler/' . $this->id);
+ if (!empty($this->file) && $this->file != "deleted" && $this->file != null && $this->thumb != 'spoiler' && hasPermission($config['mod']['spoilerimage'], $board['uri'], $this->mod) && $config['spoiler_images'])
+ $built .= ' ' . secure_link_confirm($config['mod']['link_spoilerimage'], _('Spoiler File'), _('Are you sure you want to spoiler this file?'), $board['uri'] . '/spoiler/' . $this->id);
+
+ // Move post
+ if (hasPermission($config['mod']['move'], $board['uri'], $this->mod) && $config['move_replies'])
+ $built .= ' <a title="'._('Move reply to another board').'" href="?/' . $board['uri'] . '/move_reply/' . $this->id . '">' . $config['mod']['link_move'] . '</a>';
// Edit post
if (hasPermission($config['mod']['editpost'], $board['uri'], $this->mod))
$built .= ' <a title="'._('Edit post').'" href="?/' . $board['dir'] . 'edit' . ($config['mod']['raw_html_default'] ? '_raw' : '') . '/' . $this->id . '">' . $config['mod']['link_editpost'] . '</a>';
+
if (!empty($built))
$built = '<span class="controls">' . $built . '</span>';
@@ -389,6 +408,9 @@ class Thread {
public function add(Post $post) {
$this->posts[] = $post;
}
+ public function postCount() {
+ return count($this->posts) + $this->omitted;
+ }
public function postControls() {
global $board, $config;
@@ -420,8 +442,8 @@ class Thread {
$built .= ' ' . secure_link_confirm($config['mod']['link_deletefile'], _('Delete file'), _('Are you sure you want to delete this file?'), $board['dir'] . 'deletefile/' . $this->id);
// Spoiler file (keep post)
- if (!empty($this->file) && $this->file != 'deleted' && $this->thumb != 'spoiler' && hasPermission($config['mod']['spoilerimage'], $board['uri'], $this->mod) && $config['spoiler_images'])
- $built .= ' ' . secure_link_confirm($config['mod']['link_spoilerimage'], 'Spoiler File', 'Are you sure you want to spoiler this file?', $board['uri'] . '/spoiler/' . $this->id);
+ if (!empty($this->file) && $this->file != "deleted" && $this->file != null && $this->thumb != 'spoiler' && hasPermission($config['mod']['spoilerimage'], $board['uri'], $this->mod) && $config['spoiler_images'])
+ $built .= ' ' . secure_link_confirm($config['mod']['link_spoilerimage'], _('Spoiler File'), _('Are you sure you want to spoiler this file?'), $board['uri'] . '/spoiler/' . $this->id);
// Sticky
if (hasPermission($config['mod']['sticky'], $board['uri'], $this->mod))
@@ -460,10 +482,12 @@ class Thread {
return fraction($this->filewidth, $this->fileheight, ':');
}
- public function build($index=false) {
+ public function build($index=false, $isnoko50=false) {
global $board, $config, $debug;
- $built = Element('post_thread.html', array('config' => $config, 'board' => $board, 'post' => &$this, 'index' => $index));
+ $hasnoko50 = $this->postCount() >= $config['noko50_min'];
+
+ $built = Element('post_thread.html', array('config' => $config, 'board' => $board, 'post' => &$this, 'index' => $index, 'hasnoko50' => $hasnoko50, 'isnoko50' => $isnoko50));
return $built;
}
diff --git a/inc/functions.php b/inc/functions.php
index d5348f0d..a75ebb8a 100644
--- a/inc/functions.php
+++ b/inc/functions.php
@@ -13,6 +13,7 @@ require_once 'inc/display.php';
require_once 'inc/template.php';
require_once 'inc/database.php';
require_once 'inc/events.php';
+require_once 'inc/api.php';
require_once 'inc/lib/gettext/gettext.inc';
// the user is not currently logged in as a moderator
@@ -107,7 +108,10 @@ function loadConfig() {
'|' .
str_replace('%s', $config['board_regex'], preg_quote($config['board_path'], '/')) .
preg_quote($config['dir']['res'], '/') .
- str_replace('%d', '\d+', preg_quote($config['file_page'], '/')) .
+ '(' .
+ str_replace('%d', '\d+', preg_quote($config['file_page'], '/')) . '|' .
+ str_replace('%d', '\d+', preg_quote($config['file_page50'], '/')) .
+ ')' .
'|' .
preg_quote($config['file_mod'], '/') . '\?\/.+' .
')([#?](.+)?)?$/ui';
@@ -954,6 +958,7 @@ function deletePost($id, $error_if_doesnt_exist=true, $rebuild_after=true) {
if (!$post['thread']) {
// Delete thread HTML page
file_unlink($board['dir'] . $config['dir']['res'] . sprintf($config['file_page'], $post['id']));
+ file_unlink($board['dir'] . $config['dir']['res'] . sprintf($config['file_page50'], $post['id']));
$antispam_query = prepare('DELETE FROM ``antispam`` WHERE `board` = :board AND `thread` = :thread');
$antispam_query->bindValue(':board', $board['uri']);
@@ -1047,6 +1052,9 @@ function index($page, $mod=false) {
if ($query->rowCount() < 1 && $page > 1)
return false;
+
+ $threads = array();
+
while ($th = $query->fetch(PDO::FETCH_ASSOC)) {
$thread = new Thread($th, $mod ? '?/' : $config['root'], $mod);
@@ -1088,6 +1096,7 @@ function index($page, $mod=false) {
$thread->omitted_images = $omitted['image_count'] - $num_images;
}
+ $threads[] = $thread;
$body .= $thread->build(true);
}
@@ -1096,7 +1105,8 @@ function index($page, $mod=false) {
'body' => $body,
'post_url' => $config['post_url'],
'config' => $config,
- 'boardlist' => createBoardlist($mod)
+ 'boardlist' => createBoardlist($mod),
+ 'threads' => $threads
);
}
@@ -1289,6 +1299,9 @@ function buildIndex() {
if (!$config['try_smarter'])
$antibot = create_antibot($board['uri']);
+ $api = new Api();
+ $catalog = array();
+
for ($page = 1; $page <= $config['max_pages']; $page++) {
$filename = $board['dir'] . ($page == 1 ? $config['file_index'] : sprintf($config['file_page'], $page));
@@ -1309,13 +1322,29 @@ function buildIndex() {
$content['antibot'] = $antibot;
file_write($filename, Element('index.html', $content));
+
+ // json api
+ $threads = $content['threads'];
+ $json = json_encode($api->translatePage($threads));
+ $jsonFilename = $board['dir'] . ($page-1) . ".json"; // pages should start from 0
+ file_write($jsonFilename, $json);
+
+ $catalog[$page-1] = $threads;
}
if ($page < $config['max_pages']) {
for (;$page<=$config['max_pages'];$page++) {
$filename = $board['dir'] . ($page==1 ? $config['file_index'] : sprintf($config['file_page'], $page));
file_unlink($filename);
+
+ $jsonFilename = $board['dir'] . ($page-1) . ".json";
+ file_unlink($jsonFilename);
}
}
+
+ // json api catalog
+ $json = json_encode($api->translateCatalog($catalog));
+ $jsonFilename = $board['dir'] . "catalog.json";
+ file_write($jsonFilename, $json);
}
function buildJavascript() {
@@ -1715,6 +1744,8 @@ function buildThread($id, $return = false, $mod = false) {
// Check if any posts were found
if (!isset($thread))
error($config['error']['nonexistant']);
+
+ $hasnoko50 = $thread->postCount() >= $config['noko50_min'];
$body = Element('thread.html', array(
'board' => $board,
@@ -1723,6 +1754,8 @@ function buildThread($id, $return = false, $mod = false) {
'config' => $config,
'id' => $id,
'mod' => $mod,
+ 'hasnoko50' => $hasnoko50,
+ 'isnoko50' => false,
'antibot' => $mod || $return ? false : create_antibot($board['uri'], $id),
'boardlist' => createBoardlist($mod),
'return' => ($mod ? '?' . $board['url'] . $config['file_index'] : $config['root'] . $board['dir'] . $config['file_index'])
@@ -1731,10 +1764,101 @@ function buildThread($id, $return = false, $mod = false) {
if ($config['try_smarter'] && !$mod)
$build_pages[] = thread_find_page($id);
- if ($return)
+ // json api
+ $api = new Api();
+ $json = json_encode($api->translateThread($thread));
+ $jsonFilename = $board['dir'] . $config['dir']['res'] . $id . ".json";
+ file_write($jsonFilename, $json);
+
+ if ($return) {
return $body;
+ } else {
+ $noko50fn = $board['dir'] . $config['dir']['res'] . sprintf($config['file_page50'], $id);
+ if ($hasnoko50 || file_exists($noko50fn)) {
+ buildThread50($id, $return, $mod, $thread);
+ }
- file_write($board['dir'] . $config['dir']['res'] . sprintf($config['file_page'], $id), $body);
+ file_write($board['dir'] . $config['dir']['res'] . sprintf($config['file_page'], $id), $body);
+ }
+}
+
+function buildThread50($id, $return = false, $mod = false, $thread = null) {
+ global $board, $config, $build_pages;
+ $id = round($id);
+
+ if (event('build-thread', $id))
+ return;
+
+ if (!$thread) {
+ $query = prepare(sprintf("SELECT * FROM ``posts_%s`` WHERE (`thread` IS NULL AND `id` = :id) OR `thread` = :id ORDER BY `thread`,`id` DESC LIMIT :limit", $board['uri']));
+ $query->bindValue(':id', $id, PDO::PARAM_INT);
+ $query->bindValue(':limit', $config['noko50_count']+1, PDO::PARAM_INT);
+ $query->execute() or error(db_error($query));
+
+ $num_images = 0;
+ while ($post = $query->fetch(PDO::FETCH_ASSOC)) {
+ if (!isset($thread)) {
+ $thread = new Thread($post, $mod ? '?/' : $config['root'], $mod);
+ } else {
+ if ($post['file'])
+ $num_images++;
+
+ $thread->add(new Post($post, $mod ? '?/' : $config['root'], $mod));
+ }
+ }
+
+ // Check if any posts were found
+ if (!isset($thread))
+ error($config['error']['nonexistant']);
+
+
+ if ($query->rowCount() == $config['noko50_count']+1) {
+ $count = prepare(sprintf("SELECT COUNT(`id`) as `num` FROM ``posts_%s`` WHERE `thread` = :thread UNION ALL SELECT COUNT(`id`) FROM ``posts_%s`` WHERE `file` IS NOT NULL AND `thread` = :thread", $board['uri'], $board['uri']));
+ $count->bindValue(':thread', $id, PDO::PARAM_INT);
+ $count->execute() or error(db_error($count));
+
+ $c = $count->fetch();
+ $thread->omitted = $c['num'] - $config['noko50_count'];
+
+ $c = $count->fetch();
+ $thread->omitted_images = $c['num'] - $num_images;
+ }
+
+ $thread->posts = array_reverse($thread->posts);
+ } else {
+ $allPosts = $thread->posts;
+
+ $thread->posts = array_slice($allPosts, -$config['noko50_count']);
+ $thread->omitted += count($allPosts) - count($thread->posts);
+ foreach ($allPosts as $index => $post) {
+ if ($index == count($allPosts)-count($thread->posts))
+ break;
+ if ($post->file)
+ $thread->omitted_images++;
+ }
+ }
+
+ $hasnoko50 = $thread->postCount() >= $config['noko50_min'];
+
+ $body = Element('thread.html', array(
+ 'board' => $board,
+ 'thread' => $thread,
+ 'body' => $thread->build(false, true),
+ 'config' => $config,
+ 'id' => $id,
+ 'mod' => $mod,
+ 'hasnoko50' => $hasnoko50,
+ 'isnoko50' => true,
+ 'antibot' => $mod ? false : create_antibot($board['uri'], $id),
+ 'boardlist' => createBoardlist($mod),
+ 'return' => ($mod ? '?' . $board['url'] . $config['file_index'] : $config['root'] . $board['dir'] . $config['file_index'])
+ ));
+
+ if ($return) {
+ return $body;
+ } else {
+ file_write($board['dir'] . $config['dir']['res'] . sprintf($config['file_page50'], $id), $body);
+ }
}
function rrmdir($dir) {
diff --git a/inc/mod/pages.php b/inc/mod/pages.php
index 887be16b..35713c27 100644
--- a/inc/mod/pages.php
+++ b/inc/mod/pages.php
@@ -705,6 +705,16 @@ function mod_view_thread($boardName, $thread) {
echo $page;
}
+function mod_view_thread50($boardName, $thread) {
+ global $config, $mod;
+
+ if (!openBoard($boardName))
+ error($config['error']['noboard']);
+
+ $page = buildThread50($thread, true, $mod);
+ echo $page;
+}
+
function mod_ip_remove_note($ip, $id) {
global $config, $mod;
@@ -992,6 +1002,101 @@ function mod_bumplock($board, $unbumplock, $post) {
header('Location: ?/' . sprintf($config['board_path'], $board) . $config['file_index'], true, $config['redirect_http']);
}
+function mod_move_reply($originBoard, $postID) {
+ global $board, $config, $mod;
+
+ if (!openBoard($originBoard))
+ error($config['error']['noboard']);
+
+ if (!hasPermission($config['mod']['move'], $originBoard))
+ error($config['error']['noaccess']);
+
+ $query = prepare(sprintf('SELECT * FROM ``posts_%s`` WHERE `id` = :id', $originBoard));
+ $query->bindValue(':id', $postID);
+ $query->execute() or error(db_error($query));
+ if (!$post = $query->fetch(PDO::FETCH_ASSOC))
+ error($config['error']['404']);
+
+ if (isset($_POST['board'])) {
+ $targetBoard = $_POST['board'];
+
+ if ($_POST['target_thread']) {
+ $query = prepare(sprintf('SELECT * FROM ``posts_%s`` WHERE `id` = :id', $targetBoard));
+ $query->bindValue(':id', $_POST['target_thread']);
+ $query->execute() or error(db_error($query)); // If it fails, thread probably does not exist
+ $post['op'] = false;
+ $post['thread'] = $_POST['target_thread'];
+ }
+ else {
+ $post['op'] = true;
+ }
+
+ if ($post['file']) {
+ $post['has_file'] = true;
+ $post['width'] = &$post['filewidth'];
+ $post['height'] = &$post['fileheight'];
+
+ $file_src = sprintf($config['board_path'], $board['uri']) . $config['dir']['img'] . $post['file'];
+ $file_thumb = sprintf($config['board_path'], $board['uri']) . $config['dir']['thumb'] . $post['thumb'];
+ } else {
+ $post['has_file'] = false;
+ }
+
+ // allow thread to keep its same traits (stickied, locked, etc.)
+ $post['mod'] = true;
+
+ if (!openBoard($targetBoard))
+ error($config['error']['noboard']);
+
+ // create the new post
+ $newID = post($post);
+
+ if ($post['has_file']) {
+ // move the image
+ rename($file_src, sprintf($config['board_path'], $board['uri']) . $config['dir']['img'] . $post['file']);
+ if ($post['thumb'] != 'spoiler') { //trying to move/copy the spoiler thumb raises an error
+ rename($file_thumb, sprintf($config['board_path'], $board['uri']) . $config['dir']['thumb'] . $post['thumb']);
+ }
+ }
+
+ buildIndex();
+
+ // trigger themes
+ rebuildThemes($post['op'] ? 'post-thread' : 'post');
+ // mod log
+ modLog("Moved post #${postID} to " . sprintf($config['board_abbreviation'], $targetBoard) . " (#${newID})", $originBoard);
+
+ // return to original board
+ openBoard($originBoard);
+
+ // delete original post
+ deletePost($postID);
+ buildIndex();
+
+ // open target board for redirect
+ openBoard($targetBoard);
+
+ // Find new thread on our target board
+ $query = prepare(sprintf('SELECT thread FROM ``posts_%s`` WHERE `id` = :id', $targetBoard));
+ $query->bindValue(':id', $newID);
+ $query->execute() or error(db_error($query));
+ $post = $query->fetch(PDO::FETCH_ASSOC);
+
+ // redirect
+ header('Location: ?/' . sprintf($config['board_path'], $board['uri']) . $config['dir']['res'] . sprintf($config['file_page'], $post['thread'] ? $post['thread'] : $newID) . '#' . $newID, true, $config['redirect_http']);
+ }
+
+ else {
+ $boards = listBoards();
+
+ $security_token = make_secure_link_token($originBoard . '/move_reply/' . $postID);
+
+ mod_page(_('Move reply'), 'mod/move_reply.html', array('post' => $postID, 'board' => $originBoard, 'boards' => $boards, 'token' => $security_token));
+
+ }
+
+}
+
function mod_move($originBoard, $postID) {
global $board, $config, $mod;
diff --git a/install.php b/install.php
index a510631e..8bef9e11 100644
--- a/install.php
+++ b/install.php
@@ -1,7 +1,7 @@
<?php
// Installation/upgrade file
-define('VERSION', 'v0.9.6-dev-12');
+define('VERSION', 'v0.9.6-dev-12 + <a href="https://int.vichan.net/devel/">vichan-devel-4.0.10</a>');
require 'inc/functions.php';
@@ -237,8 +237,12 @@ if (file_exists($config['has_installed'])) {
query(sprintf("ALTER TABLE `posts_%s` DROP INDEX `thread`", $_board['uri'])) or error(db_error());
}
case 'v0.9.6-dev-7':
+ case 'v0.9.6-dev-7 + <a href="https://github.com/vichan-devel/Tinyboard/">vichan-devel-4.0-gold</a>':
query("ALTER TABLE `bans` ADD `seen` BOOLEAN NOT NULL") or error(db_error());
case 'v0.9.6-dev-8':
+ case 'v0.9.6-dev-8 + <a href="https://github.com/vichan-devel/Tinyboard/">vichan-devel-4.0.1</a>':
+ query("CREATE TABLE IF NOT EXISTS `search_queries` ( `ip` varchar(39) NOT NULL, `time` int(11) NOT NULL, `query` text NOT NULL) ENGINE=MyISAM DEFAULT CHARSET=utf8;") or error(db_error());
+ case 'v0.9.6-dev-8 + <a href="https://github.com/vichan-devel/Tinyboard/">vichan-devel-4.0.2</a>':
query("ALTER TABLE `mods` CHANGE `password` `password` CHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'SHA256'") or error(db_error());
query("ALTER TABLE `mods` ADD `salt` CHAR( 32 ) NOT NULL AFTER `password`") or error(db_error());
$query = query("SELECT `id`,`password` FROM `mods`") or error(db_error());
@@ -257,7 +261,10 @@ if (file_exists($config['has_installed'])) {
$_query->execute() or error(db_error($_query));
}
}
- case 'v0.9.6-dev-9':
+ case 'v0.9.6-dev-9':
+ case 'v0.9.6-dev-9 + <a href="https://github.com/vichan-devel/Tinyboard/">vichan-devel-4.0.3</a>':
+ case 'v0.9.6-dev-9 + <a href="https://github.com/vichan-devel/Tinyboard/">vichan-devel-4.0.4-gold</a>':
+ case 'v0.9.6-dev-9 + <a href="https://github.com/vichan-devel/Tinyboard/">vichan-devel-4.0.5-gold</a>':
foreach ($boards as &$board) {
__query(sprintf("ALTER TABLE `posts_%s`
CHANGE `subject` `subject` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
@@ -358,12 +365,17 @@ if (file_exists($config['has_installed'])) {
query("ALTER TABLE `reports`
CHANGE `board` `board` VARCHAR( 58 ) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL;") or error(db_error());
case 'v0.9.6-dev-11':
+ case 'v0.9.6-dev-11 + <a href="https://int.vichan.net/devel/">vichan-devel-4.0.6</a>':
+ case 'v0.9.6-dev-11 + <a href="https://int.vichan.net/devel/">vichan-devel-4.0.7-gold</a>':
+ case 'v0.9.6-dev-11 + <a href="https://int.vichan.net/devel/">vichan-devel-4.0.8-gold</a>':
+ case 'v0.9.6-dev-11 + <a href="https://int.vichan.net/devel/">vichan-devel-4.0.9-gold</a>':
foreach ($boards as &$board) {
__query(sprintf("ALTER TABLE ``posts_%s``
CHANGE `thumb` `thumb` VARCHAR( 255 ) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
CHANGE `file` `file` VARCHAR( 255 ) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL ;",
$board['uri'])) or error(db_error());
}
+ case 'v0.9.6-dev-12':
case false:
// Update version number
file_write($config['has_installed'], VERSION);
diff --git a/install.sql b/install.sql
index db89452b..84090999 100644
--- a/install.sql
+++ b/install.sql
@@ -230,6 +230,18 @@ CREATE TABLE IF NOT EXISTS `robot` (
-- --------------------------------------------------------
--
+-- Table structure for table `search_queries`
+--
+
+CREATE TABLE IF NOT EXISTS `search_queries` (
+ `ip` varchar(39) NOT NULL,
+ `time` int(11) NOT NULL,
+ `query` text NOT NULL
+) ENGINE=MyISAM DEFAULT CHARSET=utf8;
+
+-- --------------------------------------------------------
+
+--
-- Table structure for table `theme_settings`
--
diff --git a/js/attention-bar.js b/js/attention-bar.js
new file mode 100644
index 00000000..a84cadff
--- /dev/null
+++ b/js/attention-bar.js
@@ -0,0 +1,8 @@
+$(document).ready(function(){
+ $("#attention_bar").click(function(eO){ $("#attention_bar").css("display","none");
+ $("#attention_bar_form").css("display","block"); });
+ $.get(configRoot + "attentionbar.txt", function(data) {
+ $("#attention_bar").text(data);
+ $("#attention_bar_input").val(data);
+ });
+});
diff --git a/js/catalog-link.js b/js/catalog-link.js
index cc55eb52..3d11fb89 100644
--- a/js/catalog-link.js
+++ b/js/catalog-link.js
@@ -3,8 +3,13 @@
function catalog() {
var board = $("input[name='board']");
-if (board) {
+if (board.length>0) {
+if (window.location.href.indexOf("/res/")==-1){ //if we are inside a thread
var catalog_url = 'catalog.html';
+}
+else {
+var catalog_url = '../catalog.html';
+}
var pages = document.getElementsByClassName('pages')[0];
var bottom = document.getElementsByClassName('boardlist bottom')[0]
var subtitle = document.getElementsByClassName('subtitle')[0];
@@ -13,7 +18,7 @@ var link = document.createElement('a');
link.href = catalog_url;
if (pages) {
- link.textContent = 'Catalog';
+ link.textContent = _('Catalog');
link.style.color = '#F10000';
link.style.padding = '4px';
link.style.paddingLeft = '9px';
@@ -24,7 +29,7 @@ if (pages) {
pages.appendChild(link)
}
else {
- link.textContent = '[Catalog]';
+ link.textContent = '['+_('Catalog')+']';
link.style.paddingLeft = '10px';
link.style.textDecoration = "underline";
document.body.insertBefore(link, bottom);
@@ -32,7 +37,7 @@ else {
if (subtitle) {
var link2 = document.createElement('a');
- link2.textContent = 'Catalog';
+ link2.textContent = _('Catalog');
link2.href = catalog_url;
var br = document.createElement('br');
@@ -41,4 +46,7 @@ if (subtitle) {
}
}
}
-$(document).ready(catalog);
+
+if (active_page != 'ukko') {
+ $(document).ready(catalog);
+}
diff --git a/js/chrome-board-expanding-fix.js b/js/chrome-board-expanding-fix.js
new file mode 100644
index 00000000..f0a5930a
--- /dev/null
+++ b/js/chrome-board-expanding-fix.js
@@ -0,0 +1,7 @@
+$(document).ready(function(){
+ $(".desktop-style .sub .sub").on("mouseover", function() {
+ $(this).addClass("hover");
+ }).on("mouseout", function() {
+ $(this).removeClass("hover");
+ });
+});
diff --git a/js/expand.js b/js/expand.js
index 07ce9e43..a25b79ac 100644
--- a/js/expand.js
+++ b/js/expand.js
@@ -12,10 +12,9 @@
*/
$(document).ready(function(){
- if($('div.banner').length != 0)
- return; // not index
- $('div.post.op span.omitted').each(function() {
-
+ if($('span.omitted').length == 0)
+ return; // nothing to expand
+
var do_expand = function() {
$(this)
.html($(this).text().replace(_("Click reply to view."), '<a href="javascript:void(0)">'+_("Click to expand")+'</a>.'))
@@ -28,6 +27,7 @@ $(document).ready(function(){
success: function(data) {
var last_expanded = false;
$(data).find('div.post.reply').each(function() {
+ thread.find('.hidden').remove();
var post_in_doc = thread.find('#' + $(this).attr('id'));
if(post_in_doc.length == 0) {
if(last_expanded) {
diff --git a/js/hide-threads.js b/js/hide-threads.js
index 575804b6..44bd829b 100644
--- a/js/hide-threads.js
+++ b/js/hide-threads.js
@@ -12,11 +12,9 @@
*/
$(document).ready(function(){
- if($('div.banner').length != 0)
+ if (active_page != "index" && active_page != "ukko")
return; // not index
-
- var board = $('form input[name="board"]').val().toString();
-
+
if (!localStorage.hiddenthreads)
localStorage.hiddenthreads = '{}';
@@ -37,13 +35,16 @@ $(document).ready(function(){
}
}
- if (!hidden_data[board]) {
- hidden_data[board] = {}; // id : timestamp
- }
-
var do_hide_threads = function() {
var id = $(this).children('p.intro').children('a.post_no:eq(1)').text();
var thread_container = $(this).parent();
+
+ var board = thread_container.data("board");
+
+ if (!hidden_data[board]) {
+ hidden_data[board] = {}; // id : timestamp
+ }
+
$('<a class="hide-thread-link" style="float:left;margin-right:5px" href="javascript:void(0)">[–]</a><span> </span>')
.insertBefore(thread_container.find(':not(h2,h2 *):first'))
.click(function() {
diff --git a/js/inline-expanding-filename.js b/js/inline-expanding-filename.js
new file mode 100644
index 00000000..048a0e1a
--- /dev/null
+++ b/js/inline-expanding-filename.js
@@ -0,0 +1,33 @@
+/*
+ * inline-expanding-filename.js
+ * Binds image filename link to expanding, to make kusaba x users somewhat more accustomed.
+ * https://github.com/vichan-devel/Tinyboard/blob/master/js/inline-expanding.js
+ *
+ * Released under the MIT license
+ * Copyright (c) 2012-2013 Marcin Łabanowski <marcin@6irc.net>
+ *
+ * Usage:
+ * $config['additional_javascript'][] = 'js/jquery.min.js';
+ * $config['additional_javascript'][] = 'js/inline-expanding.js';
+ * $config['additional_javascript'][] = 'js/inline-expanding-filename.js';
+ *
+ */
+
+onready(function(){
+ var inline_expanding_filename = function() {
+ $(this).find(".fileinfo > a").click(function(){
+ var imagelink = $(this).parent().parent().find('a[target="_blank"]:first');
+ if(imagelink.length > 0) {
+ imagelink.click();
+ return false;
+ }
+ });
+ };
+
+ $('div[id^="thread_"]').each(inline_expanding_filename);
+
+ // allow to work with auto-reload.js, etc.
+ $(document).bind('new_post', function(e, post) {
+ inline_expanding_filename.call(post);
+ });
+});
diff --git a/js/inline-expanding.js b/js/inline-expanding.js
index ed5a57b5..2afb61bf 100644
--- a/js/inline-expanding.js
+++ b/js/inline-expanding.js
@@ -62,5 +62,4 @@ onready(function(){
} else {
inline_expand_post.call(document);
}
-
});
diff --git a/js/jscolor/arrow.gif b/js/jscolor/arrow.gif
new file mode 100644
index 00000000..246478a8
--- /dev/null
+++ b/js/jscolor/arrow.gif
Binary files differ
diff --git a/js/jscolor/cross.gif b/js/jscolor/cross.gif
new file mode 100644
index 00000000..0ee9c7ac
--- /dev/null
+++ b/js/jscolor/cross.gif
Binary files differ
diff --git a/js/jscolor/hs.png b/js/jscolor/hs.png
new file mode 100644
index 00000000..3d94486c
--- /dev/null
+++ b/js/jscolor/hs.png
Binary files differ
diff --git a/js/jscolor/hv.png b/js/jscolor/hv.png
new file mode 100644
index 00000000..1c5e01f8
--- /dev/null
+++ b/js/jscolor/hv.png
Binary files differ
diff --git a/js/jscolor/jscolor.js b/js/jscolor/jscolor.js
new file mode 100644
index 00000000..712ab9a3
--- /dev/null
+++ b/js/jscolor/jscolor.js
@@ -0,0 +1,995 @@
+/**
+ * jscolor, JavaScript Color Picker
+ *
+ * @version 1.4.1
+ * @license GNU Lesser General Public License, http://www.gnu.org/copyleft/lesser.html
+ * @author Jan Odvarko, http://odvarko.cz
+ * @created 2008-06-15
+ * @updated 2013-04-08
+ * @link http://jscolor.com
+ */
+
+
+var jscolor = {
+
+
+ dir : '', // location of jscolor directory (leave empty to autodetect)
+ bindClass : 'color', // class name
+ binding : true, // automatic binding via <input class="...">
+ preloading : true, // use image preloading?
+
+
+ install : function() {
+ jscolor.addEvent(window, 'load', jscolor.init);
+ },
+
+
+ init : function() {
+ if(jscolor.binding) {
+ jscolor.bind();
+ }
+ if(jscolor.preloading) {
+ jscolor.preload();
+ }
+ },
+
+
+ getDir : function() {
+ if(!jscolor.dir) {
+ var detected = jscolor.detectDir();
+ jscolor.dir = detected!==false ? detected : 'jscolor/';
+ }
+ return jscolor.dir;
+ },
+
+
+ detectDir : function() {
+ var base = location.href;
+
+ var e = document.getElementsByTagName('base');
+ for(var i=0; i<e.length; i+=1) {
+ if(e[i].href) { base = e[i].href; }
+ }
+
+ var e = document.getElementsByTagName('script');
+ for(var i=0; i<e.length; i+=1) {
+ if(e[i].src && /(^|\/)jscolor\.js([?#].*)?$/i.test(e[i].src)) {
+ var src = new jscolor.URI(e[i].src);
+ var srcAbs = src.toAbsolute(base);
+ srcAbs.path = srcAbs.path.replace(/[^\/]+$/, ''); // remove filename
+ srcAbs.query = null;
+ srcAbs.fragment = null;
+ return srcAbs.toString();
+ }
+ }
+ return false;
+ },
+
+
+ bind : function() {
+ var matchClass = new RegExp('(^|\\s)('+jscolor.bindClass+')\\s*(\\{[^}]*\\})?', 'i');
+ var e = document.getElementsByTagName('input');
+ for(var i=0; i<e.length; i+=1) {
+ var m;
+ if(!e[i].color && e[i].className && (m = e[i].className.match(matchClass))) {
+ var prop = {};
+ if(m[3]) {
+ try {
+ prop = (new Function ('return (' + m[3] + ')'))();
+ } catch(eInvalidProp) {}
+ }
+ e[i].color = new jscolor.color(e[i], prop);
+ }
+ }
+ },
+
+
+ preload : function() {
+ for(var fn in jscolor.imgRequire) {
+ if(jscolor.imgRequire.hasOwnProperty(fn)) {
+ jscolor.loadImage(fn);
+ }
+ }
+ },
+
+
+ images : {
+ pad : [ 181, 101 ],
+ sld : [ 16, 101 ],
+ cross : [ 15, 15 ],
+ arrow : [ 7, 11 ]
+ },
+
+
+ imgRequire : {},
+ imgLoaded : {},
+
+
+ requireImage : function(filename) {
+ jscolor.imgRequire[filename] = true;
+ },
+
+
+ loadImage : function(filename) {
+ if(!jscolor.imgLoaded[filename]) {
+ jscolor.imgLoaded[filename] = new Image();
+ jscolor.imgLoaded[filename].src = jscolor.getDir()+filename;
+ }
+ },
+
+
+ fetchElement : function(mixed) {
+ return typeof mixed === 'string' ? document.getElementById(mixed) : mixed;
+ },
+
+
+ addEvent : function(el, evnt, func) {
+ if(el.addEventListener) {
+ el.addEventListener(evnt, func, false);
+ } else if(el.attachEvent) {
+ el.attachEvent('on'+evnt, func);
+ }
+ },
+
+
+ fireEvent : function(el, evnt) {
+ if(!el) {
+ return;
+ }
+ if(document.createEvent) {
+ var ev = document.createEvent('HTMLEvents');
+ ev.initEvent(evnt, true, true);
+ el.dispatchEvent(ev);
+ } else if(document.createEventObject) {
+ var ev = document.createEventObject();
+ el.fireEvent('on'+evnt, ev);
+ } else if(el['on'+evnt]) { // alternatively use the traditional event model (IE5)
+ el['on'+evnt]();
+ }
+ },
+
+
+ getElementPos : function(e) {
+ var e1=e, e2=e;
+ var x=0, y=0;
+ if(e1.offsetParent) {
+ do {
+ x += e1.offsetLeft;
+ y += e1.offsetTop;
+ } while(e1 = e1.offsetParent);
+ }
+ while((e2 = e2.parentNode) && e2.nodeName.toUpperCase() !== 'BODY') {
+ x -= e2.scrollLeft;
+ y -= e2.scrollTop;
+ }
+ return [x, y];
+ },
+
+
+ getElementSize : function(e) {
+ return [e.offsetWidth, e.offsetHeight];
+ },
+
+
+ getRelMousePos : function(e) {
+ var x = 0, y = 0;
+ if (!e) { e = window.event; }
+ if (typeof e.offsetX === 'number') {
+ x = e.offsetX;
+ y = e.offsetY;
+ } else if (typeof e.layerX === 'number') {
+ x = e.layerX;
+ y = e.layerY;
+ }
+ return { x: x, y: y };
+ },
+
+
+ getViewPos : function() {
+ if(typeof window.pageYOffset === 'number') {
+ return [window.pageXOffset, window.pageYOffset];
+ } else if(document.body && (document.body.scrollLeft || document.body.scrollTop)) {
+ return [document.body.scrollLeft, document.body.scrollTop];
+ } else if(document.documentElement && (document.documentElement.scrollLeft || document.documentElement.scrollTop)) {
+ return [document.documentElement.scrollLeft, document.documentElement.scrollTop];
+ } else {
+ return [0, 0];
+ }
+ },
+
+
+ getViewSize : function() {
+ if(typeof window.innerWidth === 'number') {
+ return [window.innerWidth, window.innerHeight];
+ } else if(document.body && (document.body.clientWidth || document.body.clientHeight)) {
+ return [document.body.clientWidth, document.body.clientHeight];
+ } else if(document.documentElement && (document.documentElement.clientWidth || document.documentElement.clientHeight)) {
+ return [document.documentElement.clientWidth, document.documentElement.clientHeight];
+ } else {
+ return [0, 0];
+ }
+ },
+
+
+ URI : function(uri) { // See RFC3986
+
+ this.scheme = null;
+ this.authority = null;
+ this.path = '';
+ this.query = null;
+ this.fragment = null;
+
+ this.parse = function(uri) {
+ var m = uri.match(/^(([A-Za-z][0-9A-Za-z+.-]*)(:))?((\/\/)([^\/?#]*))?([^?#]*)((\?)([^#]*))?((#)(.*))?/);
+ this.scheme = m[3] ? m[2] : null;
+ this.authority = m[5] ? m[6] : null;
+ this.path = m[7];
+ this.query = m[9] ? m[10] : null;
+ this.fragment = m[12] ? m[13] : null;
+ return this;
+ };
+
+ this.toString = function() {
+ var result = '';
+ if(this.scheme !== null) { result = result + this.scheme + ':'; }
+ if(this.authority !== null) { result = result + '//' + this.authority; }
+ if(this.path !== null) { result = result + this.path; }
+ if(this.query !== null) { result = result + '?' + this.query; }
+ if(this.fragment !== null) { result = result + '#' + this.fragment; }
+ return result;
+ };
+
+ this.toAbsolute = function(base) {
+ var base = new jscolor.URI(base);
+ var r = this;
+ var t = new jscolor.URI;
+
+ if(base.scheme === null) { return false; }
+
+ if(r.scheme !== null && r.scheme.toLowerCase() === base.scheme.toLowerCase()) {
+ r.scheme = null;
+ }
+
+ if(r.scheme !== null) {
+ t.scheme = r.scheme;
+ t.authority = r.authority;
+ t.path = removeDotSegments(r.path);
+ t.query = r.query;
+ } else {
+ if(r.authority !== null) {
+ t.authority = r.authority;
+ t.path = removeDotSegments(r.path);
+ t.query = r.query;
+ } else {
+ if(r.path === '') {
+ t.path = base.path;
+ if(r.query !== null) {
+ t.query = r.query;
+ } else {
+ t.query = base.query;
+ }
+ } else {
+ if(r.path.substr(0,1) === '/') {
+ t.path = removeDotSegments(r.path);
+ } else {
+ if(base.authority !== null && base.path === '') {
+ t.path = '/'+r.path;
+ } else {
+ t.path = base.path.replace(/[^\/]+$/,'')+r.path;
+ }
+ t.path = removeDotSegments(t.path);
+ }
+ t.query = r.query;
+ }
+ t.authority = base.authority;
+ }
+ t.scheme = base.scheme;
+ }
+ t.fragment = r.fragment;
+
+ return t;
+ };
+
+ function removeDotSegments(path) {
+ var out = '';
+ while(path) {
+ if(path.substr(0,3)==='../' || path.substr(0,2)==='./') {
+ path = path.replace(/^\.+/,'').substr(1);
+ } else if(path.substr(0,3)==='/./' || path==='/.') {
+ path = '/'+path.substr(3);
+ } else if(path.substr(0,4)==='/../' || path==='/..') {
+ path = '/'+path.substr(4);
+ out = out.replace(/\/?[^\/]*$/, '');
+ } else if(path==='.' || path==='..') {
+ path = '';
+ } else {
+ var rm = path.match(/^\/?[^\/]*/)[0];
+ path = path.substr(rm.length);
+ out = out + rm;
+ }
+ }
+ return out;
+ }
+
+ if(uri) {
+ this.parse(uri);
+ }
+
+ },
+
+
+ //
+ // Usage example:
+ // var myColor = new jscolor.color(myInputElement)
+ //
+
+ color : function(target, prop) {
+
+
+ this.required = true; // refuse empty values?
+ this.adjust = true; // adjust value to uniform notation?
+ this.hash = false; // prefix color with # symbol?
+ this.caps = true; // uppercase?
+ this.slider = true; // show the value/saturation slider?
+ this.valueElement = target; // value holder
+ this.styleElement = target; // where to reflect current color
+ this.onImmediateChange = null; // onchange callback (can be either string or function)
+ this.hsv = [0, 0, 1]; // read-only 0-6, 0-1, 0-1
+ this.rgb = [1, 1, 1]; // read-only 0-1, 0-1, 0-1
+ this.minH = 0; // read-only 0-6
+ this.maxH = 6; // read-only 0-6
+ this.minS = 0; // read-only 0-1
+ this.maxS = 1; // read-only 0-1
+ this.minV = 0; // read-only 0-1
+ this.maxV = 1; // read-only 0-1
+
+ this.pickerOnfocus = true; // display picker on focus?
+ this.pickerMode = 'HSV'; // HSV | HVS
+ this.pickerPosition = 'bottom'; // left | right | top | bottom
+ this.pickerSmartPosition = true; // automatically adjust picker position when necessary
+ this.pickerButtonHeight = 20; // px
+ this.pickerClosable = false;
+ this.pickerCloseText = 'Close';
+ this.pickerButtonColor = 'ButtonText'; // px
+ this.pickerFace = 10; // px
+ this.pickerFaceColor = 'ThreeDFace'; // CSS color
+ this.pickerBorder = 1; // px
+ this.pickerBorderColor = 'ThreeDHighlight ThreeDShadow ThreeDShadow ThreeDHighlight'; // CSS color
+ this.pickerInset = 1; // px
+ this.pickerInsetColor = 'ThreeDShadow ThreeDHighlight ThreeDHighlight ThreeDShadow'; // CSS color
+ this.pickerZIndex = 10000;
+
+
+ for(var p in prop) {
+ if(prop.hasOwnProperty(p)) {
+ this[p] = prop[p];
+ }
+ }
+
+
+ this.hidePicker = function() {
+ if(isPickerOwner()) {
+ removePicker();
+ }
+ };
+
+
+ this.showPicker = function() {
+ if(!isPickerOwner()) {
+ var tp = jscolor.getElementPos(target); // target pos
+ var ts = jscolor.getElementSize(target); // target size
+ var vp = jscolor.getViewPos(); // view pos
+ var vs = jscolor.getViewSize(); // view size
+ var ps = getPickerDims(this); // picker size
+ var a, b, c;
+ switch(this.pickerPosition.toLowerCase()) {
+ case 'left': a=1; b=0; c=-1; break;
+ case 'right':a=1; b=0; c=1; break;
+ case 'top': a=0; b=1; c=-1; break;
+ default: a=0; b=1; c=1; break;
+ }
+ var l = (ts[b]+ps[b])/2;
+
+ // picker pos
+ if (!this.pickerSmartPosition) {
+ var pp = [
+ tp[a],
+ tp[b]+ts[b]-l+l*c
+ ];
+ } else {
+ var pp = [
+ -vp[a]+tp[a]+ps[a] > vs[a] ?
+ (-vp[a]+tp[a]+ts[a]/2 > vs[a]/2 && tp[a]+ts[a]-ps[a] >= 0 ? tp[a]+ts[a]-ps[a] : tp[a]) :
+ tp[a],
+ -vp[b]+tp[b]+ts[b]+ps[b]-l+l*c > vs[b] ?
+ (-vp[b]+tp[b]+ts[b]/2 > vs[b]/2 && tp[b]+ts[b]-l-l*c >= 0 ? tp[b]+ts[b]-l-l*c : tp[b]+ts[b]-l+l*c) :
+ (tp[b]+ts[b]-l+l*c >= 0 ? tp[b]+ts[b]-l+l*c : tp[b]+ts[b]-l-l*c)
+ ];
+ }
+ drawPicker(pp[a], pp[b]);
+ }
+ };
+
+
+ this.importColor = function() {
+ if(!valueElement) {
+ this.exportColor();
+ } else {
+ if(!this.adjust) {
+ if(!this.fromString(valueElement.value, leaveValue)) {
+ styleElement.style.backgroundImage = styleElement.jscStyle.backgroundImage;
+ styleElement.style.backgroundColor = styleElement.jscStyle.backgroundColor;
+ styleElement.style.color = styleElement.jscStyle.color;
+ this.exportColor(leaveValue | leaveStyle);
+ }
+ } else if(!this.required && /^\s*$/.test(valueElement.value)) {
+ valueElement.value = '';
+ styleElement.style.backgroundImage = styleElement.jscStyle.backgroundImage;
+ styleElement.style.backgroundColor = styleElement.jscStyle.backgroundColor;
+ styleElement.style.color = styleElement.jscStyle.color;
+ this.exportColor(leaveValue | leaveStyle);
+
+ } else if(this.fromString(valueElement.value)) {
+ // OK
+ } else {
+ this.exportColor();
+ }
+ }
+ };
+
+
+ this.exportColor = function(flags) {
+ if(!(flags & leaveValue) && valueElement) {
+ var value = this.toString();
+ if(this.caps) { value = value.toUpperCase(); }
+ if(this.hash) { value = '#'+value; }
+ valueElement.value = value;
+ }
+ if(!(flags & leaveStyle) && styleElement) {
+ styleElement.style.backgroundImage = "none";
+ styleElement.style.backgroundColor =
+ '#'+this.toString();
+ styleElement.style.color =
+ 0.213 * this.rgb[0] +
+ 0.715 * this.rgb[1] +
+ 0.072 * this.rgb[2]
+ < 0.5 ? '#FFF' : '#000';
+ }
+ if(!(flags & leavePad) && isPickerOwner()) {
+ redrawPad();
+ }
+ if(!(flags & leaveSld) && isPickerOwner()) {
+ redrawSld();
+ }
+ };
+
+
+ this.fromHSV = function(h, s, v, flags) { // null = don't change
+ if(h !== null) { h = Math.max(0.0, this.minH, Math.min(6.0, this.maxH, h)); }
+ if(s !== null) { s = Math.max(0.0, this.minS, Math.min(1.0, this.maxS, s)); }
+ if(v !== null) { v = Math.max(0.0, this.minV, Math.min(1.0, this.maxV, v)); }
+
+ this.rgb = HSV_RGB(
+ h===null ? this.hsv[0] : (this.hsv[0]=h),
+ s===null ? this.hsv[1] : (this.hsv[1]=s),
+ v===null ? this.hsv[2] : (this.hsv[2]=v)
+ );
+
+ this.exportColor(flags);
+ };
+
+
+ this.fromRGB = function(r, g, b, flags) { // null = don't change
+ if(r !== null) { r = Math.max(0.0, Math.min(1.0, r)); }
+ if(g !== null) { g = Math.max(0.0, Math.min(1.0, g)); }
+ if(b !== null) { b = Math.max(0.0, Math.min(1.0, b)); }
+
+ var hsv = RGB_HSV(
+ r===null ? this.rgb[0] : r,
+ g===null ? this.rgb[1] : g,
+ b===null ? this.rgb[2] : b
+ );
+ if(hsv[0] !== null) {
+ this.hsv[0] = Math.max(0.0, this.minH, Math.min(6.0, this.maxH, hsv[0]));
+ }
+ if(hsv[2] !== 0) {
+ this.hsv[1] = hsv[1]===null ? null : Math.max(0.0, this.minS, Math.min(1.0, this.maxS, hsv[1]));
+ }
+ this.hsv[2] = hsv[2]===null ? null : Math.max(0.0, this.minV, Math.min(1.0, this.maxV, hsv[2]));
+
+ // update RGB according to final HSV, as some values might be trimmed
+ var rgb = HSV_RGB(this.hsv[0], this.hsv[1], this.hsv[2]);
+ this.rgb[0] = rgb[0];
+ this.rgb[1] = rgb[1];
+ this.rgb[2] = rgb[2];
+
+ this.exportColor(flags);
+ };
+
+
+ this.fromString = function(hex, flags) {
+ var m = hex.match(/^\W*([0-9A-F]{3}([0-9A-F]{3})?)\W*$/i);
+ if(!m) {
+ return false;
+ } else {
+ if(m[1].length === 6) { // 6-char notation
+ this.fromRGB(
+ parseInt(m[1].substr(0,2),16) / 255,
+ parseInt(m[1].substr(2,2),16) / 255,
+ parseInt(m[1].substr(4,2),16) / 255,
+ flags
+ );
+ } else { // 3-char notation
+ this.fromRGB(
+ parseInt(m[1].charAt(0)+m[1].charAt(0),16) / 255,
+ parseInt(m[1].charAt(1)+m[1].charAt(1),16) / 255,
+ parseInt(m[1].charAt(2)+m[1].charAt(2),16) / 255,
+ flags
+ );
+ }
+ return true;
+ }
+ };
+
+
+ this.toString = function() {
+ return (
+ (0x100 | Math.round(255*this.rgb[0])).toString(16).substr(1) +
+ (0x100 | Math.round(255*this.rgb[1])).toString(16).substr(1) +
+ (0x100 | Math.round(255*this.rgb[2])).toString(16).substr(1)
+ );
+ };
+
+
+ function RGB_HSV(r, g, b) {
+ var n = Math.min(Math.min(r,g),b);
+ var v = Math.max(Math.max(r,g),b);
+ var m = v - n;
+ if(m === 0) { return [ null, 0, v ]; }
+ var h = r===n ? 3+(b-g)/m : (g===n ? 5+(r-b)/m : 1+(g-r)/m);
+ return [ h===6?0:h, m/v, v ];
+ }
+
+
+ function HSV_RGB(h, s, v) {
+ if(h === null) { return [ v, v, v ]; }
+ var i = Math.floor(h);
+ var f = i%2 ? h-i : 1-(h-i);
+ var m = v * (1 - s);
+ var n = v * (1 - s*f);
+ switch(i) {
+ case 6:
+ case 0: return [v,n,m];
+ case 1: return [n,v,m];
+ case 2: return [m,v,n];
+ case 3: return [m,n,v];
+ case 4: return [n,m,v];
+ case 5: return [v,m,n];
+ }
+ }
+
+
+ function removePicker() {
+ delete jscolor.picker.owner;
+ document.getElementsByTagName('body')[0].removeChild(jscolor.picker.boxB);
+ }
+
+
+ function drawPicker(x, y) {
+ if(!jscolor.picker) {
+ jscolor.picker = {
+ box : document.createElement('div'),
+ boxB : document.createElement('div'),
+ pad : document.createElement('div'),
+ padB : document.createElement('div'),
+ padM : document.createElement('div'),
+ sld : document.createElement('div'),
+ sldB : document.createElement('div'),
+ sldM : document.createElement('div'),
+ btn : document.createElement('div'),
+ btnS : document.createElement('span'),
+ btnT : document.createTextNode(THIS.pickerCloseText)
+ };
+ for(var i=0,segSize=4; i<jscolor.images.sld[1]; i+=segSize) {
+ var seg = document.createElement('div');
+ seg.style.height = segSize+'px';
+ seg.style.fontSize = '1px';
+ seg.style.lineHeight = '0';
+ jscolor.picker.sld.appendChild(seg);
+ }
+ jscolor.picker.sldB.appendChild(jscolor.picker.sld);
+ jscolor.picker.box.appendChild(jscolor.picker.sldB);
+ jscolor.picker.box.appendChild(jscolor.picker.sldM);
+ jscolor.picker.padB.appendChild(jscolor.picker.pad);
+ jscolor.picker.box.appendChild(jscolor.picker.padB);
+ jscolor.picker.box.appendChild(jscolor.picker.padM);
+ jscolor.picker.btnS.appendChild(jscolor.picker.btnT);
+ jscolor.picker.btn.appendChild(jscolor.picker.btnS);
+ jscolor.picker.box.appendChild(jscolor.picker.btn);
+ jscolor.picker.boxB.appendChild(jscolor.picker.box);
+ }
+
+ var p = jscolor.picker;
+
+ // controls interaction
+ p.box.onmouseup =
+ p.box.onmouseout = function() { target.focus(); };
+ p.box.onmousedown = function() { abortBlur=true; };
+ p.box.onmousemove = function(e) {
+ if (holdPad || holdSld) {
+ holdPad && setPad(e);
+ holdSld && setSld(e);
+ if (document.selection) {
+ document.selection.empty();
+ } else if (window.getSelection) {
+ window.getSelection().removeAllRanges();
+ }
+ dispatchImmediateChange();
+ }
+ };
+ if('ontouchstart' in window) { // if touch device
+ p.box.addEventListener('touchmove', function(e) {
+ var event={
+ 'offsetX': e.touches[0].pageX-touchOffset.X,
+ 'offsetY': e.touches[0].pageY-touchOffset.Y
+ };
+ if (holdPad || holdSld) {
+ holdPad && setPad(event);
+ holdSld && setSld(event);
+ dispatchImmediateChange();
+ }
+ e.stopPropagation(); // prevent move "view" on broswer
+ e.preventDefault(); // prevent Default - Android Fix (else android generated only 1-2 touchmove events)
+ }, false);
+ }
+ p.padM.onmouseup =
+ p.padM.onmouseout = function() { if(holdPad) { holdPad=false; jscolor.fireEvent(valueElement,'change'); } };
+ p.padM.onmousedown = function(e) {
+ // if the slider is at the bottom, move it up
+ switch(modeID) {
+ case 0: if (THIS.hsv[2] === 0) { THIS.fromHSV(null, null, 1.0); }; break;
+ case 1: if (THIS.hsv[1] === 0) { THIS.fromHSV(null, 1.0, null); }; break;
+ }
+ holdSld=false;
+ holdPad=true;
+ setPad(e);
+ dispatchImmediateChange();
+ };
+ if('ontouchstart' in window) {
+ p.padM.addEventListener('touchstart', function(e) {
+ touchOffset={
+ 'X': e.target.offsetParent.offsetLeft,
+ 'Y': e.target.offsetParent.offsetTop
+ };
+ this.onmousedown({
+ 'offsetX':e.touches[0].pageX-touchOffset.X,
+ 'offsetY':e.touches[0].pageY-touchOffset.Y
+ });
+ });
+ }
+ p.sldM.onmouseup =
+ p.sldM.onmouseout = function() { if(holdSld) { holdSld=false; jscolor.fireEvent(valueElement,'change'); } };
+ p.sldM.onmousedown = function(e) {
+ holdPad=false;
+ holdSld=true;
+ setSld(e);
+ dispatchImmediateChange();
+ };
+ if('ontouchstart' in window) {
+ p.sldM.addEventListener('touchstart', function(e) {
+ touchOffset={
+ 'X': e.target.offsetParent.offsetLeft,
+ 'Y': e.target.offsetParent.offsetTop
+ };
+ this.onmousedown({
+ 'offsetX':e.touches[0].pageX-touchOffset.X,
+ 'offsetY':e.touches[0].pageY-touchOffset.Y
+ });
+ });
+ }
+
+ // picker
+ var dims = getPickerDims(THIS);
+ p.box.style.width = dims[0] + 'px';
+ p.box.style.height = dims[1] + 'px';
+
+ // picker border
+ p.boxB.style.position = 'absolute';
+ p.boxB.style.clear = 'both';
+ p.boxB.style.left = x+'px';
+ p.boxB.style.top = y+'px';
+ p.boxB.style.zIndex = THIS.pickerZIndex;
+ p.boxB.style.border = THIS.pickerBorder+'px solid';
+ p.boxB.style.borderColor = THIS.pickerBorderColor;
+ p.boxB.style.background = THIS.pickerFaceColor;
+
+ // pad image
+ p.pad.style.width = jscolor.images.pad[0]+'px';
+ p.pad.style.height = jscolor.images.pad[1]+'px';
+
+ // pad border
+ p.padB.style.position = 'absolute';
+ p.padB.style.left = THIS.pickerFace+'px';
+ p.padB.style.top = THIS.pickerFace+'px';
+ p.padB.style.border = THIS.pickerInset+'px solid';
+ p.padB.style.borderColor = THIS.pickerInsetColor;
+
+ // pad mouse area
+ p.padM.style.position = 'absolute';
+ p.padM.style.left = '0';
+ p.padM.style.top = '0';
+ p.padM.style.width = THIS.pickerFace + 2*THIS.pickerInset + jscolor.images.pad[0] + jscolor.images.arrow[0] + 'px';
+ p.padM.style.height = p.box.style.height;
+ p.padM.style.cursor = 'crosshair';
+
+ // slider image
+ p.sld.style.overflow = 'hidden';
+ p.sld.style.width = jscolor.images.sld[0]+'px';
+ p.sld.style.height = jscolor.images.sld[1]+'px';
+
+ // slider border
+ p.sldB.style.display = THIS.slider ? 'block' : 'none';
+ p.sldB.style.position = 'absolute';
+ p.sldB.style.right = THIS.pickerFace+'px';
+ p.sldB.style.top = THIS.pickerFace+'px';
+ p.sldB.style.border = THIS.pickerInset+'px solid';
+ p.sldB.style.borderColor = THIS.pickerInsetColor;
+
+ // slider mouse area
+ p.sldM.style.display = THIS.slider ? 'block' : 'none';
+ p.sldM.style.position = 'absolute';
+ p.sldM.style.right = '0';
+ p.sldM.style.top = '0';
+ p.sldM.style.width = jscolor.images.sld[0] + jscolor.images.arrow[0] + THIS.pickerFace + 2*THIS.pickerInset + 'px';
+ p.sldM.style.height = p.box.style.height;
+ try {
+ p.sldM.style.cursor = 'pointer';
+ } catch(eOldIE) {
+ p.sldM.style.cursor = 'hand';
+ }
+
+ // "close" button
+ function setBtnBorder() {
+ var insetColors = THIS.pickerInsetColor.split(/\s+/);
+ var pickerOutsetColor = insetColors.length < 2 ? insetColors[0] : insetColors[1] + ' ' + insetColors[0] + ' ' + insetColors[0] + ' ' + insetColors[1];
+ p.btn.style.borderColor = pickerOutsetColor;
+ }
+ p.btn.style.display = THIS.pickerClosable ? 'block' : 'none';
+ p.btn.style.position = 'absolute';
+ p.btn.style.left = THIS.pickerFace + 'px';
+ p.btn.style.bottom = THIS.pickerFace + 'px';
+ p.btn.style.padding = '0 15px';
+ p.btn.style.height = '18px';
+ p.btn.style.border = THIS.pickerInset + 'px solid';
+ setBtnBorder();
+ p.btn.style.color = THIS.pickerButtonColor;
+ p.btn.style.font = '12px sans-serif';
+ p.btn.style.textAlign = 'center';
+ try {
+ p.btn.style.cursor = 'pointer';
+ } catch(eOldIE) {
+ p.btn.style.cursor = 'hand';
+ }
+ p.btn.onmousedown = function () {
+ THIS.hidePicker();
+ };
+ p.btnS.style.lineHeight = p.btn.style.height;
+
+ // load images in optimal order
+ switch(modeID) {
+ case 0: var padImg = 'hs.png'; break;
+ case 1: var padImg = 'hv.png'; break;
+ }
+ p.padM.style.backgroundImage = "url('"+jscolor.getDir()+"cross.gif')";
+ p.padM.style.backgroundRepeat = "no-repeat";
+ p.sldM.style.backgroundImage = "url('"+jscolor.getDir()+"arrow.gif')";
+ p.sldM.style.backgroundRepeat = "no-repeat";
+ p.pad.style.backgroundImage = "url('"+jscolor.getDir()+padImg+"')";
+ p.pad.style.backgroundRepeat = "no-repeat";
+ p.pad.style.backgroundPosition = "0 0";
+
+ // place pointers
+ redrawPad();
+ redrawSld();
+
+ jscolor.picker.owner = THIS;
+ document.getElementsByTagName('body')[0].appendChild(p.boxB);
+ }
+
+
+ function getPickerDims(o) {
+ var dims = [
+ 2*o.pickerInset + 2*o.pickerFace + jscolor.images.pad[0] +
+ (o.slider ? 2*o.pickerInset + 2*jscolor.images.arrow[0] + jscolor.images.sld[0] : 0),
+ o.pickerClosable ?
+ 4*o.pickerInset + 3*o.pickerFace + jscolor.images.pad[1] + o.pickerButtonHeight :
+ 2*o.pickerInset + 2*o.pickerFace + jscolor.images.pad[1]
+ ];
+ return dims;
+ }
+
+
+ function redrawPad() {
+ // redraw the pad pointer
+ switch(modeID) {
+ case 0: var yComponent = 1; break;
+ case 1: var yComponent = 2; break;
+ }
+ var x = Math.round((THIS.hsv[0]/6) * (jscolor.images.pad[0]-1));
+ var y = Math.round((1-THIS.hsv[yComponent]) * (jscolor.images.pad[1]-1));
+ jscolor.picker.padM.style.backgroundPosition =
+ (THIS.pickerFace+THIS.pickerInset+x - Math.floor(jscolor.images.cross[0]/2)) + 'px ' +
+ (THIS.pickerFace+THIS.pickerInset+y - Math.floor(jscolor.images.cross[1]/2)) + 'px';
+
+ // redraw the slider image
+ var seg = jscolor.picker.sld.childNodes;
+
+ switch(modeID) {
+ case 0:
+ var rgb = HSV_RGB(THIS.hsv[0], THIS.hsv[1], 1);
+ for(var i=0; i<seg.length; i+=1) {
+ seg[i].style.backgroundColor = 'rgb('+
+ (rgb[0]*(1-i/seg.length)*100)+'%,'+
+ (rgb[1]*(1-i/seg.length)*100)+'%,'+
+ (rgb[2]*(1-i/seg.length)*100)+'%)';
+ }
+ break;
+ case 1:
+ var rgb, s, c = [ THIS.hsv[2], 0, 0 ];
+ var i = Math.floor(THIS.hsv[0]);
+ var f = i%2 ? THIS.hsv[0]-i : 1-(THIS.hsv[0]-i);
+ switch(i) {
+ case 6:
+ case 0: rgb=[0,1,2]; break;
+ case 1: rgb=[1,0,2]; break;
+ case 2: rgb=[2,0,1]; break;
+ case 3: rgb=[2,1,0]; break;
+ case 4: rgb=[1,2,0]; break;
+ case 5: rgb=[0,2,1]; break;
+ }
+ for(var i=0; i<seg.length; i+=1) {
+ s = 1 - 1/(seg.length-1)*i;
+ c[1] = c[0] * (1 - s*f);
+ c[2] = c[0] * (1 - s);
+ seg[i].style.backgroundColor = 'rgb('+
+ (c[rgb[0]]*100)+'%,'+
+ (c[rgb[1]]*100)+'%,'+
+ (c[rgb[2]]*100)+'%)';
+ }
+ break;
+ }
+ }
+
+
+ function redrawSld() {
+ // redraw the slider pointer
+ switch(modeID) {
+ case 0: var yComponent = 2; break;
+ case 1: var yComponent = 1; break;
+ }
+ var y = Math.round((1-THIS.hsv[yComponent]) * (jscolor.images.sld[1]-1));
+ jscolor.picker.sldM.style.backgroundPosition =
+ '0 ' + (THIS.pickerFace+THIS.pickerInset+y - Math.floor(jscolor.images.arrow[1]/2)) + 'px';
+ }
+
+
+ function isPickerOwner() {
+ return jscolor.picker && jscolor.picker.owner === THIS;
+ }
+
+
+ function blurTarget() {
+ if(valueElement === target) {
+ THIS.importColor();
+ }
+ if(THIS.pickerOnfocus) {
+ THIS.hidePicker();
+ }
+ }
+
+
+ function blurValue() {
+ if(valueElement !== target) {
+ THIS.importColor();
+ }
+ }
+
+
+ function setPad(e) {
+ var mpos = jscolor.getRelMousePos(e);
+ var x = mpos.x - THIS.pickerFace - THIS.pickerInset;
+ var y = mpos.y - THIS.pickerFace - THIS.pickerInset;
+ switch(modeID) {
+ case 0: THIS.fromHSV(x*(6/(jscolor.images.pad[0]-1)), 1 - y/(jscolor.images.pad[1]-1), null, leaveSld); break;
+ case 1: THIS.fromHSV(x*(6/(jscolor.images.pad[0]-1)), null, 1 - y/(jscolor.images.pad[1]-1), leaveSld); break;
+ }
+ }
+
+
+ function setSld(e) {
+ var mpos = jscolor.getRelMousePos(e);
+ var y = mpos.y - THIS.pickerFace - THIS.pickerInset;
+ switch(modeID) {
+ case 0: THIS.fromHSV(null, null, 1 - y/(jscolor.images.sld[1]-1), leavePad); break;
+ case 1: THIS.fromHSV(null, 1 - y/(jscolor.images.sld[1]-1), null, leavePad); break;
+ }
+ }
+
+
+ function dispatchImmediateChange() {
+ if (THIS.onImmediateChange) {
+ var callback;
+ if (typeof THIS.onImmediateChange === 'string') {
+ callback = new Function (THIS.onImmediateChange);
+ } else {
+ callback = THIS.onImmediateChange;
+ }
+ callback.call(THIS);
+ }
+ }
+
+
+ var THIS = this;
+ var modeID = this.pickerMode.toLowerCase()==='hvs' ? 1 : 0;
+ var abortBlur = false;
+ var
+ valueElement = jscolor.fetchElement(this.valueElement),
+ styleElement = jscolor.fetchElement(this.styleElement);
+ var
+ holdPad = false,
+ holdSld = false,
+ touchOffset = {};
+ var
+ leaveValue = 1<<0,
+ leaveStyle = 1<<1,
+ leavePad = 1<<2,
+ leaveSld = 1<<3;
+
+ // target
+ jscolor.addEvent(target, 'focus', function() {
+ if(THIS.pickerOnfocus) { THIS.showPicker(); }
+ });
+ jscolor.addEvent(target, 'blur', function() {
+ if(!abortBlur) {
+ window.setTimeout(function(){ abortBlur || blurTarget(); abortBlur=false; }, 0);
+ } else {
+ abortBlur = false;
+ }
+ });
+
+ // valueElement
+ if(valueElement) {
+ var updateField = function() {
+ THIS.fromString(valueElement.value, leaveValue);
+ dispatchImmediateChange();
+ };
+ jscolor.addEvent(valueElement, 'keyup', updateField);
+ jscolor.addEvent(valueElement, 'input', updateField);
+ jscolor.addEvent(valueElement, 'blur', blurValue);
+ valueElement.setAttribute('autocomplete', 'off');
+ }
+
+ // styleElement
+ if(styleElement) {
+ styleElement.jscStyle = {
+ backgroundImage : styleElement.style.backgroundImage,
+ backgroundColor : styleElement.style.backgroundColor,
+ color : styleElement.style.color
+ };
+ }
+
+ // require images
+ switch(modeID) {
+ case 0: jscolor.requireImage('hs.png'); break;
+ case 1: jscolor.requireImage('hv.png'); break;
+ }
+ jscolor.requireImage('cross.gif');
+ jscolor.requireImage('arrow.gif');
+
+ this.importColor();
+ }
+
+};
+
+
+jscolor.install();
diff --git a/js/mobile-style.js b/js/mobile-style.js
new file mode 100644
index 00000000..89e01b73
--- /dev/null
+++ b/js/mobile-style.js
@@ -0,0 +1,8 @@
+if(navigator.userAgent.match(/iPhone|iPod|iPad|Android|Opera Mini|Blackberry|PlayBook|Windows Phone|Tablet PC|Windows CE|IEMobile/i)) {
+ $('html').addClass("mobile-style");
+ device_type = "mobile";
+}
+else {
+ $('html').addClass("desktop-style");
+ device_type = "desktop";
+}
diff --git a/js/oekaki.js b/js/oekaki.js
new file mode 100644
index 00000000..6197386d
--- /dev/null
+++ b/js/oekaki.js
@@ -0,0 +1,250 @@
+$(function() {
+// Init
+var oekaki_form = '\
+ <tr id="oekaki">\
+ <th>\
+ Oekaki\
+ </th>\
+ <td>\
+ <canvas width="'+oekaki_options.width+'" height="'+oekaki_options.height+'" id="oekaki_canvas" style="border:1px solid black;-webkit-user-select: none;-moz-user-select: none;"></canvas>\
+ <p><button type="button" id="brushsize">'+_('Brush size')+'</button><input class="color" id="color" value="000000" placeholder="Color"/><button type="button" id="text">'+_('Set text')+'</button><button type="button" id="clear">'+_('Clear')+'</button><button type="button" id="save">'+_('Save')+'</button><button type="button" id="load">'+_('Load')+'</button><br/>\
+ <button type="button" id="eraser">'+_('Toggle eraser')+'</button><button type="button" id="getcolor">'+_('Get color')+'</button><button type="button" id="fill">'+_('Fill')+'</button>\
+ </p><p><textarea id="savebox"></textarea><label id="confirm_oekaki_label"><input id="confirm_oekaki" type="checkbox"/> '+_('Use oekaki instead of file?')+'</label></p>\
+ <img id="saved" style="display:none">\
+ </td>\
+ </tr>'
+
+function init_oekaki() {
+ // Add oekaki after the file input
+ $('input[type="file"]').parent().parent().after(oekaki_form);
+ // Add "edit in oekaki" links
+ $(".fileinfo .unimportant").append(' <a href="javascript:void(0)" class="edit_in_oekaki">['+_('Edit in oekaki')+']</a>');
+ // Init oekaki vars
+ canvas = $("#oekaki_canvas");
+ context = canvas[0].getContext("2d");
+ is_drawing = false;
+ text = "";
+ eraser = getcolor = fill = false;
+ context.strokeStyle = context.fillStyle = "black";
+ // Attach canvas events
+ attach_events();
+}
+
+if (active_page == 'index' || active_page == 'thread') {
+ init_oekaki();
+}
+
+//http://stackoverflow.com/a/5624139/1901658
+function hexToRgb(hex) {
+ var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+ return result ? {
+ r: parseInt(result[1], 16),
+ g: parseInt(result[2], 16),
+ b: parseInt(result[3], 16)
+ } : null;
+}
+
+//http://stackoverflow.com/a/4025958/1901658
+function arraysEqual(arr1, arr2) {
+ if(arr1.length !== arr2.length)
+ return false;
+ for(var i = arr1.length; i--;) {
+ if(arr1[i] !== arr2[i])
+ return false;
+ }
+
+ return true;
+}
+
+function getmousepos(e){
+ var r = canvas[0].getBoundingClientRect();
+ mouseX = Math.round(e.clientX - r.left);
+ mouseY = Math.round(e.clientY - r.top);
+}
+
+function setcolor(){
+ context.strokeStyle = context.fillStyle = "#"+$(".color").val();
+}
+
+function flood_fill(x, y, target){
+ var pixel = context.createImageData(1,1);
+ var color = hexToRgb("#"+$(".color").val());
+ pixel.data[0] = color.r; pixel.data[1] = color.g; pixel.data[2] = color.b; pixel.data[3] = 255;
+ var queue = [];
+ var node = [x, y];
+ queue.push(node);
+ //var iters = 0;
+ while (queue.length > 0) {
+ var n = queue.pop();
+ var data = context.getImageData(n[0], n[1], 1, 1).data;
+ var d = [data[0], data[1], data[2], data[3]];
+ var t = [target[0], target[1], target[2], target[3]];
+ if (arraysEqual(d, t) && n[0] < canvas.width() && n[1] < canvas.height() && n[0] > -1 && n[1] > -1){
+ context.putImageData(pixel, n[0], n[1]);
+ queue.push([n[0], n[1]-1]);
+ queue.push([n[0], n[1]+1]);
+ queue.push([n[0]-1, n[1]]);
+ queue.push([n[0]+1, n[1]]);
+ //iters++;
+ }
+ //if (iters%100===0){console.log(n[0]);console.log(n[1])}
+
+ }
+ return;
+}
+
+function color_under_pixel(x, y){
+ return context.getImageData(x, y, 1, 1).data;
+}
+
+function attach_events(){
+
+canvas.on("mousedown", function(e){
+ getmousepos(e);
+ $(this).css("cursor","none");
+ if (getcolor) {
+ var imagedata = color_under_pixel(mouseX, mouseY);
+ $("#color")[0].color.fromRGB(imagedata[0], imagedata[1], imagedata[2]);
+ getcolor = false;
+ setcolor();
+ }
+ else if (fill && !eraser) {
+ flood_fill(mouseX, mouseY, color_under_pixel(mouseX, mouseY));
+ fill = false;
+ }
+ else {
+ is_drawing = true;
+ context.beginPath();
+ context.moveTo(mouseX,mouseY);
+ context.fillText(text,mouseX,mouseY);
+ $("#confirm_oekaki").attr("checked",true);
+ }
+
+});
+
+canvas.on("mousemove", function(e){
+ getmousepos(e);
+ if (is_drawing) {
+ context.lineTo(mouseX,mouseY);
+ context.stroke()
+ }
+});
+
+canvas.on("mouseup mouseout", function(e){
+ context.stroke()
+ $(this).css("cursor","auto");
+ is_drawing = false;
+});
+
+$("#brushsize").on("click",function(){
+ var size = prompt("Enter brush size");
+ if (parseInt(size) == NaN) {
+ return
+ }
+ else {
+ context.lineWidth = size;
+ }
+});
+
+$(".color").on("change", setcolor);
+
+$("#text").on("click", function(e){
+ text = prompt(_("Enter some text")) || "";
+ context.font = prompt(_("Enter font or leave empty"), context.font)
+});
+
+function clear(){
+ context.beginPath();
+ context.clearRect(0,0,canvas.width(),canvas.height());
+ $("#confirm_oekaki").attr("checked",false)
+ canvas[0].height = oekaki_options.height; canvas[0].width = oekaki_options.width;
+};
+
+$("#clear").on("click", clear);
+
+$("#save").on("click",function(){
+ $("#savebox").val(canvas[0].toDataURL());
+});
+
+$("#load").on("click", function(){
+ clear();
+ var img = new Image();
+ img.src = $("#savebox").val();
+ img.onload = function(){context.drawImage(img,0,0);};
+ $("#confirm_oekaki").attr("checked",true)
+});
+
+$("#eraser").on("click", function(){
+ if (eraser) {
+ eraser = false;
+ context.strokeStyle = context.fillStyle = "#"+$(".color").val();
+ context.globalCompositeOperation = old_gco;
+ }
+ else {
+ eraser = true;
+ old_gco = context.globalCompositeOperation;
+ context.globalCompositeOperation = "destination-out";
+ context.strokeStyle = "rgba(0,0,0,1)";
+ }
+});
+
+$("#getcolor").on("click", function(){
+ getcolor = true;
+});
+
+$("#fill").on("click", function(){
+ fill = true;
+});
+
+$(".edit_in_oekaki").on("click", function(){
+ var img_link = $(this).parent().parent().parent().find("a>img.post-image").parent()[0]
+ var img = new Image();
+ img.onload = function() {
+ canvas[0].width = img.width; canvas[0].height = img.height;
+ context.drawImage(img, 0, 0);
+ }
+ img.src = $(img_link).attr("href");
+
+ if (typeof enable_oekaki === 'function') { // for upload-selection.js
+ enable_oekaki();
+ }
+ return false;
+});
+}
+
+function dataURItoBlob(dataURI) {
+ var binary = atob(dataURI.split(',')[1]);
+ var array = new Array(binary.length);
+ for(var i = 0; i < binary.length; i++) {
+ array[i] = binary.charCodeAt(i);
+ }
+ return new Blob([new Uint8Array(array)], {type: 'image/jpeg'});
+}
+
+$("form[name='post']").on("submit", function(e){
+ if ($("#confirm_oekaki").is(":checked")) {
+ e.preventDefault();
+ $("input[type='file']").remove();
+ var dataURL = canvas[0].toDataURL();
+ var blob = dataURItoBlob(dataURL);
+ var fd = new FormData(document.forms[0]);
+ fd.append("file", blob, "Oekaki.png");
+ fd.append("post", $("input[name='post']").val());
+ $.ajax({
+ type: "POST",
+ url: oekaki_options.root+"post.php",
+ data: fd,
+ processData: false,
+ contentType: false,
+ success: function(data) {
+ location.reload();
+ },
+ error: function(jq, data) {alert($('h2',jq.responseText).text());}
+ });
+ }
+
+ else {
+ return true;
+ };
+});
+});
diff --git a/js/post-hider.js b/js/post-hider.js
new file mode 120000
index 00000000..693c14d6
--- /dev/null
+++ b/js/post-hider.js
@@ -0,0 +1 @@
+hide-threads.js \ No newline at end of file
diff --git a/js/post-hover.js b/js/post-hover.js
index 263dc9ae..10542f20 100644
--- a/js/post-hover.js
+++ b/js/post-hover.js
@@ -17,13 +17,28 @@ onready(function(){
var $link = $(this);
var id;
-
- if(id = $link.text().match(/^>>(\d+)$/)) {
- id = id[1];
+ var matches;
+
+ if(matches = $link.text().match(/^>>(?:>\/([^\/]+)\/)?(\d+)$/)) {
+ id = matches[2];
} else {
return;
}
+ var board = $(this);
+ while (board.data('board') === undefined) {
+ board = board.parent();
+ }
+ var threadid = board.attr('id').replace("thread_", "");
+ board = board.data('board');
+
+ var parentboard = board;
+
+ if (matches[1] !== undefined) {
+ board = matches[1];
+ }
+
+
var $post = false;
var hovering = false;
var hovered_at;
@@ -32,28 +47,34 @@ onready(function(){
hovered_at = {'x': e.pageX, 'y': e.pageY};
var start_hover = function($link) {
- if($post.is(':visible') &&
+ if($.contains($post[0], $link[0])) {
+ // link links to itself or to op; ignore
+ }
+ else if($post.is(':visible') &&
$post.offset().top + $post.height() >= $(window).scrollTop() &&
- $post.offset().top <= $(window).scrollTop() + $(window).height()
- ) {
+ $post.offset().top <= $(window).scrollTop() + $(window).height()) {
// post is in view
$post.attr('style', 'border-style: none dashed dashed none; background: ' + $post.css('border-right-color'));
} else {
var $newPost = $post.clone();
+ $newPost.find('>.reply, >br').remove();
$newPost.find('span.mentioned').remove();
+
$newPost
.attr('id', 'post-hover-' + id)
+ .attr('data-board', board)
.addClass('post-hover')
.css('position', 'absolute')
.css('border-style', 'solid')
.css('box-shadow', '1px 1px 1px #999')
.css('display', 'block')
- .insertAfter($link.parent());
+ .addClass('reply').addClass('post')
+ .insertAfter($link.parent())
$link.trigger('mousemove');
}
};
- $post = $('div.post#reply_' + id);
+ $post = $('[data-board="' + board + '"] div.post#reply_' + id + ', [data-board="' + board + '"]div#thread_' + id);
if($post.length > 0) {
start_hover($(this));
} else {
@@ -68,15 +89,28 @@ onready(function(){
url: url,
context: document.body,
success: function(data) {
- $(data).find('div.post.reply').each(function() {
- // Not 100% sure that this doesn't break shit:
- $(document).trigger('new_post', this);
-
- if($('#' + $(this).attr('id')).length == 0)
- $('body').prepend($(this).css('display', 'none').addClass('hidden'));
- });
-
- $post = $('div.post#reply_' + id);
+ var mythreadid = $(data).find('div[id^="thread_"]').attr('id').replace("thread_", "");
+
+ if (mythreadid == threadid && parentboard == board) {
+ $(data).find('div.post.reply').each(function() {
+ if($('[data-board="' + board + '"] #' + $(this).attr('id')).length == 0) {
+ $('[data-board="' + board + '"]#thread_' + threadid + " .post.reply:first").before($(this).hide().addClass('hidden'));
+ }
+ });
+ }
+ else if ($('[data-board="' + board + '"]#thread_'+mythreadid).length > 0) {
+ $(data).find('div.post.reply').each(function() {
+ if($('[data-board="' + board + '"] #' + $(this).attr('id')).length == 0) {
+ $('[data-board="' + board + '"]#thread_' + mythreadid + " .post.reply:first").before($(this).hide().addClass('hidden'));
+ }
+ });
+ }
+ else {
+ $(data).find('div[id^="thread_"]').hide().attr('data-cached', 'yes').prependTo('form[name="postcontrols"]');
+ }
+
+ $post = $('[data-board="' + board + '"] div.post#reply_' + id + ', [data-board="' + board + '"]div#thread_' + id);
+
if(hovering && $post.length > 0) {
start_hover($link);
}
@@ -89,14 +123,14 @@ onready(function(){
return;
$post.attr('style', '');
- if($post.hasClass('hidden'))
+ if($post.hasClass('hidden') || $post.data('cached') == 'yes')
$post.css('display', 'none');
$('.post-hover').remove();
}).mousemove(function(e) {
if(!$post)
return;
- var $hover = $('#post-hover-' + id);
+ var $hover = $('#post-hover-' + id + '[data-board="' + board + '"]');
if($hover.length == 0)
return;
diff --git a/js/quick-reply.js b/js/quick-reply.js
index ea504b48..bee204b7 100644
--- a/js/quick-reply.js
+++ b/js/quick-reply.js
@@ -12,6 +12,7 @@
*
*/
+if (active_page == 'index') {
$(document).ready(function(){
if($('div.banner').length != 0)
return; // not index
@@ -43,4 +44,4 @@ $(document).ready(function(){
});
});
});
-
+}
diff --git a/js/smartphone-spoiler.js b/js/smartphone-spoiler.js
index 54c7a9a1..ee904b54 100644
--- a/js/smartphone-spoiler.js
+++ b/js/smartphone-spoiler.js
@@ -6,18 +6,28 @@
* Copyright (c) 2012 Michael Save <savetheinternet@tinyboard.org>
*
* Usage:
+ * $config['additional_javascript'][] = 'js/mobile-style.js';
* $config['additional_javascript'][] = 'js/smartphone-spoiler.js';
*
*/
onready(function(){
- if(navigator.userAgent.match(/iPhone|iPod|iPad|Android|Opera Mini|Blackberry|PlayBook|Windows Phone|Tablet PC|Windows CE|IEMobile/i)) {
- var spoilers = document.getElementsByClassName('spoiler');
- for(var i = 0; i < spoilers.length; i++) {
- spoilers[i].onmousedown = function() {
- this.style.color = 'white';
- };
- }
+ if(device_type == 'mobile') {
+ var fix_spoilers = function(where) {
+ var spoilers = where.getElementsByClassName('spoiler');
+ for(var i = 0; i < spoilers.length; i++) {
+ spoilers[i].onmousedown = function() {
+ this.style.color = 'white';
+ };
+ }
+ };
+ fix_spoilers(document);
+
+ // allow to work with auto-reload.js, etc.
+ $(document).bind('new_post', function(e, post) {
+ fix_spoilers(post);
+ });
+
}
});
diff --git a/js/toggle-locked-threads.js b/js/toggle-locked-threads.js
index 1d517a4d..fcef0a7a 100644
--- a/js/toggle-locked-threads.js
+++ b/js/toggle-locked-threads.js
@@ -40,12 +40,12 @@ $(document).ready(function(){
.click(function() {
hide_locked_threads = !hide_locked_threads;
if (hide_locked_threads) {
- $('img.icon[title="Locked"]').each(function() {
+ $('img.icon[title="Locked"], i.icon-lock.icon').each(function() {
hideLockedThread(getThreadFromIcon($(this)));
});
localStorage.hidelockedthreads = true;
} else {
- $('img.icon[title="Locked"]').each(function() {
+ $('img.icon[title="Locked"], i.icon-lock.icon').each(function() {
restoreLockedThread(getThreadFromIcon($(this)));
});
delete localStorage.hidelockedthreads;
diff --git a/js/upload-selection.js b/js/upload-selection.js
new file mode 100644
index 00000000..fb3d9131
--- /dev/null
+++ b/js/upload-selection.js
@@ -0,0 +1,67 @@
+$(function(){
+ var enabled_file = true;
+ var enabled_url = $("#upload_url").length > 0;
+ var enabled_embed = $("#upload_embed").length > 0;
+ var enabled_oekaki = $("#oekaki").length > 0;
+
+ var disable_all = function() {
+ $("#upload").hide();
+ $("#upload_file").hide();
+ $("#upload_url").hide();
+ $("#upload_embed").hide();
+ $("#oekaki").hide();
+
+ if (enabled_oekaki) {
+ if ($("#confirm_oekaki").is(':checked')) {
+ $("#confirm_oekaki").click();
+ }
+ }
+ };
+
+ enable_file = function() {
+ disable_all();
+ $("#upload").show();
+ $("#upload_file").show();
+ };
+
+ enable_url = function() {
+ disable_all();
+ $("#upload").show();
+ $("#upload_url").show();
+
+ $('label[for="file_url"]').html(_("URL"));
+ };
+
+ enable_embed = function() {
+ disable_all();
+ $("#upload_embed").show();
+ };
+
+ enable_oekaki = function() {
+ disable_all();
+ $("#oekaki").show();
+
+ if (!$("#confirm_oekaki").is(':checked')) {
+ $("#confirm_oekaki").click();
+ }
+ };
+
+ if (enabled_url || enabled_embed || enabled_oekaki) {
+ $("<tr><th>"+_("Select")+"</th><td id='upload_selection'></td></tr>").insertBefore("#upload");
+ var my_html = "<a href='javascript:void(0)' onclick='enable_file(); return false;'>"+_("File")+"</a>";
+ if (enabled_url) {
+ my_html += " / <a href='javascript:void(0)' onclick='enable_url(); return false;'>"+_("Remote")+"</a>";
+ }
+ if (enabled_embed) {
+ my_html += " / <a href='javascript:void(0)' onclick='enable_embed(); return false;'>"+_("Embed")+"</a>";
+ }
+ if (enabled_oekaki) {
+ my_html += " / <a href='javascript:void(0)' onclick='enable_oekaki(); return false;'>"+_("Oekaki")+"</a>";
+
+ $("#confirm_oekaki_label").hide();
+ }
+ $("#upload_selection").html(my_html);
+
+ enable_file();
+ }
+});
diff --git a/mod.php b/mod.php
index 0e98c4eb..dca13dfe 100644
--- a/mod.php
+++ b/mod.php
@@ -65,6 +65,7 @@ $pages = array(
'/ban' => 'secure_POST ban', // new ban
'/(\%b)/ban(&delete)?/(\d+)' => 'secure_POST ban_post', // ban poster
'/(\%b)/move/(\d+)' => 'secure_POST move', // move thread
+ '/(\%b)/move_reply/(\d+)' => 'secure_POST move_reply', // move reply
'/(\%b)/edit(_raw)?/(\d+)' => 'secure_POST edit_post', // edit post
'/(\%b)/delete/(\d+)' => 'secure delete', // delete post
'/(\%b)/deletefile/(\d+)' => 'secure deletefile', // delete file from post
@@ -92,6 +93,8 @@ $pages = array(
'/(\%b)/' . preg_quote($config['file_index'], '!') => 'view_board',
'/(\%b)/' . str_replace('%d', '(\d+)', preg_quote($config['file_page'], '!')) => 'view_board',
'/(\%b)/' . preg_quote($config['dir']['res'], '!') .
+ str_replace('%d', '(\d+)', preg_quote($config['file_page50'], '!')) => 'view_thread50',
+ '/(\%b)/' . preg_quote($config['dir']['res'], '!') .
str_replace('%d', '(\d+)', preg_quote($config['file_page'], '!')) => 'view_thread',
);
diff --git a/post.php b/post.php
index 02d97e24..f6c15c0c 100644
--- a/post.php
+++ b/post.php
@@ -79,6 +79,7 @@ if (isset($_POST['delete'])) {
$is_mod = isset($_POST['mod']) && $_POST['mod'];
$root = $is_mod ? $config['root'] . $config['file_mod'] . '?/' : $config['root'];
+ if (!$is_mod) header('X-Associated-Content: "' . $root . $board['dir'] . $config['file_index'] . '"');
header('Location: ' . $root . $board['dir'] . $config['file_index'], true, $config['redirect_http']);
} elseif (isset($_POST['report'])) {
@@ -134,6 +135,7 @@ if (isset($_POST['delete'])) {
$is_mod = isset($_POST['mod']) && $_POST['mod'];
$root = $is_mod ? $config['root'] . $config['file_mod'] . '?/' : $config['root'];
+ if (!$is_mod) header('X-Associated-Content: "' . $root . $board['dir'] . $config['file_index'] . '"');
header('Location: ' . $root . $board['dir'] . $config['file_index'], true, $config['redirect_http']);
} elseif (isset($_POST['post'])) {
@@ -168,7 +170,7 @@ if (isset($_POST['delete'])) {
error($config['error']['bot']);
// Check the referrer
- if (!isset($_SERVER['HTTP_REFERER']) || !preg_match($config['referer_match'], urldecode($_SERVER['HTTP_REFERER'])))
+ if (!isset($_SERVER['HTTP_REFERER']) || !preg_match($config['referer_match'], rawurldecode($_SERVER['HTTP_REFERER'])))
error($config['error']['referer']);
checkDNSBL();
@@ -716,6 +718,20 @@ if (isset($_POST['delete'])) {
if ($config['always_noko'] || $noko) {
$redirect = $root . $board['dir'] . $config['dir']['res'] .
sprintf($config['file_page'], $post['op'] ? $id:$post['thread']) . (!$post['op'] ? '#' . $id : '');
+
+ if (!$post['op'] && isset($_SERVER['HTTP_REFERER'])) {
+ $regex = array(
+ 'board' => str_replace('%s', '(\w{1,8})', preg_quote($config['board_path'], '/')),
+ 'page' => str_replace('%d', '(\d+)', preg_quote($config['file_page'], '/')),
+ 'page50' => str_replace('%d', '(\d+)', preg_quote($config['file_page50'], '/')),
+ 'res' => preg_quote($config['dir']['res'], '/'),
+ );
+
+ if (preg_match('/\/' . $regex['board'] . $regex['res'] . $regex['page50'] . '([?&].*)?$/', $_SERVER['HTTP_REFERER'])) {
+ $redirect = $root . $board['dir'] . $config['dir']['res'] .
+ sprintf($config['file_page50'], $post['op'] ? $id:$post['thread']) . (!$post['op'] ? '#' . $id : '');
+ }
+ }
} else {
$redirect = $root . $board['dir'] . $config['file_index'];
@@ -725,6 +741,8 @@ if (isset($_POST['delete'])) {
_syslog(LOG_INFO, 'New post: /' . $board['dir'] . $config['dir']['res'] .
sprintf($config['file_page'], $post['op'] ? $id : $post['thread']) . (!$post['op'] ? '#' . $id : ''));
+ if (!$post['mod']) header('X-Associated-Content: "' . $redirect . '"');
+
if ($post['op'])
rebuildThemes('post-thread', $board['uri']);
else
diff --git a/search.php b/search.php
new file mode 100644
index 00000000..beb8743d
--- /dev/null
+++ b/search.php
@@ -0,0 +1,170 @@
+<?php
+ require 'inc/functions.php';
+
+ if (!$config['search']['enable']) {
+ die(_("Post search is disabled"));
+ }
+
+ $queries_per_minutes = $config['search']['queries_per_minutes'];
+ $queries_per_minutes_all = $config['search']['queries_per_minutes_all'];
+ $search_limit = $config['search']['search_limit'];
+
+ $boards = $config['search']['boards'];
+
+ $body = Element('search_form.html', Array('boards' => $boards, 'board' => isset($_GET['board']) ? $_GET['board'] : false, 'search' => isset($_GET['search']) ? str_replace('"', '&quot;', utf8tohtml($_GET['search'])) : false));
+
+ if(isset($_GET['search']) && !empty($_GET['search']) && isset($_GET['board']) && in_array($_GET['board'], $boards)) {
+ $phrase = $_GET['search'];
+ $_body = '';
+
+ $query = prepare("SELECT COUNT(*) FROM ``search_queries`` WHERE `ip` = :ip AND `time` > :time");
+ $query->bindValue(':ip', $_SERVER['REMOTE_ADDR']);
+ $query->bindValue(':time', time() - ($queries_per_minutes[1] * 60));
+ $query->execute() or error(db_error($query));
+ if($query->fetchColumn() > $queries_per_minutes[0])
+ error(_('Wait a while before searching again, please.'));
+
+ $query = prepare("SELECT COUNT(*) FROM ``search_queries`` WHERE `time` > :time");
+ $query->bindValue(':time', time() - ($queries_per_minutes_all[1] * 60));
+ $query->execute() or error(db_error($query));
+ if($query->fetchColumn() > $queries_per_minutes_all[0])
+ error(_('Wait a while before searching again, please.'));
+
+
+ $query = prepare("INSERT INTO ``search_queries`` VALUES (:ip, :time, :query)");
+ $query->bindValue(':ip', $_SERVER['REMOTE_ADDR']);
+ $query->bindValue(':time', time());
+ $query->bindValue(':query', $phrase);
+ $query->execute() or error(db_error($query));
+
+ _syslog(LOG_NOTICE, 'Searched /' . $_GET['board'] . '/ for "' . $phrase . '"');
+
+ // Cleanup search queries table
+ $query = prepare("DELETE FROM ``search_queries`` WHERE `time` <= :time");
+ $query->bindValue(':time', time() - ($queries_per_minutes_all[1] * 60));
+ $query->execute() or error(db_error($query));
+
+ openBoard($_GET['board']);
+
+ $filters = Array();
+
+ function search_filters($m) {
+ global $filters;
+ $name = $m[2];
+ $value = isset($m[4]) ? $m[4] : $m[3];
+
+ if(!in_array($name, array('id', 'thread', 'subject', 'name'))) {
+ // unknown filter
+ return $m[0];
+ }
+
+ $filters[$name] = $value;
+
+ return $m[1];
+ }
+
+ $phrase = trim(preg_replace_callback('/(^|\s)(\w+):("(.*)?"|[^\s]*)/', 'search_filters', $phrase));
+
+ if(!preg_match('/[^*^\s]/', $phrase) && empty($filters)) {
+ _syslog(LOG_WARNING, 'Query too broad.');
+ $body .= '<p class="unimportant" style="text-align:center">(Query too broad.)</p>';
+ echo Element('page.html', Array(
+ 'config'=>$config,
+ 'title'=>'Search',
+ 'body'=>$body,
+ ));
+ exit;
+ }
+
+ // Escape escape character
+ $phrase = str_replace('!', '!!', $phrase);
+
+ // Remove SQL wildcard
+ $phrase = str_replace('%', '!%', $phrase);
+
+ // Use asterisk as wildcard to suit convention
+ $phrase = str_replace('*', '%', $phrase);
+
+ // Remove `, it's used by table prefix magic
+ $phrase = str_replace('`', '!`', $phrase);
+
+ $like = '';
+ $match = Array();
+
+ // Find exact phrases
+ if(preg_match_all('/"(.+?)"/', $phrase, $m)) {
+ foreach($m[1] as &$quote) {
+ $phrase = str_replace("\"{$quote}\"", '', $phrase);
+ $match[] = $pdo->quote($quote);
+ }
+ }
+
+ $words = explode(' ', $phrase);
+ foreach($words as &$word) {
+ if(empty($word))
+ continue;
+ $match[] = $pdo->quote($word);
+ }
+
+ $like = '';
+ foreach($match as &$phrase) {
+ if(!empty($like))
+ $like .= ' AND ';
+ $phrase = preg_replace('/^\'(.+)\'$/', '\'%$1%\'', $phrase);
+ $like .= '`body` LIKE ' . $phrase . ' ESCAPE \'!\'';
+ }
+
+ foreach($filters as $name => $value) {
+ if(!empty($like))
+ $like .= ' AND ';
+ $like .= '`' . $name . '` = '. $pdo->quote($value);
+ }
+
+ $like = str_replace('%', '%%', $like);
+
+ $query = prepare(sprintf("SELECT * FROM ``posts_%s`` WHERE " . $like . " ORDER BY `time` DESC LIMIT :limit", $board['uri']));
+ $query->bindValue(':limit', $search_limit, PDO::PARAM_INT);
+ $query->execute() or error(db_error($query));
+
+ if($query->rowCount() == $search_limit) {
+ _syslog(LOG_WARNING, 'Query too broad.');
+ $body .= '<p class="unimportant" style="text-align:center">('._('Query too broad.').')</p>';
+ echo Element('page.html', Array(
+ 'config'=>$config,
+ 'title'=>'Search',
+ 'body'=>$body,
+ ));
+ exit;
+ }
+
+ $temp = '';
+ while($post = $query->fetch()) {
+ if(!$post['thread']) {
+ $po = new Thread($post);
+ } else {
+ $po = new Post($post);
+ }
+ $temp .= $po->build(true) . '<hr/>';
+ }
+
+ if(!empty($temp))
+ $_body .= '<fieldset><legend>' .
+ sprintf(ngettext('%d result in', '%d results in', $query->rowCount()),
+ $query->rowCount()) . ' <a href="/' .
+ sprintf($config['board_path'], $board['uri']) . $config['file_index'] .
+ '">' .
+ sprintf($config['board_abbreviation'], $board['uri']) . ' - ' . $board['title'] .
+ '</a></legend>' . $temp . '</fieldset>';
+
+ $body .= '<hr/>';
+ if(!empty($_body))
+ $body .= $_body;
+ else
+ $body .= '<p style="text-align:center" class="unimportant">('._('No results.').')</p>';
+ }
+
+ echo Element('page.html', Array(
+ 'config'=>$config,
+ 'title'=>_('Search'),
+ 'body'=>'' . $body
+ ));
diff --git a/stylesheets/dark.css b/stylesheets/dark.css
index c4a6855c..0982c278 100644
--- a/stylesheets/dark.css
+++ b/stylesheets/dark.css
@@ -9,7 +9,7 @@ body {
font-size: 12px;
}
span.quote {
- color:#B8D962;
+ color:#B8D962;
}
h1 {
font-size: 20pt;
@@ -45,10 +45,10 @@ a.post_no:hover {
div.post.reply {
background: #333333;
border: #555555 1px solid;
- -webkit-border-radius: 10px;
- -khtml-border-radius: 10px;
- -moz-border-radius: 10px;
- border-radius: 10px;
+ -webkit-border-radius: 10px;
+ -khtml-border-radius: 10px;
+ -moz-border-radius: 10px;
+ border-radius: 10px;
}
div.post.reply.highlighted {
@@ -112,7 +112,7 @@ div.banner {
font-size: 12px;
}
div.banner a {
- color:#000;
+ color:#000;
}
input[type="submit"] {
background: #333333;
@@ -125,7 +125,7 @@ input[type="submit"]:hover {
color: #32DD72;
}
input[type="text"]:focus {
- border:#aaa 1px solid;
+ border:#aaa 1px solid;
}
p.fileinfo a:hover {
text-decoration: underline;
@@ -149,7 +149,7 @@ hr {
}
div.boardlist {
color: #999999;
- background-color: rgba(12%, 12%, 12%, 0.10);
+ background-color: rgba(12%, 12%, 12%, 0.10);
}
div.ban {
@@ -173,4 +173,4 @@ table.modlog tr th {
.desktop-style div.boardlist:nth-child(1):hover {
background-color: rgba(30%, 30%, 30%, 0.65);
-} \ No newline at end of file
+}
diff --git a/stylesheets/futaba+vichan.css b/stylesheets/futaba+vichan.css
index be0ab672..adf9baef 100644
--- a/stylesheets/futaba+vichan.css
+++ b/stylesheets/futaba+vichan.css
@@ -86,3 +86,19 @@ unimportant, .unimportant * {
table.modlog tr th {
background: #EA8;
}
+div.boardlist {
+ background-color: rgba(90%,90%,90%,0.2);
+}
+div.boardlist .sub {
+ background: none;
+}
+div.boardlist.bottom {
+ background-color:transparent;
+}
+
+.desktop-style div.boardlist:nth-child(1) {
+ text-shadow: #fff 1px 1px 1px, #fff -1px -1px 1px;
+}
+.desktop-style div.boardlist:nth-child(1):hover {
+ background-color: rgba(90%, 90%, 90%, 0.55);
+}
diff --git a/stylesheets/futaba.css b/stylesheets/futaba.css
index d30cb938..2a30bc00 100644
--- a/stylesheets/futaba.css
+++ b/stylesheets/futaba.css
@@ -75,3 +75,12 @@ unimportant, .unimportant * {
table.modlog tr th {
background: #EA8;
}
+
+.desktop-style div.boardlist:nth-child(1) {
+ text-shadow: #fff 1px 1px 1px, #fff -1px -1px 1px;
+}
+
+
+.desktop-style div.boardlist:nth-child(1):hover {
+ background-color: rgba(90%, 90%, 90%, 0.55);
+}
diff --git a/stylesheets/img/arrow.png b/stylesheets/img/arrow.png
new file mode 100644
index 00000000..f7403695
--- /dev/null
+++ b/stylesheets/img/arrow.png
Binary files differ
diff --git a/stylesheets/miku.css b/stylesheets/miku.css
index 23fb1d9d..2f55b48c 100644
--- a/stylesheets/miku.css
+++ b/stylesheets/miku.css
@@ -8,7 +8,7 @@ body {
a:link, a:visited {
text-decoration: none;
- color: #0093AB;
+ color: #00637B;
}
a:link:hover, a:visited:hover {
@@ -54,7 +54,7 @@ p.intro span.name {
}
div.post.reply.highlighted {
- background: #9988EE;
+ background: #a9d8ff;
}
div.post.reply {
@@ -84,4 +84,14 @@ hr {
div.boardlist {
color: #0093AB;
+ background-color: rgba(65%, 85%, 95%, 0.2);
+}
+
+.desktop-style div.boardlist:nth-child(1) {
+ text-shadow: #D2FFEE 1px 1px 1px, #D2FFEE -1px -1px 1px;
+}
+
+
+.desktop-style div.boardlist:nth-child(1):hover {
+ background-color: rgba(70%, 95%, 100%, 0.45);
}
diff --git a/stylesheets/style.css b/stylesheets/style.css
index e62891e2..f75cfadb 100644
--- a/stylesheets/style.css
+++ b/stylesheets/style.css
@@ -284,6 +284,7 @@ span.spoiler:hover, div.post.reply div.body span.spoiler:hover a {
div.styles {
float: right;
padding-bottom: 20px;
+
}
div.styles a {
margin: 0 10px;
@@ -403,8 +404,59 @@ table.mod.config-editor td {
table.mod.config-editor input[type="text"] {
width: 98%;
}
+.desktop-style div.boardlist:nth-child(1) {
+ position: fixed;
+ top: 0px;
+ left: 0px;
+ right: 0px;
+ /*text-shadow: white 1px 0px 6px, white 1px 0px 6px, black 1px 0px 3px;*/
+ margin-top: 0px;
+ z-index: 30;
+}
+.desktop-style div.boardlist:nth-child(1):hover {
+ background-color: rgba(90%, 90%, 90%, 0.6);
+}
+
+.desktop-style body {
+ padding-top: 20px;
+}
+
+.desktop-style .sub {
+ background: inherit;
+}
+.desktop-style .sub .sub {
+ display: inline-block;
+ text-indent: -9000px;
+ width: 7px;
+ background: url('img/arrow.png') right center no-repeat;
+}
+.desktop-style .sub .sub:hover, .desktop-style .sub .sub.hover {
+ display: inline;
+ text-indent: 0px;
+ background: inherit;
+}
+
+#attention_bar {
+ height: 1.5em;
+ width: 100%;
+ text-align: center;
+}
+#attention_bar_form {
+ display: none;
+ padding: 0;
+ margin: 0;
+}
+#attention_bar_input {
+ width: 100%;
+ padding: 0;
+ margin: 0;
+ text-align: center;
+}
+#attention_bar:hover {
+ background-color: #fff;
+ opacity: 0.8;
+}
p.intro.thread-hidden {
margin: 0px;
padding: 0px;
}
-
diff --git a/stylesheets/testorange.css b/stylesheets/testorange.css
index 7b1ce0a6..c7250de4 100644
--- a/stylesheets/testorange.css
+++ b/stylesheets/testorange.css
@@ -12,6 +12,19 @@ body {
background-position: right bottom, 0% 0%, 0% 0%;
}
+.desktop-style .bl-menu{
+background-image: url('img/testorange_bg.gif');
+ background-repeat: repeat;
+}
+
+.boardlist .board a {
+background-image: url('img/testorange_bg.gif');
+ background-repeat: repeat;
+}
+
+
+
+
a:link, a:visited, div.post.reply div.body a {
color:#ff9100;
text-decoration: none;
@@ -22,7 +35,10 @@ a:link:hover, a:visited:hover, div.post.reply div.body a:hover {
a.email span.name {
color:#ff9100 !important;
+}
+div.post.reply.highlighted {
+background: black;
}
input {
@@ -150,11 +166,10 @@ span.trip, .postertrip {
.omittedposts {
color:#707070;
}
-.reply {
+div.post.reply {
padding: 5px;
border: 1px #707070 solid !important;
- background: #282828 !important;
-
+ background: #282828;
-webkit-border-radius: 10px;
-khtml-border-radius: 10px;
-moz-border-radius: 10px;
@@ -164,45 +179,33 @@ span.trip, .postertrip {
background-color: #1e1e1e;
color: #bebebe;
}
-.doubledash {
- vertical-align:top;
- clear:both;
- float:left;
-}
-.replytitle {
- font-size: 1.2em;
- color:#fff;
- font-weight:800;
-}
-.commentpostername {
- color: #fff;
- font-weight:800;
-}
-.thumbnailmsg {
- font-size: small;
- color: #bebebe;
-}
form table th, div.banner, div.pages, .replymode, .postblock, .passvalid, .catalogmode {
background: #ff9100 url('img/testorange_f_bg.gif') repeat-x !important;
}
-div.pages {
+div.pages, div.pages a {
color: black;
border: 0;
}
-
-.abbrev {
- color:#707070;
+div.boardlist {
+ background-color: rgba(5%,5%,5%,0.2);
}
-.highlight {
- background:#000;
- border: 1px dashed #ff9100;
+div.boardlist.bottom {
+ background-color:transparent;
}
-#watchedthreads {
- background-color: #282828 !important;
+.desktop-style div.boardlist:nth-child(1) {
+ text-shadow: #000 1px 1px 1px, #500 -1px -1px 1px;
}
-.reflinkpreview {
- background-color: #282828 !important;
+
+.desktop-style div.boardlist:nth-child(1):hover {
+ background-color: rgba(0%, 0%, 0%, 0.45);
}
+p.intro span.subject {
+ color:#ee8100;
+}
+
+span.quote {
+ color: #a8c952;
+}
diff --git a/stylesheets/wasabi.css b/stylesheets/wasabi.css
index a080a12a..3da974c3 100644
--- a/stylesheets/wasabi.css
+++ b/stylesheets/wasabi.css
@@ -345,6 +345,8 @@ div.boardlist.bottom {
}
div.boardlist a {
text-decoration: none;
+ color:#ffff22;
+ text-shadow: #000 1px 1px 1px, #000 -1px -1px 1px, #950000 1px 1px 2px, #009500 -1px -1px 2px;
}
div.report {
color: #333;
@@ -389,3 +391,12 @@ div.blotter {
font-weight: bold;
text-align: center;
}
+
+.desktop-style div.boardlist:nth-child(1) {
+
+}
+
+
+.desktop-style div.boardlist:nth-child(1):hover {
+ background-color: rgba(100%, 100%, 30%, 0.35);
+}
diff --git a/templates/attention_bar.html b/templates/attention_bar.html
new file mode 100644
index 00000000..35f6b8f0
--- /dev/null
+++ b/templates/attention_bar.html
@@ -0,0 +1,6 @@
+{% if config.attention_bar %}
+<div id="attention_bar"></div>
+<form method="post" action="{{ config.root }}attentionbar.php" id="attention_bar_form">
+ <input id="attention_bar_input" name="text" value="" type="text">
+</form>
+{% endif %}
diff --git a/templates/boardlist.html b/templates/boardlist.html
new file mode 100644
index 00000000..62646845
--- /dev/null
+++ b/templates/boardlist.html
@@ -0,0 +1 @@
+I'm your overboard boardlist. You can put here anything and I reside in templates/boardlist.html
diff --git a/templates/generic_page.html b/templates/generic_page.html
index 8f608a6a..70cb15aa 100644
--- a/templates/generic_page.html
+++ b/templates/generic_page.html
@@ -20,6 +20,7 @@
</div>
</header>
+ {% include 'attention_bar.html' %}
{% include 'post_form.html' %}
{% if config.global_message %}<hr /><div class="blotter">{{ config.global_message }}</div>{% endif %}
diff --git a/templates/header.html b/templates/header.html
index 6929fdca..0f4bd260 100644
--- a/templates/header.html
+++ b/templates/header.html
@@ -40,4 +40,4 @@
.recaptchatable, #recaptcha_area tr, #recaptcha_area td, #recaptcha_area th {
padding: 0 !important;
}
- {% endraw %}</style>{% endif %} \ No newline at end of file
+ {% endraw %}</style>{% endif %}
diff --git a/templates/index.html b/templates/index.html
index d5e6cd18..39576e13 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -2,6 +2,18 @@
<html>
<head>
<meta charset="utf-8">
+
+ <script type="text/javascript">
+ {% if config.quick_reply %}
+ var new_reply_string = "{{ config.button_reply }}";
+ {% endif %}
+ {% if not no_post_form %}
+ var active_page = "index";
+ {% else %}
+ var active_page = "ukko";
+ {% endif %}
+ </script>
+
{% include 'header.html' %}
<title>{{ board.url }} - {{ board.title|e }}</title>
</head>
@@ -23,8 +35,15 @@
</div>
</header>
- {% include 'post_form.html' %}
-
+ {% include 'attention_bar.html' %}
+ {% if not no_post_form %}
+ {% include 'post_form.html' %}
+ {% else %}
+ {% include 'boardlist.html' %}
+ {% endif %}
+
+ {{ config.ad.top }}
+
{% if config.global_message %}<hr /><div class="blotter">{{ config.global_message }}</div>{% endif %}
<hr />
<form name="postcontrols" action="{{ config.post_url }}" method="post">
@@ -37,6 +56,9 @@
[<a {% if page.selected %}class="selected"{% endif %}{% if not page.selected %}href="{{ page.link }}"{% endif %}>{{ page.num }}</a>]{% if loop.last %} {% endif %}
{% endfor %} {{ btn.next }}</div>
{{ boardlist.bottom }}
+
+ {{ config.ad.bottom }}
+
<footer>
<p class="unimportant" style="margin-top:20px;text-align:center;">Powered by <a href="http://tinyboard.org/">Tinyboard</a> {{ config.version }} | <a href="http://tinyboard.org/">Tinyboard</a> Copyright &copy; 2010-2013 Tinyboard Development Group</p>
{% for footer in config.footer %}<p class="unimportant" style="text-align:center;">{{ footer }}</p>{% endfor %}
diff --git a/templates/main.js b/templates/main.js
index 78054a3b..05a8fd5f 100644
--- a/templates/main.js
+++ b/templates/main.js
@@ -265,7 +265,15 @@ function ready() {
onready(init);
-{% endraw %}{% if config.google_analytics %}{% raw %}
+{% endraw %}
+
+var oekaki_options = {
+ width: {{config.oekaki.width}},
+ height: {{config.oekaki.height}},
+ root: '{{config.root}}'
+};
+
+{% if config.google_analytics %}{% raw %}
var _gaq = _gaq || [];_gaq.push(['_setAccount', '{% endraw %}{{ config.google_analytics }}{% raw %}']);{% endraw %}{% if config.google_analytics_domain %}{% raw %}_gaq.push(['_setDomainName', '{% endraw %}{{ config.google_analytics_domain }}{% raw %}']){% endraw %}{% endif %}{% if not config.google_analytics_domain %}{% raw %}_gaq.push(['_setDomainName', 'none']){% endraw %}{% endif %}{% raw %};_gaq.push(['_trackPageview']);(function() {var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);})();{% endraw %}{% endif %}
diff --git a/templates/post_form.html b/templates/post_form.html
index 7c8894c1..c2bb25d0 100644
--- a/templates/post_form.html
+++ b/templates/post_form.html
@@ -25,26 +25,33 @@
{{ antibot.html() }}
</th>
<td>
+ {% if config.field_email_selectbox %}
+ <select name="email" id="email_selectbox" autocomplete="off">
+ <option value=""></option>
+ <option value="sage">sage</option>
+ <option value="noko">noko</option>
+ </select>
+ {% else %}
<input type="text" name="email" size="25" maxlength="40" autocomplete="off">
+ {% endif %}
+ {{ antibot.html() }}
+ {% if not (not (config.field_disable_subject or (id and config.field_disable_reply_subject)) or (mod and post.mod|hasPermission(config.mod.bypass_field_disable, board.uri))) %}
+ <input accesskey="s" style="margin-left:2px;" type="submit" name="post" value="{% if id %}{{ config.button_reply }}{% else %}{{ config.button_newtopic }}{% endif %}" />{% if config.spoiler_images %} <input id="spoiler" name="spoiler" type="checkbox"> <label for="spoiler">{% trans %}Spoiler Image{% endtrans %}</label> {% endif %}
+ {% endif %}
{{ antibot.html() }}
</td>
</tr>{% endif %}
- <tr>
- {% if not (config.field_disable_subject or (id and config.field_disable_reply_subject)) or (mod and post.mod|hasPermission(config.mod.bypass_field_disable, board.uri)) %}<th>
+ {% if not (config.field_disable_subject or (id and config.field_disable_reply_subject)) or (mod and post.mod|hasPermission(config.mod.bypass_field_disable, board.uri)) %}<tr>
+ <th>
{% trans %}Subject{% endtrans %}
{{ antibot.html() }}
</th>
<td>
<input style="float:left;" type="text" name="subject" size="25" maxlength="100" autocomplete="off">
- {% else %}<th>
- {% trans %}Submit{% endtrans %}
- {{ antibot.html() }}
- </th>
- <td>
- {% endif %}
<input accesskey="s" style="margin-left:2px;" type="submit" name="post" value="{% if id %}{{ config.button_reply }}{% else %}{{ config.button_newtopic }}{% endif %}" />{% if config.spoiler_images %} <input id="spoiler" name="spoiler" type="checkbox"> <label for="spoiler">{% trans %}Spoiler Image{% endtrans %}</label>{% endif %}
</td>
</tr>
+ {% endif %}
<tr>
<th>
{% trans %}Comment{% endtrans %}
@@ -53,6 +60,11 @@
<td>
<textarea name="body" id="body" rows="5" cols="35"></textarea>
{{ antibot.html() }}
+ {% if not (not (config.field_disable_subject or (id and config.field_disable_reply_subject)) or (mod and post.mod|hasPermission(config.mod.bypass_field_disable, board.uri))) %}
+ {% if not (not config.field_disable_email or (mod and post.mod|hasPermission(config.mod.bypass_field_disable, board.uri))) %}
+ <input accesskey="s" style="margin-left:2px;" type="submit" name="post" value="{% if id %}{{ config.button_reply }}{% else %}{{ config.button_newtopic }}{% endif %}" />{% if config.spoiler_images %} <input id="spoiler" name="spoiler" type="checkbox"> <label for="spoiler">{% trans %}Spoiler Image{% endtrans %}</label>{% endif %}
+ {% endif %}
+ {% endif %}
</td>
</tr>
{% if config.recaptcha %}
@@ -67,15 +79,14 @@
</td>
</tr>
{% endif %}
- <tr>
+ <tr id="upload">
<th>
{% trans %}File{% endtrans %}
</th>
<td>
- <input type="file" name="file">
+ <input type="file" name="file" id="upload_file">
{% if config.allow_upload_by_url %}
- <br>
- <div style="float:none;text-align:left">
+ <div style="float:none;text-align:left" id="upload_url">
<label for="file_url">{% trans %}Or URL{% endtrans %}</label>:
<input style="display:inline" type="text" id="file_url" name="file_url" size="35">
</div>
@@ -84,7 +95,7 @@
</td>
</tr>
{% if config.enable_embedding %}
- <tr>
+ <tr id="upload_embed">
<th>
{% trans %}Embed{% endtrans %}
{{ antibot.html() }}
diff --git a/templates/post_thread.html b/templates/post_thread.html
index 0ee3f6fa..97fad11c 100644
--- a/templates/post_thread.html
+++ b/templates/post_thread.html
@@ -1,7 +1,7 @@
{% filter remove_whitespace %}
{# tabs and new lines will be ignored #}
-<div id="thread_{{ post.id }}">
+<div id="thread_{{ post.id }}" data-board="{{ board.uri }}">
{% if post.embed %}
{{ post.embed }}
@@ -128,6 +128,13 @@
{% if index %}
<a href="{{ post.root }}{{ board.dir }}{{ config.dir.res }}{{ config.file_page|sprintf(post.id) }}">[{% trans %}Reply{% endtrans %}]</a>
{% endif %}
+ {% if isnoko50 %}
+ <a href="{{ post.root }}{{ board.dir }}{{ config.dir.res }}{{ config.file_page|sprintf(post.id) }}">[{% trans %}View All{% endtrans %}]</a>
+ {% endif %}
+ {% if hasnoko50 and not isnoko50 %}
+ {% set lastcount = config.noko50_count %}
+ <a href="{{ post.root }}{{ board.dir }}{{ config.dir.res }}{{ config.file_page50|sprintf(post.id) }}">[{% trans %}Last 1 Post{% plural lastcount %}Last {{ count }} Posts{% endtrans %}]</a>
+ {% endif %}
{{ post.postControls }}
</p>
<div class="body">
diff --git a/templates/search_form.html b/templates/search_form.html
new file mode 100644
index 00000000..4c8bd077
--- /dev/null
+++ b/templates/search_form.html
@@ -0,0 +1,19 @@
+<div class="ban">
+ <h2>{% trans %}Search{% endtrans %}</h2>
+ <form style="display:inline" action="" method="get">
+ <p>
+ <label style="display:inline" for="search">{% trans %}Phrase:{% endtrans %}</label>
+ <input id="search" name="search" type="text" size="40" value="{{ search }}">
+ <select name="board">
+ <option value="none">{% trans %}Select board{% endtrans %}&hellip;</option>
+ {% for b in boards %}
+ <option value="{{ b }}"{% if b == board %} selected{% endif %}>/{{ b }}/</option>
+ {% endfor %}
+ </select>
+ <input type="submit" value="{% trans %}Search{% endtrans %}" />
+ </p>
+ </form>
+ <p style="font-size:8pt;margin:5px">
+ {% trans %}Search is case-insensitive and based on keywords. To match exact phrases, use "quotes". Use an asterisk (*) for wildcard.</p><p style="font-size:8pt;margin:5px">You may apply the following filters to your searches: <strong>id</strong>, <strong>thread</strong>, <strong>subject</strong>, and <strong>name</strong>. To apply a filter, simply add to your query, for example, <em>name:Anonymous</em> or <em>subject:"Some Thread"</em>. Wildcards cannot be used in filters.{% endtrans %}
+ </p>
+</div>
diff --git a/templates/themes/ukko/info.php b/templates/themes/ukko/info.php
new file mode 100644
index 00000000..34d59d4e
--- /dev/null
+++ b/templates/themes/ukko/info.php
@@ -0,0 +1,53 @@
+<?php
+ $theme = Array();
+
+ // Theme name
+ $theme['name'] = 'Ukko';
+ // Description (you can use Tinyboard markup here)
+ $theme['description'] = 'Board with threads and messages from all boards';
+ $theme['version'] = 'v0.1';
+
+ // Theme configuration
+ $theme['config'] = Array();
+
+ $theme['config'][] = Array(
+ 'title' => 'Board name',
+ 'name' => 'title',
+ 'type' => 'text',
+ 'default' => 'Ukko'
+ );
+ $theme['config'][] = Array(
+ 'title' => 'Board URI',
+ 'name' => 'uri',
+ 'type' => 'text',
+ 'comment' => '(ukko for example)'
+ );
+ $theme['config'][] = Array(
+ 'title' => 'Subtitle',
+ 'name' => 'subtitle',
+ 'type' => 'text',
+ 'comment' => '(%s = thread limit. for example "%s freshly bumped threads")'
+ );
+ $theme['config'][] = Array(
+ 'title' => 'Excluded boards',
+ 'name' => 'exclude',
+ 'type' => 'text',
+ 'comment' => '(space seperated)'
+ );
+ $theme['config'][] = Array(
+ 'title' => 'Number of threads',
+ 'name' => 'thread_limit',
+ 'type' => 'text',
+ 'default' => '15',
+ );
+ // Unique function name for building everything
+ $theme['build_function'] = 'ukko_build';
+ $theme['install_callback'] = 'ukko_install';
+
+ if(!function_exists('ukko_install')) {
+ function ukko_install($settings) {
+ if (!file_exists($settings['uri']))
+ @mkdir($settings['uri'], 0777) or error("Couldn't create " . $settings['uri'] . ". Check permissions.", true);
+ }
+ }
+
diff --git a/templates/themes/ukko/theme.php b/templates/themes/ukko/theme.php
new file mode 100644
index 00000000..c60d029a
--- /dev/null
+++ b/templates/themes/ukko/theme.php
@@ -0,0 +1,103 @@
+<?php
+ require 'info.php';
+
+ function ukko_build($action, $settings) {
+ $ukko = new ukko();
+ $ukko->settings = $settings;
+ $ukko->build();
+ }
+
+ class ukko {
+ public $settings;
+ public function build($mod = false) {
+ global $config;
+ $boards = listBoards();
+
+ $body = '';
+ $overflow = array();
+ $board = array(
+ 'url' => $this->settings['uri'],
+ 'name' => $this->settings['title'],
+ 'title' => sprintf($this->settings['subtitle'], $this->settings['thread_limit'])
+ );
+
+ $query = '';
+ foreach($boards as &$_board) {
+ if(in_array($_board['uri'], explode(' ', $this->settings['exclude'])))
+ continue;
+ $query .= sprintf("SELECT *, '%s' AS `board` FROM ``posts_%s`` WHERE `thread` IS NULL UNION ALL ", $_board['uri'], $_board['uri']);
+ }
+ $query = preg_replace('/UNION ALL $/', 'ORDER BY `bump` DESC', $query);
+ $query = query($query) or error(db_error());
+
+ $count = 0;
+ $threads = array();
+ while($post = $query->fetch()) {
+
+ if(!isset($threads[$post['board']])) {
+ $threads[$post['board']] = 1;
+ } else {
+ $threads[$post['board']] += 1;
+ }
+
+ if($count < $this->settings['thread_limit']) {
+ openBoard($post['board']);
+ $thread = new Thread($post, $mod ? '?/' : $config['root'], $mod);
+
+ $posts = prepare(sprintf("SELECT * FROM ``posts_%s`` WHERE `thread` = :id ORDER BY `id` DESC LIMIT :limit", $post['board']));
+ $posts->bindValue(':id', $post['id']);
+ $posts->bindValue(':limit', ($post['sticky'] ? $config['threads_preview_sticky'] : $config['threads_preview']), PDO::PARAM_INT);
+ $posts->execute() or error(db_error($posts));
+
+ $num_images = 0;
+ while ($po = $posts->fetch()) {
+ if ($po['file'])
+ $num_images++;
+
+ $thread->add(new Post($po, $mod ? '?/' : $config['root'], $mod));
+
+ }
+ if ($posts->rowCount() == ($post['sticky'] ? $config['threads_preview_sticky'] : $config['threads_preview'])) {
+ $ct = prepare(sprintf("SELECT COUNT(`id`) as `num` FROM ``posts_%s`` WHERE `thread` = :thread UNION ALL SELECT COUNT(`id`) FROM `posts_%s` WHERE `file` IS NOT NULL AND `thread` = :thread", $post['board'], $post['board']));
+ $ct->bindValue(':thread', $post['id'], PDO::PARAM_INT);
+ $ct->execute() or error(db_error($count));
+
+ $c = $ct->fetch();
+ $thread->omitted = $c['num'] - ($post['sticky'] ? $config['threads_preview_sticky'] : $config['threads_preview']);
+
+ $c = $ct->fetch();
+ $thread->omitted_images = $c['num'] - $num_images;
+ }
+
+
+ $thread->posts = array_reverse($thread->posts);
+ $body .= '<h2><a href="' . $config['root'] . $post['board'] . '">/' . $post['board'] . '/</a></h2>';
+ $body .= $thread->build(true);
+ } else {
+ $page = 'index';
+ if(floor($threads[$post['board']] / $config['threads_per_page']) > 0) {
+ $page = floor($threads[$post['board']] / $config['threads_per_page']) + 1;
+ }
+ $overflow[] = array('id' => $post['id'], 'board' => $post['board'], 'page' => $page . '.html');
+ }
+
+ $count += 1;
+ }
+
+ $body .= '<script> var overflow = ' . json_encode($overflow) . '</script>';
+ $body .= '<script type="text/javascript" src="ukko.js"></script>';
+
+ file_write($this->settings['uri'] . '/index.html', Element('index.html', array(
+ 'config' => $config,
+ 'board' => $board,
+ 'no_post_form' => true,
+ 'body' => $body,
+ 'boardlist' => createBoardlist($mod)
+ )));
+
+ file_write($this->settings['uri'] . '/ukko.js', Element('themes/ukko/ukko.js', array()));
+ }
+
+ };
+
+?>
diff --git a/templates/themes/ukko/thumb.png b/templates/themes/ukko/thumb.png
new file mode 100644
index 00000000..49c4237b
--- /dev/null
+++ b/templates/themes/ukko/thumb.png
Binary files differ
diff --git a/templates/themes/ukko/ukko.js b/templates/themes/ukko/ukko.js
new file mode 100644
index 00000000..4a36b394
--- /dev/null
+++ b/templates/themes/ukko/ukko.js
@@ -0,0 +1,115 @@
+(function(){
+
+var cache = new Array(),
+ thread = false,
+ loading = false,
+ ukkotimer = false;
+if (localStorage.hiddenboards !== null) {
+ localStorage.hiddenboards = "{}";
+}
+
+// Load data from HTML5 localStorage
+var hiddenboards = JSON.parse(localStorage.hiddenboards);
+
+var storeboards = function() {
+ localStorage.hiddenboards = JSON.stringify(hiddenboards);
+};
+
+$(document).ready(function() {
+ var addukkohide = function() {
+ var ukkohide = $('<a href="javascript:void(0);" class="unimportant ukkohide"></a>');
+ var board = $(this).next().data("board");
+ var hr = $("<hr />");
+
+ $(this).append(ukkohide);
+ $(this).append(hr);
+
+ if (hiddenboards[board] !== true) {
+ ukkohide.html(_("(hide threads from this board)"));
+ hr.hide();
+ }
+ else {
+ ukkohide.html(_("(show threads from this board)"));
+ $(this).next().hide();
+ }
+ ukkohide.click(function() {
+ hiddenboards[board] = (hiddenboards[board] !== true);
+ if (hiddenboards[board] !== true) {
+ $('[data-board="'+board+'"]:not([data-cached="yes"])').show().prev().
+ find('.ukkohide').html(_("(hide threads from this board)")).
+ parent().find('hr').hide();
+ }
+ else {
+ $('[data-board="'+board+'"]:not([data-cached="yes"])').hide().prev().
+ find('.ukkohide').html(_("(show threads from this board)"))
+ .parent().find('hr').show();
+ }
+ storeboards();
+ return false;
+ });
+
+ };
+ $("h2").each(addukkohide);
+
+ $('.pages').hide();
+ var loadnext = function() {
+ if (overflow.length == 0) {
+ $('.pages').show().html(_("No more threads to display"));
+ }
+ while($(window).scrollTop() + $(window).height() + 1000 > $(document).height() && !loading && overflow.length > 0) {
+ var page = '../' + overflow[0].board + '/' + overflow[0].page;
+ thread = $('div#thread_' + overflow[0].id + '[data-board="' + overflow[0].board + '"]');
+ if (thread.length > 0 && thread.attr("data-cached") !== 'yes') { // already present
+ overflow.shift();
+ continue;
+ }
+
+ var boardheader = $('<h2><a href="/' + overflow[0].board + '/">/' + overflow[0].board + '/</a> </h2>');
+
+ if($.inArray(page, cache) != -1) {
+ if (thread.length > 0) {
+ $('div[id*="thread_"]').last().after(thread.attr('data-board', overflow[0].board).attr("data-cached", "no").css('display', 'block'));
+ boardheader.insertBefore(thread);
+ addukkohide.call(boardheader);
+ $(document).trigger('new_post', thread);
+ }
+ overflow.shift();
+ } else {
+ loading = true;
+ $('.pages').show().html(_("Loading..."));
+ $.get(page, function(data) {
+ cache.push(page);
+
+ $(data).find('div[id*="thread_"]').each(function() {
+ var checkout = $(this).attr('id').replace('thread_', '');
+ if ($('div#thread_' + checkout + '[data-board="' + overflow[0].board + '"]').length == 0) {
+ $('form[name="postcontrols"]').prepend($(this).css('display', 'none').attr("data-cached", "yes").attr('data-board', overflow[0].board));
+ }
+ });
+
+ thread = $('div#thread_' + overflow[0].id + '[data-board="' + overflow[0].board + '"][data-cached="yes"]');
+
+ if(thread.length > 0) {
+ $('div[id*="thread_"]').last().after(thread.attr('data-board', overflow[0].board).attr("data-cached", "no").css('display', 'block'));
+ boardheader.insertBefore(thread);
+ addukkohide.call(boardheader);
+ $(document).trigger('new_post', thread);
+ }
+ overflow.shift();
+
+ loading = false;
+ $('.pages').hide().html("");
+ });
+ break;
+ }
+ }
+ clearTimeout(ukkotimer);
+ ukkotimer = setTimeout(loadnext, 1000);
+ };
+
+ $(window).on('scroll', loadnext);
+
+ ukkotimer = setTimeout(loadnext, 1000);
+});
+
+})();
diff --git a/templates/thread.html b/templates/thread.html
index e8c43ef7..76d50f94 100644
--- a/templates/thread.html
+++ b/templates/thread.html
@@ -2,6 +2,11 @@
<html>
<head>
<meta charset="utf-8">
+
+ <script type="text/javascript">
+ var active_page = "thread";
+ </script>
+
{% include 'header.html' %}
<title>{{ board.url }} - {% if config.thread_subject_in_title and thread.subject %}{{ thread.subject }}{% else %}{{ board.title|e }}{% endif %}</title>
</head>
@@ -25,8 +30,12 @@
<div class="banner">{% trans %}Posting mode: Reply{% endtrans %} <a class="unimportant" href="{{ return }}">[{% trans %}Return{% endtrans %}]</a></div>
+ {% include 'attention_bar.html' %}
+
{% include 'post_form.html' %}
+ {{ config.ad.top }}
+
{% if config.global_message %}<hr /><div class="blotter">{{ config.global_message }}</div>{% endif %}
<hr />
<form name="postcontrols" action="{{ config.post_url }}" method="post">
@@ -38,6 +47,9 @@
<a href="{{ return }}">[{% trans %}Return{% endtrans %}]</a>
{{ boardlist.bottom }}
+
+ {{ config.ad.bottom }}
+
<footer>
<p class="unimportant" style="margin-top:20px;text-align:center;">Powered by <a href="http://tinyboard.org/">Tinyboard</a> {{ config.version }} | <a href="http://tinyboard.org/">Tinyboard</a> Copyright &copy; 2010-2013 Tinyboard Development Group</p>
{% for footer in config.footer %}<p class="unimportant" style="text-align:center;">{{ footer }}</p>{% endfor %}
diff --git a/tools/benchmark.php b/tools/benchmark.php
new file mode 100644
index 00000000..8c97364c
--- /dev/null
+++ b/tools/benchmark.php
@@ -0,0 +1,52 @@
+#!/usr/bin/php
+<?php
+/*
+ * benchmark.php - benchmarks thumbnailing methods
+ *
+ */
+
+require dirname(__FILE__) . '/inc/cli.php';
+require 'inc/image.php';
+
+// move back to this directory
+chdir(dirname(__FILE__));
+
+if(count($argv) != 2)
+ die("Usage: {$argv[0]} [file]\n");
+
+$file = $argv[1];
+$extension = strtolower(substr($file, strrpos($file, '.') + 1));
+$out = tempnam($config['tmp'], 'thumb');
+$count = 300;
+
+function benchmark($method) {
+ global $config, $file, $extension, $out, $count;
+
+ $config['thumb_method'] = $method;
+
+ printf("Method: %s\nThumbnailing %d times... ", $method, $count);
+
+ $start = microtime(true);
+ for($i = 0; $i < $count; $i++) {
+ $image = new Image($file, $extension);
+ $thumb = $image->resize(
+ $config['thumb_ext'] ? $config['thumb_ext'] : $extension,
+ $config['thumb_width'],
+ $config['thumb_height']
+ );
+
+ $thumb->to($out);
+ $thumb->_destroy();
+ $image->destroy();
+ }
+ $end = microtime(true);
+
+ printf("Took %.2f seconds (%.2f/second; %.2f ms)\n", $end - $start, $rate = ($count / ($end - $start)), 1000 / $rate);
+
+ unlink($out);
+}
+
+benchmark('gd');
+benchmark('imagick');
+benchmark('convert');
+
diff --git a/tools/i18n_compile.php b/tools/i18n_compile.php
new file mode 100755
index 00000000..ba193767
--- /dev/null
+++ b/tools/i18n_compile.php
@@ -0,0 +1,44 @@
+#!/usr/bin/php
+<?php
+
+/*
+ * i18n_compile.php - compiles the i18n
+ *
+ * Options:
+ * -l [locale], --locale=[locale]
+ * Compiles [locale] locale.
+ *
+ */
+
+require dirname(__FILE__) . '/inc/cli.php';
+
+// parse command line
+$opts = getopt('l:', Array('locale:'));
+$options = Array();
+
+$options['locale'] = isset($opts['l']) ? $opts['l'] : (isset($opts['locale']) ? $opts['locale'] : false);
+
+if ($options['locale']) $locales = array($options['locale']);
+else die("Error: no locales specified; use -l switch, eg. -l pl_PL\n");
+
+foreach ($locales as $loc) {
+ if (file_exists ($locdir = "inc/locale/".$loc)) {
+ if (!is_dir ($locdir)) {
+ continue;
+ }
+ }
+ else {
+ die("Error: $locdir does not exist\n");
+ }
+
+ // Generate tinyboard.po
+ if (file_exists($locdir."/LC_MESSAGES/tinyboard.po")) $join = "-j";
+ else $join = "";
+ passthru("cd $locdir/LC_MESSAGES;
+ msgfmt tinyboard.po -o tinyboard.mo");
+
+ // Generate javascript.po
+ passthru("cd tools/inc/lib/jsgettext/;
+ php po2json.php -i ../../../../$locdir/LC_MESSAGES/javascript.po \
+ -o ../../../../$locdir/LC_MESSAGES/javascript.js");
+}
diff --git a/tools/i18n_extract.php b/tools/i18n_extract.php
new file mode 100755
index 00000000..4c32b688
--- /dev/null
+++ b/tools/i18n_extract.php
@@ -0,0 +1,47 @@
+#!/usr/bin/php
+<?php
+
+/*
+ * i18n_extract.php - extracts the strings and updates all locales
+ *
+ * Options:
+ * -l [locale], --locale=[locale]
+ * Updates only [locale] locale. If it does not exist yet, we create a new directory.
+ *
+ */
+
+require dirname(__FILE__) . '/inc/cli.php';
+
+// parse command line
+$opts = getopt('l:', Array('locale:'));
+$options = Array();
+
+$options['locale'] = isset($opts['l']) ? $opts['l'] : (isset($opts['locale']) ? $opts['locale'] : false);
+
+$locales = glob("inc/locale/*");
+$locales = array_map("basename", $locales);
+
+if ($options['locale']) $locales = array($options['locale']);
+
+
+foreach ($locales as $loc) {
+ if (file_exists ($locdir = "inc/locale/".$loc)) {
+ if (!is_dir ($locdir)) {
+ continue;
+ }
+ }
+ else {
+ mkdir($locdir);
+ mkdir($locdir."/LC_MESSAGES");
+ }
+
+ // Generate tinyboard.po
+ if (file_exists($locdir."/LC_MESSAGES/tinyboard.po")) $join = "-j";
+ else $join = "";
+ passthru("cd $locdir/LC_MESSAGES;
+ xgettext -d tinyboard -L php --from-code utf-8 $join -c $(find ../../../../ -name \*.php)");
+
+ // Generate javascript.po
+ passthru("cd $locdir/LC_MESSAGES;
+ xgettext -d javascript -L Python --force-po --from-code utf-8 $join -c $(find ../../../../js/ ../../../../templates/ -name \*.js)");
+}
diff --git a/tools/inc/cli.php b/tools/inc/cli.php
new file mode 100644
index 00000000..294609cd
--- /dev/null
+++ b/tools/inc/cli.php
@@ -0,0 +1,100 @@
+<?php
+
+/*
+ * This script will look for Tinyboard in the following places (in order):
+ * - $TINYBOARD_PATH environment varaible
+ * - ./
+ * - ./Tinyboard/
+ * - ../
+ */
+
+set_time_limit(0);
+$shell_path = getcwd();
+
+if(getenv('TINYBOARD_PATH') !== false)
+ $dir = getenv('TINYBOARD_PATH');
+elseif(file_exists('inc/functions.php'))
+ $dir = false;
+elseif(file_exists('Tinyboard') && is_dir('Tinyboard') && file_exists('Tinyboard/inc/functions.php'))
+ $dir = 'Tinyboard';
+elseif(file_exists('../inc/functions.php'))
+ $dir = '..';
+else
+ die("Could not locate Tinyboard directory!\n");
+
+if($dir && !chdir($dir))
+ die("Could not change directory to {$dir}\n");
+
+if(!getenv('TINYBOARD_PATH')) {
+ // follow symlink
+ chdir(realpath('inc') . '/..');
+}
+
+putenv('TINYBOARD_PATH=' . getcwd());
+
+require 'inc/functions.php';
+require 'inc/mod.php';
+
+$mod = Array(
+ 'id' => -1,
+ 'type' => ADMIN,
+ 'username' => '?',
+ 'boards' => Array('*')
+);
+
+function get_httpd_privileges() {
+ global $config, $shell_path, $argv;
+
+ if(php_sapi_name() != 'cli')
+ die("get_httpd_privileges(): invoked from HTTP client.\n");
+
+ echo "Dropping priviledges...\n";
+
+ if(!is_writable('.'))
+ die("get_httpd_privileges(): web directory is not writable\n");
+
+ $filename = '.' . md5(rand()) . '.php';
+ $inc_filename = '.' . md5(rand()) . '.php';
+
+ echo "Copying rebuilder to web directory...\n";
+
+ // replace "/inc/cli.php" with its new filename
+ passthru("cat " . escapeshellarg($shell_path . '/' . $_SERVER['PHP_SELF']) . " | sed \"s/'\/inc\/cli\.php'/'\/{$inc_filename}'/\" > {$filename}");
+
+ $inc_header = "<?php\n";
+
+ // copy environment
+ $env = explode("\n", shell_exec('printenv | grep ^TINYBOARD'));
+ foreach($env as $line) {
+ if(!empty($line))
+ $inc_header .= "putenv('" . addslashes($line) . "');\n";
+ }
+
+ // copy command line arguments
+ $inc_header .= "\$argv = " . var_export($argv, true) . ";\n";
+
+ // copy this file
+ file_put_contents($inc_filename, $inc_header . substr($inc = file_get_contents(__FILE__), strpos($inc, "\n")));
+
+ chmod($filename, 0666);
+ chmod($inc_filename, 0666);
+
+ if(preg_match('/^https?:\/\//', $config['root'])) {
+ $url = $config['root'] . $filename;
+ } elseif($host = getenv('TINYBOARD_HOST')) {
+ $url = 'http://' . $host . $config['root'] . $filename;
+ } else {
+ // assume localhost
+ $url = 'http://localhost' . $config['root'] . $filename;
+ }
+
+ echo "Downloading $url\n";
+
+ passthru('curl -s -N ' . escapeshellarg($url));
+
+ unlink($filename);
+ unlink($inc_filename);
+
+ exit(0);
+}
+
diff --git a/tools/inc/lib/jsgettext/JSParser.php b/tools/inc/lib/jsgettext/JSParser.php
new file mode 100644
index 00000000..adba59bc
--- /dev/null
+++ b/tools/inc/lib/jsgettext/JSParser.php
@@ -0,0 +1,94 @@
+<?php
+
+class JSParser {
+
+ protected $content;
+ protected $keywords;
+ protected $regs = array();
+ protected $regsCounter = 0;
+ protected $strings = array();
+ protected $stringsCounter = 0;
+
+ protected function _extractRegs($match) {
+ $this->regs[$this->regsCounter] = $match[1];
+ $id = "<<reg{$this->regsCounter}>>";
+ $this->regsCounter++;
+ return $id;
+ }
+ protected function _extractStrings($match) {
+ $this->strings[$this->stringsCounter] = $this->importRegExps($match[0]);
+ $id = "<<s{$this->stringsCounter}>>";
+ $this->stringsCounter++;
+ return $id;
+ }
+ protected function importRegExps($input) {
+ $regs = $this->regs;
+ return preg_replace_callback("#<<reg(\d+)>>#", function ($match) use($regs) {
+ return $regs[$match[1]];
+ }, $input);
+ }
+
+ protected function importStrings($input) {
+ $strings = $this->strings;
+ return preg_replace_callback("#<<s(\d+)>>#", function ($match) use($strings) {
+ return $strings[$match[1]];
+ }, $input);
+ }
+
+ public function __construct($file, $keywords = '_') {
+ $this->content = file_get_contents($file);
+ $this->keywords = (array)$keywords;
+ }
+
+ public function parse() {
+ $output = $this->content; //htmlspecialchars($this->content, ENT_NOQUOTES);
+
+ // extract reg exps
+ $output = preg_replace_callback(
+ '# ( / (?: (?>[^/\\\\]++) | \\\\\\\\ | (?<!\\\\)\\\\(?!\\\\) | \\\\/ )+ (?<!\\\\)/ ) [a-z]* \b #ix',
+ array($this, '_extractRegs'), $output
+ );
+
+ // extract strings
+ $output = preg_replace_callback(
+ array(
+ '# " ( (?: (?>[^"\\\\]++) | \\\\\\\\ | (?<!\\\\)\\\\(?!\\\\) | \\\\" )* ) (?<!\\\\)" #ix',
+ "# ' ( (?: (?>[^'\\\\]++) | \\\\\\\\ | (?<!\\\\)\\\\(?!\\\\) | \\\\' )* ) (?<!\\\\)' #ix"
+ ), array($this, '_extractStrings'), $output
+ );
+
+ // delete line comments
+ $output = preg_replace("#(//.*?)$#m", '', $output);
+
+ // delete multiline comments
+ $output = preg_replace('#/\*(.*?)\*/#is', '', $output);
+
+ $strings = $this->strings;
+ $output = preg_replace_callback("#<<s(\d+)>>#", function($match) use($strings) {
+ return $strings[$match[1]];
+ }, $output);
+
+ $keywords = implode('|', $this->keywords);
+
+ $strings = array();
+
+ // extract func calls
+ preg_match_all(
+ '# (?:'.$keywords.') \(\\ *" ( (?: (?>[^"\\\\]++) | \\\\\\\\ | (?<!\\\\)\\\\(?!\\\\) | \\\\" )* ) (?<!\\\\)"\\ *\) #ix',
+ $output, $matches, PREG_SET_ORDER
+ );
+
+ foreach ($matches as $m) $strings[] = stripslashes($m[1]);
+
+ $matches = array();
+ preg_match_all(
+ "# (?:$keywords) \(\\ *' ( (?: (?>[^'\\\\]++) | \\\\\\\\ | (?<!\\\\)\\\\(?!\\\\) | \\\\' )* ) (?<!\\\\)'\\ *\) #ix",
+ $output, $matches, PREG_SET_ORDER
+ );
+
+ foreach ($matches as $m) $strings[] = stripslashes($m[1]);
+
+ return $strings;
+ }
+}
+?>
diff --git a/tools/inc/lib/jsgettext/PoeditParser.php b/tools/inc/lib/jsgettext/PoeditParser.php
new file mode 100644
index 00000000..eebd816e
--- /dev/null
+++ b/tools/inc/lib/jsgettext/PoeditParser.php
@@ -0,0 +1,83 @@
+<?php
+
+require_once 'PoeditString.php';
+
+class PoeditParser {
+
+ protected $file;
+ protected $header = '';
+ protected $strings = array();
+
+ protected function _fixQuotes($str) {
+ return stripslashes($str);
+ }
+
+ public function __construct($file) {
+ $this->file = $file;
+ }
+
+ public function parse() {
+ $contents = file_get_contents($this->file);
+
+ $parts = preg_split('#(\r\n|\n){2}#', $contents, -1, PREG_SPLIT_NO_EMPTY);
+
+ $this->header = array_shift($parts);
+
+ foreach ($parts as $part) {
+
+ // parse comments
+ $comments = array();
+ preg_match_all('#^\\#: (.*?)$#m', $part, $matches, PREG_SET_ORDER);
+ foreach ($matches as $m) $comments[] = $m[1];
+
+ $isFuzzy = preg_match('#^\\#, fuzzy$#im', $part) ? true : false;
+
+ preg_match_all('# ^ (msgid|msgstr)\ " ( (?: (?>[^"\\\\]++) | \\\\\\\\ | (?<!\\\\)\\\\(?!\\\\) | \\\\" )* ) (?<!\\\\)" $ #ixm', $part, $matches2, PREG_SET_ORDER);
+
+ $k = $this->_fixQuotes($matches2[0][2]);
+ $v = !empty($matches2[1][2]) ? $this->_fixQuotes($matches2[1][2]) : '';
+
+ $this->strings[$k] = new PoeditString($k, $v, $isFuzzy, $comments);
+ }
+ }
+
+ public function merge($strings) {
+ foreach ((array)$strings as $str) {
+ if (!in_array($str, array_keys($this->strings))) {
+ $this->strings[$str] = new PoeditString($str);
+ }
+ }
+ }
+
+ public function getHeader() {
+ return $this->header;
+ }
+
+ public function getStrings() {
+ return $this->strings;
+ }
+
+ public function getJSON() {
+ $str = array();
+ foreach ($this->strings as $s) {
+ if ($s->value) $str[$s->key] = $s->value;
+ }
+ return json_encode($str);
+ }
+
+ public function toJSON($outputFilename, $varName = 'l10n') {
+ $str = "$varName = " . $this->getJSON() . ";";
+ return file_put_contents($outputFilename, $str) !== false;
+ }
+
+ public function save($filename = null) {
+ $data = $this->header . "\n\n";
+ foreach ($this->strings as $str) {
+ $data .= $str;
+ }
+ return file_put_contents($filename ? $filename : $this->file, $data) !== false;
+ }
+}
+
+
+?>
diff --git a/tools/inc/lib/jsgettext/PoeditString.php b/tools/inc/lib/jsgettext/PoeditString.php
new file mode 100644
index 00000000..3d59d004
--- /dev/null
+++ b/tools/inc/lib/jsgettext/PoeditString.php
@@ -0,0 +1,29 @@
+<?php
+
+class PoeditString {
+ public $key;
+ public $value;
+ public $fuzzy;
+ public $comments;
+
+ function __construct($key, $value = '', $fuzzy = false, $comments = array()) {
+ $this->key = $key;
+ $this->value = $value;
+ $this->fuzzy = $fuzzy;
+ $this->comments = (array)$comments;
+ }
+
+ public function __toString() {
+ $str ='';
+ foreach ($this->comments as $c) {
+ $str .= "#: $c\n";
+ }
+ if ($this->fuzzy) $str .= "#, fuzzy\n";
+ $str .= 'msgid "'.str_replace('"', '\\"', $this->key).'"' . "\n";
+ $str .= 'msgstr "'.str_replace('"', '\\"', $this->value).'"' . "\n";
+ $str .= "\n";
+ return $str;
+ }
+}
+
+?> \ No newline at end of file
diff --git a/tools/inc/lib/jsgettext/jsgettext.php b/tools/inc/lib/jsgettext/jsgettext.php
new file mode 100644
index 00000000..6c70f0b4
--- /dev/null
+++ b/tools/inc/lib/jsgettext/jsgettext.php
@@ -0,0 +1,63 @@
+<?php
+
+ require_once 'JSParser.php';
+ require_once 'PoeditParser.php';
+
+ function buildOptions($args) {
+ $options = array(
+ 'files' => array(),
+ '-o' => null,
+ '-k' => '_'
+ );
+ $len = count($args);
+ $i = 1;
+ while ($i < $len) {
+ if (preg_match('#^-[a-z]$#i', $args[$i])) {
+ $options[$args[$i]] = isset($args[$i+1]) ? trim($args[$i+1]) : true;
+ $i += 2;
+ }
+ else {
+ $options['files'][] = $args[$i];
+ $i++;
+ }
+ }
+ return $options;
+ }
+
+ $options = buildOptions($argv);
+
+ if (!file_exists($options['-o'])) {
+ touch($options['-o']);
+ }
+
+ if (!is_writable($options['-o'])) {
+ die("Invalid output file name. Make sure it exists and is writable.");
+ }
+
+ $inputFiles = $options['files'];
+
+ if (empty($inputFiles)) {
+ die("You did not provide any input file.");
+ }
+
+ $poeditParser = new PoeditParser($options['-o']);
+ $poeditParser->parse();
+
+ $errors = array();
+
+ foreach ($inputFiles as $f) {
+ if (!is_readable($f) || !preg_match('#\.js$#', $f)) {
+ $errors[] = ("$f is not a valid javascript file.");
+ continue;
+ }
+ $jsparser = new JSParser($f, explode(' ', $options['-k']));
+ $jsStrings = $jsparser->parse();
+ $poeditParser->merge($jsStrings);
+ }
+
+ if (!empty($errors)) {
+ echo "\nThe following errors occured:\n" . implode("\n", $errors) . "\n";
+ }
+
+ $poeditParser->save();
+?>
diff --git a/tools/inc/lib/jsgettext/po2json.php b/tools/inc/lib/jsgettext/po2json.php
new file mode 100644
index 00000000..847b0899
--- /dev/null
+++ b/tools/inc/lib/jsgettext/po2json.php
@@ -0,0 +1,42 @@
+<?php
+
+ require_once 'PoeditParser.php';
+
+ function buildOptions($args) {
+ $options = array(
+ '-o' => null,
+ '-i' => null,
+ '-n' => 'l10n'
+ );
+ $len = count($args);
+ $i = 0;
+ while ($i < $len) {
+ if (preg_match('#^-[a-z]$#i', $args[$i])) {
+ $options[$args[$i]] = isset($args[$i+1]) ? trim($args[$i+1]) : true;
+ $i += 2;
+ }
+ else {
+ $options[] = $args[$i];
+ $i++;
+ }
+ }
+ return $options;
+ }
+
+ $options = buildOptions($argv);
+
+ if (!file_exists($options['-i']) || !is_readable($options['-i'])) {
+ die("Invalid input file. Make sure it exists and is readable.");
+ }
+
+ $poeditParser = new PoeditParser($options['-i']);
+ $poeditParser->parse();
+
+ if ($poeditParser->toJSON($options['-o'], $options['-n'])) {
+ $strings = count($poeditParser->getStrings());
+ echo "Successfully exported " . count($strings) . " strings.\n";
+ }
+ else {
+ echo "Cannor write to file '{$options['-o']}'.\n";
+ }
+?>
diff --git a/tools/rebuild.php b/tools/rebuild.php
new file mode 100755
index 00000000..3b5c0de2
--- /dev/null
+++ b/tools/rebuild.php
@@ -0,0 +1,105 @@
+#!/usr/bin/php
+<?php
+
+/*
+ * rebuild.php - rebuilds all static files
+ *
+ * Command line arguments:
+ * -q, --quiet
+ * Suppress output.
+ *
+ * --quick
+ * Do not rebuild posts.
+ *
+ * -b, --board <string>
+ * Rebuild only the specified board.
+ *
+ * -f, --full
+ * Rebuild replies as well as threads (re-markup).
+ *
+ */
+
+require dirname(__FILE__) . '/inc/cli.php';
+
+if(!is_writable($config['file_script'])) {
+ get_httpd_privileges();
+}
+
+$start = microtime(true);
+
+// parse command line
+$opts = getopt('qfb:', Array('board:', 'quick', 'full', 'quiet'));
+$options = Array();
+
+$options['board'] = isset($opts['board']) ? $opts['board'] : (isset($opts['b']) ? $opts['b'] : false);
+$options['quiet'] = isset($opts['q']) || isset($opts['quiet']);
+$options['quick'] = isset($opts['quick']);
+$options['full'] = isset($opts['full']) || isset($opts['f']);
+
+if(!$options['quiet'])
+ echo "== Tinyboard {$config['version']} ==\n";
+
+if(!$options['quiet'])
+ echo "Clearing template cache...\n";
+
+load_twig();
+$twig->clearCacheFiles();
+
+if(!$options['quiet'])
+ echo "Regenerating theme files...\n";
+rebuildThemes('all');
+
+if(!$options['quiet'])
+ echo "Generating Javascript file...\n";
+buildJavascript();
+
+$main_js = $config['file_script'];
+
+$boards = listBoards();
+
+foreach($boards as &$board) {
+ if($options['board'] && $board['uri'] != $options['board'])
+ continue;
+
+ if(!$options['quiet'])
+ echo "Opening board /{$board['uri']}/...\n";
+ openBoard($board['uri']);
+
+ if($config['file_script'] != $main_js) {
+ // different javascript file
+ if(!$options['quiet'])
+ echo "Generating Javascript file...\n";
+ buildJavascript();
+ }
+
+
+ if(!$options['quiet'])
+ echo "Creating index pages...\n";
+ buildIndex();
+
+ if($options['quick'])
+ continue; // do no more
+
+ if($options['full']) {
+ $query = query(sprintf("SELECT `id` FROM `posts_%s`", $board['uri'])) or error(db_error());
+ while($post = $query->fetch()) {
+ if(!$options['quiet'])
+ echo "Rebuilding #{$post['id']}...\n";
+ rebuildPost($post['id']);
+ }
+ }
+
+ $query = query(sprintf("SELECT `id` FROM `posts_%s` WHERE `thread` IS NULL", $board['uri'])) or error(db_error());
+ while($post = $query->fetch()) {
+ if(!$options['quiet'])
+ echo "Rebuilding #{$post['id']}...\n";
+ buildThread($post['id']);
+ }
+}
+
+if(!$options['quiet'])
+ printf("Complete! Took %g seconds\n", microtime(true) - $start);
+
+unset($board);
+modLog('Rebuilt everything using tools/rebuild.php');
+