forked from leftypol/leftypol
czaks
8 years ago
121 changed files with 4568 additions and 1143 deletions
Binary file not shown.
@ -0,0 +1,82 @@ |
|||
/* |
|||
* catalog-search.js |
|||
* - Search and filters threads when on catalog view |
|||
* - Optional shortcuts 's' and 'esc' to open and close the search. |
|||
* |
|||
* Usage: |
|||
* $config['additional_javascript'][] = 'js/jquery.min.js'; |
|||
* $config['additional_javascript'][] = 'js/comment-toolbar.js'; |
|||
*/ |
|||
if (active_page == 'catalog') { |
|||
onready(function () { |
|||
'use strict'; |
|||
|
|||
// 'true' = enable shortcuts
|
|||
var useKeybinds = true; |
|||
|
|||
// trigger the search 400ms after last keystroke
|
|||
var delay = 400; |
|||
var timeoutHandle; |
|||
|
|||
//search and hide none matching threads
|
|||
function filter(search_term) { |
|||
$('.replies').each(function () { |
|||
var subject = $(this).children('.intro').text().toLowerCase(); |
|||
var comment = $(this).clone().children().remove(':lt(2)').end().text().trim().toLowerCase(); |
|||
search_term = search_term.toLowerCase(); |
|||
|
|||
if (subject.indexOf(search_term) == -1 && comment.indexOf(search_term) == -1) { |
|||
$(this).parents('div[id="Grid"]>.mix').css('display', 'none'); |
|||
} else { |
|||
$(this).parents('div[id="Grid"]>.mix').css('display', 'inline-block'); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
function searchToggle() { |
|||
var button = $('#catalog_search_button')[0]; |
|||
|
|||
if (!button.dataset.expanded) { |
|||
button.dataset.expanded = '1'; |
|||
button.innerText = 'Close'; |
|||
$('.catalog_search').append(' <input id="search_field" style="border: inset 1px;">'); |
|||
$('#search_field').focus(); |
|||
} else { |
|||
delete button.dataset.expanded; |
|||
button.innerText = 'Search'; |
|||
$('.catalog_search').children().last().remove(); |
|||
$('div[id="Grid"]>.mix').each(function () { $(this).css('display', 'inline-block'); }); |
|||
} |
|||
} |
|||
|
|||
$('.threads').before('<span class="catalog_search">[<a id="catalog_search_button" style="text-decoration:none; cursor:pointer;"></a>]</span>'); |
|||
$('#catalog_search_button').text('Search'); |
|||
|
|||
$('#catalog_search_button').on('click', searchToggle); |
|||
$('.catalog_search').on('keyup', 'input#search_field', function (e) { |
|||
window.clearTimeout(timeoutHandle); |
|||
timeoutHandle = window.setTimeout(filter, 400, e.target.value); |
|||
}); |
|||
|
|||
if (useKeybinds) { |
|||
// 's'
|
|||
$('body').on('keydown', function (e) { |
|||
if (e.which === 83 && e.target.tagName === 'BODY' && !(e.ctrlKey || e.altKey || e.shiftKey)) { |
|||
e.preventDefault(); |
|||
if ($('#search_field').length !== 0) { |
|||
$('#search_field').focus(); |
|||
} else { |
|||
searchToggle(); |
|||
} |
|||
} |
|||
}); |
|||
// 'esc'
|
|||
$('.catalog_search').on('keydown', 'input#search_field', function (e) { |
|||
if (e.which === 27 && !(e.ctrlKey || e.altKey || e.shiftKey)) { |
|||
window.clearTimeout(timeoutHandle); |
|||
searchToggle(); |
|||
} |
|||
}); |
|||
} |
|||
}); |
|||
} |
@ -0,0 +1,191 @@ |
|||
/* |
|||
* file-selector.js - Add support for drag and drop file selection, and paste from clipbboard on supported browsers. |
|||
* |
|||
* Usage: |
|||
* $config['additional_javascript'][] = 'js/jquery.min.js'; |
|||
* $config['additional_javascript'][] = 'js/file-selector.js'; |
|||
*/ |
|||
function init_file_selector(max_images) { |
|||
|
|||
$(document).ready(function () { |
|||
// add options panel item
|
|||
if (window.Options && Options.get_tab('general')) { |
|||
Options.extend_tab('general', '<label id="file-drag-drop"><input type="checkbox">' + _('Drag and drop file selection') + '</label>'); |
|||
|
|||
$('#file-drag-drop>input').on('click', function() { |
|||
if ($('#file-drag-drop>input').is(':checked')) { |
|||
localStorage.file_dragdrop = 'true'; |
|||
} else { |
|||
localStorage.file_dragdrop = 'false'; |
|||
} |
|||
}); |
|||
|
|||
if (typeof localStorage.file_dragdrop === 'undefined') localStorage.file_dragdrop = 'true'; |
|||
if (localStorage.file_dragdrop === 'true') $('#file-drag-drop>input').prop('checked', true); |
|||
} |
|||
}); |
|||
|
|||
// disabled by user, or incompatible browser.
|
|||
if (localStorage.file_dragdrop == 'false' || !(window.URL.createObjectURL && window.File)) |
|||
return; |
|||
|
|||
// multipost not enabled
|
|||
if (typeof max_images == 'undefined') { |
|||
var max_images = 1; |
|||
} |
|||
|
|||
$('<div class="dropzone-wrap" style="display: none;">'+ |
|||
'<div class="dropzone" tabindex="0">'+ |
|||
'<div class="file-hint">'+_('Select/drop/paste files here')+'</div>'+ |
|||
'<div class="file-thumbs"></div>'+ |
|||
'</div>'+ |
|||
'</div>'+ |
|||
'</div>').prependTo('#upload td'); |
|||
|
|||
var files = []; |
|||
$('#upload_file').remove(); // remove the original file selector
|
|||
$('.dropzone-wrap').css('user-select', 'none').show(); // let jquery add browser specific prefix
|
|||
|
|||
function addFile(file) { |
|||
if (files.length == max_images) |
|||
return; |
|||
|
|||
files.push(file); |
|||
addThumb(file); |
|||
} |
|||
|
|||
function removeFile(file) { |
|||
files.splice(files.indexOf(file), 1); |
|||
} |
|||
|
|||
function getThumbElement(file) { |
|||
return $('.tmb-container').filter(function(){return($(this).data('file-ref')==file);}); |
|||
} |
|||
|
|||
function addThumb(file) { |
|||
|
|||
var fileName = (file.name.length < 24) ? file.name : file.name.substr(0, 22) + '…'; |
|||
var fileType = file.type.split('/')[0]; |
|||
var fileExt = file.type.split('/')[1]; |
|||
var $container = $('<div>') |
|||
.addClass('tmb-container') |
|||
.data('file-ref', file) |
|||
.append( |
|||
$('<div>').addClass('remove-btn').html('✖'), |
|||
$('<div>').addClass('file-tmb'), |
|||
$('<div>').addClass('tmb-filename').html(fileName) |
|||
) |
|||
.appendTo('.file-thumbs'); |
|||
|
|||
var $fileThumb = $container.find('.file-tmb'); |
|||
if (fileType == 'image') { |
|||
// if image file, generate thumbnail
|
|||
var objURL = window.URL.createObjectURL(file); |
|||
$fileThumb.css('background-image', 'url('+ objURL +')'); |
|||
} else { |
|||
$fileThumb.html('<span>' + fileExt.toUpperCase() + '</span>'); |
|||
} |
|||
} |
|||
|
|||
$(document).on('ajax_before_post', function (e, formData) { |
|||
for (var i=0; i<max_images; i++) { |
|||
var key = 'file'; |
|||
if (i > 0) key += i + 1; |
|||
if (typeof files[i] === 'undefined') break; |
|||
formData.append(key, files[i]); |
|||
} |
|||
}); |
|||
|
|||
// clear file queue and UI on success
|
|||
$(document).on('ajax_after_post', function () { |
|||
files = []; |
|||
$('.file-thumbs').empty(); |
|||
}); |
|||
|
|||
var dragCounter = 0; |
|||
var dropHandlers = { |
|||
dragenter: function (e) { |
|||
e.stopPropagation(); |
|||
e.preventDefault(); |
|||
|
|||
if (dragCounter === 0) $('.dropzone').addClass('dragover'); |
|||
dragCounter++; |
|||
}, |
|||
dragover: function (e) { |
|||
// needed for webkit to work
|
|||
e.stopPropagation(); |
|||
e.preventDefault(); |
|||
}, |
|||
dragleave: function (e) { |
|||
e.stopPropagation(); |
|||
e.preventDefault(); |
|||
|
|||
dragCounter--; |
|||
if (dragCounter === 0) $('.dropzone').removeClass('dragover'); |
|||
}, |
|||
drop: function (e) { |
|||
e.stopPropagation(); |
|||
e.preventDefault(); |
|||
|
|||
$('.dropzone').removeClass('dragover'); |
|||
dragCounter = 0; |
|||
|
|||
var fileList = e.originalEvent.dataTransfer.files; |
|||
for (var i=0; i<fileList.length; i++) { |
|||
addFile(fileList[i]); |
|||
} |
|||
} |
|||
}; |
|||
|
|||
|
|||
// attach handlers
|
|||
$(document).on(dropHandlers); |
|||
|
|||
$(document).on('click', '.dropzone .remove-btn', function (e) { |
|||
e.stopPropagation(); |
|||
|
|||
var file = $(e.target).parent().data('file-ref'); |
|||
|
|||
getThumbElement(file).remove(); |
|||
removeFile(file); |
|||
}); |
|||
|
|||
$(document).on('keypress click', '.dropzone', function (e) { |
|||
e.stopPropagation(); |
|||
|
|||
// accept mouse click or Enter
|
|||
if ((e.which != 1 || e.target.className != 'file-hint') && |
|||
e.which != 13) |
|||
return; |
|||
|
|||
var $fileSelector = $('<input type="file" multiple>'); |
|||
|
|||
$fileSelector.on('change', function (e) { |
|||
if (this.files.length > 0) { |
|||
for (var i=0; i<this.files.length; i++) { |
|||
addFile(this.files[i]); |
|||
} |
|||
} |
|||
$(this).remove(); |
|||
}); |
|||
|
|||
$fileSelector.click(); |
|||
}); |
|||
|
|||
$(document).on('paste', function (e) { |
|||
var clipboard = e.originalEvent.clipboardData; |
|||
if (typeof clipboard.items != 'undefined' && clipboard.items.length != 0) { |
|||
|
|||
//Webkit
|
|||
for (var i=0; i<clipboard.items.length; i++) { |
|||
if (clipboard.items[i].kind != 'file') |
|||
continue; |
|||
|
|||
//convert blob to file
|
|||
var file = new File([clipboard.items[i].getAsFile()], 'ClipboardImage.png', {type: 'image/png'}); |
|||
addFile(file); |
|||
} |
|||
} |
|||
}); |
|||
|
|||
} |
@ -0,0 +1,165 @@ |
|||
if (active_page == 'index' || active_page == 'thread') |
|||
$(function(){ |
|||
|
|||
var gallery_view = false; |
|||
|
|||
$('hr:first').before('<div id="gallery-view" style="text-align:right"><a class="unimportant" href="javascript:void(0)">-</a></div>'); |
|||
$('#gallery-view a').html(gallery_view ? _("Disable gallery mode") : _("Enable gallery mode")).click(function() { |
|||
gallery_view = !gallery_view; |
|||
$(this).html(gallery_view ? _("Disable gallery mode") : _("Enable gallery mode")); |
|||
toggle_gview(document); |
|||
}); |
|||
|
|||
var toggle_gview = function(elem) { |
|||
if (gallery_view) { |
|||
$(elem).find('img.post-image').parent().each(function() { |
|||
this.oldonclick = this.onclick; |
|||
this.onclick = handle_click; |
|||
$(this).attr('data-galid', Math.random()); |
|||
}); |
|||
} |
|||
else { |
|||
$(elem).find('img.post-image').parent().each(function() { |
|||
if (this.onclick == handle_click) this.onclick = this.oldonclick; |
|||
}); |
|||
} |
|||
}; |
|||
|
|||
$(document).on('new_post', toggle_gview); |
|||
|
|||
var gallery_opened = false; |
|||
|
|||
var handle_click = function(e) { |
|||
e.stopPropagation(); |
|||
e.preventDefault(); |
|||
|
|||
if (!gallery_opened) open_gallery(); |
|||
|
|||
gallery_setimage($(this).attr('data-galid')); |
|||
}; |
|||
|
|||
var handler, images, active, toolbar; |
|||
|
|||
var open_gallery = function() { |
|||
$('body').css('overflow', 'hidden'); |
|||
|
|||
gallery_opened = true; |
|||
|
|||
handler = $("<div id='alert_handler'></div>").hide().appendTo('body').css('text-align', 'left'); |
|||
|
|||
$("<div id='alert_background'></div>").click(close_gallery).appendTo(handler); |
|||
|
|||
images = $("<div id='gallery_images'></div>").appendTo(handler); |
|||
toolbar = $("<div id='gallery_toolbar'></div>").appendTo(handler); |
|||
active = $("<div id='gallery_main'></div>").appendTo(handler); |
|||
|
|||
active.on('click', function() { |
|||
close_gallery(); |
|||
}); |
|||
|
|||
$('img.post-image').parent().each(function() { |
|||
var thumb = $(this).find('img').attr('src'); |
|||
|
|||
var i = $('<img>').appendTo(images); |
|||
i.attr('src', thumb); |
|||
i.attr('data-galid-th', $(this).attr('data-galid')); |
|||
|
|||
i.on('click', function(e) { |
|||
gallery_setimage($(this).attr('data-galid-th')); |
|||
}); |
|||
}); |
|||
|
|||
$("<a href='javascript:void(0)'><i class='fa fa-times'></i></div>") |
|||
.click(close_gallery).appendTo(toolbar); |
|||
|
|||
$('body').on('keydown.gview', function(e) { |
|||
if (e.which == 39 || e.which == 40) { // right or down arrow
|
|||
gallery_setimage(+1); |
|||
e.preventDefault(); |
|||
} |
|||
else if (e.which == 37 || e.which == 38) { // left or up arrow
|
|||
gallery_setimage(-1); |
|||
e.preventDefault(); |
|||
} |
|||
}); |
|||
|
|||
handler.fadeIn(400); |
|||
}; |
|||
|
|||
var gallery_setimage = function(a) { |
|||
if (a == +1 || a == -1) { |
|||
var meth = (a == -1) ? 'prev' : 'next'; |
|||
a = $('#gallery_images img.active')[meth]().attr('data-galid-th'); |
|||
if (!a) return; |
|||
} |
|||
|
|||
$('#gallery_images img.active').removeClass('active'); |
|||
|
|||
var thumb = $('#gallery_images [data-galid-th="'+a+'"]'); |
|||
var elem = $('a[data-galid="'+a+'"]'); |
|||
|
|||
thumb.addClass('active'); |
|||
|
|||
var topscroll = thumb.position().top + images.scrollTop(); |
|||
topscroll -= images.height() / 2; |
|||
topscroll += thumb.height() / 2; |
|||
images.animate({'scrollTop': topscroll}, 300); |
|||
|
|||
var img = elem.attr('href'); |
|||
|
|||
active.find('img, video').fadeOut(200, function() { $(this).remove(); }); |
|||
|
|||
var i; |
|||
if (img.match(/player\.php/)) { |
|||
img = img.replace(/.*player\.php\?v=|&t=.*/g, ''); |
|||
} |
|||
if (img.match(/\.webm$|\.mp4$|\.ogv$/i)) { // We are handling video nao
|
|||
i = $('<video>'); |
|||
i.attr('src', img); |
|||
i.attr('autoplay', true); |
|||
i.attr('controls', true); |
|||
i.appendTo(active); |
|||
i.hide(); |
|||
} |
|||
else { // Just a plain image
|
|||
i = $('<img>'); |
|||
i.attr('src', img); |
|||
i.appendTo(active); |
|||
i.hide(); |
|||
} |
|||
|
|||
// Let's actually preload the next few images
|
|||
var nextimg = $('#gallery_images active'); |
|||
for (var j = 0; j < 3; j++) { |
|||
nextimg = nextimg.next(); |
|||
var attr; |
|||
if (attr = nextimg.attr('data-gaild-th')) { |
|||
var href = $('a[data-galid="'+attr+'"]').attr('href'); |
|||
if (href.match(/\.webm|\.mp4|\.ogv/i)) { j--; continue; } |
|||
if ($('[data-galid-preload="'+attr+'"]').length) continue; |
|||
var img = $('<img>').attr('src', href).attr('data-galid-preload', attr).hide().appendTo('body').on('load', function() { $(this).remove(); }); |
|||
} |
|||
else break; |
|||
} |
|||
|
|||
i.one('load canplay', function() { |
|||
i.css('left', 'calc(50% - '+i.width()+'px / 2)'); |
|||
i.css('top', 'calc(50% - '+i.height()+'px / 2)'); |
|||
i.fadeIn(200); |
|||
}).on('click', function(e) { |
|||
e.stopPropagation(); |
|||
gallery_setimage(+1); |
|||
}); |
|||
}; |
|||
|
|||
var close_gallery = function() { |
|||
$('body').css('overflow', 'auto'); |
|||
|
|||
gallery_opened = false; |
|||
|
|||
$('body').off('keydown.gview'); |
|||
|
|||
handler.fadeOut(400, function() { handler.remove(); }); |
|||
}; |
|||
|
|||
}); |
@ -0,0 +1,182 @@ |
|||
/* image-hover.js |
|||
* This script is copied almost verbatim from https://github.com/Pashe/8chanX/blob/2-0/8chan-x.user.js
|
|||
* All I did was remove the sprintf dependency and integrate it into 8chan's Options as opposed to Pashe's. |
|||
* I also changed initHover() to also bind on new_post. |
|||
* Thanks Pashe for using WTFPL. |
|||
*/ |
|||
|
|||
if (active_page === "catalog" || active_page === "thread" || active_page === "index") { |
|||
$(document).on('ready', function(){ |
|||
|
|||
if (window.Options && Options.get_tab('general')) { |
|||
Options.extend_tab("general", |
|||
"<fieldset><legend>Image hover</legend>" |
|||
+ ("<label class='image-hover' id='imageHover'><input type='checkbox' /> "+_('Image hover')+"</label>") |
|||
+ ("<label class='image-hover' id='catalogImageHover'><input type='checkbox' /> "+_('Image hover on catalog')+"</label>") |
|||
+ ("<label class='image-hover' id='imageHoverFollowCursor'><input type='checkbox' /> "+_('Image hover should follow cursor')+"</label>") |
|||
+ "</fieldset>"); |
|||
} |
|||
|
|||
$('.image-hover').on('change', function(){ |
|||
var setting = $(this).attr('id'); |
|||
|
|||
localStorage[setting] = $(this).children('input').is(':checked'); |
|||
}); |
|||
|
|||
if (!localStorage.imageHover || !localStorage.catalogImageHover || !localStorage.imageHoverFollowCursor) { |
|||
localStorage.imageHover = 'false'; |
|||
localStorage.catalogImageHover = 'false'; |
|||
localStorage.imageHoverFollowCursor = 'false'; |
|||
} |
|||
|
|||
if (getSetting('imageHover')) $('#imageHover>input').prop('checked', 'checked'); |
|||
if (getSetting('catalogImageHover')) $('#catalogImageHover>input').prop('checked', 'checked'); |
|||
if (getSetting('imageHoverFollowCursor')) $('#imageHoverFollowCursor>input').prop('checked', 'checked'); |
|||
|
|||
function getFileExtension(filename) { //Pashe, WTFPL
|
|||
if (filename.match(/\.([a-z0-9]+)(&loop.*)?$/i) !== null) { |
|||
return filename.match(/\.([a-z0-9]+)(&loop.*)?$/i)[1]; |
|||
} else if (filename.match(/https?:\/\/(www\.)?youtube.com/)) { |
|||
return 'Youtube'; |
|||
} else { |
|||
return "unknown: " + filename; |
|||
} |
|||
} |
|||
|
|||
function isImage(fileExtension) { //Pashe, WTFPL
|
|||
return ($.inArray(fileExtension, ["jpg", "jpeg", "gif", "png"]) !== -1); |
|||
} |
|||
|
|||
function isVideo(fileExtension) { //Pashe, WTFPL
|
|||
return ($.inArray(fileExtension, ["webm", "mp4"]) !== -1); |
|||
} |
|||
|
|||
function isOnCatalog() { |
|||
return window.active_page === "catalog"; |
|||
} |
|||
|
|||
function isOnThread() { |
|||
return window.active_page === "thread"; |
|||
} |
|||
|
|||
function getSetting(key) { |
|||
return (localStorage[key] == 'true'); |
|||
} |
|||
|
|||
function initImageHover() { //Pashe, influenced by tux, et al, WTFPL
|
|||
if (!getSetting("imageHover") && !getSetting("catalogImageHover")) {return;} |
|||
|
|||
var selectors = []; |
|||
|
|||
if (getSetting("imageHover")) {selectors.push("img.post-image", "canvas.post-image");} |
|||
if (getSetting("catalogImageHover") && isOnCatalog()) { |
|||
selectors.push(".thread-image"); |
|||
$(".theme-catalog div.thread").css("position", "inherit"); |
|||
} |
|||
|
|||
function bindEvents(el) { |
|||
$(el).find(selectors.join(", ")).each(function () { |
|||
if ($(this).parent().data("expanded")) {return;} |
|||
|
|||
var $this = $(this); |
|||
|
|||
$this.on("mousemove", imageHoverStart); |
|||
$this.on("mouseout", imageHoverEnd); |
|||
$this.on("click", imageHoverEnd); |
|||
}); |
|||
} |
|||
|
|||
bindEvents(document.body); |
|||
$(document).on('new_post', function(e, post) { |
|||
bindEvents(post); |
|||
}); |
|||
} |
|||
|
|||
function imageHoverStart(e) { //Pashe, anonish, WTFPL
|
|||
var hoverImage = $("#chx_hoverImage"); |
|||
|
|||
if (hoverImage.length) { |
|||
if (getSetting("imageHoverFollowCursor")) { |
|||
var scrollTop = $(window).scrollTop(); |
|||
var imgY = e.pageY; |
|||
var imgTop = imgY; |
|||
var windowWidth = $(window).width(); |
|||
var imgWidth = hoverImage.width() + e.pageX; |
|||
|
|||
if (imgY < scrollTop + 15) { |
|||
imgTop = scrollTop; |
|||
} else if (imgY > scrollTop + $(window).height() - hoverImage.height() - 15) { |
|||
imgTop = scrollTop + $(window).height() - hoverImage.height() - 15; |
|||
} |
|||
|
|||
if (imgWidth > windowWidth) { |
|||
hoverImage.css({ |
|||
'left': (e.pageX + (windowWidth - imgWidth)), |
|||
'top' : imgTop, |
|||
}); |
|||
} else { |
|||
hoverImage.css({ |
|||
'left': e.pageX, |
|||
'top' : imgTop, |
|||
}); |
|||
} |
|||
|
|||
hoverImage.appendTo($("body")); |
|||
} |
|||
|
|||
return; |
|||
} |
|||
|
|||
var $this = $(this); |
|||
|
|||
var fullUrl; |
|||
if ($this.parent().attr("href").match("src")) { |
|||
fullUrl = $this.parent().attr("href"); |
|||
} else if (isOnCatalog()) { |
|||
fullUrl = $this.attr("data-fullimage"); |
|||
if (!isImage(getFileExtension(fullUrl))) {fullUrl = $this.attr("src");} |
|||
} |
|||
|
|||
if (isVideo(getFileExtension(fullUrl))) {return;} |
|||
|
|||
hoverImage = $('<img id="chx_hoverImage" src="'+fullUrl+'" />'); |
|||
|
|||
if (getSetting("imageHoverFollowCursor")) { |
|||
var size = $this.parents('.file').find('.unimportant').text().match(/\b(\d+)x(\d+)\b/), |
|||
maxWidth = $(window).width(), |
|||
maxHeight = $(window).height(); |
|||
|
|||
var scale = Math.min(1, maxWidth / size[1], maxHeight / size[2]); |
|||
hoverImage.css({ |
|||
"position" : "absolute", |
|||
"z-index" : 101, |
|||
"pointer-events": "none", |
|||
"width" : size[1] + "px", |
|||
"height" : size[2] + "px", |
|||
"max-width" : (size[1] * scale) + "px", |
|||
"max-height" : (size[2] * scale) + "px", |
|||
'left' : e.pageX, |
|||
'top' : imgTop, |
|||
}); |
|||
} else { |
|||
hoverImage.css({ |
|||
"position" : "fixed", |
|||
"top" : 0, |
|||
"right" : 0, |
|||
"z-index" : 101, |
|||
"pointer-events": "none", |
|||
"max-width" : "100%", |
|||
"max-height" : "100%", |
|||
}); |
|||
} |
|||
hoverImage.appendTo($("body")); |
|||
if (isOnThread()) {$this.css("cursor", "none");} |
|||
} |
|||
|
|||
function imageHoverEnd() { //Pashe, WTFPL
|
|||
$("#chx_hoverImage").remove(); |
|||
} |
|||
|
|||
initImageHover(); |
|||
}); |
|||
} |
@ -0,0 +1,116 @@ |
|||
/***************************************************************** |
|||
* ------- WARNING! --------- * |
|||
***************************************************************** |
|||
* This script is at the current time undocumented and * |
|||
* unsupported. It is still a work in progress and will likely * |
|||
* change. You are on your own. * |
|||
*****************************************************************/ |
|||
|
|||
+function() { |
|||
|
|||
var uniq = function(a) { |
|||
var b = {}; |
|||
var c = []; |
|||
a.forEach(function(i) { |
|||
if (!b[i]) { |
|||
c.push(i); |
|||
b[i] = true; |
|||
} |
|||
}); |
|||
return c; |
|||
}; |
|||
|
|||
|
|||
if (active_page == 'thread' || active_page == 'index') { |
|||
var board = null; |
|||
|
|||
$(function() { |
|||
board = $('input[name="board"]').first().val(); |
|||
}); |
|||
|
|||
$(document).on('ajax_after_post', function(e, r) { |
|||
var threads = JSON.parse(localStorage.obthreads || '[]'); |
|||
|
|||
var thread = null; |
|||
if (active_page == 'index') { |
|||
thread = r.id|0; |
|||
} |
|||
else { |
|||
thread = $('[id^="thread_"]').first().attr('id').replace("thread_", "")|0; |
|||
} |
|||
|
|||
threads.push([board, thread]); |
|||
threads = uniq(threads); |
|||
localStorage.obthreads = JSON.stringify(threads); |
|||
}); |
|||
} |
|||
|
|||
var loaded = false; |
|||
$(function() { |
|||
loaded = true; |
|||
}); |
|||
|
|||
var activate = function() { |
|||
if (document.location.hash != '#own') return false; |
|||
|
|||
if (loaded) late_activate(); |
|||
else $(function() { late_activate(); }); |
|||
|
|||
return true; |
|||
}; |
|||
|
|||
var late_activate = function() { |
|||
$('[id^="thread_"]').remove(); |
|||
|
|||
var threads = JSON.parse(localStorage.obthreads || '[]'); |
|||
|
|||
threads.forEach(function(v) { |
|||
var board = v[0]; |
|||
var thread = v[1]; |
|||
var url = "/"+board+"/res/"+thread+".html"; |
|||
|
|||
$.get(url, function(html) { |
|||
var s = $(html).find('[id^="thread_"]'); |
|||
|
|||
s[0].bumptime = (new Date(s.find("time").last().attr("datetime"))).getTime(); |
|||
|
|||
var added = false; |
|||
$('[id^="thread_"]').each(function() { |
|||
if (added) return; |
|||
if (s[0].bumptime > this.bumptime) { |
|||
added = true; |
|||
s.insertBefore(this); |
|||
} |
|||
}); |
|||
if (!added) { |
|||
s.appendTo('[name="postcontrols"]'); |
|||
} |
|||
|
|||
s.find('.post.reply').addClass('hidden').hide().slice(-3).removeClass('hidden').show(); |
|||
|
|||
s.find('.post.reply.hidden').next().addClass('hidden').hide(); // Hide <br> elements
|
|||
|
|||
var posts_omitted = s.find('.post.reply.hidden').length; |
|||
var images_omitted = s.find('.post.reply.hidden img').length; |
|||
|
|||
if (posts_omitted > 0) { |
|||
var omitted = $(fmt('<span class="omitted">'+_('{0} posts and {1} images omitted.')+' '+_('Click reply to view.')+'</span>', |
|||
[posts_omitted, images_omitted])); |
|||
|
|||
omitted.appendTo(s.find('.post.op')); |
|||
} |
|||
|
|||
var reply = $('<a href="'+url+'">['+_('Reply')+']</a>').appendTo(s.find('.intro').first()); |
|||
|
|||
$(document).trigger('new_post', s[0]); |
|||
}); |
|||
}); |
|||
}; |
|||
|
|||
$(window).on("hashchange", function() { |
|||
return !activate(); |
|||
}); |
|||
activate(); |
|||
|
|||
|
|||
}(); |
@ -0,0 +1,116 @@ |
|||
/* |
|||
* thread-stats.js |
|||
* - Adds statistics of the thread below the posts area |
|||
* - Shows ID post count beside each postID on hover |
|||
* |
|||
* Usage: |
|||
* $config['additional_javascript'][] = 'js/jquery.min.js'; |
|||
* $config['additional_javascript'][] = 'js/thread-stats.js'; |
|||
*/ |
|||
if (active_page == 'thread') { |
|||
$(document).ready(function(){ |
|||
//check if page uses unique ID
|
|||
var IDsupport = ($('.poster_id').length > 0); |
|||
var thread_id = (document.location.pathname + document.location.search).split('/'); |
|||
thread_id = thread_id[thread_id.length -1].split('+')[0].split('-')[0].split('.')[0]; |
|||
|
|||
$('.boardlist.bottom, footer') |
|||
.first() |
|||
.before('<div id="thread_stats"></div>'); |
|||
var el = $('#thread_stats'); |
|||
el.prepend(_('Page')+' <span id="thread_stats_page">?</span>'); |
|||
if (IDsupport){ |
|||
el.prepend('<span id="thread_stats_uids">0</span> UIDs | '); |
|||
} |
|||
el.prepend('<span id="thread_stats_images">0</span> '+_('images')+' | '); |
|||
el.prepend('<span id="thread_stats_posts">0</span> '+_('replies')+' | '); |
|||
delete el; |
|||
function update_thread_stats(){ |
|||
var op = $('#thread_'+ thread_id +' > div.post.op:not(.post-hover):not(.inline)').first(); |
|||
var replies = $('#thread_'+ thread_id +' > div.post.reply:not(.post-hover):not(.inline)'); |
|||
// post count
|
|||
$('#thread_stats_posts').text(replies.length); |
|||
// image count
|
|||
$('#thread_stats_images').text(replies.filter(function(){ |
|||
return $(this).find('> .files').text().trim() != false; |
|||
}).length); |
|||
// unique ID count
|
|||
if (IDsupport) { |
|||
var opID = op.find('> .intro > .poster_id').text(); |
|||
var ids = {}; |
|||
replies.each(function(){ |
|||
var cur = $(this).find('> .intro > .poster_id'); |
|||
var curID = cur.text(); |
|||
if (ids[curID] === undefined) { |
|||
ids[curID] = 0; |
|||
} |
|||
ids[curID]++; |
|||
}); |
|||
if (ids[opID] === undefined) { |
|||
ids[opID] = 0; |
|||
} |
|||
ids[opID]++; |
|||
var cur = op.find('>.intro >.poster_id'); |
|||
cur.find('+.posts_by_id').remove(); |
|||
cur.after('<span class="posts_by_id"> ('+ ids[cur.text()] +')</span>'); |
|||
replies.each(function(){ |
|||
cur = $(this).find('>.intro >.poster_id'); |
|||
cur.find('+.posts_by_id').remove(); |
|||
cur.after('<span class="posts_by_id"> ('+ ids[cur.text()] +')</span>'); |
|||
}); |
|||
var size = function(obj) { |
|||
var size = 0, key; |
|||
for (key in obj) { |
|||
if (obj.hasOwnProperty(key)) size++; |
|||
} |
|||
return size; |
|||
}; |
|||
$('#thread_stats_uids').text(size(ids)); |
|||
} |
|||
var board_name = $('input[name="board"]').val(); |
|||
$.getJSON('//'+ document.location.host +'/'+ board_name +'/threads.json').success(function(data){ |
|||
var found, page = '???'; |
|||
for (var i=0;data[i];i++){ |
|||
var threads = data[i].threads; |
|||
for (var j=0; threads[j]; j++){ |
|||
if (parseInt(threads[j].no) == parseInt(thread_id)) { |
|||
page = data[i].page +1; |
|||
found = true; |
|||
break; |
|||
} |
|||
} |
|||
if (found) break; |
|||
} |
|||
$('#thread_stats_page').text(page); |
|||
if (!found) $('#thread_stats_page').css('color','red'); |
|||
else $('#thread_stats_page').css('color',''); |
|||
}); |
|||
} |
|||
// load the current page the thread is on.
|
|||
// uses ajax call so it gets loaded on a delay (depending on network resources available)
|
|||
var thread_stats_page_timer = setInterval(function(){ |
|||
var board_name = $('input[name="board"]').val(); |
|||
$.getJSON('//'+ document.location.host +'/'+ board_name +'/threads.json').success(function(data){ |
|||
var found, page = '???'; |
|||
for (var i=0;data[i];i++){ |
|||
var threads = data[i].threads; |
|||
for (var j=0; threads[j]; j++){ |
|||
if (parseInt(threads[j].no) == parseInt(thread_id)) { |
|||
page = data[i].page +1; |
|||
found = true; |
|||
break; |
|||
} |
|||
} |
|||
if (found) break; |
|||
} |
|||
$('#thread_stats_page').text(page); |
|||
if (!found) $('#thread_stats_page').css('color','red'); |
|||
else $('#thread_stats_page').css('color',''); |
|||
}); |
|||
},30000); |
|||
$('body').append('<style>.posts_by_id{display:none;}.poster_id:hover+.posts_by_id{display:initial}</style>'); |
|||
update_thread_stats(); |
|||
$('#update_thread').click(update_thread_stats); |
|||
$(document).on('new_post',update_thread_stats); |
|||
}); |
|||
} |
@ -1 +1 @@ |
|||
Subproject commit 2c272dffca0f3d7b7163bd82ba15629f54409278 |
|||
Subproject commit f11220032e7edb8349516d3751d3aaee0fd0de68 |
@ -0,0 +1,216 @@ |
|||
<?php |
|||
require_once("inc/functions.php"); |
|||
|
|||
if (!$config['smart_build']) { |
|||
die('You need to enable $config["smart_build"]'); |
|||
} |
|||
|
|||
$config['smart_build'] = false; // Let's disable it, so we can build the page for real |
|||
|
|||
function after_open_board() { global $config; |
|||
$config['smart_build'] = false; |
|||
}; |
|||
|
|||
function sb_board($b, $page = 1) { global $config, $build_pages; $page = (int)$page; |
|||
if ($page < 1) return false; |
|||
if (!openBoard($b)) return false; |
|||
if ($page > $config['max_pages']) return false; |
|||
$config['try_smarter'] = true; |
|||
$build_pages = array($page); |
|||
buildIndex("skip"); |
|||
return true; |
|||
} |
|||
|
|||
function sb_api_board($b, $page = 0) { $page = (int)$page; |
|||
return sb_board($b, $page + 1); |
|||
} |
|||
|
|||
function sb_thread($b, $thread, $slugcheck = false) { global $config; $thread = (int)$thread; |
|||
if ($thread < 1) return false; |
|||
|
|||
if (!preg_match('/^'.$config['board_regex'].'$/u', $b)) return false; |
|||
|
|||
if (Cache::get("thread_exists_".$b."_".$thread) == "no") return false; |
|||
|
|||
$query = prepare(sprintf("SELECT MAX(`id`) AS `max` FROM ``posts_%s``", $b)); |
|||
if (!$query->execute()) return false; |
|||
|
|||
$s = $query->fetch(PDO::FETCH_ASSOC); |
|||
$max = $s['max']; |
|||
|
|||
if ($thread > $max) return false; |
|||
|
|||
$query = prepare(sprintf("SELECT `id` FROM ``posts_%s`` WHERE `id` = :id AND `thread` IS NULL", $b)); |
|||
$query->bindValue(':id', $thread); |
|||
|
|||
if (!$query->execute() || !$query->fetch(PDO::FETCH_ASSOC) ) { |
|||
Cache::set("thread_exists_".$b."_".$thread, "no"); |
|||
return false; |
|||
} |
|||
|
|||
if ($slugcheck && $config['slugify']) { |
|||
global $request; |
|||
|
|||
$link = link_for(array("id" => $thread), $slugcheck === 50, array("uri" => $b)); |
|||
$link = "/".$b."/".$config['dir']['res'].$link; |
|||
|
|||
if ($link != $request) { |
|||
header("Location: $link", true, 301); |
|||
die(); |
|||
} |
|||
} |
|||
|
|||
if ($slugcheck == 50) { // Should we really generate +50 page? Maybe there are not enough posts anyway |
|||
global $request; |
|||
$r = str_replace("+50", "", $request); |
|||
$r = substr($r, 1); // Cut the slash |
|||
|
|||
if (file_exists($r)) return false; |
|||
} |
|||
|
|||
if (!openBoard($b)) return false; |
|||
buildThread($thread); |
|||
return true; |
|||
} |
|||
|
|||
function sb_thread_slugcheck($b, $thread) { |
|||
return sb_thread($b, $thread, true); |
|||
} |
|||
function sb_thread_slugcheck50($b, $thread) { |
|||
return sb_thread($b, $thread, 50); |
|||
} |
|||
|
|||
function sb_api($b) { global $config, $build_pages; |
|||
if (!openBoard($b)) return false; |
|||
$config['try_smarter'] = true; |
|||
$build_pages = array(-1); |
|||
buildIndex(); |
|||
return true; |
|||
} |
|||
|
|||
function sb_ukko() { |
|||
rebuildTheme("ukko", "post-thread"); |
|||
return true; |
|||
} |
|||
|
|||
function sb_catalog($b) { |
|||
if (!openBoard($b)) return false; |
|||
|
|||
rebuildTheme("catalog", "post-thread", $b); |
|||
return true; |
|||
} |
|||
|
|||
function sb_recent() { |
|||
rebuildTheme("recent", "post-thread"); |
|||
return true; |
|||
} |
|||
|
|||
function sb_sitemap() { |
|||
rebuildTheme("sitemap", "all"); |
|||
return true; |
|||
} |
|||
|
|||
$entrypoints = array(); |
|||
|
|||
$entrypoints['/%b/'] = 'sb_board'; |
|||
$entrypoints['/%b/'.$config['file_index']] = 'sb_board'; |
|||
$entrypoints['/%b/'.$config['file_page']] = 'sb_board'; |
|||
$entrypoints['/%b/%d.json'] = 'sb_api_board'; |
|||
if ($config['api']['enabled']) { |
|||
$entrypoints['/%b/threads.json'] = 'sb_api'; |
|||
$entrypoints['/%b/catalog.json'] = 'sb_api'; |
|||
} |
|||
|
|||
$entrypoints['/%b/'.$config['dir']['res'].$config['file_page']] = 'sb_thread_slugcheck'; |
|||
$entrypoints['/%b/'.$config['dir']['res'].$config['file_page50']] = 'sb_thread_slugcheck50'; |
|||
if ($config['slugify']) { |
|||
$entrypoints['/%b/'.$config['dir']['res'].$config['file_page_slug']] = 'sb_thread_slugcheck'; |
|||
$entrypoints['/%b/'.$config['dir']['res'].$config['file_page50_slug']] = 'sb_thread_slugcheck50'; |
|||
} |
|||
if ($config['api']['enabled']) { |
|||
$entrypoints['/%b/'.$config['dir']['res'].'%d.json'] = 'sb_thread'; |
|||
} |
|||
|
|||
$entrypoints['/*/'] = 'sb_ukko'; |
|||
$entrypoints['/*/index.html'] = 'sb_ukko'; |
|||
$entrypoints['/recent.html'] = 'sb_recent'; |
|||
$entrypoints['/%b/catalog.html'] = 'sb_catalog'; |
|||
$entrypoints['/sitemap.xml'] = 'sb_sitemap'; |
|||
|
|||
$reached = false; |
|||
|
|||
$request = $_SERVER['REQUEST_URI']; |
|||
list($request) = explode('?', $request); |
|||
|
|||
foreach ($entrypoints as $id => $fun) { |
|||
$id = '@^' . preg_quote($id, '@') . '$@u'; |
|||
|
|||
$id = str_replace('%b', '('.$config['board_regex'].')', $id); |
|||
$id = str_replace('%d', '([0-9]+)', $id); |
|||
$id = str_replace('%s', '[a-zA-Z0-9-]+', $id); |
|||
|
|||
$matches = null; |
|||
|
|||
if (preg_match ($id, $request, $matches)) { |
|||
array_shift($matches); |
|||
|
|||
$reached = call_user_func_array($fun, $matches); |
|||
|
|||
break; |
|||
} |
|||
} |
|||
|
|||
function die_404() { global $config; |
|||
if (!$config['page_404']) { |
|||
header("HTTP/1.1 404 Not Found"); |
|||
header("Status: 404 Not Found"); |
|||
echo "<h1>404 Not Found</h1><p>Page doesn't exist<hr><address>vichan</address>"; |
|||
} |
|||
else { |
|||
header("Location: ".$config['page_404']); |
|||
} |
|||
header("X-Accel-Expires: 120"); |
|||
die(); |
|||
} |
|||
|
|||
if ($reached) { |
|||
if ($request[strlen($request)-1] == '/') { |
|||
$request .= 'index.html'; |
|||
} |
|||
$request = '.'.$request; |
|||
|
|||
if (!file_exists($request)) { |
|||
die_404(); |
|||
} |
|||
|
|||
header("HTTP/1.1 200 OK"); |
|||
header("Status: 200 OK"); |
|||
if (preg_match('/\.json$/', $request)) { |
|||
header("Content-Type", "application/json"); |
|||
} |
|||
elseif (preg_match('/\.js$/', $request)) { |
|||
header("Content-Type", "text/javascript; charset=utf-8"); |
|||
} |
|||
elseif (preg_match('/\.xml$/', $request)) { |
|||
header("Content-Type", "application/xml"); |
|||
} |
|||
else { |
|||
header("Content-Type", "text/html; charset=utf-8"); |
|||
} |
|||
header("Cache-Control: public, nocache, no-cache, max-age=0, must-revalidate"); |
|||
header("Expires: Fri, 22 Feb 1991 06:00:00 GMT"); |
|||
header("Last-Modified: ".date('r', filemtime($request))); |
|||
|
|||
//if (isset ($_SERVER['HTTP_ACCEPT_ENCODING']) && preg_match('/gzip/', $_SERVER['HTTP_ACCEPT_ENCODING']) && file_exists($request.".gz")) { |
|||
// header("Content-Encoding: gzip"); |
|||
// $file = fopen($request.".gz", 'r'); |
|||
//} |
|||
//else { |
|||
$file = fopen($request, 'r'); |
|||
//} |
|||
fpassthru($file); |
|||
fclose($file); |
|||
} |
|||
else { |
|||
die_404(); |
|||
} |
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 248 KiB After Width: | Height: | Size: 348 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,34 @@ |
|||
// Animated Icons |
|||
// -------------------------- |
|||
|
|||
.@{fa-css-prefix}-spin { |
|||
-webkit-animation: fa-spin 2s infinite linear; |
|||
animation: fa-spin 2s infinite linear; |
|||
} |
|||
|
|||
.@{fa-css-prefix}-pulse { |
|||
-webkit-animation: fa-spin 1s infinite steps(8); |
|||
animation: fa-spin 1s infinite steps(8); |
|||
} |
|||
|
|||
@-webkit-keyframes fa-spin { |
|||
0% { |
|||
-webkit-transform: rotate(0deg); |
|||
transform: rotate(0deg); |
|||
} |
|||
100% { |
|||
-webkit-transform: rotate(359deg); |
|||
transform: rotate(359deg); |
|||
} |
|||
} |
|||
|
|||
@keyframes fa-spin { |
|||
0% { |
|||
-webkit-transform: rotate(0deg); |
|||
transform: rotate(0deg); |
|||
} |
|||
100% { |
|||
-webkit-transform: rotate(359deg); |
|||
transform: rotate(359deg); |
|||
} |
|||
} |
@ -1,20 +1,26 @@ |
|||
// Mixins |
|||
// -------------------------- |
|||
|
|||
.fa-icon() { |
|||
display: inline-block; |
|||
font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration |
|||
font-size: inherit; // can't have font-size inherit on line above, so need to override |
|||
text-rendering: auto; // optimizelegibility throws things off #1094 |
|||
-webkit-font-smoothing: antialiased; |
|||
-moz-osx-font-smoothing: grayscale; |
|||
|
|||
} |
|||
|
|||
.fa-icon-rotate(@degrees, @rotation) { |
|||
filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=@rotation); |
|||
-webkit-transform: rotate(@degrees); |
|||
-moz-transform: rotate(@degrees); |
|||
-ms-transform: rotate(@degrees); |
|||
-o-transform: rotate(@degrees); |
|||
transform: rotate(@degrees); |
|||
} |
|||
|
|||
.fa-icon-flip(@horiz, @vert, @rotation) { |
|||
filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=@rotation, mirror=1); |
|||
-webkit-transform: scale(@horiz, @vert); |
|||
-moz-transform: scale(@horiz, @vert); |
|||
-ms-transform: scale(@horiz, @vert); |
|||
-o-transform: scale(@horiz, @vert); |
|||
transform: scale(@horiz, @vert); |
|||
} |
|||
|
@ -0,0 +1,34 @@ |
|||
// Spinning Icons |
|||
// -------------------------- |
|||
|
|||
.#{$fa-css-prefix}-spin { |
|||
-webkit-animation: fa-spin 2s infinite linear; |
|||
animation: fa-spin 2s infinite linear; |
|||
} |
|||
|
|||
.#{$fa-css-prefix}-pulse { |
|||
-webkit-animation: fa-spin 1s infinite steps(8); |
|||
animation: fa-spin 1s infinite steps(8); |
|||
} |
|||
|
|||
@-webkit-keyframes fa-spin { |
|||
0% { |
|||
-webkit-transform: rotate(0deg); |
|||
transform: rotate(0deg); |
|||
} |
|||
100% { |
|||
-webkit-transform: rotate(359deg); |
|||
transform: rotate(359deg); |
|||
} |
|||
} |
|||
|
|||
@keyframes fa-spin { |
|||
0% { |
|||
-webkit-transform: rotate(0deg); |
|||
transform: rotate(0deg); |
|||
} |
|||
100% { |
|||
-webkit-transform: rotate(359deg); |
|||
transform: rotate(359deg); |
|||
} |
|||
} |
@ -1,20 +1,26 @@ |
|||
// Mixins |
|||
// -------------------------- |
|||
|
|||
@mixin fa-icon() { |
|||
display: inline-block; |
|||
font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration |
|||
font-size: inherit; // can't have font-size inherit on line above, so need to override |
|||
text-rendering: auto; // optimizelegibility throws things off #1094 |
|||
-webkit-font-smoothing: antialiased; |
|||
-moz-osx-font-smoothing: grayscale; |
|||
|
|||
} |
|||
|
|||
@mixin fa-icon-rotate($degrees, $rotation) { |
|||
filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}); |
|||
-webkit-transform: rotate($degrees); |
|||
-moz-transform: rotate($degrees); |
|||
-ms-transform: rotate($degrees); |
|||
-o-transform: rotate($degrees); |
|||
transform: rotate($degrees); |
|||
} |
|||
|
|||
@mixin fa-icon-flip($horiz, $vert, $rotation) { |
|||
filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}); |
|||
-webkit-transform: scale($horiz, $vert); |
|||
-moz-transform: scale($horiz, $vert); |
|||
-ms-transform: scale($horiz, $vert); |
|||
-o-transform: scale($horiz, $vert); |
|||
transform: scale($horiz, $vert); |
|||
} |
|||
|
@ -0,0 +1,15 @@ |
|||
<table class='fileboard'> |
|||
<tr> |
|||
<th> <!-- checkbox --> |
|||
<th>{% trans %}No.{% endtrans %} |
|||
<th>{% trans %}Name{% endtrans %} |
|||
<th>{% trans %}File{% endtrans %} |
|||
<th>{% trans %}Tag{% endtrans %} |
|||
<th>{% trans %}Subject{% endtrans %} |
|||
<th>{% trans %}Size{% endtrans %} |
|||
<th>{% trans %}Date{% endtrans %} |
|||
<th>{% trans %}Replies{% endtrans %} |
|||
<th> <!-- reply --> |
|||
</tr> |
|||
{{ body }} |
|||
</table> |
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue