diff --git a/inc/instance-config.php b/inc/instance-config.php index 15bf674b..e4ed6285 100644 --- a/inc/instance-config.php +++ b/inc/instance-config.php @@ -27,6 +27,8 @@ $config['boards'] = array( $config['prepended_foreign_boards'] = array( 'overboard' => '/overboard/', + 'sfw' => '/sfw/', + 'alt' => '/alt/', 'cytube' => 'https://tv.leftypol.org/' ); diff --git a/templates/themes/catalog/info.php b/templates/themes/catalog/info.php index 67999c88..d78278fd 100644 --- a/templates/themes/catalog/info.php +++ b/templates/themes/catalog/info.php @@ -84,18 +84,11 @@ 'comment' => 'Check this if you wish to show a nice tooltip with info about the thread on mouse over.' ); $theme['config'][] = Array( - 'title' => 'Build overboard catalog', + 'title' => 'Build overboard catalogs', 'name' => 'has_overboard', 'type' => 'checkbox', 'default' => false, - 'comment' => 'Check this if you wish to create a catalog for the overboard.' - ); - $theme['config'][] = Array( - 'title' => 'Overboard location (default \'overboard\')', - 'name' => 'overboard_location', - 'type' => 'text', - 'default' => 'overboard', - 'comment' => 'Fill in the location of the overboard directory. Default is \'overboard\' which corresponds to ./overboard' + 'comment' => 'Check this if you wish to create a catalog for the overboard. Requires Overboards theme.' ); $theme['config'][] = Array( 'title' => 'Max posts in catalog overboard', diff --git a/templates/themes/catalog/theme.php b/templates/themes/catalog/theme.php index 0ec58259..32cc1818 100644 --- a/templates/themes/catalog/theme.php +++ b/templates/themes/catalog/theme.php @@ -9,6 +9,11 @@ $b = new Catalog($settings); $boards = explode(' ', $settings['boards']); + if (isset($settings['has_overboard']) && $settings['has_overboard']) { + // Include overboard settings so that we can find them all and process exclusions + require "templates/themes/overboards/overboards.php"; + } + // Possible values for $action: // - all (rebuild everything, initialization) // - news (news has been updated) @@ -28,14 +33,16 @@ } } if(isset($settings['has_overboard']) && $settings['has_overboard']) { - $board = $settings['overboard_location']; - $action = generation_strategy("sb_catalog", array($board)); - if ($action == 'delete') { - file_unlink($config['dir']['home'] . $board . '/catalog.html'); - file_unlink($config['dir']['home'] . $board . '/index.rss'); - } - elseif ($action == 'rebuild') { - $b->buildOverboardCatalog($settings, $boards); + foreach ($overboards_config as &$overboard) { + $included_boards = array_diff(listBoards(true), $overboard['exclude']); + $action = generation_strategy("sb_catalog", array($overboard)); + if ($action == 'delete') { + file_unlink($config['dir']['home'] . $overboard . '/catalog.html'); + file_unlink($config['dir']['home'] . $overboard . '/index.rss'); + } + elseif ($action == 'rebuild') { + $b->buildOverboardCatalog($overboard['uri'], $settings, $included_boards); + } } } } elseif ($action == 'post-thread' || ($settings['update_on_posts'] && $action == 'post') || ($settings['update_on_posts'] && $action == 'post-delete') @@ -51,7 +58,12 @@ print_err("catalog_build calling Catalog.build 2"); $b->build($settings, $board); if(isset($settings['has_overboard']) && $settings['has_overboard']) { - $b->buildOverboardCatalog($settings, $boards); + foreach ($overboards_config as &$overboard) { + if ($overboard['uri'] == $board) { + $included_boards = array_diff(listBoards(true), $overboard['exclude']); + $b->buildOverboardCatalog($board, $settings, $included_boards); + } + } } } } @@ -297,7 +309,7 @@ $recent_posts = $this->generateRecentPosts($threads); $this->saveForBoard($randSettings['uri'], $recent_posts, - $config['root'] . $randSettings['uri']); + $config['root'] . $randSettings['uri'], true); } /** @@ -347,10 +359,8 @@ /** * Build and save the HTML of the catalog for the overboard */ - public function buildOverboardCatalog($settings, $boards) { + public function buildOverboardCatalog($board_name, $settings, $boards) { global $config; - - $board_name = $settings['overboard_location']; if (array_key_exists($board_name, $this->threadsCache)) { $threads = $this->threadsCache[$board_name]; @@ -372,7 +382,7 @@ // Generate data for the template $recent_posts = $this->generateRecentPosts($threads); - $this->saveForBoard($board_name, $recent_posts, '/' . $settings['overboard_location'], true); + $this->saveForBoard($board_name, $recent_posts, '/' . $board_name, true); // Build the overboard JSON outputs if ($config['api']['enabled']) { diff --git a/templates/themes/overboards/info.php b/templates/themes/overboards/info.php new file mode 100644 index 00000000..e2b7f127 --- /dev/null +++ b/templates/themes/overboards/info.php @@ -0,0 +1,23 @@ + 'Overboards', + // Description (you can use Tinyboard markup here) + 'description' => 'Add one or more overboards, such as a normal overboard and a SFW overboard.', + 'version' => 'v0.1', + // Unique function name for building and installing whatever's necessary + 'build_function' => 'overboards_build', + ); + + // Theme configuration + $theme['config'] = array( + array( + 'title' => 'Edit overboards.php manually to add, remove or modify overboards', + 'name' => 'instruct1', + 'type' => 'checkbox', + 'default' => false, + 'comment' => 'Located at templates/themes/overboards/overboards.php' + ), + + ); diff --git a/templates/themes/overboards/overboard.js b/templates/themes/overboards/overboard.js new file mode 100644 index 00000000..c07e8452 --- /dev/null +++ b/templates/themes/overboards/overboard.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 = $('

/' + + data.board + '/

'); + + // 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/overboards/overboards.php b/templates/themes/overboards/overboards.php new file mode 100644 index 00000000..a152409b --- /dev/null +++ b/templates/themes/overboards/overboards.php @@ -0,0 +1,34 @@ + array('b', 'games', 'music') + */ + $thread_limit = 30; + + // Define list of overboards + $overboards_config = array( + array( + 'title' => 'Overboard', + 'uri' => 'overboard', + 'subtitle' => '30 most recently bumped threads', + 'exclude' => array('assembly', 'assembly_archive', 'gulag'), + 'thread_limit' => $thread_limit, + ), + array( + 'title' => 'SFW Overboard', + 'uri' => 'sfw', + 'subtitle' => '30 most recently bumped threads from work-safe boards', + 'exclude' => array('assembly', 'assembly_archive', 'gulag', 'b'), + 'thread_limit' => $thread_limit, + ), + array( + 'title' => 'Alternate Overboard', + 'uri' => 'alt', + 'subtitle' => '30 most recently bumped threads from smaller interest boards', + 'exclude' => array('assembly', 'assembly_archive', 'gulag', 'leftypol', 'b', 'meta'), + 'thread_limit' => $thread_limit, + ), + ); + +?> diff --git a/templates/themes/overboards/theme.php b/templates/themes/overboards/theme.php new file mode 100644 index 00000000..34b9b6b0 --- /dev/null +++ b/templates/themes/overboards/theme.php @@ -0,0 +1,235 @@ +build($overboard)); + file_write($overboard['uri'] . '/overboard.js', + Element('themes/overboards/overboard.js', array())); + } + } + } + + /** + * Encapsulation of the theme's internals + */ + class overboards { + private $settings; + + function __construct($settings) { + $this->settings = $this->parseSettings($settings); + } + + /** + * Parse and validate configuration parameters passed from the UI + */ + private function parseSettings(&$settings) { + foreach ($settings as &$overboard) { + if (!is_int($overboard['thread_limit']) || $overboard['thread_limit'] < 1) + { + error('thread_limit must be an integer above 1.', true); + } + if (!is_array($overboard['exclude'])) + { + error('Exclude list must be array of strings.', true); + } + foreach ($overboard['exclude'] as &$board) { + if (!is_string($board)){ + error('Exclude list must be array of strings.', true); + } + } + + } + + return $settings; + } + + /** + * Obtain list of all threads from all non-excluded boards + */ + private function fetchThreads($overboard) { + $query = ''; + $boards = listBoards(true); + + foreach ($boards as $b) { + if (in_array($b, $overboard['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 = '

/' . + $post['board'] . '/

'; + // The thread itself + $html .= $thread->build(true); + + return $html; + } + + /** + * Query the required information and generate the HTML + */ + public function build($overboard, $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($overboard); + $total_count = count($threads); + // Top threads displayed on load + $top_threads = array_splice($threads, 0, $overboard['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' => $overboard['uri'] . "/", + 'url' => $overboard['uri'], + 'title' => $overboard['title'], + 'subtitle' => str_replace('%s', $overboard['thread_limit'], + strval(min($overboard['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/overboards/thumb.png b/templates/themes/overboards/thumb.png new file mode 100644 index 00000000..eb616ef7 Binary files /dev/null and b/templates/themes/overboards/thumb.png differ