Last active
July 25, 2017 11:29
-
-
Save Juribiyan/6243e9c9f368bb97a13d61d1bad46efe to your computer and use it in GitHub Desktop.
0chan-utilities 1.0 alpha 2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name 0chan Utilities alpha | |
// @namespace http://0chan.hk/userjs | |
// @version 1.0.7 | |
// @description Y ur mom succ? | |
// @author Snivy [0xf330f91f] | |
// @match https://0chan.hk/* | |
// @grant none | |
// @icon https://raw.githubusercontent.com/Juribiyan/0chan-utilities/master/icon.png | |
// ==/UserScript== | |
/*'use strict';*/ // can't do because of eval() | |
const icons = | |
`<svg style="position: absolute; width: 0; height: 0; overflow: hidden;" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | |
<defs> | |
<symbol id="i-logo" viewBox="0 0 32 32"> | |
<path d="M19.454 21.933c-0.59 0.339-0.39 1.151-0.322 1.371 0.011 0.033-0.006 0.068-0.038 0.080-0.222 0.092-0.448 0.174-0.678 0.245-0.032 0.010-0.067-0.005-0.080-0.037-0.092-0.213-0.464-0.962-1.136-0.845-0.672 0.118-0.763 0.948-0.776 1.18-0.001 0.034-0.030 0.061-0.064 0.063-0.24 0.011-0.481 0.011-0.721 0-0.034-0.001-0.061-0.028-0.064-0.063-0.013-0.232-0.104-1.062-0.776-1.18-0.671-0.117-1.044 0.633-1.135 0.845-0.014 0.032-0.049 0.047-0.081 0.037-0.229-0.071-0.456-0.152-0.678-0.245-0.032-0.013-0.049-0.047-0.038-0.080 0.067-0.221 0.268-1.032-0.322-1.371-0.591-0.338-1.2 0.241-1.359 0.41-0.024 0.025-0.063 0.027-0.090 0.007-0.191-0.145-0.376-0.299-0.553-0.459-0.025-0.024-0.028-0.061-0.008-0.089 0.14-0.185 0.61-0.88 0.17-1.398-0.438-0.517-1.209-0.181-1.417-0.076-0.031 0.015-0.068 0.005-0.087-0.024-0.13-0.201-0.25-0.407-0.36-0.62-0.017-0.029-0.006-0.066 0.022-0.085 0.196-0.126 0.875-0.621 0.643-1.257-0.234-0.636-1.075-0.58-1.306-0.553-0.034 0.005-0.066-0.018-0.073-0.051-0.052-0.233-0.095-0.468-0.125-0.704-0.005-0.034 0.018-0.065 0.051-0.073 0.228-0.052 1.037-0.286 1.037-0.962s-0.809-0.91-1.037-0.963c-0.033-0.007-0.056-0.039-0.051-0.072 0.031-0.236 0.073-0.471 0.125-0.705 0.007-0.033 0.039-0.054 0.073-0.051 0.231 0.029 1.072 0.083 1.305-0.553s-0.446-1.129-0.642-1.255c-0.029-0.019-0.039-0.056-0.022-0.086 0.11-0.211 0.23-0.418 0.36-0.619 0.018-0.030 0.056-0.039 0.087-0.024 0.208 0.105 0.979 0.441 1.417-0.077 0.438-0.517-0.029-1.213-0.17-1.397-0.020-0.028-0.017-0.066 0.008-0.089 0.177-0.162 0.361-0.315 0.553-0.459 0.027-0.022 0.065-0.018 0.088 0.007 0.16 0.169 0.769 0.746 1.36 0.409s0.39-1.151 0.322-1.371c-0.011-0.033 0.006-0.067 0.038-0.080 0.222-0.092 0.448-0.174 0.678-0.245 0.032-0.010 0.067 0.006 0.080 0.037 0.092 0.213 0.464 0.962 1.136 0.845 0.672-0.118 0.763-0.948 0.776-1.18 0.002-0.034 0.030-0.060 0.064-0.063 0.24-0.011 0.481-0.011 0.721 0 0.034 0.002 0.063 0.028 0.064 0.063 0.013 0.232 0.104 1.062 0.776 1.18 0.671 0.117 1.044-0.632 1.136-0.845 0.013-0.031 0.048-0.047 0.080-0.037 0.23 0.071 0.456 0.152 0.678 0.245 0.032 0.013 0.049 0.047 0.038 0.080-0.067 0.221-0.268 1.034 0.322 1.371s1.2-0.24 1.36-0.409c0.022-0.025 0.061-0.029 0.088-0.008 0.191 0.145 0.376 0.299 0.553 0.46 0.025 0.022 0.029 0.060 0.008 0.087-0.14 0.185-0.608 0.88-0.17 1.398s1.209 0.182 1.417 0.077c0.031-0.015 0.068-0.006 0.086 0.024 0.13 0.201 0.25 0.407 0.36 0.619 0.017 0.029 0.007 0.067-0.022 0.086-0.195 0.125-0.875 0.62-0.642 1.255s1.074 0.581 1.305 0.553c0.034-0.005 0.066 0.018 0.073 0.051 0.052 0.232 0.095 0.467 0.125 0.704 0.005 0.034-0.018 0.066-0.051 0.073-0.228 0.053-1.037 0.287-1.037 0.963s0.809 0.911 1.037 0.962c0.033 0.007 0.056 0.039 0.051 0.073-0.031 0.236-0.073 0.471-0.125 0.704-0.007 0.033-0.039 0.056-0.073 0.051-0.231-0.027-1.072-0.083-1.305 0.553s0.446 1.13 0.642 1.257c0.029 0.018 0.039 0.056 0.022 0.085-0.11 0.211-0.23 0.418-0.36 0.62-0.018 0.029-0.056 0.038-0.086 0.023-0.208-0.104-0.979-0.441-1.417 0.077-0.438 0.518 0.029 1.213 0.17 1.398 0.020 0.027 0.017 0.065-0.008 0.089-0.177 0.16-0.361 0.314-0.553 0.459-0.027 0.020-0.065 0.018-0.088-0.008-0.16-0.169-0.769-0.746-1.36-0.409zM16 14.106c0.014 0 0.028 0 0.041 0 0.193 0.005 0.361-0.061 0.496-0.197l1.987-1.97c0.189-0.188 0.266-0.434 0.215-0.695-0.051-0.26-0.215-0.461-0.46-0.564-0.7-0.294-1.47-0.458-2.278-0.458-3.218 0-5.827 2.587-5.827 5.778 0 0.802 0.164 1.565 0.462 2.259 0.105 0.243 0.307 0.407 0.569 0.456 0.262 0.051 0.511-0.024 0.7-0.211l1.987-1.97c0.136-0.136 0.203-0.301 0.199-0.493 0-0.014-0.001-0.027-0.001-0.041 0-1.046 0.855-1.894 1.91-1.894zM17.908 15.958c0 0.014 0.001 0.027 0.001 0.041 0 1.045-0.855 1.893-1.91 1.893-0.014 0-0.027 0-0.041 0-0.193-0.005-0.36 0.063-0.496 0.197l-1.987 1.97c-0.189 0.188-0.266 0.435-0.215 0.695s0.215 0.461 0.46 0.564c0.7 0.296 1.47 0.458 2.279 0.458 3.217 0 5.826-2.587 5.826-5.778 0-0.802-0.164-1.565-0.462-2.26-0.105-0.243-0.307-0.405-0.569-0.456s-0.511 0.025-0.7 0.213l-1.986 1.97c-0.136 0.135-0.203 0.301-0.2 0.492zM16 14.991c-0.562 0-1.017 0.451-1.017 1.009s0.455 1.008 1.017 1.008c0.562 0 1.017-0.451 1.017-1.008s-0.455-1.009-1.017-1.009z"></path> | |
</symbol> | |
<symbol id="i-1chan" viewBox="0 0 32 32"> | |
<path d="M15.001 2.001c-8.284 0-15 6.267-15 14 0 7.731 6.716 13.998 15 13.998s15-6.267 15-13.998c0-7.734-6.716-14-15-14zM18.001 9.999l1.998 1.002v9.999l-1.998 0.999v-12zM16 8.001h-2.001l-6 4.998 4.002 2.001v9h3.999v-15.999zM15.001 5.735c6.073 0 10.998 4.597 10.998 10.266 0 5.667-4.925 10.264-10.998 10.264s-10.998-4.597-10.998-10.264c0-5.669 4.925-10.266 10.998-10.266z"></path> | |
</symbol> | |
<symbol id="i-spinner" viewBox="0 0 32 32"> | |
<path d="M23.957 20.347c-0.113-0.066-0.198-0.113-0.31-0.18-1.528-0.735-2.461-2.367-2.447-4.088 0.005-1.767 1.030-3.354 2.608-4.209 1.143-0.617 2.105-1.639 2.61-3.037 0.765-2.116 0.13-4.592-1.571-6.066-2.712-2.324-6.729-1.531-8.432 1.434-0.569 0.987-0.794 2.097-0.727 3.153 0.118 1.684-0.763 3.281-2.209 4.1l-0.059 0.040c-1.521 0.886-3.382 0.86-4.852-0.104-0.35-0.239-0.758-0.439-1.172-0.567-2.135-0.784-4.618-0.118-6.075 1.634-2.286 2.738-1.479 6.741 1.443 8.437 1.856 1.075 4.074 0.931 5.766-0.12 1.439-0.935 3.262-1.044 4.752-0.18l0.224 0.13c1.49 0.862 2.279 2.487 2.22 4.221-0.106 2.008 0.924 3.996 2.778 5.071 2.7 1.566 6.146 0.59 7.648-2.152 1.318-2.622 0.387-6.021-2.197-7.518zM4.28 18.198c-1.153-0.669-1.573-2.154-0.89-3.338 0.666-1.157 2.145-1.58 3.326-0.895s1.573 2.154 0.89 3.338c-0.695 1.141-2.173 1.564-3.326 0.895zM14.783 18.115c-1.153-0.669-1.573-2.152-0.89-3.338 0.68-1.186 2.145-1.58 3.326-0.895 1.179 0.685 1.571 2.154 0.89 3.338-0.683 1.186-2.173 1.564-3.326 0.895zM19.977 9.085c-1.153-0.669-1.573-2.152-0.893-3.338 0.683-1.186 2.145-1.58 3.326-0.895s1.573 2.154 0.89 3.338c-0.68 1.186-2.173 1.564-3.323 0.895zM19.963 27.289c-1.153-0.669-1.571-2.154-0.89-3.34 0.666-1.155 2.145-1.578 3.326-0.893 1.153 0.669 1.573 2.152 0.89 3.338-0.666 1.157-2.145 1.578-3.326 0.895z"></path> | |
</symbol> | |
</defs> | |
</svg>` | |
document.body.insertAdjacentHTML('afterBegin', `<div style="display:none">${icons}</div>`) | |
const pubSub = { | |
emit: function(channel, data) { | |
localStorage['ZU-message'] = '(clear)' | |
localStorage['ZU-message'] = JSON.stringify({ | |
channel: channel, | |
data: data | |
}) | |
}, | |
init: function() { | |
window.addEventListener('storage', ev => { | |
if (ev.key !== 'ZU-message') return; | |
if (ev.newValue === '(clear)') return; | |
let data = null | |
try { | |
data = JSON.parse(ev.newValue) | |
} | |
catch(e) { | |
console.warn('Error processing message', e) | |
} | |
if (!data || !data.channel) return; | |
let handler = this.subscriptions[data.channel] | |
if (handler) { | |
handler(data.data) | |
} | |
}) | |
this.initialized = true | |
}, | |
subscribe: function(channel, fn) { | |
if (! this.initialized) | |
this.init() | |
this.subscriptions[channel] = fn | |
}, | |
ubsubscribe: function(channel) { | |
delete this.subscriptions[channel] | |
}, | |
subscriptions: {} | |
} | |
const favicon = { | |
sprite: '011001001100110001111110110111101100110101000101111010011001010110001000000110011001101010100101000100010100111101110001001100111110111110101001001001111100011001010010010001101010011011111110110000111100110010001100110101000', | |
getCoordinate: function(i) { | |
if (i == 'k') i = 10 | |
if (i == '+') i = 11 | |
i = +i | |
let x = 0, width | |
for (let j=0; j<=11; j++) { | |
width = (j==1 || j>9) ? 3 : 4 | |
if (j===i) return { | |
x: x, | |
width: width | |
} | |
else x += width | |
} | |
}, | |
init: function() { | |
let spriteCanvas = document.createElement('canvas') | |
this.mainCanvas = document.createElement('canvas') | |
spriteCanvas.width = 45 | |
spriteCanvas.height = 5 | |
this.mainCanvas.width = 16 | |
this.mainCanvas.height = 16 | |
this.spriteCtx = spriteCanvas.getContext("2d") | |
// fill sprite canvas | |
let imgData = this.spriteCtx.createImageData(45, 5) | |
this.sprite.split('').forEach((px, i) => { | |
let j = i * 4 | |
imgData.data[j+0] = | |
imgData.data[j+1] = | |
imgData.data[j+2] = +px * 255 | |
imgData.data[j+3] = 255 | |
}) | |
this.spriteCtx.putImageData(imgData, 0, 0) | |
// save original favicon | |
this.link = document.querySelector("link[rel=icon]") | |
this.originalImage = new Image() | |
this.originalURL = this.link.href | |
this.originalImage.src = this.originalURL | |
this.originalImage.addEventListener("load", () => { | |
this.originalImageLoaded = true | |
}) | |
}, | |
set n(n) { | |
this._n = n | |
if (! this.originalImageLoaded) return; | |
if (n <= 0) { | |
this.link.href = this.originalURL | |
return | |
} | |
let digits = n.toString(10) | |
if (digits.length > 3) { // How fucking optimistic | |
let k = Math.floor(n / 1000) | |
if (k > 9) // IT'S OVER 9000!!! | |
k = 9 | |
digits = `${k}k+` | |
} | |
digits = digits.split('').map(this.getCoordinate) | |
, totalWidth = digits.reduce((width, digit) => width + digit.width + 1, 1) | |
let ctx = this.mainCanvas.getContext('2d') | |
ctx.clearRect(0,0,16,16) | |
ctx.drawImage(this.originalImage, 0, 0) | |
ctx.fillStyle = '#000' | |
let offsetLeft= 16-totalWidth | |
ctx.fillRect(offsetLeft, 9, totalWidth, 7) | |
let x = offsetLeft + 1 | |
digits.forEach(digit => { | |
let data = this.spriteCtx.getImageData(digit.x, 0, digit.width, 5) | |
ctx.putImageData(data, x, 10) | |
x += (digit.width + 1) | |
}) | |
// Make shadow | |
let imgData = ctx.getImageData(0, 0, 16, 16) | |
x = offsetLeft | |
let y = 8 | |
for (; x < 16; x++) { | |
imgData.data = this.darken(imgData.data, (y*16 + x) * 4) | |
} | |
offsetLeft-- | |
if (offsetLeft >= 0) { | |
for (; y < 16; y++) { | |
imgData.data = this.darken(imgData.data, (y*16 + offsetLeft) * 4) | |
} | |
} | |
ctx.putImageData(imgData, 0, 0) | |
this.link.href = this.mainCanvas.toDataURL() | |
}, | |
get n() { | |
return this._n | |
}, | |
_n: 0, | |
darken: function(dataArray, index) { | |
[0,1,2].forEach(i => { | |
dataArray[index+i] -= this.darkenAmount | |
}) | |
return dataArray | |
}, | |
darkenAmount: 50 | |
} | |
var appObserver, contentObserver, | |
content, contentVue, | |
singleThread, singleThreadVue, | |
sidebar, sidebarVue, sidebarObserver, | |
alerts, alertsVue, | |
awaitBoardList, | |
postQuotation = null, | |
lastActiveTextarea | |
, state = { | |
initialized: false | |
} | |
, version = 0.3 | |
const SAGE_THREAD = 14965 | |
var momInRoom = { | |
mainCSS: | |
`.post-img-thumbnail { | |
opacity: 0.2 ; | |
filter: blur(4px) grayscale(50%) ; | |
}}`, | |
hoverCSS: | |
`.post-img .post-img-thumbnail, | |
.post-img .post-img-full { | |
transition: filter 0.3s, opacity 0.3s !important; | |
} | |
.post-img .post-img-thumbnail:hover, | |
.post-img .post-img-full:hover { | |
opacity: 1; | |
filter: none; | |
}`, | |
fullBlurCSS: | |
`.post-img-full { | |
opacity: 0.2 ; | |
filter: blur(4px) grayscale(50%) ; | |
}`, | |
toggle: function(val, noLoop=false) { | |
let quickBtn = document.querySelector('#ZU-quickaction-momInRoom') | |
if (quickBtn) { | |
quickBtn.classList.toggle('active', val) | |
} | |
if (val) { | |
injector.inject('ZU-mom-in-room', this.mainCSS) | |
} | |
else { | |
injector.remove('ZU-mom-in-room') | |
injector.remove('ZU-mom-in-room-full') | |
} | |
if (! noLoop) { | |
pubSub.emit('momInRoom', val) | |
} | |
else { | |
document.querySelector('#ZU-SP-momInRoom').checked = val | |
} | |
}, | |
toggleHover: function(val) { | |
if (val) { | |
injector.inject('ZU-unmask-on-hover', this.hoverCSS) | |
if (settings.momInRoom) { | |
injector.inject('ZU-mom-in-room-full', this.fullBlurCSS) | |
} | |
} | |
else { | |
injector.remove('ZU-unmask-on-hover') | |
injector.remove('ZU-mom-in-room-full') | |
} | |
}, | |
init: function() { | |
pubSub.subscribe('momInRoom', on => this.toggle(on, 'noLoop')) | |
} | |
} | |
const share = { | |
sites: { | |
'1chan': { | |
name: "1chan.ca", | |
link: (url, description) => `https://1chan.ca/live/addXS?link=${url}&description=${description}`, | |
icon: { | |
type: 'svg', | |
name: '1chan', | |
color: '#E42727' | |
}, | |
width: 150, | |
height: 50 | |
}, | |
telegram: { | |
name: 'Telegram', | |
link: (url, description) => `https://telegram.me/share/url?url=${url}&text=${description}`, | |
icon: { | |
type: 'fa', | |
name: 'telegram', | |
color: '#2ca5e0' | |
}, | |
width: 600, | |
height: 600 | |
}, | |
overnullch: { | |
name: 'Овернульч', | |
link: (url, description) => `http://0chan.one/live/overnullchlive.html?url=${url}&description=${description}`, | |
icon: { | |
type: 'svg', | |
extraClass: "fa-spin", | |
name: 'spinner', | |
color: "#16a085" | |
}, | |
width: 600, | |
height: 150 | |
}, | |
'1chanpl': { | |
name: "1chan.pl", | |
link: (url, description) => `https://1chan.pl/live/addXS?link=${url}&description=${description}`, | |
icon: { | |
type: 'svg', | |
name: '1chan', | |
color: '#dc143c' | |
}, | |
width: 150, | |
height: 50 | |
}, | |
}, | |
dropdown: function(url, description) { | |
url = encodeURIComponent(url) | |
return Object.keys(this.sites).reduce((htm, siteID) => { | |
let site = this.sites[siteID] | |
return htm + ` | |
<li> | |
<a class="ZU-share-link" data-url="${url}" data-description="${description}" data-site="${siteID}" href="javascript:void(0)"> | |
${site.icon | |
? `<span class="pull-left"><span${site.icon.color ? ` style="color:${site.icon.color}"` : ''}>` + | |
(site.icon.type == 'fa' | |
? `<i class="fa fa-${site.icon.name}"></i>` | |
: `<svg class="ZU-svg ZU-svg-16 ${site.icon.extraClass ? site.icon.extraClass : ''}"> | |
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#i-${site.icon.name}"></use> | |
</svg>` | |
) + `</span></span>` | |
: '' | |
} | |
${site.name}</a> | |
</li>` | |
}, '<ul class="dropdown-menu ZU-share-dropdown ZU-dropdown">') + '</ul>' | |
}, | |
handleClick: function(link) { | |
let selectedText = postQuotation | |
, description = encodeURIComponent(selectedText ? selectedText.replace(/\n/g, ' ') : link.dataset.description) | |
, site = this.sites[link.dataset.site] | |
window.open(site.link(link.dataset.url, description), | |
'targetWindow', | |
`toolbar=no,location=0,status=no,menubar=no,scrollbars=yes,resizable=yes,width=${site.width || 666},height=${site.height || 555}`) | |
} | |
} | |
var sideBar = { | |
init: () => { | |
app.$bus.on('toggleSidebar', sideBar.handleToggle) | |
}, | |
handleToggle: () => { | |
if (window.innerWidth > 767) { | |
document.querySelector('.headmenu').classList.add('ZU-sidemenu-animation-allowed') | |
settings.hideSidebar = !settings.hideSidebar | |
} | |
}, | |
toggle: function(hide) { | |
if (hide) { | |
document.body.classList.add('ZU-sidebar-hidden') | |
} | |
else { | |
document.body.classList.remove('ZU-sidebar-hidden') | |
} | |
} | |
} | |
const refresher = { | |
init: function() { | |
if (state.type !== 'thread') return; | |
let refreshIcon = document.querySelector('.btn-default .fa-refresh') | |
if (! refreshIcon) return; | |
let btn = refreshIcon.findParent('button') | |
btn.classList.add('ZU-refresh-btn') | |
btn.insertAdjacentHTML('afterBegin', `<div class="ZU-refresh-progressbar"></div><div class="ZU-refreshbtn-shadow-overlay"></div>`) | |
contentVue.checkNewReplies = () => { | |
this.timeoutStop() | |
contentVue.isFetchingMore = !0 | |
let e = contentVue.posts[contentVue.posts.length - 1] | |
, postsBefore = contentVue.posts.length | |
return contentVue.fetch(e.id).then(() => { | |
contentVue.isFetchingMore = !1 | |
let newPosts = contentVue.posts.length - postsBefore | |
if (newPosts && document.hidden) | |
favicon.n += newPosts | |
setTimeout(this.timeoutStart.bind(this), 400) | |
}) | |
} | |
this.initialized = true | |
this.reset() | |
}, | |
initialized: false, | |
reset: function(s = settings.updateInterval) { | |
if (s) | |
injector.inject('ZU-thread-update-interval', ` | |
.ZU-refresh-progressbar.ZU-rpb-full { | |
transition: width ${s}s linear; | |
width: 100%; | |
opacity: 1; | |
}`) | |
if (! this.initialized) return; | |
this.timeoutStop() | |
setTimeout(() => this.timeoutStart(), 500) | |
}, | |
timeout: null, | |
timeoutStart: function() { | |
if (! settings.updateInterval) return; | |
let progressBar = document.querySelector('.ZU-refresh-progressbar') | |
if (! progressBar) return; | |
void(progressBar.offsetWidth) // Animation won't start without this for some reason | |
progressBar.classList.add('ZU-rpb-full') | |
this.timeout = setTimeout(() => { | |
if (contentVue && contentVue.checkNewReplies) { | |
contentVue.checkNewReplies.bind(this)() | |
} | |
}, settings.updateInterval * 1000) | |
}, | |
timeoutStop: function() { | |
clearTimeout(this.timeout) | |
let progressBar = document.querySelector('.ZU-refresh-progressbar') | |
if (! progressBar) return; | |
void(progressBar.offsetWidth) // Animation won't start without this for some reason | |
progressBar.classList.remove('ZU-rpb-full') | |
} | |
} | |
const catalog = { | |
enabledOn: ['home', 'favourite', 'watched', 'board'], | |
toggle: function(on=settings.catalogMode) { | |
let quickBtn = document.querySelector('#ZU-quickaction-catalogMode') | |
if (quickBtn) | |
quickBtn.classList.toggle('active', on) | |
if (this.isApplicable && on) { | |
injector.inject('ZU-catalog-mode', this.css) | |
} | |
else { | |
injector.remove('ZU-catalog-mode') | |
} | |
resetAllFormPositions() | |
}, | |
css: ` | |
.thread-tree { | |
display: none; | |
} | |
div[board-id] { | |
width: 250px; | |
min-width: 250px; | |
display: inline-block; | |
height: 300px; | |
max-height: 300px; | |
min-height: 300px; | |
vertical-align: top; | |
margin: 4px !important; | |
} | |
.post-button { | |
padding: 0 4px; | |
} | |
.ZU-thread-controls { | |
display: none; | |
} | |
.thread-separator { | |
display: none; | |
} | |
.thread { | |
height: 100%; | |
} | |
:not(.post-popup) > .post { | |
margin: 0; | |
width: 100%; | |
min-width: 0; | |
max-height: 300px; | |
min-height: 100%; | |
} | |
:not(.post-popup) > .post > .post-body { | |
max-height: 257px; | |
height: 257px; | |
overflow: auto; | |
min-height: 100%; | |
} | |
:not(.post-popup) > .post > .post-footer { | |
margin-top: 0; | |
} | |
.post-id > span { | |
display: none; | |
} | |
.post-header .pull-right { | |
float: none !important; | |
position: absolute; | |
right: 0; | |
top: 0; | |
padding: 2px 10px; | |
background: linear-gradient(to right, rgba(255, 35, 35, 0) 0px, white 18px 100%); | |
} | |
.post-header .pull-right:hover { | |
z-index: 2; | |
} | |
.post-header { | |
padding: 0 !important; | |
} | |
.post-id { | |
background: linear-gradient(to left, rgba(255, 35, 35, 0) 0px, white 18px, white 100%); | |
z-index: 2; | |
position: relative; | |
padding: 2px 10px; | |
display: inline-block; | |
padding-right: 20px; | |
} | |
.post-body-message { | |
overflow: hidden !important; | |
max-height: none !important; | |
} | |
.post-popup { | |
z-index: 3; | |
} | |
.reply-form { | |
max-width: none; | |
z-index: 3; | |
} | |
.ZU-noko-label { | |
display: none | |
} | |
.threads-scroll-spy + div { | |
margin-top: 15px; | |
} | |
.threads-scroll-spy { | |
z-index: 3; | |
} | |
`, | |
get isApplicable() { | |
return this.enabledOn.indexOf(state.type) !== -1 | |
} | |
} | |
var settings = { | |
defaults: { | |
thumbNoScroll: true, | |
momInRoom: false, | |
unmaskOnHover: true, | |
hideSidebar: false, | |
hiddenBoards: [], | |
noko: true, | |
updateInterval: 10, | |
catalogMode: false | |
}, | |
_: {}, | |
hooks: { | |
momInRoom: momInRoom.toggle.bind(momInRoom), | |
unmaskOnHover: momInRoom.toggleHover.bind(momInRoom), | |
hideSidebar: sideBar.toggle.bind(sideBar), | |
updateInterval: refresher.reset.bind(refresher), | |
catalogMode: catalog.toggle.bind(catalog) | |
}, | |
save: function() { | |
this._.hiddenBoards = this.hiddenBoards | |
localStorage['ZU-settings'] = JSON.stringify(this._) | |
}, | |
init: function() { | |
let localSettins = LSfetchJSON('ZU-settings') || {} | |
, allSettings = Object.assign(this.defaults, localSettins) | |
Object.keys(allSettings).forEach(key => { | |
let value = allSettings[key] | |
if (typeof value !== "object") { | |
Object.defineProperty(this, key, { | |
set: function(val) { | |
this._[key] = val | |
if (this.hooks.hasOwnProperty(key)) { | |
this.hooks[key](val) | |
} | |
this.save() | |
}, | |
get: function() { | |
return this._[key] | |
} | |
}) | |
} | |
this[key] = value | |
}) | |
} | |
} | |
// Hides threads from unwanted boards on index page | |
const boardHider = { | |
enabled: false, | |
enable: function() { | |
if (this.enabled) { | |
return | |
} | |
this.enabled = true | |
this.refresh() | |
}, | |
disable: function() { | |
if (this.enabled) { | |
injector.remove('ZU-hide-boards') | |
} | |
this.enabled = false | |
}, | |
refresh: function() { | |
if (settings.hiddenBoards.length) { | |
let css | |
if (this.enabled) { | |
css = settings.hiddenBoards.map(boardID => `div[board-id="${boardID}"]`).join(', ') | |
+ ' {display: none} ' | |
injector.inject('ZU-hide-boards', css) | |
} | |
css = settings.hiddenBoards.map(boardID => `.sidemenu-board-item a[href="/${boardID}"] .ZU-board-hide-icon`).join(', ') | |
+ ' {display: none !important} ' | |
+ settings.hiddenBoards.map(boardID => `.sidemenu-board-item a[href="/${boardID}"] .ZU-board-unhide-icon`).join(', ') | |
+ ' {display: block}' | |
+ settings.hiddenBoards.map(boardID => `.sidemenu-board-item a[href="/${boardID}"]`).join(', ') | |
+ ' {text-decoration: line-through!important; }' | |
+ settings.hiddenBoards.map(boardID => `.sidemenu-board-item a[href="/${boardID}"] .sidemenu-board-title`).join(', ') | |
+ ' {color:#808080!important; }' | |
injector.inject('ZU-hide-boards-ui', css) | |
} | |
else { | |
injector.remove('ZU-hide-boards') | |
injector.remove('ZU-hide-boards-ui') | |
} | |
}, | |
toggleBoard: function(dir) { | |
let index = settings.hiddenBoards.indexOf(dir) | |
if (index >= 0) { | |
settings.hiddenBoards.splice(index, 1) | |
} | |
else { | |
settings.hiddenBoards.push(dir) | |
} | |
this.refresh() | |
settings.save() | |
} | |
} | |
var eventDispatcher = { | |
click: function(e) { | |
// Close alerts with one click | |
let alertsWrapper = e.path.find(el => el.classList && el.classList.contains('alerts-wrapper')) | |
if (alertsWrapper) { | |
alertsWrapper.__vue__.alerts = [] | |
} | |
// Thread updating and expanding | |
if (e.target.classList && e.target.classList.contains('ZU-expand-thread')) { | |
let thread = e.path.find(el => (el.classList && el.classList.contains('thread'))) | |
if (thread) { | |
expandThread(thread) | |
} | |
} | |
if (e.target.classList && e.target.classList.contains('ZU-update-thread')) { | |
let thread = e.path.find(el => (el.classList && el.classList.contains('thread'))) | |
if (thread) { | |
updateThread(thread) | |
} | |
} | |
// No scroll | |
let img = e.path.find(el => el.classList && el.classList.contains('post-img')) | |
if (img) { | |
if (settings.thumbNoScroll) { | |
img.__vue__.noScroll = true | |
} | |
} | |
// Board hiding | |
let hideBtn = e.path.find(el => el.classList && el.classList.contains('ZU-boardhideunhide')) | |
if (hideBtn) { | |
e.preventDefault() | |
e.stopPropagation() | |
let dir = hideBtn.findParent('a').getAttribute('href').replace(/\//g, '') | |
boardHider.toggleBoard(dir) | |
} | |
// Share | |
let shareBtn = e.path.find(el => el.classList && el.classList.contains('ZU-share-btn')) | |
if (shareBtn) { | |
shareBtn.querySelector('.ZU-share-dropdown').classList.toggle('ZU-dropdown-show') | |
} | |
// Share link | |
let shareLink = e.path.find(el => el.classList && el.classList.contains('ZU-share-link')) | |
if (shareLink) { | |
share.handleClick(shareLink) | |
} | |
// Sage | |
let sage = e.path.find(el => el.classList && el.classList.contains('ZU-sage-btn')) | |
if (sage) { | |
router.push(`sage/${SAGE_THREAD}`) | |
} | |
// Mention | |
let mention = e.path.find(el => el.classList && el.classList.contains('ZU-mention-btn')) | |
if (mention) { | |
mentionPost(mention.findParent('.post')) | |
} | |
// Popup slosing | |
if (e.path.find(el => el.classList && el.classList.contains('ZU-settings-btn'))) { | |
document.querySelector('#ZU-settings').classList.toggle('ZU-dropdown-show') | |
} | |
else if (! e.path.find(el => el.classList && el.classList.contains('ZU-settings-dropdown'))) { | |
document.querySelector('#ZU-settings').classList.remove('ZU-dropdown-show') | |
} | |
if (! e.path.find(el => el.classList && (el.classList.contains('ZU-share-btn') || el.classList.contains('ZU-share-btn')))) { | |
Array.prototype.forEach.call(document.querySelectorAll('.ZU-share-dropdown'), dd => dd.classList.remove('ZU-dropdown-show')) | |
} | |
}, | |
mousedown: function(e) { | |
// Quote on reply | |
let replyBtn = e.path.find(el => el.classList && (el.classList.contains('post-button-reply') || el.classList.contains('ZU-quote-on-click'))) | |
if (replyBtn) { | |
if (replyBtn.classList.contains('ZU-qoc-from-anywhere')) { | |
let selection = getSelection() | |
if (selection) | |
postQuotation = selection.text | |
} | |
else | |
postQuotation = getPostQuotation(replyBtn.findParent('.post'), replyBtn.classList.contains('ZU-qoc-textonly') || replyBtn.classList.contains('post-button-reply')) | |
} | |
}, | |
focusin: function(e) { | |
// Last active textarea | |
if (e.target.matches('.reply-form-message textarea')) { | |
lastActiveTextarea = e.target | |
} | |
}, | |
change: function(e) { | |
// Noko | |
let noko = e.path.find(el => el.classList && el.classList.contains('ZU-noko')) | |
if (noko) { | |
settings.noko = noko.checked | |
Array.prototype.forEach.call(document.querySelectorAll('.ZU-noko'), otherNoko => { | |
if (otherNoko !== noko) | |
otherNoko.checked = noko.checked | |
}) | |
} | |
}, | |
focus: function(e) { | |
favicon.n = 0 | |
} | |
} | |
function getPostQuotation(post, withoutNumber = false) { | |
if (! post) return null; | |
let postData = getPostDataFromDOM(post) | |
if (! postData) return null; | |
let text = `>>${postData.id}` | |
, selection = getSelection() | |
if (selection && selection.post && selection.post == post) { | |
let selectedText = selection.text | |
.replace(/^\s/, '').replace(/\s$/, '') // remove leading and trailing whitespaces | |
.replace(/^>/gm, ' >').replace(/^/gm, '>') // add quotation marks | |
if (withoutNumber) | |
return selectedText + '\n' | |
text += '\n' + selectedText | |
} | |
else if (withoutNumber) return null; | |
text += '\n' | |
return text | |
} | |
function mentionPost(post) { | |
let text = postQuotation | |
if (! text) return; | |
postQuotation = null | |
let textarea = lastActiveTextarea | |
if (textarea && textarea.offsetParent) { | |
if (textarea.value && !textarea.value.match(/\n$/)) { | |
text = '\n'+text | |
} | |
textarea.value += text | |
textarea.dispatchEvent(new Event('input', { | |
'bubbles': true, | |
'cancelable': true | |
})) | |
textarea.focus() | |
} | |
else { | |
setClipboard(text) | |
nativeAlert('success', `Номер поста ${text.match(/[^>0-9\s]/g) ? 'и цитата скопированы' : 'скопирован'} в буфер обмена`) | |
} | |
} | |
function sageContinue() { | |
if (!postQuotation || singleThreadVue.thread.id != SAGE_THREAD) return; | |
let text = postQuotation | |
postQuotation = null | |
contentVue.isReplyToOpPostFormShown = true | |
app.$bus.once('refreshContentDone', () => { | |
window.scrollTo(0,document.body.scrollHeight) | |
let textarea = document.querySelector('.threads > div > .reply-form .reply-form-message textarea') | |
if (textarea.value && !textarea.value.match(/\n$/)) { | |
text = '\n'+text | |
} | |
textarea.value += text | |
textarea.dispatchEvent(new Event('input', { | |
'bubbles': true, | |
'cancelable': true | |
})) | |
textarea.focus() | |
}) | |
} | |
function getSelection() { | |
if (! document.getSelection) return null; | |
let selection = document.getSelection() | |
if (selection.type !== "Range") return null; | |
let selectedText = selection.toString() | |
if (! selectedText) return null; | |
return { | |
text: selectedText, | |
post: selection.anchorNode.findParent('.post') | |
} | |
} | |
function setClipboard(text) { | |
if (! document.execCommand) return; | |
let input | |
try { | |
input = document.createElement('textarea') | |
document.body.appendChild(input) | |
input.value = text | |
input.focus() | |
input.select() | |
document.execCommand('Copy') | |
} | |
catch(e) { | |
console.warn('[u0] Unable to set clipboard') | |
} | |
input.remove() | |
} | |
function expandThread(threadDOM) { | |
let threadVue = threadVueFromDOM(threadDOM) | |
, threadID, opID, firstShownReplyID | |
if (!threadVue) { | |
console.warn('[0u] Unable to expand thread', e) | |
return | |
} | |
try { | |
threadID = threadVue.thread.id | |
, opID = threadVue.opPost.id | |
, firstShownReplyID = threadVue.lastPosts[0].id | |
} | |
catch(e) { | |
console.warn('[0u] Unable to expand thread', e) | |
return | |
} | |
getPosts(threadID, opID, firstShownReplyID) | |
.then(posts => { | |
threadVue.lastPosts = posts.concat(threadVue.lastPosts) | |
Array.prototype.forEach.call(threadDOM.querySelectorAll('.ZU-delete-on-threadexpand'), el => el.remove()) | |
}) | |
.catch(handleNetworkError) | |
} | |
function updateThread(thread) { | |
let threadVue, threadID, lastReplyID | |
if (thread instanceof Element) { | |
threadVue = threadVueFromDOM(thread) | |
} | |
else { | |
threadVue = thread | |
} | |
if (!threadVue) { | |
console.warn('[0u] Unable to update thread', e) | |
return | |
} | |
try { | |
threadID = threadVue.thread.id | |
lastReplyID = (threadVue.lastPosts[threadVue.lastPosts.length-1] || threadVue.opPost).id | |
} | |
catch(e) { | |
console.warn('[0u] Unable to update thread', e) | |
return | |
} | |
getPosts(threadID, lastReplyID) | |
.then(posts => { | |
threadVue.lastPosts = threadVue.lastPosts.concat(posts) | |
}) | |
.catch(handleNetworkError) | |
} | |
const router = { | |
push: function(path) { | |
if (path.indexOf('/') !== 0) { | |
path = '/'+path | |
} | |
if (document.location.pathname == path) { | |
this.reload() | |
} | |
else { | |
app.$router.push({path: path}) | |
} | |
}, | |
reload: () => app.$bus.emit('refreshContent'), | |
setupInterceptor: function(doDebug=false) { | |
app.$router.push = (route, e, n) => { | |
let thread, threadID, postID | |
if (doDebug) | |
console.log('ROUTE:', route) | |
if ( | |
!settings.noko | |
&& state.type !== "thread" | |
&& route.hasOwnProperty('name') | |
&& route.name === "thread" | |
&& route.hasOwnProperty('hash') | |
&& (postID = route.hash.split('#')[1]) | |
&& route.hasOwnProperty('params') | |
&& (threadID = route.params.threadId) | |
&& !settings.catalogMode | |
) { | |
// Quick reply case | |
let threadVue = contentVue.threads.find(thr => thr.thread.id == threadID) | |
if ( | |
threadVue // Thread exists on page | |
&& !document.querySelector(`a[data-post="${postID}"], a[href$="#${postID}"]`) // No link to new posts exists | |
) { | |
try { | |
if (doDebug) | |
console.log('Route intercepted (quick reply)') | |
updateThread(threadVue) | |
} | |
catch(e) { | |
console.error(e) | |
app.$router.history.push(route, e, n) | |
} | |
} | |
// New thread case | |
else if ( | |
state.type === "board" // New threads may be created only from a board view | |
&& route.params.dir === contentVue.board.dir | |
&& !document.querySelector(`a[href*="/${threadID}"]`) // Thread does not exist yet | |
) { | |
if (doDebug) | |
console.log('Route intercepted (new thread)') | |
this.reload() | |
} | |
else { | |
app.$router.history.push(route, e, n) | |
} | |
} | |
else { | |
app.$router.history.push(route, e, n) | |
} | |
} | |
} | |
} | |
function setupAlertInterceptor() { | |
alertsVue.addAlert = function(t, e, a) { | |
var s = this | |
, n = { | |
type: t, | |
text: e | |
} | |
if (n.type === 'error' && n.text.indexOf('checking_browser')!== -1) { | |
let anusAlert = { | |
type: 'info', | |
text: 'Производится проверка ануса...' | |
} | |
this.alerts.unshift(anusAlert) | |
fuckCF(e, anusAlert, s) | |
} | |
else { | |
this.alerts.unshift(n), | |
setTimeout(function() { | |
s.closeAlert(n) | |
}, a || 3500) | |
} | |
} | |
} | |
// Thanks anoñchik from /userjs/ | |
function fuckCF(response, alertToClose, alertCloserContext) { | |
let query = 'jschl_vc=' + response.match(/jschl_vc" value="([^"]+)/)[1] + '&pass=' + response.match(/pass" value="([^"]+)/)[1] + '&jschl_answer=', | |
// basis for simplest eval | |
a = {value: 0}, t = location.host | |
eval( response.match(/b,r,e,a,k,i,n,g,f, ([^;]+)/)[1] + '; ' + response.match(/getElementById\('challenge-form'\);\s+;([^']+)/)[1] ) | |
query += a.value | |
let xhr = new XMLHttpRequest() | |
xhr.open("GET", '/cdn-cgi/l/chk_jschl?'+query, true) | |
xhr.onreadystatechange = function() { | |
if (xhr.readyState !== xhr.DONE) return; | |
alertCloserContext.closeAlert(alertToClose) | |
if (xhr.status == 200) { | |
// check if post is being sent | |
let sendingBtn = document.querySelector('.reply-form .btn-primary[disabled] .fa-spinner') | |
if (sendingBtn) { | |
try { | |
sendingBtn.findParent('.reply-form').__vue__.send() | |
return | |
} | |
catch(e) {} | |
} | |
// check if new threads are being fetched | |
if (contentVue.fetchingMore) { | |
contentVue.fetchingMore = false | |
if (app.$router.currentRoute.name === 'thread') { | |
/*singleThreadVue*/contentVue.checkNewReplies() | |
} | |
else { | |
contentVue.getMoreThreads() | |
} | |
return | |
} | |
// check if route has changed | |
if (! document.querySelector('#content div')) { | |
router.reload() | |
return | |
} | |
// default behavior | |
alertCloserContext.addAlert('success', 'Проверка ануса пройдена. Повторите попытку') | |
} | |
else { | |
alertCloserContext.addAlert('error', 'Проверка ануса провалилась.') | |
} | |
} | |
setTimeout(() => { | |
xhr.send() | |
}, 4000) | |
} | |
function handleReplyForm(form) { | |
// Add noko button | |
form.querySelector('.reply-form-message + div .pull-right').insertAdjacentHTML('beforeBegin', ` | |
<label class="ZU-noko-label" title="После отправки сообщения переместиться к треду"><input class="ZU-noko" type="checkbox"${settings.noko ? 'checked' : '' }> Noko</label>`) | |
// Add quote from selection | |
if (postQuotation) { | |
let textarea = form.querySelector('textarea') | |
textarea.value = postQuotation | |
postQuotation = null | |
textarea.dispatchEvent(new Event('input', { | |
'bubbles': true, | |
'cancelable': true | |
})) | |
textarea.focus() | |
} | |
// Reposition | |
repositionReplyForm(form) | |
} | |
function addSettingsButtons() { | |
let showCatBtn = catalog.isApplicable | |
document.querySelector('.headmenu-buttons-left').insertAdjacentHTML('beforeEnd', ` | |
<div class="btn-group ZU-nomargin-btn-group"> | |
<button title="0chan Utilities v.${version}" type="button" class="ZU-panel-btn btn btn-link ZU-btn-link ZU-svg-container-btn ZU-settings-btn"> | |
<svg class="ZU-svg ZU-svg-32"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#i-logo"></use></svg> | |
</button> | |
${showCatBtn ? | |
`<button title="Режим каталога" id="ZU-quickaction-catalogMode" data-prop="catalogMode" class="btn btn-link ZU-btn-link ZU-catalog-btn${settings.catalogMode ? ' active' : ''}"> | |
<i class="fa fa-th ZU-onactive-hide"></i> | |
<i class="fa fa-th-list ZU-onactive-show"></i> | |
</button>` : '' } | |
<button title="Мамка в комнате" id="ZU-quickaction-momInRoom" data-prop="momInRoom" class="btn btn-link ZU-btn-link ZU-nsfw-btn${settings.momInRoom ? ' active' : ''}"> | |
<i class="fa fa-low-vision"></i> | |
</button> | |
</div> | |
`) | |
injector.inject('ZU-headmenu-shift', `.headmenu-title { left: ${showCatBtn ? 170 : 130}px !important; }`) | |
;['catalogMode', 'momInRoom'].forEach(prop => { | |
let btn = document.querySelector(`#ZU-quickaction-${prop}`) | |
if (! btn) return; | |
btn.addEventListener('click', ev => { | |
let btn = ev.target.findParent('button') | |
, on = !btn.classList.contains('active') | |
settings[prop] = document.querySelector(`#ZU-SP-${prop}`).checked = on | |
}) | |
}) | |
} | |
function handleBoardItem(board) { | |
if (board.querySelector('.ZU-boardhideunhide')) return; | |
board.insertAdjacentHTML('afterBegin', | |
`<span class="pull-left sidemenu-board-icons ZU-boardhideunhide"> | |
<span title="Скрыть" class="ZU-board-hide-icon"> | |
<i class="fa fa-minus-square-o"></i> | |
</span> | |
<span title="Раскрыть" class="ZU-board-unhide-icon"> | |
<i class="fa fa-plus-square-o"></i> | |
</span> | |
</span>`) | |
} | |
function init() { | |
if (typeof content.__vue__ === 'undefined') { | |
setupVueGetter() // *swoosh* — and __vue__ is available outside dev tools! | |
} | |
contentVue = content.__vue__ | |
contentObserver = forAllNodes([ | |
{ | |
selector: '.thread > div > div > div > .post-body', | |
fn: handleThread | |
}, | |
{ | |
selector: '.post', | |
fn: handlePost | |
}, | |
{ | |
selector: '.sidemenu-board-item a', | |
fn: handleBoardItem | |
}, | |
{ | |
selector: '.reply-form', | |
fn: handleReplyForm | |
} | |
], content.parentElement, {subtree: true, queryChildren: true}) | |
sidebar = document.querySelector('#sidebar') | |
sidebarVue = sidebar.__vue__ | |
awaitBoardList = forAllNodes([ | |
{ | |
selector: '.sidemenu-boards-list', | |
fn: boardList => { | |
app.$nextTick(()=>awaitBoardList.stop()) | |
sidebarObserver = forAllNodes([ | |
{ | |
selector: '.sidemenu-board-item a', | |
fn: handleBoardItem | |
} | |
], boardList, {queryChildren: true}) | |
} | |
} | |
], sidebar, {queryChildren: true}) | |
favicon.init() | |
router.setupInterceptor() | |
settings.init() | |
sideBar.init() | |
boardHider.refresh() | |
momInRoom.init() | |
state.initialized = true | |
} | |
function handlePost(post) { | |
let extraIconsContainer = post.querySelector('.post-footer .pull-right') | |
if (!extraIconsContainer || extraIconsContainer.querySelector('.ZU-sage-btn')) | |
return; | |
extraIconsContainer.insertAdjacentHTML('afterBegin', ` | |
<span title="Упомянуть" class="post-button ZU-mention-btn ZU-quote-on-click"><i class="fa fa-angle-double-right"></i></span> | |
<span title="SAGE!" class="post-button ZU-sage-btn ZU-quote-on-click"><i class="fa fa-arrow-down"></i></span>`) | |
let postData = getPostDataFromDOM(post) | |
if (!postData) return; | |
if (postData.isPopup) { | |
repositionPopup(post.parentNode) | |
} | |
else if (postData.isOpPost) { | |
extraIconsContainer.insertAdjacentHTML('beforeBegin', ` | |
<div class="pull-left"> | |
<span title="Поделиться" class="post-button ZU-share-btn ZU-quote-on-click ZU-qoc-from-anywhere"> | |
<i class="fa fa-share-alt"></i> | |
${share.dropdown(`${document.location.protocol}//${document.location.host}/${postData.dir}/${postData.threadID}`, postData.title)} | |
</span> | |
</div>`) | |
} | |
} | |
function repositionPopup(popup) { | |
let left = + popup.style.left.replace('px', '') | |
, top = + popup.style.top.replace('px', '') | |
, width = popup.offsetWidth | |
, height = popup.offsetHeight | |
let bcr = popup.getBoundingClientRect() | |
if (bcr.bottom > document.documentElement.clientHeight) { | |
popup.style.top = (top - height - 20)+'px' | |
} | |
let offsetRight = bcr.right - document.documentElement.clientWidth | |
if (offsetRight > 0) { | |
popup.style.left = (left - offsetRight)+'px' | |
} | |
} | |
function repositionReplyForm(form) { | |
form.style.marginLeft = 0 | |
let bcr = form.getBoundingClientRect() | |
, offsetRight = bcr.right - document.documentElement.clientWidth + 5/*+ existingOffset*/ | |
form.style.marginLeft = `${offsetRight > 0 ? -offsetRight : 0}px` | |
} | |
function resetAllFormPositions() { | |
Array.prototype.forEach.call(document.querySelectorAll('.reply-form'), repositionReplyForm) | |
} | |
function handleThread(thread) { | |
thread = thread.findParent('.thread') | |
let threadVue = threadVueFromDOM(thread) | |
if (! threadVue) return; | |
thread.parentNode.setAttribute('board-id', threadVue.thread.board.dir) | |
addThreadControls(thread, threadVue) | |
} | |
function threadVueFromDOM(thread) { | |
try { | |
let threadID = thread.__vue__.thread.thread.id | |
return contentVue.threads.find(thread => thread.thread.id == threadID) | |
} | |
catch(e) { | |
console.warn('[0u] Unable to find thread model', thread, e) | |
return null | |
} | |
} | |
function getPostDataFromDOM(post) { | |
try { | |
let postVue = post.parentNode.__vue__ | |
if (postVue.post) { | |
return { | |
id: postVue.post.id, | |
isOpPost: postVue.post.isOpPost, | |
dir: postVue.thread.board.dir, | |
threadID: postVue.thread.id, | |
title: postVue.thread.title, | |
isPopup: false, | |
postVue: postVue, | |
boardName: postVue.thread.board.name | |
} | |
} | |
else if (postVue.$el.classList.contains('post-popup')) { | |
let popupVue = postVue.$parent.popupPost | |
return { | |
id: popupVue.id, | |
isOpPost: popupVue.isOpPost, | |
dir: popupVue.boardDir, | |
threadID: popupVue.threadId, | |
isPopup: true, | |
popupVue: popupVue, | |
postVue: postVue | |
} | |
} | |
else return null | |
} | |
catch(e) { | |
console.warn('[0u] Unable to find post model', post, e) | |
return null | |
} | |
} | |
function addThreadControls(threadDOM, threadVue) { | |
let controlsContainer = Array.prototype.find.call(threadDOM.querySelectorAll(':scope > div > div'), div => div.style.fontWeight == 'bold') | |
if (!controlsContainer || controlsContainer.classList.contains('ZU-thread-controls')) return; | |
let href = controlsContainer.querySelector('a').getAttribute('href') | |
if (threadVue.skippedPosts) { | |
controlsContainer.querySelector('span').classList.add('ZU-delete-on-threadexpand') | |
controlsContainer.insertAdjacentHTML('beforeEnd', `<span class="ZU-expand-thread-container ZU-delete-on-threadexpand"> | <a href="${href}" onclick="return false" class="ZU-expand-thread">Развернуть</a></span>`) | |
} | |
controlsContainer.insertAdjacentHTML('beforeEnd', `<span class="ZU-update-thread-container"> | <a href="${href}" onclick="return false" class="ZU-update-thread">Обновить</a></span>`) | |
controlsContainer.classList.add('ZU-thread-controls') | |
} | |
var settingsPanelPage = { | |
modules: { | |
checkbox: { | |
build: checkbox => ` | |
<div class="form-group"> | |
<label class="control-label col-md-8">${checkbox.title}</label> | |
<div class="col-md-12"> | |
<div class="checkbox"> | |
<label><input data-id="${checkbox.id}" id="ZU-SP-${checkbox.id}" type="checkbox"${settings[checkbox.id] ? ' checked' : ''}> ${checkbox.description}</label> | |
</div> | |
</div> | |
</div>`, | |
events: { | |
change: ev => { | |
let checkbox = ev.target | |
try { | |
settings[checkbox.dataset.id] = checkbox.checked | |
} | |
catch(e) { | |
console.warn('[0u] Unable to handle checkbox change', checkbox) | |
} | |
} | |
} | |
} | |
}, | |
controls: [ | |
{ | |
type: 'checkbox', | |
id: 'momInRoom', | |
title: "Мамка в комнате", | |
description: "Маскировать все картинки" | |
}, | |
{ | |
type: 'checkbox', | |
id: 'unmaskOnHover', | |
title: "Раскрывать по наведению", | |
description: "Раскрывать замаскированные картинки по наведению" | |
}, | |
{ | |
type: 'checkbox', | |
id: 'thumbNoScroll', | |
title: "Разворот без скролла", | |
description: "Не скроллить при разворачивании картинок" | |
} | |
], | |
awaitInstall: function() { | |
let panelWaiter = forAllNodes([{ | |
selector: '.profile-page .row', | |
fn: container => { | |
app.$nextTick(()=>panelWaiter.stop()) | |
this.install(container) | |
} | |
}], content, {sutree: true, queryChildren: true}) | |
}, | |
install: function(container) { | |
container.insertAdjacentHTML('beforeEnd', ` | |
<div class="col-md-12"> | |
<form> | |
<div class="panel panel-default"> | |
<div class="panel-heading"> | |
<b>0chan Utilities v.${version}</b> | |
</div> | |
<div class="panel-body"> | |
<div class="form-horizontal"> | |
<div class="form-horizontal"> | |
${this.controls.reduce((htm, control) => htm + this.modules[control.type].build(control), '')} | |
</div> | |
</div> | |
</div> | |
<div class="panel-footer text-right"> | |
Изменения сохраняются автоматически. | |
</div> | |
</div> | |
</form> | |
</div>`) | |
this.controls.forEach(control => { | |
let allEvents = Object.assign(Object.create(this.modules[control.type].events || {}), control.events || {}) | |
, controlDOM = document.querySelector(`#ZU-SP-${control.id}`) | |
if (! controlDOM) return; | |
for (let eventName in allEvents) { | |
controlDOM.addEventListener(eventName, allEvents[eventName]) | |
} | |
}) | |
} | |
} | |
var settingsPanel = { | |
modules: { | |
checkbox: { | |
build: checkbox => ` | |
<li title="${checkbox.description || checkbox.title}"> | |
<label for="ZU-SP-${checkbox.id}"><input data-id="${checkbox.id}" id="ZU-SP-${checkbox.id}" type="checkbox"${settings[checkbox.id] ? ' checked' : ''}> ${checkbox.title}</label> | |
</li>`, | |
events: { | |
change: ev => { | |
let checkbox = ev.target | |
settings[checkbox.dataset.id] = checkbox.checked | |
} | |
} | |
}, | |
slider: { | |
build: slider => ` | |
<li title="${slider.description || slider.title}"> | |
<label for="ZU-SP-${slider.id}">${slider.title} | |
<span class="ZU-SP-slider-value">(${slider.displayValue ? slider.displayValue(settings[slider.id]) : settings[slider.id]})</span> | |
<input type="range" id="ZU-SP-${slider.id}" data-id="${slider.id}" value="${settings[slider.id]}" min="${slider.min}" max="${slider.max}" step="${slider.step}"> | |
</label> | |
</li>`, | |
events: { | |
change: (ev, sliderObj) => { | |
let sliderDOM = ev.target | |
, val = +sliderDOM.value | |
settings[sliderObj.id] = val | |
}, | |
input: (ev, sliderObj) => { | |
let sliderDOM = ev.target | |
, val = +sliderDOM.value | |
sliderDOM.findParent('label').querySelector('.ZU-SP-slider-value').innerText = `(${sliderObj.displayValue(val)})` | |
} | |
} | |
} | |
}, | |
controls: [ | |
{ | |
type: 'checkbox', | |
id: 'momInRoom', | |
title: "Мамка в комнате", | |
description: "Маскировать все картинки" | |
}, | |
{ | |
type: 'checkbox', | |
id: 'unmaskOnHover', | |
title: "Раскрывать по наведению", | |
description: "Раскрывать замаскированные картинки по наведению" | |
}, | |
{ | |
type: 'checkbox', | |
id: 'thumbNoScroll', | |
title: "Разворот без скролла", | |
description: "Не скроллить при разворачивании картинок" | |
}, | |
{ | |
type: 'slider', | |
id: 'updateInterval', | |
title: "Период обновления", | |
title: "Период обновления треда", | |
min: 0, | |
step: 5, | |
max: 60, | |
condition: () => state.type==="thread", | |
displayValue: val => val ? `${val} с` : "Выкл." | |
}, | |
{ | |
type: 'checkbox', | |
id: 'catalogMode', | |
title: "Режим каталога", | |
description: "Отображать треды в виде каталога", | |
condition: () => catalog.isApplicable, | |
}, | |
], | |
install: function(container) { | |
let controls = this.controls.filter(control => !control.condition || control.condition()) | |
document.querySelector('.headmenu').insertAdjacentHTML('beforeEnd', ` | |
<ul class="dropdown-menu ZU-settings-dropdown ZU-dropdown" id="ZU-settings"> | |
${controls.reduce((htm, control) => htm + this.modules[control.type].build(control), '')} | |
</ul>`) | |
controls.forEach(control => { | |
if (state.condition && !state.condition()) return; | |
let allEvents = Object.assign(Object.create(this.modules[control.type].events || {}), control.events || {}) | |
, controlDOM = document.querySelector(`#ZU-SP-${control.id}`) | |
if (! controlDOM) return; | |
for (let eventName in allEvents) { | |
controlDOM.addEventListener(eventName, ev => allEvents[eventName](ev, control)) | |
} | |
}) | |
} | |
} | |
var ZURouter = { | |
currentRoute: 'initial', | |
enter: { | |
account: settingsPanelPage.awaitInstall.bind(settingsPanel), | |
home: boardHider.enable.bind(boardHider), | |
thread: sageContinue | |
}, | |
leave: { | |
home: boardHider.disable.bind(boardHider), | |
thread: refresher.timeoutStop | |
}, | |
handleRoute: function(type) { | |
catalog.toggle() | |
if (this.enter.hasOwnProperty(type)) { | |
this.enter[type]() | |
} | |
if (type !== this.currentRoute && this.leave.hasOwnProperty(this.currentRoute)) { | |
this.leave[this.currentRoute]() | |
} | |
this.currentRoute = type | |
} | |
} | |
// CSS injector | |
var injector = { | |
inject: function(alias, css) { | |
var id = `injector:${alias}` | |
var existing = document.getElementById(id) | |
if(existing) { | |
existing.innerHTML = css | |
return | |
} | |
var head = document.head || document.getElementsByTagName('head')[0] | |
, style = document.createElement('style'); | |
style.type = 'text/css' | |
style.id = id | |
if (style.styleSheet) { | |
style.styleSheet.cssText = css | |
} else { | |
style.appendChild(document.createTextNode(css)) | |
} | |
head.appendChild(style) | |
}, | |
remove: function(alias) { | |
var id = `injector:${alias}` | |
var style = document.getElementById(id) | |
if(style) { | |
var head = document.head || document.getElementsByTagName('head')[0] | |
if(head) | |
head.removeChild(document.getElementById(id)) | |
} | |
} | |
} | |
function forAllNodes(selFnMap, parent=document.body, options={}) { | |
let config = Object.assign({ | |
autoStart: true, // whether or not observer shall start observing immediately | |
subtree: false, | |
childList: true, | |
queryChildren: false //whether or not inserted nodes shall be searched for selector-matching elements | |
}, options) | |
, afterClass | |
// Setup observer | |
let observer = new MutationObserver(mutations => { | |
mutations.forEach(mutation => { | |
Array.prototype.forEach.call(mutation.addedNodes, node => { | |
if (node.nodeType !== Node.ELEMENT_NODE) return; | |
selFnMap.forEach(sf => { | |
if (node.matches(sf.selector)) { | |
sf.fn(node) | |
} | |
else if (config.queryChildren || sf.queryChildren) { | |
let foundChildren = node.querySelectorAll(sf.selector) | |
if (foundChildren) { | |
Array.prototype.forEach.call(foundChildren, childNode => { | |
sf.fn(childNode) | |
}) | |
} | |
} | |
}) | |
}) | |
}) | |
}) | |
function start() { | |
// Handle existing nodes | |
selFnMap.forEach(sf => { | |
let existingNodes = parent.querySelectorAll(sf.selector) | |
Array.prototype.forEach.call(existingNodes, node => { | |
sf.fn(node) | |
}) | |
}) | |
// Handle future nodes | |
observer.observe(parent, config) | |
} | |
if (config.autoStart) { | |
start() | |
} | |
return { | |
start: start, | |
stop: () => observer.disconnect() | |
} | |
} | |
function externallyResolvingPromise() { | |
let promiseResolve, promiseReject | |
, promise = new Promise(function(resolve, reject){ | |
promiseResolve = resolve; | |
promiseReject = reject; | |
}); | |
return { | |
promise: promise, | |
resolve: promiseResolve, | |
reject: promiseReject | |
} | |
} | |
// Element.matches() polyfill | |
;[Element.prototype, Text.prototype].forEach(e => { | |
e.matches || (e.matches=e.matchesSelector || function(selector) { | |
var matches = document.querySelectorAll(selector) | |
return Array.prototype.some.call(matches, e => { | |
return e === this | |
}) | |
}) | |
e.findParent = function(selector) { | |
let node = this | |
while(node && !node.matches(selector)) { | |
node = node.parentNode | |
if (! node.matches) return null; | |
} | |
return node | |
} | |
}) | |
// event path polyfill | |
;(e => { | |
if (e.hasOwnProperty('path')) return; | |
Object.defineProperty(e, 'path', { | |
get: function() { | |
if (! this.target) return []; | |
if (this.pathCached) return this.pathCached; | |
let path = [] | |
, node = this.target | |
while(node && node != document.body) { | |
path.push(node) | |
node = node.parentNode | |
} | |
this.pathCached = path | |
return path | |
}, | |
enumerable: true, | |
configurable: false | |
}) | |
})(Event.prototype) | |
function setupVueGetter() { | |
Object.defineProperty(Element.prototype, '__vue__', { | |
get: function() { | |
let stack = [app] | |
, child, found = null | |
while (stack.length && !found) { | |
child = stack.pop() | |
if (child && child.$el && child.$el === this) { | |
found = child | |
} | |
else if (child && child.$children) | |
stack = stack.concat(child.$children) | |
} | |
return found | |
}, | |
enumerable: true, | |
configurable: false | |
}) | |
} | |
function getPosts(threadID, after, before) { | |
return new Promise((resolve, reject) => { | |
fetch(`${document.location.protocol}//${document.location.host}/api/thread?thread=${threadID}${after ? '&after='+after : ''}` | |
, {credentials: 'same-origin'} | |
).then(res => { | |
if (res.ok) { | |
res.json().then(resObj => { | |
if (resObj.posts && resObj.posts.length) { | |
let posts = resObj.posts | |
if (before) { | |
posts = posts.filter(post => (+post.id) < (+before) ) | |
} | |
resolve(posts) | |
} | |
else { | |
resolve([]) | |
} | |
}) | |
.catch(e => reject(e)) | |
} | |
else { | |
res.text().then(text => console.warn('[0u] Bad response: ', text)).catch(nop) | |
reject(res.status) | |
} | |
}) | |
.catch(e => reject(e)) | |
}) | |
} | |
function handleNetworkError(err) { | |
nativeAlert('error', 'Сетевая ошибка') | |
if (err) | |
console.error(err) | |
} | |
// GUI alerts | |
// Types available: info, error, success | |
function nativeAlert(type, message) { | |
type = 'alert' + type.charAt(0).toUpperCase() + type.slice(1) | |
app.$bus.emit(type, message) | |
} | |
function LSfetchJSON(key) { | |
let val = null, data = localStorage[key] | |
if (typeof data !== 'undefined') { | |
try { | |
val = JSON.parse(data) | |
} | |
catch(e) { | |
console.error(e) | |
localStorage.removeItem(key) | |
} | |
} | |
return val | |
} | |
function start() { | |
let sidebarExtPromise = externallyResolvingPromise() | |
, contentExtPromise = externallyResolvingPromise() | |
Promise.all([sidebarExtPromise.promise, contentExtPromise.promise]).then(() => { | |
appObserver.stop() | |
forAllNodes([{ | |
selector: '#content > div', | |
fn: onFreshContent | |
}], document.querySelector('#content')) | |
}) | |
appObserver = forAllNodes([ | |
{ | |
selector: '#sidebar', | |
fn: () => sidebarExtPromise.resolve() | |
}, | |
{ | |
selector: '#content', | |
fn: () => contentExtPromise.resolve() | |
} | |
], document.body, {subtree: true, queryChildren: true}) | |
Object.keys(eventDispatcher).forEach(evType => { | |
document.addEventListener(evType, eventDispatcher[evType], true) | |
}) | |
window.addEventListener('resize', resetAllFormPositions) | |
} | |
start() | |
function onFreshContent() { | |
try { | |
state.type = app.$router.currentRoute.name | |
} | |
catch(e) { | |
console.warn('[0u] Unable to determine app state', e) | |
} | |
content = document.querySelector('#content > div') | |
if (state.type==='thread') | |
singleThread = document.querySelector('.post-op').parentNode.parentNode | |
if (! state.initialized) { | |
init() | |
} | |
else { | |
contentVue = content.__vue__ | |
if (state.type==='thread') | |
singleThreadVue = singleThread.__vue__ | |
} | |
alerts = document.querySelector('.alerts-wrapper') | |
alertsVue = alerts.__vue__ | |
setupAlertInterceptor() | |
addSettingsButtons() | |
settingsPanel.install() | |
ZURouter.handleRoute(state.type) | |
refresher.init() | |
} | |
injector.inject('ZU-global', ` | |
.btn-open-sidebar { | |
display: inline-block !important; | |
} | |
.headmenu-title { | |
white-space: nowrap; | |
} | |
.sidebar, #content { | |
transition: margin-left 0.3s cubic-bezier(0, 0.85, 0.72, 0.99); | |
will-change: margin-left; | |
} | |
.ZU-sidebar-hidden .sidebar { | |
margin-left: -250px; | |
} | |
.ZU-sidebar-hidden #content { | |
margin-left: 0; | |
} | |
.headmenu.ZU-sidemenu-animation-allowed { | |
transition: left 0.3s cubic-bezier(0, 0.85, 0.72, 0.99) | |
} | |
.ZU-sidebar-hidden .headmenu { | |
left: 0; | |
} | |
.ZU-settings-dropdown { | |
margin: 4px; | |
top: 30px; | |
padding: 6px 14px; | |
} | |
.ZU-dropdown { | |
display: block; | |
transform: translate(0px, 10px); | |
transition: transform 0.2s, opacity 0.2s, visibility 0s 0.2s; | |
opacity: 0; | |
user-select: none; | |
visibility: hidden; | |
} | |
.ZU-dropdown-show { | |
transform: translate(0px, 0); | |
opacity: 1; | |
visibility: visible; | |
transition: transform 0.2s, opacity 0.2s; | |
} | |
.ZU-settings-dropdown label, | |
.ZU-noko-label { | |
font-weight: normal; | |
margin-bottom: 0; | |
} | |
.ZU-noko-label { | |
vertical-align: middle; | |
} | |
.reply-form-limit-counter { | |
min-width: 60px; | |
display: inline-block; | |
} | |
.ZU-noko { | |
margin: 0; | |
vertical-align: -1px; | |
} | |
.ZU-settings-dropdown input[type="radio"], | |
.ZU-settings-dropdown input[type="checkbox"] { | |
margin: 3px 0 0; | |
line-height: normal; | |
vertical-align: top; | |
} | |
.ZU-settings-dropdown li { | |
margin: 4px 0; | |
} | |
.ZU-svg-container-btn { | |
font-size: 0; | |
padding: 0; | |
line-height: 0; | |
} | |
.ZU-svg { | |
fill: currentColor; | |
} | |
.ZU-svg-32 { | |
height: 32px; | |
width: 32px; | |
} | |
.ZU-svg-16 { | |
height: 16px; | |
width: 16px; | |
} | |
.dropdown-menu .ZU-svg-16 { | |
margin-right: 5px; | |
} | |
.ZU-share-dropdown { | |
left: 74px; | |
} | |
.ZU-boardhideunhide { | |
position: absolute; | |
left: 0; | |
opacity: 0; | |
transition: opacity 0.2s, color 0.2s; | |
} | |
.ZU-boardhideunhide:hover { | |
color: #3ccd9d; | |
} | |
.sidemenu-board-item:hover .ZU-boardhideunhide { | |
opacity: 1 | |
} | |
.ZU-board-unhide-icon { | |
display: none; | |
} | |
.ZU-sage-btn:hover { | |
color: #bc1a1a; | |
} | |
body > textarea { | |
position:fixed; | |
} | |
.ZU-show { | |
display:block; | |
} | |
.ZU-refresh-progressbar, | |
.ZU-refreshbtn-shadow-overlay { | |
position: absolute; | |
left: 0; | |
top: 0; | |
height: 100%; | |
} | |
.ZU-refreshbtn-shadow-overlay { | |
width: 100%; | |
} | |
.ZU-refresh-btn { | |
box-shadow: none!important; | |
overflow: hidden; | |
} | |
.ZU-refresh-btn:active .ZU-refreshbtn-shadow-overlay { | |
box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); | |
} | |
.ZU-refresh-progressbar { | |
background: linear-gradient(to bottom, transparent 0%, rgba(22, 160, 133, 0.48) 100%); | |
width: 0%; | |
opacity: 0; | |
transition: width 0s 0.4s, opacity 0.4s; | |
} | |
.ZU-refresh-btn i, | |
.ZU-refresh-btn span { | |
position: relative; | |
} | |
.ZU-nomargin-btn-group { | |
margin-left: -1px; | |
float: right; | |
} | |
.ZU-panel-btn { | |
width: 40px; | |
} | |
.ZU-onactive-show, | |
.active .ZU-onactive-hide { | |
display: none; | |
} | |
.active .ZU-onactive-show { | |
display: block; | |
} | |
.ZU-btn-link { | |
color: #333333 !important; | |
text-decoration: none !important; | |
} | |
.ZU-btn-link.active { | |
color: #333; | |
background-color: #e6e6e6; | |
border: 1px solid #adadad; | |
outline: none !important; | |
background-image: none; | |
box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); | |
} | |
.post-img .post-embed .post-embed-play-btn { | |
z-index: 2 | |
} | |
`) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment