diff --git a/templates/themes/catalog/info.php b/templates/themes/catalog/info.php
index 67999c88..d971f2a9 100644
--- a/templates/themes/catalog/info.php
+++ b/templates/themes/catalog/info.php
@@ -76,6 +76,13 @@
'default' => false,
'comment' => 'Enable catalog for the Rand theme. This requires the Rand theme to be enabled.'
);
+ $theme['config'][] = Array(
+ 'title' => 'Enable SFW overboard catalog',
+ 'name' => 'enable_sfwoverboard',
+ 'type' => 'checkbox',
+ 'default' => false,
+ 'comment' => 'Enable catalog for the sfwoverboard theme. This requires the sfwoverboard theme to be enabled.'
+ );
$theme['config'][] = Array(
'title' => 'Use tooltipster',
'name' => 'use_tooltipster',
diff --git a/templates/themes/catalog/theme.php b/templates/themes/catalog/theme.php
index 0ec58259..3c89148f 100644
--- a/templates/themes/catalog/theme.php
+++ b/templates/themes/catalog/theme.php
@@ -92,6 +92,14 @@
{
$b->buildRand();
}
+ // FIXME: Check that sfwoverboard is actually enabled
+ if ($settings['enable_sfwoverboard'] && (
+ $action === 'all' || $action === 'post' ||
+ $action === 'post-thread' || $action === 'post-delete' || $action === 'rebuild'))
+ {
+ $b->buildsfwoverboard();
+ }
+
}
// Wrap functions in a class so they don't interfere with normal Tinyboard operations
@@ -297,7 +305,43 @@
$recent_posts = $this->generateRecentPosts($threads);
$this->saveForBoard($randSettings['uri'], $recent_posts,
- $config['root'] . $randSettings['uri']);
+ $config['root'] . $randSettings['uri'], true);
+ }
+ public function buildsfwoverboard() {
+ global $config;
+ print_err("Catalog.buildsfwoverboard");
+
+ $sfwoverboardSettings = themeSettings('sfwoverboard');
+ $queries = array();
+ $threads = array();
+
+ $exclusions = explode(' ', $sfwoverboardSettings['exclude']);
+ $boards = array_diff(listBoards(true), $exclusions);
+
+ foreach ($boards as $b) {
+ if (array_key_exists($b, $this->threadsCache)) {
+ $threads = array_merge($threads, $this->threadsCache[$b]);
+ } else {
+ $queries[] = $this->buildThreadsQuery($b);
+ }
+ }
+
+ // Fetch threads from boards that haven't beenp processed yet
+ if (!empty($queries)) {
+ $sql = implode(' UNION ALL ', $queries);
+ $res = query($sql) or error(db_error());
+ $threads = array_merge($threads, $res->fetchAll(PDO::FETCH_ASSOC));
+ }
+
+ // Sort in bump order
+ usort($threads, function($a, $b) {
+ return strcmp($b['bump'], $a['bump']);
+ });
+ // Generate data for the template
+ $recent_posts = $this->generateRecentPosts($threads);
+
+ $this->saveForBoard($sfwoverboardSettings['uri'], $recent_posts,
+ $config['root'] . $sfwoverboardSettings['uri'], true);
}
/**
diff --git a/templates/themes/sfwoverboard/info.php b/templates/themes/sfwoverboard/info.php
new file mode 100644
index 00000000..d641571b
--- /dev/null
+++ b/templates/themes/sfwoverboard/info.php
@@ -0,0 +1,56 @@
+ 'SFW Overboard',
+ // Description (you can use Tinyboard markup here)
+ 'description' => 'An additional overboard, intended to list a different set of boards.',
+ 'version' => 'v0.1',
+ // Unique function name for building and installing whatever's necessary
+ 'build_function' => 'sfwoverboard_build',
+ 'install_callback' => 'sfwoverboard_install',
+ );
+
+ // Theme configuration
+ $theme['config'] = array(
+ array(
+ 'title' => 'Board name',
+ 'name' => 'title',
+ 'type' => 'text',
+ 'default' => 'SFW Overboard',
+ ),
+ array(
+ 'title' => 'Board URI',
+ 'name' => 'uri',
+ 'type' => 'text',
+ 'default' => '.',
+ 'comment' => '("mixed", for example)',
+ ),
+ array(
+ 'title' => 'Subtitle',
+ 'name' => 'subtitle',
+ 'type' => 'text',
+ 'comment' => '(%s = thread limit, for example "%s coolest threads")',
+ ),
+ array(
+ 'title' => 'Excluded boards',
+ 'name' => 'exclude',
+ 'type' => 'text',
+ 'comment' => '(space seperated)',
+ ),
+ array(
+ 'title' => 'Number of threads',
+ 'name' => 'thread_limit',
+ 'type' => 'text',
+ 'default' => '15',
+ ),
+ );
+
+ if (!function_exists('sfwoverboard_install')) {
+ function sfwoverboard_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/sfwoverboard/semirand.js b/templates/themes/sfwoverboard/semirand.js
new file mode 100644
index 00000000..c07e8452
--- /dev/null
+++ b/templates/themes/sfwoverboard/semirand.js
@@ -0,0 +1,158 @@
+$(document).ready(function() {
+ var cachedPages = [],
+ loading = false,
+ timer = null;
+
+ // Load data from HTML5 localStorage
+ var hiddenBoards = JSON.parse(localStorage.getItem('hiddenboards'));
+
+ var storeHiddenBoards = function() {
+ localStorage.setItem('hiddenboards', JSON.stringify(hiddenBoards));
+ };
+
+ // No board are hidden by default
+ if (!hiddenBoards) {
+ hiddenBoards = {};
+ storeHiddenBoards();
+ }
+
+ // Hide threads from the same board and remember for next time
+ var onHideClick = function(e) {
+ e.preventDefault();
+ var board = $(this).parent().next().data('board'),
+ threads = $('[data-board="'+board+'"]:not([data-cached="yes"])'),
+ btns = threads.prev().find('.threads-toggle'),
+ hrs = btns.next();
+
+ if (hiddenBoards[board]) {
+ threads.show();
+ btns.find('.threads-toggle').html(_('(hide threads from this board)'));
+ hrs.hide();
+ } else {
+ threads.hide();
+ btns.html(_('(show threads from this board)'));
+ hrs.show();
+ }
+
+ hiddenBoards[board] = !hiddenBoards[board];
+ storeHiddenBoards();
+ };
+
+ // Add a hiding link and horizontal separator to each thread
+ var addHideButton = function() {
+ var board = $(this).next().data('board'),
+ // Create the link and separator
+ button = $('')
+ .click(onHideClick),
+ myHr = $('
');
+
+ // Insert them after the board name
+ $(this).append(' ').append(button).append(myHr);
+
+ if (hiddenBoards[board]) {
+ button.html(_('(show threads from this board)'));
+ $(this).next().hide();
+ } else {
+ button.html(_('(hide threads from this board)'));
+ myHr.hide();
+ }
+ };
+
+ $('h2').each(addHideButton);
+
+ var appendThread = function(elem, data) {
+ var boardLink = $('');
+
+ // Push the thread after the currently last one
+ $('div[id*="thread_"]').last()
+ .after(elem.data('board', data.board)
+ .data('cached', 'no')
+ .show());
+ // Add the obligatory board link
+ boardLink.insertBefore(elem);
+ // Set up the hiding link
+ addHideButton.call(boardLink);
+ // Trigger an event to let the world know that we have a new thread aboard
+ $(document).trigger('new_post', elem);
+ };
+
+ var attemptLoadNext = function() {
+ if (!ukko_overflow.length) {
+ $('.pages').show().html(_('No more threads to display'));
+ return;
+ }
+
+ var viewHeight = $(window).scrollTop() + $(window).height(),
+ pageHeight = $(document).height();
+ // Keep loading deferred threads as long as we're close to the bottom of the
+ // page and there are threads remaining
+ while(viewHeight + 1000 > pageHeight && !loading && ukko_overflow.length > 0) {
+ // Take the first unloaded post
+ var post = ukko_overflow.shift(),
+ page = modRoot + post.board + '/' + post.page;
+
+ var thread = $('div#thread_' + post.id + '[data-board="' + post.board + '"]');
+ // Check that the thread hasn't been inserted yet
+ if (thread.length && thread.data('cached') !== 'yes') {
+ continue;
+ }
+
+ // Check if we've already downloaded the index page on which this thread
+ // is located
+ if ($.inArray(page, cachedPages) !== -1) {
+ if (thread.length) {
+ appendThread(thread, post);
+ }
+ // Otherwise just load the page and cache its threads
+ } else {
+ // Make sure that no other thread does the job that we're about to do
+ loading = true;
+ $('.pages').show().html(_('Loading…'));
+
+ // Retrieve the page from the server
+ $.get(page, function(data) {
+ cachedPages.push(page);
+
+ // Cache each retrieved thread
+ $(data).find('div[id*="thread_"]').each(function() {
+ var thread_id = $(this).attr('id').replace('thread_', '');
+
+ // Check that this thread hasn't already been loaded somehow
+ if ($('div#thread_' + thread_id + '[data-board="' +
+ post.board + '"]').length)
+ {
+ return;
+ }
+
+ // Hide the freshly loaded threads somewhere at the top
+ // of the page for now
+ $('form[name="postcontrols"]')
+ .prepend($(this).hide()
+ .data('cached', 'yes')
+ .data('data-board', post.board));
+ });
+
+ // Find the current thread in the newly retrieved ones
+ thread = $('div#thread_' + post.id + '[data-board="' +
+ post.board + '"][data-cached="yes"]');
+
+ if (thread.length) {
+ appendThread(thread, post);
+ }
+
+ // Release the lock
+ loading = false;
+ $('.pages').hide().html('');
+ });
+ break;
+ }
+ }
+
+ clearTimeout(timer);
+ // Check again in one second
+ timer = setTimeout(attemptLoadNext, 1000);
+ };
+
+ attemptLoadNext();
+});
diff --git a/templates/themes/sfwoverboard/theme.php b/templates/themes/sfwoverboard/theme.php
new file mode 100644
index 00000000..993e7fd0
--- /dev/null
+++ b/templates/themes/sfwoverboard/theme.php
@@ -0,0 +1,223 @@
+build());
+ file_write($settings['uri'] . '/semirand.js',
+ Element('themes/sfwoverboard/semirand.js', array()));
+ }
+ }
+
+ /**
+ * Encapsulation of the theme's internals
+ */
+ class sfwoverboard {
+ private $settings;
+
+ function __construct($settings) {
+ $this->settings = $this->parseSettings($settings);
+ }
+
+ /**
+ * Parse and validate configuration parameters passed from the UI
+ */
+ private function parseSettings($settings) {
+ if (!is_numeric($settings['thread_limit']))
+ {
+ error('Invalid configuration parameters.', true);
+ }
+
+ $settings['exclude'] = explode(' ', $settings['exclude']);
+ $settings['thread_limit'] = intval($settings['thread_limit']);
+
+ if ($settings['thread_limit'] < 1)
+ {
+ error('Invalid configuration parameters.', true);
+ }
+
+ return $settings;
+ }
+
+ /**
+ * Obtain list of all threads from all non-excluded boards
+ */
+ private function fetchThreads() {
+ $query = '';
+ $boards = listBoards(true);
+
+ foreach ($boards as $b) {
+ if (in_array($b, $this->settings['exclude']))
+ continue;
+ // Threads are those posts that have no parent thread
+ $query .= "SELECT *, '$b' AS `board` FROM ``posts_$b`` " .
+ "WHERE `thread` IS NULL UNION ALL ";
+ }
+
+ $query = preg_replace('/UNION ALL $/', 'ORDER BY `bump` DESC', $query);
+ $result = query($query) or error(db_error());
+
+ return $result->fetchAll(PDO::FETCH_ASSOC);
+ }
+
+ /**
+ * Retrieve all replies to a given thread
+ */
+ private function fetchReplies($board, $thread_id, $preview_count) {
+ $query = prepare("SELECT * FROM (SELECT * FROM ``posts_$board`` WHERE `thread` = :id ORDER BY `time` DESC LIMIT :limit) as
+ t ORDER BY t.time ASC");
+ $query->bindValue(':id', $thread_id, PDO::PARAM_INT);
+ $query->bindValue(':limit', $preview_count, PDO::PARAM_INT);
+ $query->execute() or error(db_error($query));
+
+ return $query->fetchAll(PDO::FETCH_ASSOC);
+ }
+
+ /**
+ * Retrieve count of images and posts in a thread
+ */
+ private function fetchThreadCount($board, $thread_id, $preview_count) {
+ $query = prepare("SELECT SUM(t.num_files) as file_count, COUNT(t.id) as post_count FROM (SELECT * FROM ``posts_$board`` WHERE `thread` = :id ORDER BY `time` DESC LIMIT :offset , 18446744073709551615) as t;");
+ $query->bindValue(':id', $thread_id, PDO::PARAM_INT);
+ $query->bindValue(':offset', $preview_count, PDO::PARAM_INT);
+ $query->execute() or error(db_error($query));
+
+ return $query->fetch(PDO::FETCH_ASSOC);
+ }
+
+ /**
+ * Build the HTML of a single thread in the catalog
+ */
+ private function buildOne($post, $mod = false) {
+ global $config;
+
+ openBoard($post['board']);
+ $thread = new Thread($post, $mod ? '?/' : $config['root'], $mod);
+ // Number of replies to a thread that are displayed beneath it
+ $preview_count = $post['sticky'] ? $config['threads_preview_sticky'] :
+ $config['threads_preview'];
+ $replies = $this->fetchReplies($post['board'], $post['id'], $preview_count);
+
+ $disp_replies = $replies;
+ foreach ($disp_replies as $reply) {
+ // Append the reply to the thread as it's being built
+ $thread->add(new Post($reply, $mod ? '?/' : $config['root'], $mod));
+ }
+
+ $threadCount = $this->fetchThreadCount($post['board'], $post['id'], $preview_count);
+ $thread->omitted = $threadCount['post_count'];
+ $thread->omitted_images = $threadCount['file_count'];
+
+ // Board name and link
+ $html = '';
+ // The thread itself
+ $html .= $thread->build(true);
+
+ return $html;
+ }
+
+ /**
+ * Query the required information and generate the HTML
+ */
+ public function build($mod = false) {
+ if (!isset($this->settings)) {
+ error('Theme is not configured properly.');
+ }
+
+ global $config;
+
+ $html = '';
+ $overflow = array();
+
+ // Fetch threads from all boards and chomp the first 'n' posts, depending
+ // on the setting
+ $threads = $this->fetchThreads();
+ $total_count = count($threads);
+ // Top threads displayed on load
+ $top_threads = array_splice($threads, 0, $this->settings['thread_limit']);
+ // Number of processed threads by board
+ $counts = array();
+
+ // Output threads up to the specified limit
+ foreach ($top_threads as $post) {
+ if (array_key_exists($post['board'], $counts)) {
+ ++$counts[$post['board']];
+ } else {
+ $counts[$post['board']] = 1;
+ }
+
+ $html .= $this->buildOne($post, $mod);
+ }
+
+ foreach ($threads as $post) {
+ if (array_key_exists($post['board'], $counts)) {
+ ++$counts[$post['board']];
+ } else {
+ $counts[$post['board']] = 1;
+ }
+
+ $page = 'index';
+ $board_page = floor($counts[$post['board']] / $config['threads_per_page']);
+ if ($board_page > 0) {
+ $page = $board_page + 1;
+ }
+ $overflow[] = array(
+ 'id' => $post['id'],
+ 'board' => $post['board'],
+ 'page' => $page . '.html'
+ );
+ }
+
+ $html .= '';
+ $html .= '';
+
+ return Element('index.html', array(
+ 'config' => $config,
+ 'board' => array(
+ 'dir' => $this->settings['uri'] . "/",
+ 'url' => $this->settings['uri'],
+ 'title' => $this->settings['title'],
+ 'subtitle' => str_replace('%s', $this->settings['thread_limit'],
+ strval(min($this->settings['subtitle'], $total_count))),
+ ),
+ 'no_post_form' => true,
+ 'body' => $html,
+ 'mod' => $mod,
+ 'boardlist' => createBoardlist($mod),
+ ));
+ }
+
+ };
+
+ if (!function_exists('array_column')) {
+ /**
+ * Pick out values from subarrays by given key
+ */
+ function array_column($array, $key) {
+ $result = [];
+ foreach ($array as $val) {
+ $result[] = $val[$key];
+ }
+ return $result;
+ }
+ }
+
diff --git a/templates/themes/sfwoverboard/thumb.png b/templates/themes/sfwoverboard/thumb.png
new file mode 100644
index 00000000..eb616ef7
Binary files /dev/null and b/templates/themes/sfwoverboard/thumb.png differ