From 7c6eef81c55d6f5ccfa6ebba759afcec7b79115d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Grabovsk=C3=BD?= Date: Tue, 2 Jun 2015 15:28:06 +0200 Subject: [PATCH] Add a semirandom theme This theme displays random threads intermixed with recently bumped ones. The number of each is configurable. --- templates/themes/semirand/info.php | 70 ++++++++++ templates/themes/semirand/semirand.js | 158 +++++++++++++++++++++ templates/themes/semirand/theme.php | 254 ++++++++++++++++++++++++++++++++++ templates/themes/semirand/thumb.png | Bin 0 -> 13085 bytes 4 files changed, 482 insertions(+) create mode 100644 templates/themes/semirand/info.php create mode 100644 templates/themes/semirand/semirand.js create mode 100644 templates/themes/semirand/theme.php create mode 100644 templates/themes/semirand/thumb.png diff --git a/templates/themes/semirand/info.php b/templates/themes/semirand/info.php new file mode 100644 index 00000000..255935c1 --- /dev/null +++ b/templates/themes/semirand/info.php @@ -0,0 +1,70 @@ + 'Mixed All/Random Overboard', + // Description (you can use Tinyboard markup here) + 'description' => 'Board with threads from all boards with most recently bumped and random ones intermixed', + 'version' => 'v0.1', + // Unique function name for building and installing whatever's necessary + 'build_function' => 'semirand_build', + 'install_callback' => 'semirand_install', + ); + + // Theme configuration + $theme['config'] = array( + array( + 'title' => 'Board name', + 'name' => 'title', + 'type' => 'text', + 'default' => 'Semirandom', + ), + 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', + ), + array( + 'title' => 'Random threads', + 'name' => 'random_count', + 'comment' => '(number of consecutive random threads)', + 'type' => 'text', + 'default' => '1', + ), + array( + 'title' => 'Bumped threads', + 'name' => 'bumped_count', + 'comment' => '(number of consecutive recent threads)', + 'type' => 'text', + 'default' => '1', + ), + ); + + if (!function_exists('semirand_install')) { + function semirand_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/semirand/semirand.js b/templates/themes/semirand/semirand.js new file mode 100644 index 00000000..c07e8452 --- /dev/null +++ b/templates/themes/semirand/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 = $('

/' + + 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/semirand/theme.php b/templates/themes/semirand/theme.php new file mode 100644 index 00000000..1acecbfb --- /dev/null +++ b/templates/themes/semirand/theme.php @@ -0,0 +1,254 @@ +build()); + file_write($settings['uri'] . '/semirand.js', + Element('themes/semirand/semirand.js', array())); + } + } + + /** + * Encapsulation of the theme's internals + */ + class semirand { + 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']) || + !is_numeric($settings['random_count']) || + !is_numeric($settings['recent_count'])) + { + error('Invalid configuration parameters.', true); + } + + $settings['exclude'] = explode(' ', $settings['exclude']); + $settings['thread_limit'] = intval($settings['thread_limit']); + $settings['random_count'] = intval($settings['random_count']); + $settings['recent_count'] = intval($settings['recent_count']); + + if ($settings['thread_limit'] < 1 || + $settings['random_count'] < 1 || + $settings['recent_count'] < 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) { + $query = prepare("SELECT * FROM ``posts_$board`` WHERE `thread` = :id"); + $query->bindValue(':id', $thread_id, PDO::PARAM_INT); + $query->execute() or error(db_error($query)); + + return $query->fetchAll(PDO::FETCH_ASSOC); + } + + /** + * Intersperse random threads between those in bump order + */ + private function shuffleThreads($threads) { + $random_count = $this->settings['random_count']; + $recent_count = $this->settings['recent_count']; + $total = count($threads); + + // Storage for threads that will be randomly interspersed + $shuffled = array(); + + // Ratio of bumped / all threads + $topRatio = $recent_count / ($recent_count + $random_count); + // Shuffle the bottom half of all threads + $random = array_splice($threads, floor($total * $topRatio)); + shuffle($random); + + // Merge the random and sorted threads into one sequence. The pattern + // starts at random threads + while (!empty($threads)) { + $shuffled = array_merge($shuffled, + array_splice($random, 0, $random_count), + array_splice($threads, 0, $recent_count)); + } + + return $shuffled; + } + + /** + * 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); + $replies = $this->fetchReplies($post['board'], $post['id']); + // Number of replies to a thread that are displayed beneath it + $preview_count = $post['sticky'] ? $config['threads_preview_sticky'] : + $config['threads_preview']; + + // Chomp the last few replies + $disp_replies = array_splice($replies, 0, $preview_count); + $disp_img_count = 0; + foreach ($disp_replies as $reply) { + if ($reply['files'] !== '') + ++$disp_img_count; + + // Append the reply to the thread as it's being built + $thread->add(new Post($reply, $mod ? '?/' : $config['root'], $mod)); + } + + // Count the number of omitted image replies + $omitted_img_count = count(array_filter($replies, function($p) { + return $p['files'] !== ''; + })); + + // Set the corresponding omitted numbers on the thread + if (!empty($replies)) { + $thread->omitted = count($replies); + $thread->omitted_images = $omitted_img_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($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->shuffleThreads($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( + '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/semirand/thumb.png b/templates/themes/semirand/thumb.png new file mode 100644 index 0000000000000000000000000000000000000000..eb616ef7d662bddc7b4fec8311ee52df55772aab GIT binary patch literal 13085 zcmV+&GvdsNP)$TuqcCM2ML5E&^DvdXhyU3EWLNX_xi1OJLmjq-Fy2@3lU?50wMML z-mP2TJ$>hO*Z<#tKkp4b@{vDYK6!HcD>k3J`_PWpUtXK3z3eys+5GYIhKZ%|{s;f& z8=28kz`F=Q~h%|G^Zke!^|Th5gVq!g^72c*+?0U#(1f`e4RnReZ!#k#@6 z{J=1?4A2S)K`;mvFb1T(>XL7k+@*6v6B8gf`92~8WMDAu1&qZ!x?eB=23mn3APB1S zi^ah~(i+y#1L|Yn{0XZF1PG)75Plp4$bd95rnN>u_@{w@7&1*i{!e-e_!hiy=s^HO zcp=eK0KTva=qX@>d*q%1dNF{(3x*zgF`%b_UJPLH^Fj|j3+O4J2l8uZ7g2DMga!j3 z2o)IIoo!i8T>vupd7=m1nH7D5NB;ElV{ad;eC=2(v%Pr3mZksvEgE?FW$y@o?iSDk z8u{6~?oVC3({Hk&*IaSpe;qc|_~6ysJY`|FjC%S9@QXqZ1i);Qyr8>+dgcLm;m`yB z@bAA55Tkk+pD)3mne+&AgHUR!HQMWlluB3Y7l;?gFec%%NM#-IL-pfCs!%2*kb1e^ z4t_FG%PXxw>6nB+iTL5Oh}6#|w4N-jBkCCrqJn4~*ord-bu^t3&a0}>JKyM(!i3rzK3SABuU2_73@oJYR_p#`b z5kyFC;{+0)Icoq!(*M}Vtp_o6?_+~I2dsgS`t+i1)$-f7wQDWg5msOJsrxguqxI_G>Hgc|Ej`~-Q=|dHL_|eQ8ZfxE6 zko}5DeSC@D@z(atc_UMht-2T*kmu(4mN6OB>naSK{?gLaa{rd0l@lk3RC;_HgiYnT zUcHG^^KM5CytBKc(`yzIA*|#g^?Tz0a{1Z34kI%NQxC8 z(A1il%Zv;GV}J;x0D1k)bl=#}FOdQoPaN=#e{tfEZr}MExB0qLnL1$GxlM0;9faZb zQGL$}+kbg@;Cs0z&+~T;dFM}>Q-`cV(SNdPHVd7sGQRtWtz2FhfTNG5pS({TYy@BV z_l?hdRg^Pq&(-b2U(Em)pZ=|5kYawc(aa+^7HhJ=0GvUh$4=W{=Hf zolBm&`|)h1G31QOQ}-;heXSpSU5s41_t)Pf(z&Pp=Yid?y|_7Za`rn9F8j`P|N5r)>ZJj25Jl(p}M=B6|@U2*>*e(j__TA|;)#sBQLjf01r*WchCK5C4byw1Zre$D;c z@AG@Uo4Nj)u)0i*IcI2FXUKv7^7YiEm-x55x$~_bn$^?R=fAjg{EK&OdgVTQV$1mh z2QalYXUxwYdOW-DZ5cKVfj|Awqg0s84Q8Mt%Z|<*BcHj&HhDwc8$S9vZJsADlHzSWVmDs(XN@b z>4UrXj1As+3uyQy0aE>G_(>oFOlkPpL3sMK*g6>kfUJrGKQwQCwFg2_C=sJj!Vwg~ zvk#To(JOF z>B42a)Yq3?|3;o4bUY$br9zsBkc(Qg+VS1d@C8%Fzyddv zrCb@biUKbM(w}R$Qm8w`26JVb$A2plX?JegZ7T{qQ5pib;d`<$Ioh6G>>D4}?S@M^ zfBGaZ3>Jp-)#InLJGL93fwRVm)48piWuxX&HmuA99U8iL)8fhVeG_Bt^XH4(CtIga zDrT*|$;zVFSGE?;R7yE%42}UJNr?cpg-%#sENU;OzAhLypuYY0Zt+(F$u`j~N z<=5YF-PeBav#)&jmF*)>WVT#$l4}X9DyWho4S3387dm65P<*$vOxc3`& z8Rn-K7SFk_{Hu?i_`-iW>tFkt+pql2o!@)o@9Y((VZ_p0e_nryYb=&AtYK}DK!YZ& zqyk!H+O!>51I22I&XAlF`9E_air2K(0EwGkUl{s+kn_>cV69R)Kg9= z?bk-+0%QkYzvGg*W5-6z*-Cx!&#%5N2))rAqksRyxl41!!}H(kxAU*c?p$un=AG=k zo$k*TcS%t;c0IJTcvX2A7(e&TFKv7GyYBwromc+$drI4ekKA);Vqd?ob5Go}az(kJ zo54SP@JrWy=mT0qVLXG`QNxti+G__K`p7 zO8hw_mI>?1J*mfHiIUb*Dj8~}m6V`$^n@!josDM(!mtPk2;x)XXCeTQ)|v>{aSB8E z7k~KyaQ&|GFpi%LNk{g`dDm+tdLj*sgVw;AG3fnsJ*Pprg{qF_C z0L~y(q$Dx`(+1B2+k#L55eTF}UdS&D6Ql$q@)m&vj$l~ic?f_X`{R_C1E>E1I2H?io`XoasmL(n2szgx?~&e^9rIX zZXinR8mar}697vXA)r!H27bM|EPa=0owCeKDwRn&Y1?s_83ev<8pM1PNT+USiNsCR{VWtABJXw)HWEbqWXE0}*IPp*9!9a$2qU-NS!vW3=N`Q0+oR>u zP%gJ?%jSuR&Dl)0R4fa|QjWnWsCw-d0@GS{8kMP|H7C36%IlagW05mtv}O#TW9e(g z7(tv_MaMcMIwN`s&jXMlQY`vJ2GKQ(DRhk@?6giDdU)o<(VMTk?&57*2g;>FsbFUM z3}Kk2BN+CF!<#m5o@*T-fJmjl2#FyPX@*Il zUM0XTOz03=vrC5P9QoGc*_T~r6bHtJ?NBnuVK1#1z*tfRJ-4`82PCN1Dq1PavTf4< z1Wg){0Y-vjh@2w`Ms}u9E(0TGR905%?Y0*L3SH^@Oc;!t-G{kbSs);R1|UZKSr)*> z!r%Xog(Y+OTzmJ``@DK}#Z1l4Mi?KZB;vz;X z0CA=I=no&>G(0#`%7;?X>Y+7qh5#HH7bZsa7e`=WXmV3H>6_ki%fMqlIz4srfgj!9 zT%J)Xu(JhZxR!we!zA2mMF_x)}1ShW9{(t6q{>5Fm9+n;4XM{={8_ zmNM|%X1f#kzE+X}Y67LSR+{uPAc{vKoH33>QAsQVU*?O&es{<;Y?6TvgQSFmB;v#- zrb$O8!61n`xDo4s^~KhD5ySyfsnGAVTWv20Lm~o(nzV*U^hp7%5g9Vhgh^VWRwTGa z$mR>~vX7NJ2VUZi;$@NBpK*Yq`Bdm|c2tY}`4!-L(s!K|Ol3Hm3 zlEi=jNJ-F&lmhjPjyP$?xI`qzVf8@pI-O3l-IkaqX@ZK}4P!_Yv1ER@W^ZdL7;Gqr zNcgd528LZ(FalZq-;ey9LsZHPYt+KSR zP+w`3@@XR-AY5pa>gY_6R0dre+=vzs8$opLI))2&&}}PI!YS%{kycS0PWsFmv;-1F zDsFF5bjUCag1`^MP)el~0_nKkh_p_zGeRIu(dtHw0c;hC-|se_0V8W*U1gZ2k;`Ru z{&d%yX$&)#|G-+k!||L5)lPd;&|vRpMCJ6|fL z?NrguXGJRT{Yb;{Yi-iQ;%e^sKKkOyE3ZdnFLoWUr0Wx*>w=?ef*8qE0EIx#7-y@* za2zL{O52t#m6BRBA%tmhVXl&nA{9T~OfbfU;Mr6vn{raN-RPlFKT1}|8!}p zTW`JXjywM7#jXRkz6VDUx({jh4u+g_vslRFvQ93O-nM1SYp=cTrWQkS2-}~-(QEzV*T~QNJ5)YDkDTxc3v`E?6fimZ8AfMYjI=pr3WI5Z% zNYBokZfDqF{{Z85slPv)EsPFMye^yLwuu&J6jc3=hb$*U$``fafaFOHKnUH;2je0f zIXo}mxpQYf^{Kyp``h2tZnx9v)QdI-Ag!N7cCr2nDIrS9(Dxzqwe)4^1)did!$=B< znBZ0}!yFq0XFwGCS}OorbykWAiHM+)lJK&Ib*UG^^TOoh(B$Os0}nhBN&iLMjJq%p zNb&R?cMU-M%tCGkTq%gJbYQN(*T^$C1P%f7% zrsE>RFi6HS>uYa<7|+v_>koY3cL3ncZ+=5mhvSR49i5D(;+}*ion(g>fS^eRA(!a3 zy{cHL*J^14m6CI_%bT}uwQLK30=Ffi_MFI;br?v`(_Yhen_A1!OD;aY&~Al_5sMv= zCSWWIoDvd@)zT5DTByLM}_ZLTnI>k=D!*CODffxm>v|FLc`-zus&~ zDVJATg|^$*_S$XNZFY=6IY3O3mH`F6l#(%y0_vXhqzaX$$lTU+s^eHd8oEURY&-}g zN`_xC)_dc%0eaMLW`kR|FP}f%UTSwduhkY|5H?zF#|=8J7a<@bg9Aze0!>OQ1;_}X z87N1bq`{#HQ7zeyBQH8;M!gl0HD;+pr|dWWpR38!3(88%%oGZ=g%?< zsuQE*J9i;RF0isRkI1!VjM#~@bJNx45}C!J5pD?1nbw++08#5C3Q(--jj9a+gv0>5 z;gFJ)L_<;+B$j}{4a-cW?M$w@vM{q;H5t$4audZ|A(!=+D;47maRRPIUpn5^6ohPc zap~Obg3jjp`umyS6ji*WLC0366TwqFf!L5>0Nq5C5H1wL3=(o-EXj~_!Gs|-c~O7O z5F%~cj%iRJrSFHH7x+F9#m-$524Uz2o(coHupet(%NdhXtm}>Zt%(G21CAkHcpeGX zLO`6E8p!NS)+rX7^Jj%+Scd4cMKPVtI_Z>|PL~TPfLqXNw=`-+x)TI~lVc)Dm!zka zK*13}$BjOX1cdR}a|4G1kxa$o>7dq0Pa=BZT40FdM96?yrf^bjpco4oXCiB4whd)+ zxl~_1Fgbt*r98#lj_Zf*(m-i&XhM>G?$l$AxhXqaER1fEj02HgQ+p2#foNUZE%64C z1_+W@*Ike%6;(Ytp6wEn?uH2#%?Y#&Gy!nNfHA{z3@e4mbSRY!qg!$P?)Mnc`UV9f zx-f$wqBW1KV@Itq7H@PA`GN1YI~s*g%+nBR3c@JcG;9!-7t-QleP(9a4WyMx+u6a1 zi>}>%`D;WfQ#pUSIx`i@0F(DZ1fuon?gn;2NXbKZp&3Vrq&3Cksl)&n2h9{v2u)Af z8p6=O|Jd}@VkehL4Gt8F#gbtN+cLIn8MiFQbo$)PMcfE-WbfR4_3X?sZ5V;uw5&7& zVX}=v_rAARU?TnXKA8wWC1aUnFrc(-w_2T!=Xp|-(3%)%E;uu|uuLbN%H+}p=e8*j zxi-?uEC6!OgfQ()Y1C;hD}>bxM1sTpM$2Z2}CkMpq05=v+mUv8kK5A2x0Kdrp=c$DkoLgOf$2P zPWPo8O8~iTWoA92AVyz{47m{TRy$$Xh4T2WJvU1g66j;+4=NpcK^p<%E+!=bL2pb2 zu-N?9`eLLhYE=?SS9Qttq*LjU;mPr_9cd@cwANuLT~`}rObr-)qsh0S(YoZ7z%Wh2v>5Vo z-*7%#cC2)=8K9R1DWnU{FeD(%f7 z^FO=q)beZI{%?w-1?4dlg$>gMPV98&OGt&KK%!? zm6?p`Ao}+{eBg$gFI|{BpGw(`2}DK_Ov3~KF1YLapvg%&1j_3KAxK~Pf+H#E7^$mY z{w4+-kaQf0HXaxPh*5rtaaO}QZ?s!VN@U0wuGHIrpa2?; zrqUW2v%q=9-kS`bAygZ+9gVzIX}9a1@4F*oW6dCBpiR>x0MoYf#e$i3kn>Knb@I&V zi7gY??!9KHJj@U+%NiLO&YI5r!s5)_(mmh&&Rc%vb%w!LA%qB}jKe^!T61jMFbpL_ z&-0lgrIeX={2*vHnybr96{R;s6_PW;yUfBW{^ zZ{M|RmuZ?xYtQ!`VTL6Az!QekX;w9XMq->XfuRhRmMgYxIgYbu$Nr;d9*2zwrU~0k z<+nSWi%c-^-subv4~Btu(&G@5JJ9Ks@EHwBj-YsvcyJ=!wksiv|F84b2aIC^pQt~`Uh;wBBGU*daKoD z%xHCJcD}Mazu2nQ1k%88adBnk)aj|9(-|M=fAaXFtFCBzdYXW2$B|MpB#X1*i4hGD z_lfY|0Q85h_V4lpC-)%I2o$13zeWT*I`E9ea9g zbW^k0T9v+1s_g~~OUs_u&KLShrGgw6(Q->#Y%NKUnm}V@+pekR?4zl zD-BXQok~fqR)HM3A+(aT3WTNkCFup@6XUDeZ8h7ObWTZX)>nULN1I|JTENOuRVy6? z;pvmdEz2&HN{b7NtJ0~jGy*?Jr_;qkA>*V0kfUg|8;6cQ5r1%1Fg`dk==$y9(ZS`# z#g*!cX_#6mBvgu|7n0KP_Ld;@xiFMerfJlwRT)ZT#?18m=;-jO?NuwQuWBl7OT7^- z0F_*(m?027XN&GSPCyZUeD^M6EYAHAf6AA=b*Gol{!#pUH)vA`iK_6zp$7&V89fE` zz(4%q4|ezF|KdrHAW;Qg81&Hl51o54pog9UdJ5>pfc2j;B%&?{pr3gcbGOPx_(|yD z|F(A};8hga`c!rIU2+o=63hl6Vc#Lh4n{T+M;$g17uJzM6d8vR85I@W0Cz-CQC4M@ zH4qXYKp$2msf1sS<9Xn5*nJySUd@Y2s5j0Ej%KtS(}s zudtterYKC}>K*W>r5*rqa<^*dLGFt$l<%zZxN(b3PUKDV=oO5@Tlt@+qk znUQrk7C@P|ZFAAZ+~lwK7i@j6EUOs+-8^fvMz9y7BCTF(!tE9go`H>&M`a1PM9S;_g7WKAOe8uD|90l%~Xf~fmY{x z^$ZId;5%~ls+y}Qd|Pdc&PyFKOe3`dfZzQy#EkRZ7cZ^MZwTbH32T=OTk%3&Le`K! zPUtmjPOnkjfs&vH9%ekdDKxS5Ck6bw8`nuOD z6SKpgp59~H1J&m*^?!Uk04YBDUFD8@b44En`=Q5f#E2T7>;#q&q=I; zWfa`HS2V1e5)l+C$|6Lm3)-3kQ36o_g_=?kd1$D>BS4@~QiC3GO3E=nDB^iwFi-#$ z1ENC(E$%zyi3?cv(c?V*Uu0-cI+HqxixymYbK98bCi@JY3?Thq+l+J1-Two-^MoLY0LxM!$i{<* zns&hTQF?cXn!Eb%E$&`gyHDo-cSG+k9Ub%=enUnNMPX$NtZToz?B>oU_B@%p;ebn^1*Qf%_RUR!xn;V&O_kDgaPcc@luCsztWlk#fn% zWwovzK>0z`=OCE3<`r*@IrvF@BmnD*K`w)B)K`6*c79LMO{Y_`oof~EEW}aep&ggk zeGvQns>JW(6F=B`=H>UAo9dH3+b-J8rrc|JX_kvCR>kj*1Aw@{y{(nhuAIu;_qG6l zDf?L82PnO` zY7I+4BZ&e42;jnDrA~1JIiNeB-jXD_FIYl;vS4)^?lAWucW_6;juK-Az-bdVKmkIM zL?|T~BY_Y>0?CCgr%e>z$rn)L+3BAsE?kqZedlKU>W$#q;Y{~-`L~pWp6yOx5vC?)?{(o zhi}}jRR~6X-`Cfldwjv{w;#-kZk)JrHGo1hXsj)P4_~{oIoU8Bv4Y>4|l?!Ar0dq27npCk#*O(t#O&h?hE*z`U7ihn$IWB*6+?Wv3D zjn>Q-@rTo=-CQDB>B)&1*$4OUZLF(F%SbzT^vHz^7YYjt&z(I*DLor?vZ}JOth_8e zE&alU^R*2%TAfxNvewp?#>QryuC>0QAulgaj;y|2b+f48c6~LWV#l+(w3CQL6h*!; zzbLh~Ew$O2*l0VOk|XDi}OuK4q!rHji;%gsik=Dr@G{+?H^B`#XBth%(I zw#`0a)Sw@JI2aV#?}g`=pFVLUFRyUixUqBQJOQ1=JrWpW;xqvP`FQu?hh{Ipz|~h2 z|8c@F3IYN%{5}NjI~O->^aKQmrF`y{s3i-Zl|%;tMxHY?9vV>;-JK)}f)guUt>Uc~ z(FM!OFbn}V@CN1`0>>iEC>RW)C^>jXS3h5~(STVNBcxKPJ1=9XOWInSJUl%CbGf-W zc8!mZe{TTXRDJ8wF+(VIAY_5xhp-M&z!(Ei%zF9<1`$GCK=k0}>sMP_<>P&yAUJ$| zeLEWL`!RCGbBjBwS=pJR?jNnuz)!>z0H_Sn@efD{^NUNn_8ZEJ4pq-#o}8LcLS68& zzc=`3%%b-7iQUe3@#;b;BwUX>eCY6ppKVRNcJ;mY-YY4;6&-mj^6bT|%*1Wmww9F? zT#P=yXZP+O4js=-OKNM=!(HSTI*|f0+HI`HOR4r|IF7n;li?1UBD(9AEk|ibTBg$~ zi**Gs0G!)!m;aE$kNv5Kx)#Cee+ys;OGrC#gIcA|F1|5w>}aD-JLkzK<*!gBrr~2I z1Ox_4VLcrZ%OY{koIpVWUvICT4I!R>U2S^P(EIz`>yIW;5|17~-EZ`0Ae3Pc0)|L{ z01$zWZta}IQc;|dT03q|MCSV+;J|(;q)$^^{MeTsYD_F1wPHo=vPA=Cj;_4P^Hs&% zsM(=Y{ka=Cr8(y5n_rH6ZoMinv|IRSZE@z11>+kN@&ki#MP^gY&E~)W@R%@1Uz|H) z*`%iU-0mZW3AdAj=dJzM?@6#~wHmvq8zfN}_xRkXt4XCbEw=Mf^JdTM0PqIz`soCM zeEkBP-4uj6b$UAv#J&TEMok#c^ER8^ZZTU_N~J>-5MWP@x57iMRH-OYj-5UmVOAo< z4HiWZappmoBH?b=3(<7%o*$WDQV#8*;+eiZL;KBYXjTOE_Xgm)4ei@lTFW)w!egJi z6@4jm+B3~%bt+>W0P*M*E((s)Q)eFhY`wkWW=MEI`Kc>?!u^c)P%)r)-htDzcl@oQ zvS#$$If|~nEg?gIivN1#YIGnbe#HIb5tfCagd!}kENg6ONQsSpapiKBVcUO*5dxiw zei-JiXKJ+BPDjT~nejM94B$KDOcb20-SYxPgi;Z(3krduvLFcn2z5$bZRx`!hyLtK zm`V~sEU_6u5|PSFu-K?3{9&EwJyRRt0Y*PDm zwd>7lZ#8ctNVHJ`n8l(%5XY&!Gyp&lW)S@A1symaA3kaVMhGxqD!7?+lnjw1YBKAw zBvK+O8AX0hwpQOpsp#X=<;f@JZ{PXN{ijSZ$F>?+73b(jhgSG^w8CncMl775X{?nf{B9Vx>44X zV*m)IE&y1}x=C-rg^9@kNR(j2NOBbjLiG2CmVz+DPzr#l!~k|r^B7|w9WW~Lhq&m% zr~9;iim{Nu^F1%a}#LXFr!TD(i zvqcgMj0;?mN=QN!_v+j4c6~iaJg=>tIifbDp4q)&(TN|=^y%Bjpf|kw#>RgA)f@N4 zI(p4$w)xE{uk1QFOyNq;P(*+N5#9V{*fp*ljFqx-4Elu`NaVp-1HU1`3_@-KDK|0= zAUCM+9TXV`qgR^Hm$TAZKp z+N$MaH1%GVYEON+UvL1=+c2V7PDT+YD`4!Jai9}kB1Y}?Uw%)lo%D?A4bF{Qx$5=+ z!vd$EcLIQAL7@PSb8mk;tC$5=u6Cl0Mp*W^wH?N*Bb!nQ9xBjyfQkdP%4uT^DkZp6 zamam$0&<3-_Op^B<+43Mt#-CWt^fwNAL10Qv}#bYu0sl}#K20xy2z5<$o>Q*3=#uG zAt5SagI#K}(;ACZVs@0;P>TieJj+t0k3!LZ+RBp^L4kp&4^zASnW_sX)|8Z#s8s69 z*W&lAwHYgN@O|A=&PNtyrfM`wyVZmdLYR>_LRpFt!3>B+ZYfxogVwaH*Gn@qVsp3|qrB95_F z*CfU2&-`g~c~<@Ha(nB|s@(K?ZGM(9D>Y@qmb#hh-ox?BUwnOP#P%;fX_6SdK-CHe=@IPre)YRnWi`I)>Y*sTb*ozFdq_UfrYn%TN7D^}y>gvs5 zyg6c0MA+a*MonQb_h^7B>BjZj4Y^aiS>9n%V$MpT$zpiOmwk3fczIj(g3w?<$kA5c zXXWbZ>lu%3TKd3WrXqPy%&G;-rtV4Qx^+XGqCP2O-Zw9a4sqz*;bmvyxaN$34^Pw# z90wSyf&&r9*%q{KRspN@P>&o#cnh|fyN7Z{8^|(v&1%*Sn~rd2@@znW z2?Z;Kva0G=7CjOg7P{oA<%4_nG+j}D^U<3YL+h&>K2|GGSB44gJ1%I&i$}K}@HbZl zdTST}k#K)sY^cs-!NCiKhhKhwYjE$7y5`ox>Xv|D4^JiI%d$2gEmp~3=dA*D5K2H& zV^Av@PExg>!!LT^JNRy zuX<_2c6-{OOxxVoHs@u6j_wB2S%;IT`)vqLnXxu+6U%RYjTv3e=eshoW)cm?R0o7Lq+MH z&d15SLtMo9SZ*vE?W{>i7EGDmeFv<1X*C7N&B-Y!C|I^^nbm3y?bT($h@PjDlEzJV zP|r&ogR4?v9v@|$)UW$hZSI9DQ9-Il3|$u|U)pm0`t^v22LO=2rrTGae>r>Z6W0>1 z3?KdwVtKX1?|gS{K}L%F;2KB*0PK0^ub&(}2QXNIdw5jpf2{XA=DvFwQD>k;I^HmJ z+323(>)q(BST@{1_D+#YBCB0rE2E+3Waj|D!i5VdrArns&M7Di3G`Rdn+6*N5lXM@ zdNSy$xfL$TH?ucfXFog|4RDZM=% zpe^70)|!P27x6rQB`$8|isc8lfAR91$q4=4o{lD%rJa`_2(ZK;qG511mExi4kA#JW zjT$v7B_+kRieZ#2)7q-Ne0)!9RWV_>^0dTF=gEHMB0PAuQsck)*_Hl&9u9%5TDc;$ zSC|_-B_JR`rh*F%4hr*D6Tuj)5(o9}^ZNSNX3d(VQmMSXy=Tvwy=BYCp@W71APK>} z3zF;taIV#)Qb7R&{*?@XLdjx`T>N0N0CJk`l#disLMh8&t=@n!9x$N4$zo+$R^#O< zqoSNRa(LN8qvOvV8NdGfnP1GxSx|t0C-|zUep{{Px@@0{qz7=fD4;nH4VDV+M zg;dlv$6U+t3F$TYlTGpq`10dCdHH!KPMkP;^k_mtLPSJF-qlO!&rd1-8*>oDIvVPm zYbydCm|Afna>R2p3_8)<>sPTEhZx+xAq;>iLIe)@7?v|v@H#+t=t-4 zXbnisxKXdwv4Cr;Z%?20(9t8`_YNB{fBt+yl&UJLtQJdVM*7kv&;Iu)g{g?2{9z-w zE+0TMKq5ftuVgorP8ii%5HjihT! zTg|`_tw%tgggmh76iOv;=hMonErRg$=wN1`%EEPD9_F_w;r90q$4B5ubke*RkVd z^qP6|<^s^d+x4eJX|~QZWa8kWu%K{*_1V<=fn2Zma|@n%y>iANFJCp0yzqrxmp#2a reY*FKkWdGc{auBjf~V9BzajY#Mi|@$;@7mp00000NkvXXu0mjfP;YH% literal 0 HcmV?d00001