forked from flanchan/doushio
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
654 lines
15 KiB
654 lines
15 KiB
var optSpecs = [];
|
|
var nashi = {opts: []}, inputMinSize = 300, fullWidthExpansion = false;
|
|
var shortcutKeys = {};
|
|
|
|
function extract_num(q) {
|
|
return parseInt(q.attr('id'), 10);
|
|
}
|
|
|
|
function parent_post($el) {
|
|
return $el.closest('article, section');
|
|
}
|
|
|
|
function parent_model($el) {
|
|
var $a = parent_post($el);
|
|
var op = extract_num($a);
|
|
if (!op)
|
|
return null;
|
|
if ($a.is('section'))
|
|
return Threads.get(op);
|
|
var $s = $a.parent('section');
|
|
if (!$s.length) {
|
|
// when we have better hover/inline expansion we will have to
|
|
// deal with this, probably by setting data-op on the post
|
|
console.warn($a, "'s parent is not thread?!");
|
|
return null;
|
|
}
|
|
var num = op;
|
|
op = extract_num($s);
|
|
return Threads.lookup(num, op);
|
|
}
|
|
|
|
(function () {
|
|
|
|
/* OPTIONS LIST */
|
|
optSpecs.push(option_inline_expansion);
|
|
if (window.devicePixelRatio > 1)
|
|
optSpecs.push(option_high_res);
|
|
optSpecs.push(option_thumbs);
|
|
optSpecs.push(option_autocomplete);
|
|
optSpecs.push(option_backlinks);
|
|
optSpecs.push(option_reply_at_right);
|
|
optSpecs.push(option_theme);
|
|
optSpecs.push(option_last_n);
|
|
|
|
|
|
_.defaults(options, {
|
|
lastn: config.THREAD_LAST_N,
|
|
inlinefit: 'width',
|
|
});
|
|
options = new Backbone.Model(options);
|
|
|
|
|
|
nashi.upload = !!$('<input type="file"/>').prop('disabled');
|
|
|
|
if (window.screen && screen.width <= 480) {
|
|
inputMinSize = 50;
|
|
fullWidthExpansion = true;
|
|
}
|
|
|
|
function load_ident() {
|
|
try {
|
|
var id = JSON.parse(localStorage.ident);
|
|
if (id.name)
|
|
$name.val(id.name);
|
|
if (id.email)
|
|
$email.val(id.email);
|
|
}
|
|
catch (e) {}
|
|
}
|
|
|
|
function save_ident() {
|
|
try {
|
|
var name = $name.val(), email = $email.val();
|
|
if (email == 'misaki') {
|
|
$email.val('');
|
|
$.getScript(mediaURL + 'js/login.js');
|
|
email = false;
|
|
}
|
|
else if (is_sage(email) && !is_noko(email))
|
|
email = false;
|
|
var id = {};
|
|
if (name || email) {
|
|
if (name)
|
|
id.name = name;
|
|
if (email)
|
|
id.email = email;
|
|
localStorage.setItem('ident', JSON.stringify(id));
|
|
}
|
|
else
|
|
localStorage.removeItem('ident');
|
|
}
|
|
catch (e) {}
|
|
}
|
|
|
|
options.on('change', function () {
|
|
try {
|
|
localStorage.options = JSON.stringify(options);
|
|
}
|
|
catch (e) {}
|
|
});
|
|
|
|
/* LAST N CONFIG */
|
|
|
|
function option_last_n(n) {
|
|
if (!reasonable_last_n(n))
|
|
return;
|
|
$.cookie('lastn', n);
|
|
// should really load/hide posts as appropriate
|
|
}
|
|
option_last_n.id = 'lastn';
|
|
option_last_n.label = '[Last #]';
|
|
option_last_n.type = 'positive';
|
|
|
|
oneeSama.lastN = options.get('lastn');
|
|
options.on('change:lastn', function (model, lastN) {
|
|
oneeSama.lastN = lastN;
|
|
});
|
|
|
|
/* THEMES */
|
|
|
|
var themes = [
|
|
'moe',
|
|
'gar',
|
|
'mawaru',
|
|
'moon',
|
|
'ashita',
|
|
'console',
|
|
'tea',
|
|
'higan',
|
|
];
|
|
|
|
function option_theme(theme) {
|
|
if (theme) {
|
|
var css = theme + '.css?v=' + themeVersion;
|
|
$('#theme').attr('href', mediaURL + 'css/' + css);
|
|
}
|
|
}
|
|
option_theme.id = 'board.$BOARD.theme';
|
|
option_theme.label = 'Theme';
|
|
option_theme.type = themes;
|
|
|
|
/* THUMBNAIL OPTIONS */
|
|
|
|
var revealSetup = false;
|
|
|
|
function option_thumbs(type) {
|
|
$.cookie('thumb', type);
|
|
// really ought to apply the style immediately
|
|
// need pinky/mid distinction in the model to do properly
|
|
oneeSama.thumbStyle = type;
|
|
var hide = type == 'hide';
|
|
if (hide)
|
|
$('img').hide();
|
|
else
|
|
$('img').show();
|
|
|
|
if (hide && !revealSetup)
|
|
$DOC.on('click', 'article', reveal_thumbnail);
|
|
else if (!hide && revealSetup)
|
|
$DOC.off('click', 'article', reveal_thumbnail);
|
|
revealSetup = hide;
|
|
}
|
|
option_thumbs.id = 'board.$BOARD.thumbs';
|
|
option_thumbs.label = 'Thumbnails';
|
|
option_thumbs.type = thumbStyles;
|
|
|
|
/* Alt-click a post to reveal its thumbnail if hidden */
|
|
function reveal_thumbnail(event) {
|
|
if (!event.altKey)
|
|
return;
|
|
var $article = $(event.target);
|
|
var $img = $article.find('img');
|
|
if ($img.length) {
|
|
with_dom(function () { $img.show(); });
|
|
return false;
|
|
}
|
|
|
|
/* look up the image info and make the thumbnail */
|
|
var thread = Threads.get(extract_num($article.closest('section')));
|
|
if (!thread)
|
|
return;
|
|
var post = thread.get('replies').get(extract_num($article));
|
|
if (!post)
|
|
return;
|
|
var info = post.get('image');
|
|
if (!info)
|
|
return;
|
|
|
|
with_dom(function () {
|
|
var img = oneeSama.gazou_img(info, false);
|
|
var $img = $.parseHTML(flatten(img.html).join(''));
|
|
$article.find('figcaption').after($img);
|
|
});
|
|
return false;
|
|
}
|
|
|
|
/* REPLY AT RIGHT */
|
|
|
|
function option_reply_at_right(r) {
|
|
if (r)
|
|
$('<style/>', {
|
|
id: 'reply-at-right',
|
|
text: 'aside { margin: -26px 0 2px auto; }',
|
|
}).appendTo('head');
|
|
else
|
|
$('#reply-at-right').remove();
|
|
}
|
|
option_reply_at_right.id = 'replyright';
|
|
option_reply_at_right.label = '[Reply] at right';
|
|
option_reply_at_right.type = 'checkbox';
|
|
|
|
/* AUTOCOMPLETE */
|
|
|
|
function option_autocomplete(b) {
|
|
if (postForm)
|
|
postForm.model.set('autocomplete', b);
|
|
}
|
|
option_autocomplete.id = 'autocomplete';
|
|
option_autocomplete.label = 'Auto-complete';
|
|
option_autocomplete.type = 'checkbox';
|
|
|
|
/* BACKLINKS */
|
|
|
|
function option_backlinks(b) {
|
|
if (b)
|
|
$('small').remove();
|
|
else
|
|
show_backlinks();
|
|
}
|
|
option_backlinks.id = 'nobacklinks';
|
|
option_backlinks.label = 'Backlinks';
|
|
option_backlinks.type = 'revcheckbox';
|
|
|
|
function show_backlinks() {
|
|
if (load_thread_backlinks) {
|
|
with_dom(function () {
|
|
$('section').each(function () {
|
|
load_thread_backlinks($(this));
|
|
});
|
|
});
|
|
load_thread_backlinks = null;
|
|
return;
|
|
}
|
|
|
|
Threads.each(function (thread) {
|
|
thread.get('replies').each(function (reply) {
|
|
if (reply.has('backlinks'))
|
|
reply.trigger('change:backlinks');
|
|
});
|
|
});
|
|
}
|
|
|
|
var load_thread_backlinks = function ($section) {
|
|
var op = extract_num($section);
|
|
var replies = Threads.get(op).get('replies');
|
|
$section.find('blockquote a').each(function () {
|
|
var $a = $(this);
|
|
var m = $a.attr('href').match(/^\d*#(\d+)$/);
|
|
if (!m)
|
|
return;
|
|
var destId = parseInt(m[1], 10);
|
|
if (!replies.get(destId)) // local backlinks only for now
|
|
return;
|
|
var src = replies.get(extract_num(parent_post($a)));
|
|
if (!src)
|
|
return;
|
|
var update = {};
|
|
update[destId] = op;
|
|
add_post_links(src, update, op);
|
|
});
|
|
};
|
|
|
|
/* INLINE EXPANSION */
|
|
|
|
function option_inline_expansion() {
|
|
/* TODO: do it live */
|
|
}
|
|
option_inline_expansion.id = 'inlinefit';
|
|
option_inline_expansion.label = 'Expansion';
|
|
option_inline_expansion.type = ['none', 'full', 'width', 'height', 'both'];
|
|
option_inline_expansion.labels = ['no', 'full-size', 'fit to width',
|
|
'fit to height', 'fit to both'];
|
|
|
|
function option_high_res() {
|
|
}
|
|
option_high_res.id = 'nohighres';
|
|
option_high_res.label = 'High-res expansions';
|
|
option_high_res.type = 'revcheckbox';
|
|
|
|
$DOC.on('mouseup', 'img, video', function (event) {
|
|
/* Bypass expansion for non-left mouse clicks */
|
|
if (options.get('inlinefit') != 'none' && event.which > 1) {
|
|
var img = $(this);
|
|
img.data('skipExpand', true);
|
|
setTimeout(function () {
|
|
img.removeData('skipExpand');
|
|
}, 100);
|
|
}
|
|
});
|
|
|
|
$DOC.on('click', 'img, video', function (event) {
|
|
if (options.get('inlinefit') != 'none') {
|
|
var $target = $(this);
|
|
if (!$target.data('skipExpand'))
|
|
toggle_expansion($target, event);
|
|
}
|
|
});
|
|
|
|
function toggle_expansion(img, event) {
|
|
var href = img.parent().attr('href');
|
|
if (/^\.\.\/outbound\//.test(href))
|
|
return;
|
|
if (event.metaKey)
|
|
return;
|
|
event.preventDefault();
|
|
var expand = !img.data('thumbSrc');
|
|
img.closest('article').toggleClass('expanded', expand);
|
|
var $imgs = img;
|
|
if (THREAD && (event.altKey || event.shiftKey)) {
|
|
var post = img.closest('article');
|
|
if (post.length)
|
|
$imgs = post.nextAll(':has(img):lt(4)').andSelf();
|
|
else
|
|
$imgs = img.closest('section').children(
|
|
':has(img):lt(5)');
|
|
$imgs = $imgs.find('img');
|
|
}
|
|
|
|
with_dom(function () {
|
|
$imgs.each(function () {
|
|
var $img = $(this);
|
|
if (expand)
|
|
expand_image($img);
|
|
else {
|
|
contract_image($img, event);
|
|
event = null; // de-zoom to first image only
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function contract_image($img, event) {
|
|
var thumb = $img.data('thumbSrc');
|
|
if (!thumb)
|
|
return;
|
|
// try to keep the thumbnail in-window for large images
|
|
var h = $img.height();
|
|
var th = parseInt($img.data('thumbHeight'), 10);
|
|
if (event) {
|
|
var y = $img.offset().top, t = $(window).scrollTop();
|
|
if (y < t && th < h)
|
|
window.scrollBy(0, Math.max(th - h,
|
|
y - t - event.clientY + th/2));
|
|
}
|
|
if (fullWidthExpansion)
|
|
contract_full_width(parent_post($img));
|
|
$img.replaceWith($('<img>')
|
|
.width($img.data('thumbWidth')).height(th)
|
|
.attr('src', thumb));
|
|
}
|
|
|
|
function expand_image($img) {
|
|
if ($img.data('thumbSrc'))
|
|
return;
|
|
var a = $img.parent();
|
|
var href = a.attr('href');
|
|
if (!href)
|
|
return;
|
|
var cap = a.siblings('figcaption').text();
|
|
var dims = cap.match(/(\d+)x(\d+)/);
|
|
var video = /^Video/.test(cap);
|
|
if (!dims)
|
|
return;
|
|
var tw = $img.width(), th = $img.height();
|
|
var w = parseInt(dims[1], 10), h = parseInt(dims[2], 10);
|
|
// if this is a high-density screen, reduce image size appropriately
|
|
var r = window.devicePixelRatio;
|
|
if (!options.get('nohighres') && !video && r && r > 1) {
|
|
var min = 1000;
|
|
if ((w > min || h > min) && w/r > tw && h/r > th) {
|
|
w /= r;
|
|
h /= r;
|
|
}
|
|
}
|
|
|
|
$img.remove();
|
|
$img = $(video ? '<video>' : '<img>', {
|
|
src: href,
|
|
width: w, height: h,
|
|
data: {
|
|
thumbWidth: tw, thumbHeight: th,
|
|
thumbSrc: $img.attr('src'),
|
|
},
|
|
prop: video ? {autoplay: true, loop: true} : {},
|
|
}).appendTo(a);
|
|
|
|
var fit = options.get('inlinefit');
|
|
if (fit != 'none') {
|
|
var both = fit == 'both';
|
|
fit_to_window($img, w, h, both || fit == 'width',
|
|
both || fit == 'height');
|
|
}
|
|
}
|
|
|
|
function fit_to_window($img, w, h, widthFlag, heightFlag) {
|
|
var $post = parent_post($img);
|
|
var overX = 0, overY = 0;
|
|
if (widthFlag) {
|
|
var innerWidth = $(window).innerWidth();
|
|
var rect = $post.length && $post[0].getBoundingClientRect();
|
|
if ($post.is('article')) {
|
|
if (fullWidthExpansion && w > innerWidth) {
|
|
overX = w - innerWidth;
|
|
expand_full_width($img, $post, rect);
|
|
heightFlag = false;
|
|
}
|
|
else {
|
|
overX = rect.right - innerWidth;
|
|
|
|
// right-side posts might fall off the left side of the page
|
|
// account for them + left margin
|
|
if (rect.left < 0) {
|
|
// there has to be a better way...
|
|
function m($el) {
|
|
var m = parseInt($el.css('marginLeft'), 10);
|
|
var p = parseInt($el.css('paddingLeft'), 10);
|
|
return (m || 0) + (p || 0);
|
|
}
|
|
var $sec = $post.closest('section');
|
|
var margin = m($('body')) + m($post) + m($sec) + 13;
|
|
overX -= rect.left - margin;
|
|
}
|
|
}
|
|
}
|
|
else if ($post.is('section'))
|
|
overX = w - (innerWidth - rect.left*2);
|
|
}
|
|
if (heightFlag) {
|
|
overY = h - ($(window).innerHeight() - 20);
|
|
}
|
|
|
|
var aspect = h / w;
|
|
var newW, newH;
|
|
if (overX > 0) {
|
|
newW = w - overX;
|
|
newH = aspect * newW;
|
|
}
|
|
if (overY > 0) {
|
|
// might have to fit to both width and height
|
|
var maybeH = h - overY;
|
|
if (!newH || maybeH < newH) {
|
|
newH = maybeH;
|
|
newW = newH / aspect;
|
|
}
|
|
}
|
|
|
|
if (newW > 50 && newH > 50)
|
|
$img.width(newW).height(newH);
|
|
}
|
|
|
|
function expand_full_width($img, $post, rect) {
|
|
if ($post.hasClass('floop')) {
|
|
$post.removeClass('floop');
|
|
$post.data('crouching-floop', true);
|
|
}
|
|
var img = $img[0].getBoundingClientRect();
|
|
$img.css('margin-left', -img.left + 'px');
|
|
var over = rect.right - img.right;
|
|
if (over > 0) {
|
|
$post.css({
|
|
'margin-right': -over+'px',
|
|
'padding-right': 0,
|
|
'border-right': 'none',
|
|
});
|
|
}
|
|
}
|
|
|
|
function contract_full_width($post) {
|
|
if ($post.css('margin-right')[0] == '-') {
|
|
$post.css({
|
|
'margin-right': '',
|
|
'padding-right': '',
|
|
'border-right': '',
|
|
});
|
|
}
|
|
if ($post.data('crouching-floop')) {
|
|
$post.addClass('floop');
|
|
$post.removeData('crouching-floop');
|
|
}
|
|
}
|
|
|
|
/* SHORTCUT KEYS */
|
|
|
|
var shortcuts = [
|
|
{label: 'New post', name: 'new', which: 78},
|
|
{label: 'Image spoiler', name: 'togglespoiler', which: 73},
|
|
{label: 'Finish post', name: 'done', which: 83},
|
|
{label: 'Flip side', name: 'flip', which: 70},
|
|
];
|
|
|
|
function toggle_shortcuts(event) {
|
|
event.preventDefault();
|
|
var $shortcuts = $('#shortcuts');
|
|
if ($shortcuts.length)
|
|
return $shortcuts.remove();
|
|
$shortcuts = $('<div/>', {
|
|
id: 'shortcuts',
|
|
click: select_shortcut,
|
|
keyup: change_shortcut,
|
|
});
|
|
shortcuts.forEach(function (s) {
|
|
var value = String.fromCharCode(shortcutKeys[s.name]);
|
|
var $label = $('<label>', {text: s.label});
|
|
$('<input>', {
|
|
id: s.name, maxlength: 1, val: value,
|
|
}).prependTo($label);
|
|
$label.prepend(document.createTextNode('Alt+'));
|
|
$shortcuts.append($label, '<br>');
|
|
});
|
|
$shortcuts.appendTo('#options-panel');
|
|
}
|
|
|
|
function select_shortcut(event) {
|
|
if ($(event.target).is('input'))
|
|
$(event.target).val('');
|
|
}
|
|
|
|
function change_shortcut(event) {
|
|
if (event.which == 13)
|
|
return false;
|
|
var $input = $(event.target);
|
|
var letter = $input.val();
|
|
if (!(/^[a-z]$/i.exec(letter)))
|
|
return;
|
|
var which = letter.toUpperCase().charCodeAt(0);
|
|
var name = $input.attr('id');
|
|
if (!(name in shortcutKeys))
|
|
return;
|
|
shortcutKeys[name] = which;
|
|
|
|
var shorts = options.get('shortcuts')
|
|
if (!_.isObject(shorts)) {
|
|
shorts = {};
|
|
shorts[name] = which;
|
|
options.set('shortcuts', shorts);
|
|
}
|
|
else {
|
|
shorts[name] = which;
|
|
options.trigger('change'); // force save
|
|
}
|
|
|
|
$input.blur();
|
|
}
|
|
|
|
_.defer(function () {
|
|
load_ident();
|
|
var save = _.debounce(save_ident, 1000);
|
|
function prop() {
|
|
if (postForm)
|
|
postForm.propagate_ident();
|
|
save();
|
|
}
|
|
$name.input(prop);
|
|
$email.input(prop);
|
|
|
|
optSpecs.forEach(function (spec) {
|
|
spec.id = spec.id.replace(/\$BOARD/g, BOARD);
|
|
});
|
|
|
|
$('<a id="options">Options</a>').click(function () {
|
|
var $opts = $('#options-panel');
|
|
if (!$opts.length)
|
|
$opts = make_options_panel().appendTo('body');
|
|
if ($opts.is(':hidden'))
|
|
oneeSama.trigger('renderOptions', $opts);
|
|
$opts.toggle('fast');
|
|
}).insertAfter('#sync');
|
|
|
|
optSpecs.forEach(function (spec) {
|
|
spec(options.get(spec.id));
|
|
});
|
|
|
|
var prefs = options.get('shortcuts') || {};
|
|
shortcuts.forEach(function (s) {
|
|
shortcutKeys[s.name] = prefs[s.name] || s.which;
|
|
});
|
|
});
|
|
|
|
function make_options_panel() {
|
|
var $opts = $('<div/>', {"class": 'modal', id: 'options-panel'});
|
|
$opts.change(function (event) {
|
|
var $o = $(event.target), id = $o.attr('id'), val;
|
|
var spec = _.find(optSpecs, function (s) {
|
|
return s.id == id;
|
|
});
|
|
if (!spec)
|
|
return;
|
|
if (spec.type == 'checkbox')
|
|
val = !!$o.prop('checked');
|
|
else if (spec.type == 'revcheckbox')
|
|
val = !$o.prop('checked');
|
|
else if (spec.type == 'positive')
|
|
val = Math.max(parseInt($o.val(), 10), 1);
|
|
else
|
|
val = $o.val();
|
|
options.set(id, val);
|
|
with_dom(function () {
|
|
spec(val);
|
|
});
|
|
});
|
|
optSpecs.forEach(function (spec) {
|
|
var id = spec.id;
|
|
if (nashi.opts.indexOf(id) >= 0)
|
|
return;
|
|
var val = options.get(id), $input, type = spec.type;
|
|
if (type == 'checkbox' || type == 'revcheckbox') {
|
|
var b = (type == 'revcheckbox') ? !val : val;
|
|
$input = $('<input type="checkbox" />')
|
|
.prop('checked', b ? 'checked' : null);
|
|
}
|
|
else if (type == 'positive') {
|
|
$input = $('<input />', {
|
|
width: '4em',
|
|
maxlength: 4,
|
|
val: val,
|
|
});
|
|
}
|
|
else if (type instanceof Array) {
|
|
$input = $('<select/>');
|
|
var labels = spec.labels || {};
|
|
type.forEach(function (item, i) {
|
|
var label = labels[i] || item;
|
|
$('<option/>')
|
|
.text(label).val(item)
|
|
.appendTo($input);
|
|
});
|
|
if (type.indexOf(val) >= 0)
|
|
$input.val(val);
|
|
}
|
|
var $label = $('<label/>').attr('for', id).text(spec.label);
|
|
$opts.append($input.attr('id', id), ' ', $label, '<br>');
|
|
});
|
|
if (!nashi.shortcuts) {
|
|
$opts.append($('<a/>', {
|
|
href: '#', text: 'Shortcuts',
|
|
click: toggle_shortcuts,
|
|
}));
|
|
}
|
|
oneeSama.trigger('initOptions', $opts);
|
|
return $opts.hide();
|
|
}
|
|
|
|
})();
|