diff options
68 files changed, 3323 insertions, 143 deletions
@@ -1,6 +1,15 @@ 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. + +Support and announcements: https://int.vichan.net/devel/ + 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/config.php b/inc/config.php index d7bb8ecc..2a17edfa 100644 --- a/inc/config.php +++ b/inc/config.php @@ -476,8 +476,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. @@ -518,6 +520,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. @@ -536,7 +541,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( @@ -685,7 +690,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 @@ -711,9 +721,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 @@ -796,7 +820,7 @@ // ); // Whether or not to put brackets around the whole board list - $config['boardlist_wrap_bracket'] = true; + $config['boardlist_wrap_bracket'] = false; // Show page navigation links at the top as well. $config['page_nav_top'] = false; @@ -821,6 +845,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'] = ''; @@ -999,6 +1032,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'; @@ -1098,6 +1132,9 @@ // 'color:red;font-weight:bold' // Change tripcode style; optional //); + // 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 @@ -1355,6 +1392,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 7cd69134..6f42cdf9 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 + header($_SERVER['SERVER_PROTOCOL'] . ' 400 Bad Request'); // Is there a reason to disable this? if (isset($_POST['json_response'])) { @@ -375,12 +389,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>'; @@ -446,6 +465,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; @@ -477,8 +499,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)) @@ -517,12 +539,14 @@ class Thread { return fraction($this->filewidth, $this->fileheight, ':'); } - public function build($index=false) { + public function build($index=false, $isnoko50=false) { global $board, $config, $debug; - event('show-thread', $this); + $hasnoko50 = $this->postCount() >= $config['noko50_min']; - $built = Element('post_thread.html', array('config' => $config, 'board' => $board, 'post' => &$this, 'index' => $index)); + event('show-thread', $this); + + $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 f7421f68..ac74ac91 100644 --- a/inc/functions.php +++ b/inc/functions.php @@ -107,7 +107,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'; @@ -948,6 +951,8 @@ 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'])); + file_unlink($board['dir'] . $config['dir']['res'] . sprintf('%d.json', $post['id'])); $antispam_query = prepare('DELETE FROM ``antispam`` WHERE `board` = :board AND `thread` = :thread'); $antispam_query->bindValue(':board', $board['uri']); @@ -995,6 +1000,7 @@ function deletePost($id, $error_if_doesnt_exist=true, $rebuild_after=true) { if (isset($rebuild) && $rebuild_after) { buildThread($rebuild); + buildIndex(); } return true; @@ -1353,8 +1359,10 @@ function buildIndex() { $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); + if ($config['api']['enabled']) { + $jsonFilename = $board['dir'] . ($page - 1) . '.json'; + file_unlink($jsonFilename); + } } } @@ -1851,6 +1859,9 @@ 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']; + $antibot = $mod || $return ? false : create_antibot($board['uri'], $id); $body = Element('thread.html', array( 'board' => $board, @@ -1859,7 +1870,9 @@ function buildThread($id, $return = false, $mod = false) { 'config' => $config, 'id' => $id, 'mod' => $mod, - 'antibot' => $mod || $return ? false : create_antibot($board['uri'], $id), + 'hasnoko50' => $hasnoko50, + 'isnoko50' => false, + 'antibot' => $antibot, 'boardlist' => createBoardlist($mod), 'return' => ($mod ? '?' . $board['url'] . $config['file_index'] : $config['root'] . $board['dir'] . $config['file_index']) )); @@ -1867,11 +1880,6 @@ function buildThread($id, $return = false, $mod = false) { if ($config['try_smarter'] && !$mod) $build_pages[] = thread_find_page($id); - if ($return) - return $body; - - file_write($board['dir'] . $config['dir']['res'] . sprintf($config['file_page'], $id), $body); - // json api if ($config['api']['enabled']) { $api = new Api(); @@ -1879,6 +1887,96 @@ function buildThread($id, $return = false, $mod = false) { $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, $antibot); + } + + file_write($board['dir'] . $config['dir']['res'] . sprintf($config['file_page'], $id), $body); + } +} + +function buildThread50($id, $return = false, $mod = false, $thread = null, $antibot = false) { + global $board, $config, $build_pages; + $id = round($id); + + if ($antibot) + $antibot->reset(); + + 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 : ($antibot ? $antibot : 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 c1f35c87..5166a348 100644 --- a/inc/mod/pages.php +++ b/inc/mod/pages.php @@ -712,6 +712,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; @@ -963,6 +973,104 @@ 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']); + } + } + + // build index + buildIndex(); + // build new thread + buildThread($newID); + + // trigger themes + rebuildThemes('post', $targetBoard); + // 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, $pdo; diff --git a/install.php b/install.php index c39f62dd..51d5f133 100644 --- a/install.php +++ b/install.php @@ -1,7 +1,7 @@ <?php // Installation/upgrade file -define('VERSION', 'v0.9.6-dev-21'); +define('VERSION', 'v0.9.6-dev-21 + <a href="https://int.vichan.net/devel/">vichan-devel-4.4.90</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,6 +365,10 @@ 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, @@ -365,12 +376,15 @@ if (file_exists($config['has_installed'])) { $board['uri'])) or error(db_error()); } case 'v0.9.6-dev-12': + case 'v0.9.6-dev-12 + <a href="https://int.vichan.net/devel/">vichan-devel-4.0.10</a>': + case 'v0.9.6-dev-12 + <a href="https://int.vichan.net/devel/">vichan-devel-4.0.11-gold</a>': foreach ($boards as &$board) { query(sprintf("ALTER TABLE ``posts_%s`` ADD INDEX `ip` (`ip`)", $board['uri'])) or error(db_error()); } case 'v0.9.6-dev-13': query("ALTER TABLE ``antispam`` ADD INDEX `expires` (`expires`)") or error(db_error()); case 'v0.9.6-dev-14': + case 'v0.9.6-dev-14 + <a href="https://int.vichan.net/devel/">vichan-devel-4.0.12</a>': foreach ($boards as &$board) { query(sprintf("ALTER TABLE ``posts_%s`` DROP INDEX `body`, @@ -387,6 +401,7 @@ if (file_exists($config['has_installed'])) { ADD INDEX `list_threads` (`thread`, `sticky`, `bump`)", $board['uri'])) or error(db_error()); } case 'v0.9.6-dev-16': + case 'v0.9.6-dev-16 + <a href="https://int.vichan.net/devel/">vichan-devel-4.0.13</a>': query("ALTER TABLE ``bans`` ADD INDEX `seen` (`seen`)") or error(db_error()); case 'v0.9.6-dev-17': query("ALTER TABLE ``ip_notes`` @@ -472,6 +487,7 @@ if (file_exists($config['has_installed'])) { query("DROP TABLE ``bans``") or error(db_error()); // Replace with new table query("RENAME TABLE ``bans_new_temp`` TO ``bans``") or error(db_error()); + case 'v0.9.6-dev-21': case false: // Update version number file_write($config['has_installed'], VERSION); diff --git a/install.sql b/install.sql index 462cd2f0..bb54b9b4 100644 --- a/install.sql +++ b/install.sql @@ -237,6 +237,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 Binary files differnew file mode 100644 index 00000000..246478a8 --- /dev/null +++ b/js/jscolor/arrow.gif diff --git a/js/jscolor/cross.gif b/js/jscolor/cross.gif Binary files differnew file mode 100644 index 00000000..0ee9c7ac --- /dev/null +++ b/js/jscolor/cross.gif diff --git a/js/jscolor/hs.png b/js/jscolor/hs.png Binary files differnew file mode 100644 index 00000000..3d94486c --- /dev/null +++ b/js/jscolor/hs.png diff --git a/js/jscolor/hv.png b/js/jscolor/hv.png Binary files differnew file mode 100644 index 00000000..1c5e01f8 --- /dev/null +++ b/js/jscolor/hv.png 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-vd-old.js b/js/quick-reply-vd-old.js new file mode 100644 index 00000000..bee204b7 --- /dev/null +++ b/js/quick-reply-vd-old.js @@ -0,0 +1,47 @@ +/* + * quick-reply.js + * https://github.com/savetheinternet/Tinyboard/blob/master/js/quick-reply.js + * + * Released under the MIT license + * Copyright (c) 2012 Michael Save <savetheinternet@tinyboard.org> + * + * Usage: + * $config['quick_reply'] = true; + * $config['additional_javascript'][] = 'js/jquery.min.js'; + * $config['additional_javascript'][] = 'js/quick-reply.js'; + * + */ + +if (active_page == 'index') { +$(document).ready(function(){ + if($('div.banner').length != 0) + return; // not index + + txt_new_topic = $('form[name=post] input[type=submit]').val(); + txt_new_reply = txt_new_topic == _('Submit') ? txt_new_topic : new_reply_string; + + undo_quick_reply = function() { + $('div.banner').remove(); + $('form[name=post] input[type=submit]').val(txt_new_topic); + $('form[name=post] input[name=quick-reply]').remove(); + } + + $('div.post.op').each(function() { + var id = $(this).children('p.intro').children('a.post_no:eq(1)').text(); + $('<a href="#">['+_("Quick reply")+']</a>').insertAfter($(this).children('p.intro').children('a:last')).click(function() { + $('div.banner').remove(); + $('<div class="banner">'+fmt(_("Posting mode: Replying to <small>>>{0}</small>"), [id])+' <a class="unimportant" onclick="undo_quick_reply()" href="javascript:void(0)">['+_("Return")+']</a></div>') + .insertBefore('form[name=post]'); + $('form[name=post] input[type=submit]').val(txt_new_reply); + + $('<input type="hidden" name="quick-reply" value="' + id + '">').appendTo($('form[name=post]')); + + $('form[name=post] textarea').select(); + + window.scrollTo(0, 0); + + return false; + }); + }); +}); +} 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(); + } +}); @@ -68,6 +68,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 @@ -95,6 +96,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', ); @@ -181,7 +181,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(); @@ -739,6 +739,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']; @@ -748,6 +762,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('"', '"', 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..7980e452 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,8 @@ 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 +} + +div.report { + color: #666666; +} diff --git a/stylesheets/dark_roach.css b/stylesheets/dark_roach.css index 6de59553..f05a89ae 100644 --- a/stylesheets/dark_roach.css +++ b/stylesheets/dark_roach.css @@ -84,6 +84,8 @@ input[type="text"], input[type="password"], textarea { text-shadow: none; text-transform: none; word-spacing: normal; + background: #333333; + color: #999999; } form table tr td { text-align: left; @@ -281,12 +283,12 @@ span.heading { font-weight: bold; } span.spoiler { - background: #000033; - color: #000033; + background: #000000; + color: #000000; padding: 0px 1px; } div.post.reply div.body span.spoiler a { - color: #000033; + color: #000000; } span.spoiler:hover, div.post.reply div.body span.spoiler:hover a { color: #999999; @@ -469,3 +471,12 @@ p.intro.thread-hidden { margin: 0px; padding: 0px; } +input[type="submit"], select { +background: #333333; +border: 1px solid #666666; +color: #999999; +} +input[type="submit"]:hover +{ + color: #9999CC; +} 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 Binary files differnew file mode 100644 index 00000000..f7403695 --- /dev/null +++ b/stylesheets/img/arrow.png 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 ef12a5f5..cfc37980 100644 --- a/stylesheets/style.css +++ b/stylesheets/style.css @@ -288,6 +288,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; @@ -421,7 +422,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; -}
\ No newline at end of file +} 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 0458ed5a..fb7bab07 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> @@ -25,8 +37,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.page_nav_top %} <div class="pages top"> {% for page in pages %} @@ -55,6 +74,9 @@ </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 © 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 235b52db..6136b11f 100644 --- a/templates/main.js +++ b/templates/main.js @@ -277,7 +277,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/mod/move_reply.html b/templates/mod/move_reply.html new file mode 100644 index 00000000..bf7c2f75 --- /dev/null +++ b/templates/mod/move_reply.html @@ -0,0 +1,41 @@ +<form action="?/{{ board }}/move_reply/{{ post }}" method="post"> + <input type="hidden" name="token" value="{{ token }}"> + <table> + <tr> + <th> + {% trans 'Post ID' %} + </th> + <td> + >>>{{ config.board_abbreviation|sprintf(board) }}{{ post }} + </td> + </tr> + <tr> + <th>{% trans 'Target board' %}</th> + <td> + <ul style="list-style:none;padding:0"> + {% for targetboard in boards %} + <li> + <input type="radio" name="board" value="{{ targetboard.uri }}" id="ban-board-{{ targetboard.uri }}" {% if boards|count == 2 %}checked{% endif %}> + <label style="display:inline" for="ban-board-{{ targetboard.uri }}"> + {{ config.board_abbreviation|sprintf(targetboard.uri) }} - {{ targetboard.title|e }} + </label> + </li> + {% endfor %} + </ul> + </td> + <td> + </tr> + <tr> + <th>{% trans 'Target thread' %}</th> + <td> + <p>If you do not want to make the post into a new thread, please specify the thread you want to move the reply to:</p> + <input type="text" name="target_thread"/> + </td> + </tr> + </table> + + <ul style="padding:0;text-align:center;list-style:none"> + <li><input type="submit" value="{% trans 'Move reply' %}"></li> + </ul> +</form> + 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 5a825fac..70c03f8e 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 %}…</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/catalog/catalog.css b/templates/themes/catalog/catalog.css index 048dd656..c24b9d62 100644 --- a/templates/themes/catalog/catalog.css +++ b/templates/themes/catalog/catalog.css @@ -35,4 +35,8 @@ div.thread { div.thread:hover { background: #D6DAF0; border-color: #B7C5D9; -}
\ No newline at end of file +} + +strong { + display: block; +} 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 Binary files differnew file mode 100644 index 00000000..49c4237b --- /dev/null +++ b/templates/themes/ukko/thumb.png 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 © 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'); + |
