NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Tabun fixes // @version 30.10 // @description Несколько улучшений для табуна // // @updateURL https://raw.githubusercontent.com/lxyd/tabun-fixes/master/dist/tabun-fixes.meta.js // @downloadURL https://raw.githubusercontent.com/lxyd/tabun-fixes/master/dist/tabun-fixes.user.js // // @grant none // // @include http://tabun.everypony.ru/* // @match http://tabun.everypony.ru/* // @include http://tabun.everypony.info/* // @match http://tabun.everypony.info/* // @include https://tabun.everypony.ru/* // @match https://tabun.everypony.ru/* // @include https://tabun.everypony.info/* // @match https://tabun.everypony.info/* // @author eeyup (geostyle mod) // ==/UserScript== (function(document, fn) { var script = document.createElement('script') script.setAttribute("type", "text/javascript") script.textContent = '(' + fn + ')(window, window.document, jQuery)' document.body.appendChild(script) // run the script document.body.removeChild(script) // clean up })(document, function(window, document, $) { // a bit of mimic require.js's way to define a module, but // - no dynamic file loading (!) // - no module hierarchy supported: module name is interpreted as a plain string // - no requirejs plugins, no shim etc var define = (function() { var awaiting = {} , unresolved = {} , modules = {} , nextName = null /** * Define a module with a given name in a way syntactically close to require.js * * Ways to call: * * 1) define('module-name', ['deps', 'list'], module) * 2) define('module-name', module) * * 3) define('next-module-name', []) - speciall call with name and no module is * meant to be used by a build system to * set next-module-name for future calls * 4) define(['deps', 'list'], module) - like #1 but using next-module-name as name * 5) define(module) - like #2 but using next-module-name as name * */ function define(name, deps, module) { if (typeof name == 'string' && Array.isArray(deps) && !module) { nextName = name return } if (typeof name != 'string') { if (!nextName) { throw new Error("Module name no set. Be sure to pass module name or call define('next-name', []) before defining a module") } module = deps deps = name name = nextName } if (typeof module == 'undefined') { module = deps deps = null } if (defined(name) || awaiting[name]) { throw new Error("Module '" + name + "' already defined") } nextName = null // consume next-module-name deps = deps || [] awaiting[name] = { deps: deps, module: module, } deps.forEach(function(d) { if (!defined(d)) { unresolved[d] = unresolved[d] || [] unresolved[d].push(name) } }) tryToDefineAwaiting(name) } function tryToDefineAwaiting(name) { var m = awaiting[name] if (m && m.deps.every(defined)) { modules[name] = createModule(m.module, m.deps) delete awaiting[name] ;(unresolved[name] || []).forEach(tryToDefineAwaiting) delete unresolved[name] } } function createModule(module, deps) { if (typeof module == 'function') { return module.apply(null, deps.map(require)) } else { return module } } function require(name) { if (!defined(name)) { throw new Error("Module '" + name + "' not defined") } return modules[name] } function defined(name) { return typeof modules[name] != 'undefined' } modules['require'] = require return define })() define('app', []) define(['module', 'deep', 'require'], function(Module, deep, require) { function App(id) { this._id = id // для отделения хранимых настроек разных приложений на случай, если их будет несколько this._moduleIds = [] // id модулей в порядке их добавления в приложение /** * _modules содержит словать id -> объект вида { * id: 'module-id', // уникальный идентификатор модуля в приложении * module: {}, // объект модуля, либо добавленный в приложение явно, либо созданный с помощью * // конструктора, полученного через require(id) * installParams: [], // дополнительные параметры, привязанные к установленному модулю, например * // служебная информация для gui-config'а * initArgs: [], // аргументы, которые будут переданы модулю при инициализации (метод init) * } */ this._modules = {} this._started = false // эти данные будут загружены позже this._configs = null // сохранённые конфиги модулей this._enabled = null // сохранённые данные о состоянии модулей (enabled/disabled) this._dirty = {} // id модулей, которые стали dirty: они не делают update или remove до перезагрузки страницы } /** * Добавить модуль в приложение * * @moduleOrId - экземпляр модуля или строковый id в таблице конструкторов модулей * @installParams - параметры для установки модуля: * { * id: 'module-id', // строковый идентификатор модуля. Optional, если в moduleOrId был передан ключ * defaultEnabled: true/false, // включён ли модуль по умолчанию, когда настроек ещё нет. Optional: по умолчанию false * ... // прочие необязательные параметры установки для дальнейшего использования * } * @initArgs... - аргументы для module.init() (передаются после параметра config) */ App.prototype.add = function app_addModule(moduleOrId, installParams/*, initArgs...*/) { this._checkStarted(false) installParams = installParams || {} var module, id if (typeof moduleOrId == "string") { id = installParams.id || moduleOrId module = new (require(moduleOrId))() } else { if (!installParams.id) { throw new Error("Install parameters must contain 'id' field") } id = installParams.id module = moduleOrId } this._moduleIds.push(id) this._modules[id] = { id: id, module: module, installParams: installParams, initArgs: [].slice.call(arguments, 2), } module.onAdded(this, id) return this // chain } /** * Запустить приложение с добавленными модулями */ App.prototype.start = function app_start() { this._checkStarted(false) this._started = true // больше нельзя вызвать add и start, зато можно делать всё остальное var data = this._loadData() this._configs = data.configs this._enabled = data.enabled for (var id in this._modules) { var moduleInfo = this._modules[id] if (this._enabled[id] == null && moduleInfo.installParams.defaultEnabled) { this._enabled[id] = true } var args = [this._configs[id] || null].concat(moduleInfo.initArgs) var cfg = moduleInfo.module.init.apply(moduleInfo.module, args) this._configs[id] = deep.clone(cfg) } this._saveData() for (var id in this._modules) { var moduleInfo = this._modules[id] if (this._enabled[id]) { moduleInfo.module.attach(deep.clone(this._configs[id])) } } return this // chain } App.prototype.getModuleIds = function app_getModuleIds() { return this._moduleIds } App.prototype.getModule = function app_getModule(id) { return this._modules[id].module } App.prototype.getModuleInstallParams = function app_getModuleInstallParams(moduleOrId) { var id = this._getModuleId(moduleOrId) return this._modules[id].installParams } App.prototype.isModuleEnabled = function app_isModuleEnabled(moduleOrId) { this._checkStarted(true) return this._enabled[this._getModuleId(moduleOrId)] } App.prototype.isModuleDirty = function app_isModuleDirty(moduleOrId) { this._checkStarted(true) return this._dirty[this._getModuleId(moduleOrId)] } App.prototype.setModuleEnabled = function app_setModuleEnabled(moduleOrId, enabled) { this._checkStarted(true) enabled = !!enabled var id = this._getModuleId(moduleOrId) if (this.isModuleEnabled(id) == enabled) { return } this._enabled[id] = enabled this._postSaveData() if (this._dirty[id]) { return } try { if (enabled) { this._modules[id].module.attach(deep.clone(this._configs[id])) } else { if (this._modules[id].module.detach() == false) { this._dirty[id] = true } } } catch (err) { this.log("Error " + (enabled ? "enabling" : "disabling") + " module " + id, err) this._dirty[id] = true this._enabled[id] = false } } App.prototype.getModuleConfig = function app_getModuleConfig(moduleOrId) { this._checkStarted(true) var id = this._getModuleId(moduleOrId) return deep.clone(this._configs[id]) } App.prototype.setModuleConfig = function app_setModuleConfig(moduleOrId, config, omitModuleUpdate) { this._checkStarted(true) var id = this._getModuleId(moduleOrId) if (deep.equals(this._configs[id], config)) { return } this._configs[id] = deep.clone(config) this._postSaveData() if (omitModuleUpdate || this._dirty[id] || !this._enabled[id]) { return } try { if (this._modules[id].module.update(config) == false) { this._dirty[id] = true } } catch (err) { this.log("Error updating module " + id, err) this._dirty[id] = true this._enabled[id] = false } } App.prototype.reconfigure = function app_reconfigure() { this._checkStarted(true) data = this._loadData() for (id in data.enabled) { if (this._modules[id]) { this.setModuleEnabled(id, data.enabled[id]) } } for (id in data.configs) { if (this._modules[id]) { this.setModuleConfig(id, data.configs[id]) } } } App.prototype.getId = function app_getId() { return this._id } App.prototype.log = function app_log(/*args*/) { if (console && console.log) { console.log.apply(console, arguments) } } App.prototype._checkStarted = function app_checkStarted(started) { if (this._started != started) { throw new Error(this._started ? "App is already started" : "App is not started yet") } } App.prototype._getModuleId = function app_getModuleId(moduleOrId) { if (typeof moduleOrId == "string") { return moduleOrId } else { return moduleOrId.getId() } } App.prototype._loadData = function app_loadData() { var data try { data = JSON.parse(localStorage.getItem(this._id + '-data') || "{}") || {} data.configs = data.configs || {} data.enabled = data.enabled || {} } catch (err) { this.log(err) data = { configs: {}, enabled: {}, } } return data } App.prototype._saveData = function app_saveData() { delete this._saveDataTimeout var data = { configs: this._configs, enabled: this._enabled, } localStorage.setItem(this._id + '-data', JSON.stringify(data)) } App.prototype._postSaveData = function app_postSaveData() { if (!this._saveDataTimeout) { this._saveDataTimeout = setTimeout(this._saveData.bind(this), 0) } } return App }) define('deep', []) define({ clone: function deepClone(o) { var r, i, keys if (o && typeof o == 'object') { r = Array.isArray(o) ? [] : {} keys = Object.keys(o) for (i = 0; i < keys.length; i++) { r[keys[i]] = deepClone(o[keys[i]]) } } return r || o }, equals: function deepEquals(a, b) { if (!a || typeof a != 'object') { return a === b } if (!b || typeof b != 'object' || Array.isArray(a) != Array.isArray(b)) { return false } var ka = Object.keys(a) if (ka.length != Object.keys(b).length) { return false } for (var i = 0; i < ka.length; i++) { if (!(ka[i] in b)) { return false } if (!deepEquals(a[ka[i]], b[ka[i]])) { return false } } return true }, }) define('hook', []) define(function() { // configuration values before, after, orig, proxies look like: // [{ // "key": obj, // "vals": { // "someFieldName": fieldValueOrArrayOfValues, // ... // }, // }] var cfg = { before: [], after: [], orig: [], proxies: [], } var global = this /** * Add a function to be run before (after) execution some other * function available at root.path * * @param root - optional, global object if ommited * @param path - path to root's field * @param fn - function to execute before (after) root.path * if fn returns false, execution is terminated * @param options { * after : boolean - execute fn after function (default false) * force : boolean - force re-writing a proxy even if already written * (for the case it has been removed somewhere else) * } */ function addHook(root, path, fn, options) { var args = parseArgs.apply(this, arguments) root = args.root path = args.path fn = args.fn options = args.options var cur = getPath(root, path) if (typeof cur != 'function') { throw new Error("Only functions might be hooked") } addCfgElement(options.after ? cfg.after : cfg.before, root, path, fn) var proxy = getCfg(cfg.proxies, root, path) , orig = getCfg(cfg.orig, root, path) var write = false if (!proxy) { // proxy was not written yet orig = cur proxy = createProxy(root, path, cur) write = true } else if (cur !== proxy && options.force) { // proxy was written, but currently there is another function at root[path] // according to the force option, consider proxy to be thrown away orig = cur proxy = createProxy(root, path, cur) write = true } // otherwise consider proxy written and functional (but possibly hidden behind another proxy) if (write) { setCfg(cfg.orig, root, path, orig) setCfg(cfg.proxies, root, path, proxy) setPath(root, path, proxy) } } /** * Remove previously added hook * * @param root - optional, global object if ommited * @param path - path to root's field * @param fn - function to execute before (after) root.path * if fn returns false, execution is terminated * @param options { * after : boolean - execute fn after function (default false) * } */ function removeHook(root, path, fn, options) { var args = parseArgs.apply(this, arguments) root = args.root path = args.path fn = args.fn options = args.options removeCfgElement(options.after ? cfg.after : cfg.before, root, path, fn) if ((getCfg(cfg.after, root, path)||[]).length + (getCfg(cfg.before, root, path)||[]).length == 0) { removeAllHooks(root, path) } } function removeAllHooks(root, path) { var args = parseArgs.apply(this, arguments) root = args.root path = args.path removeCfg(cfg.before, root, path) removeCfg(cfg.after, root, path) var orig = getCfg(cfg.orig, root, path) , proxy = getCfg(cfg.proxies, root, path) , cur = getPath(root, path) // replace function with orig if we are currently on top of proxy chain if (cur === proxy) { setPath(root, path, orig) } // if cur !== proxy, leak a bit of memory by leaving and forgetting // our proxy in root.path proxy chain removeCfg(cfg.proxies, root, path) removeCfg(cfg.orig, root, path) } function parseArgs(root, path, fn, options) { if (typeof root == 'string') { options = fn fn = path path = root root = global } if (!root) { root = global } if (!path) { throw new Error("Path must not be empty") } if (typeof options == 'boolean') { options = { after: options } } else { options = options || {} } var p = path.split('.') for (var i = 0; i < p.length-1; i++) { root = root[p[i]] path = p[i+1] if (!root) { throw new Error("Path not found: " + p[i]) } } return { root: root, path: path, fn: fn, options: options, } } function createProxy(root, path, orig) { return function() { return doProxy(root, path, orig, arguments) } } function doProxy(root, path, orig, args) { var hooks , res , origRes hooks = getCfg(cfg.before, root, path) || [] // if any before-hook returns a value, stop // execution and return that value for (var i = 0; i < hooks.length; i++) { try { res = hooks[i].call(root, args) } catch (err) { return } if (typeof res != 'undefined') { return res } } origRes = orig.apply(root, args) hooks = getCfg(cfg.after, root, path) || [] for (var i = 0; i < hooks.length; i++) { try { res = hooks[i].call(root, origRes, args) } catch (err) { return } if (typeof res != 'undefined') { return res } } return origRes } function getPath(root, path) { return root[path] } function setPath(root, path, val) { root[path] = val } function getCfg(cfg, key, path) { return (findCfgForKey(cfg, key)||{})[path] } function setCfg(cfg, key, path, val) { var vals = findCfgForKey(cfg, key) if (!vals) { vals = {} cfg.push({ key: key, vals: vals, }) } vals[path] = val } function removeCfg(cfg, key, path) { var vals = findCfgForKey(cfg, key) if (!vals) { return } delete vals[path] if (Object.keys(vals).length == 0) { removeCfgForKey(cfg, key) } } function addCfgElement(cfg, key, path, val) { removeCfgElement(cfg, key, path, val) var vals = findCfgForKey(cfg, key) if (!vals) { vals = {} cfg.push({ key: key, vals: vals, }) } vals[path] = vals[path] || [] if (!Array.isArray(vals[path])) { throw new Error("Cannot add to non-array element") } vals[path].push(val) } function removeCfgElement(cfg, key, path, val) { var vals = findCfgForKey(cfg, key) if (!vals || !(path in vals)) { return } var idx = -1 for (var i = 0; i < vals[path].length; i++) { if (vals[path][i] === val) { idx = i break } } if (idx >= 0) { vals[path].splice(idx, 1) } if (vals[path].length == 0) { delete vals[path] } if (Object.keys(vals).length == 0) { removeCfgForKey(cfg, key) } } function findCfgForKey(cfg, key) { for (var i = 0; i < cfg.length; i++) { var o = cfg[i] if (key == o.key) { return o.vals } } } function removeCfgForKey(cfg, key) { var idx = -1 for (var i = 0; i < cfg.length; i++) { var o = cfg[i] if (key == o.key) { idx = i break } } if (idx >= 0) { cfg.splice(idx, 1) } } return { add: addHook, remove: removeHook, removeAll: removeAllHooks, } }) define('module', []) define(function() { function Module() { } Module.prototype.onAdded = function module_onAdded(app, id) { this._app = app this._id = id } Module.prototype.isEnabled = function module_isEnabled() { return this._app.isModuleEnabled(this._id) } Module.prototype.isDirty = function module_isDirty() { return this._app.isModuleDirty(this._id) } Module.prototype.getId = function module_getId() { return this._id } Module.prototype.getApp = function module_getApp() { return this._app } Module.prototype.getInstallParams = function module_getInstallParams() { return this._app.getModuleInstallParams(this._id) } Module.prototype.getConfig = function module_getConfig() { return this._app.getModuleConfig(this._id) } Module.prototype.saveConfig = function module_saveConfig(config) { this._app.setModuleConfig(this._id, config, true) } // Реализации по умолчанию для методов жизненного цикла Module.prototype.getLabel = function module_getLabel() { return this._id } Module.prototype.init = function module_init(config/*, initArgs...*/) { return config } Module.prototype.attach = function module_attach(config) { throw new Error("Not implemented") } Module.prototype.update = function module_update(config) { if (this.detach() == false) { return false } this.attach(config) return true } Module.prototype.detach = function module_detach() { return false } return Module }) define('add-onclick-to-spoilers', []) /** * Этот модуль добавляет пустой атрибут onclik к заголовкам спойлеров, * чтобы VimFx воспринимал спойлеры как кликабельный объект и давал * кликать по ним без мышки, с помощью клавиатуры */ define(['jquery', 'module', 'ls-hook'], function($, Module, lsHook) { function AddOnClickToSpoilersModule() { } AddOnClickToSpoilersModule.prototype = new Module() AddOnClickToSpoilersModule.prototype.attach = function addOnClickToSpoilers_attach(config) { this._hook = this.onDataLoaded.bind(this) lsHook.add('ls_comments_load_after', this._hook) lsHook.add('ls_userfeed_get_more_after', this._hook) this.addAttr() } AddOnClickToSpoilersModule.prototype.detach = function addOnClickToSpoilers_detach() { lsHook.remove('ls_comments_load_after', this._hook) lsHook.remove('ls_userfeed_get_more_after', this._hook) this._hook = null this.removeAttr() } AddOnClickToSpoilersModule.prototype.onDataLoaded = function addOnClickToSpoilers_onDataLoaded() { this.addAttr() } AddOnClickToSpoilersModule.prototype.addAttr = function addOnClickToSpoilers_addAttr() { $('.spoiler-title').each(function() { if (!this.hasAttribute('onclick')) { this.setAttribute('onclick', '') } }) } AddOnClickToSpoilersModule.prototype.removeAttr = function addOnClickToSpoilers_removeAttr() { // в старой версии табуна спойлеры создавались с onclick="return true;" // соответственно, из этих спойлеров удалять атрибут не будем: удаляем только // пустые атрибуты, созданные этим плагином $('.spoiler-title').each(function() { if (this.getAttribute('onclick') == '') { this.removeAttribute('onclick') } }) } return AddOnClickToSpoilersModule }) define('alter-links-to-mirrors', []) define(['module'], function(Module) { function AlterLinksToMirrorsModule() { } AlterLinksToMirrorsModule.prototype = new Module() AlterLinksToMirrorsModule.prototype.init = function alterLinksToMirrors_init(config) { this.host = window.location.host this.mirrors = [ 'tabun.everypony.ru', 'tabun.everypony.info', 'табун.всепони.рф', ].filter(function(h) { return h != this.host }) return config } AlterLinksToMirrorsModule.prototype.getLabel = function alterLinksToMirrors_getLabel() { return "Открывать ссылки на другие зеркала (" + this.mirrors.join(', ') + ") на текущем зеркале (" + this.host + ")" } AlterLinksToMirrorsModule.prototype.attach = function alterLinksToMirrors_attach(config) { this.handler = (function(ev) { this.changeAnchorHrefForClick(closestAnchor(ev.target)) }).bind(this) document.addEventListener('click', this.handler, true) } AlterLinksToMirrorsModule.prototype.detach = function alterLinksToMirrors_detach() { document.removeEventListener('click', this.handler, true) this.handler = null } AlterLinksToMirrorsModule.prototype.changeAnchorHrefForClick = function alterLinksToMirrors_changeAnchorHrefForClick(a) { if (!isAnchorWithHref(a) || this.mirrors.indexOf(a.hostname) < 0 || isSiteRootAnchor(a)) { // Ничего не трогаем, если нам дали не элемент <a>, // либо элемент <a> без атрибута href, // либо ссылку на левый сайт, не являющийся зеркалом табуна // Также ссылки на корни зеркал трогать не будем: // они, вероятно, ведут туда намеренно return } var backup = a.href // на время клика подменим hostname a.hostname = this.host // сразу после клика вернём всё как было setTimeout(function() { a.href = backup }, 0) } function closestAnchor(el) { while (el instanceof HTMLElement) { if (el.nodeName.toUpperCase() == 'A') { return el } else { el = el.parentNode } } return null } function isAnchorWithHref(el) { return el instanceof HTMLElement && el.nodeName.toUpperCase() == 'A' && el.href } function isSiteRootAnchor(a) { return !a.pathname || a.pathname == '/' } return AlterLinksToMirrorsModule }) define('alter-same-page-links', []) define(['module'], function(Module) { var mirrors = [ 'tabun.everypony.ru', 'tabun.everypony.info', 'табун.всепони.рф', ] function AlterSamePageLinksModule() { } AlterSamePageLinksModule.prototype = new Module() AlterSamePageLinksModule.prototype.init = function alterSamePageLinks_init(config) { this.cssClass = this.getApp().getId() + '-same-page-anchor' return config } AlterSamePageLinksModule.prototype.getLabel = function alterSamePageLinks_getLabel() { return "При клике на ссылку на коммент, находящийся на текущей странице, сразу скроллить на него (такие ссылки будут зеленеть при наведении)" } AlterSamePageLinksModule.prototype.attach = function alterSamePageLinks_attach(config) { this.clickHandler = this.onClick.bind(this) this.mouseOverHandler = this.onMouseOver.bind(this) this.mouseOutHandler = this.onMouseOut.bind(this) document.addEventListener('click', this.clickHandler) document.addEventListener('mouseover', this.mouseOverHandler) document.addEventListener('mouseout', this.mouseOutHandler) this.style = $('<style>').text( 'A.' + this.cssClass + ', ' + 'A.' + this.cssClass + ':hover, ' + 'A.' + this.cssClass + ':visited {color: #0A0 !important}') .appendTo(document.head) } AlterSamePageLinksModule.prototype.detach = function alterSamePageLinks_detach() { document.removeEventListener('click', this.clickHandler) document.removeEventListener('mouseover', this.mouseOverHandler) document.removeEventListener('mouseout', this.mouseOutHandler) this.clickHandler = null this.mouseOverHandler = null this.mouseOutHandler = null this.style.remove() this.style = null $('A.' + this.cssClass).removeClass(this.cssClass) } AlterSamePageLinksModule.prototype.onClick = function alterSamePageLinks_onClick(ev) { if ( ev.which != null && ev.which != 1 || ev.button != null && ev.button != 0 ) { return } var a = closestAnchor(ev.target) , id = getLinkedCommentId(a) if (!isSamePageComment(id)) { return } if (ev.defaultPrevented) { return // something has already handled this event } // TODO : remove this line window.location.hash = "comment" + id /* TODO : uncomment this part // update #hash part of the url avoiding immediate scrolling via mungling anchor's name var elCommentAnchor = $('#comment_id_' + id + ' A[name="comment' + id + '"]') elCommentAnchor.attr('name', 'mungle-comment' + id) window.location.hash = "comment" + id elCommentAnchor.attr('name', 'comment' + id) // TODO : implement for new tabun version // smooth scroll to the element ls.comments.scrollToComment(id) // TODO : remember clicked link and add back link to the target comment */ ev.stopImmediatePropagation() ev.preventDefault() return false } AlterSamePageLinksModule.prototype.onMouseOver = function alterSamePageLinks_onMouseOver(ev) { var a = closestAnchor(ev.target) if (isSamePageComment(getLinkedCommentId(a))) { $(a).addClass(this.cssClass) } } AlterSamePageLinksModule.prototype.onMouseOut = function alterSamePageLinks_onMouseOut(ev) { var a = closestAnchor(ev.target) $(a).removeClass(this.cssClass) } function closestAnchor(el) { while (el instanceof HTMLElement) { if (el.nodeName.toUpperCase() == 'A') { return el } else { el = el.parentNode } } return null } function isAnchorWithHref(el) { return el instanceof HTMLElement && el.nodeName.toUpperCase() == 'A' && el.href } var reCommentInPath = new RegExp('^/comments/([^/]+)/*$') , reCommentInHash = new RegExp('^#comment(.+)$') function getLinkedCommentId(a) { var res if (!isAnchorWithHref(a)) { return null } if (mirrors.indexOf(a.host) < 0) { return null } if (null != (res = (reCommentInPath.exec(a.pathname)||[])[1])) { return res } if (null != (res = (reCommentInHash.exec(a.hash)||[])[1])) { return res } return null } function isSamePageComment(id) { return id != null && document.getElementById('comment_id_' + id) != null } return AlterSamePageLinksModule }) define('autospoiler-images', []) define(['jquery', 'module', 'basic-cfg-panel-applet', 'ls-hook'], function($, Module, BasicCfgPanelApplet, lsHook) { function AutospoilerImagesModule() { } AutospoilerImagesModule.prototype = new Module() AutospoilerImagesModule.prototype.init = function autospoilerImages_init(config) { config = config || { width: 1000, height: 500, inCommentsOnly: true, } this.attrName = this.getApp() + '-' + this.getId() + '-data' return config } AutospoilerImagesModule.prototype.getLabel = function autospoilerImages_getLabel() { return "Автоматически спойлерить картинки" } AutospoilerImagesModule.prototype.attach = function autospoilerImages_attach(config) { this.processPage() this._hook = this.processPage.bind(this) lsHook.add('ls_comments_load_after', this._hook) lsHook.add('ls_userfeed_get_more_after', this._hook) } AutospoilerImagesModule.prototype.detach = function autospoilerImages_detach() { lsHook.remove('ls_comments_load_after', this._hook) lsHook.remove('ls_userfeed_get_more_after', this._hook) delete this._hook this.unprocessPage() } AutospoilerImagesModule.prototype.processPage = function autospoilerImages_processPage() { $('IMG').not('.spoiler-body IMG').each(function(_, e) { // HACK: XXX: 40 px is arbitrary non-loaded image width // TODO: implement more reliable way to determine not loaded image if (e.width > 40 || e.height > 40) { this.processImage(e) } else { // either wait for full load // or just let the img element find out the image's size this.waitForImage(e) } }.bind(this)) } AutospoilerImagesModule.prototype.unprocessPage = function autospoilerImages_unprocessPage() { $('SPAN.spoiler').each(function(_, e) { var data = $(e).data(this.attrName) if (data && data.spoileredElement) { e.parentNode.insertBefore(data.spoileredElement, e) e.parentNode.removeChild(e) } }.bind(this)) } AutospoilerImagesModule.prototype.waitForImage = function autospoilerImages_waitForImage(e) { var timeout = setTimeout(function() { this.processImage(e) }.bind(this), 1000) , loadListener = function() { clearTimeout(timeout) this.processImage(e) }.bind(this) e.addEventListener('load', loadListener) } AutospoilerImagesModule.prototype.processImage = function autospoilerImages_processImage(e) { // HACK: prevent rare double-spoilering if ($(e).is('.spoiler IMG')) { return } // HACK: prevent processing image after module is disabled if (!this.isEnabled()) { return } var cfg = this.getConfig() if (cfg.inCommentsOnly && !$(e).is('.comment IMG')) { return } if (e.width > cfg.width) { this.spoiler(e, 'ширина ' + e.width + 'px') } else if (e.height > cfg.height) { this.spoiler(e, 'высота ' + e.height + 'px') } } AutospoilerImagesModule.prototype.spoiler = function autospoilerImages_spoiler(img, reason) { var spoilerBody $(img).after( $('<SPAN>') .attr('class', 'spoiler') .data(this.attrName, { spoileredElement: img }) .append( $('<SPAN>') .attr('class', 'spoiler-title') .attr('onclick', '') .text('[КАРТИНКА (' + reason + ')]'), spoilerBody = $('<SPAN>') .attr('class', 'spoiler-body') .css({display: 'none'}) ) ) spoilerBody.append(img) } AutospoilerImagesModule.prototype.createCfgPanelApplet = function autospoilerImages_createCfgPanelApplet() { var txtWidth = $('<input>') .attr('type', 'text') .attr('name', 'width') .css({ width: 35, }) var txtHeight = $('<input>') .attr('type', 'text') .attr('name', 'height') .css({ width: 35, }) var chkInCommentsOnly = $('<input>') .attr('type', 'checkbox') .attr('name', 'inCommentsOnly') var lblInCommentsOnly = $('<label>') .text(' автоспойлерить только в комментариях') .prepend(chkInCommentsOnly) return new BasicCfgPanelApplet( "Автоматически спойлерить картинки", " больше", txtWidth, "px шириной или ", txtHeight, "px высотой", " ( ", lblInCommentsOnly, ")" ) } return AutospoilerImagesModule }) define('basic-cfg-panel-applet', []) define(['jquery', 'deep', 'cfg-panel-applet'], function($, deep, CfgPanelApplet) { function BasicCfgPanelApplet(label/*, elements... */) { this._label = label this._elements = [].slice.call(arguments, 1) this._ui = null this._chkEnabled = null } BasicCfgPanelApplet.prototype = new CfgPanelApplet() BasicCfgPanelApplet.prototype.build = function basicCfgPanelApplet_build() { this._ui = $('<div>') this._chkEnabled = $('<input type="checkbox">') $('<label>').append(this._chkEnabled, this._label).appendTo(this._ui) this._ui.append(this._elements) this._chkEnabled.on('change', function() { this.updateControlsEnabled(getVal(this._chkEnabled)) }.bind(this)) return this._ui[0] } BasicCfgPanelApplet.prototype.setData = function basicCfgPanelApplet_setData(enabled, config) { CfgPanelApplet.prototype.setData.apply(this, arguments) // call to super() setVal(this._chkEnabled, enabled) this.updateControls(config) // possibly overridden this.updateControlsEnabled(enabled) // possibly overridden } BasicCfgPanelApplet.prototype.getEnabled = function basicCfgPanelApplet_getEnabled() { return getVal(this._chkEnabled) } // functions to be overridden by children classes BasicCfgPanelApplet.prototype.getConfig = function basicCfgPanelApplet_getConfig() { var res = CfgPanelApplet.prototype.getConfig.apply(this, arguments) // call to super() res = res || {} this._ui.find('INPUT[name],TEXTAREA[name]').each(function() { var el = $(this) res[el.attr('name')] = getVal(el) }) return res } BasicCfgPanelApplet.prototype.updateControls = function basicCfgPanelApplet_updateControls(config) { config = config || {} this._ui.find('INPUT[name],TEXTAREA[name]').each(function() { var el = $(this) , name = el.attr('name') if (name in config) { setVal(el, config[name]) } }) } BasicCfgPanelApplet.prototype.updateControlsEnabled = function basicCfgPanelApplet_updateControlsEnabled(enabled) { this._ui.find('INPUT[name],TEXTAREA[name]').each(function() { var el = $(this) el.attr('disabled', enabled ? null : 'disabled') }) } // utility functions function getVal(el) { // TODO: radio if (el.is('INPUT[type=checkbox]')) { return el.is(':checked') } else if (el.is('INPUT[type=number]')) { return parseFloat(el.val()) } else { return el.val() } } function setVal(el, val) { // TODO: radio if (el.is('INPUT[type=checkbox]')) { el.attr('checked', val ? 'checked' : null) } else { return el.val(val) } } return BasicCfgPanelApplet }) define('cfg-panel-applet', []) define(['jquery', 'deep'], function($, deep) { /** * Interface: * - build() -> HtmlElement * - setData(enabled, config) * - getEnabled() -> bool * - getConfig() -> {} */ function CfgPanelApplet() { } // functions to be overridden by children classes CfgPanelApplet.prototype.build = function cfgPanelApplet_build() { throw new Error("not implemented") } CfgPanelApplet.prototype.setData = function cfgPanelApplet_setData(enabled, config) { this.config = config this.enabled = enabled } CfgPanelApplet.prototype.getEnabled = function cfgPanelApplet_getEnabled() { return this.enabled } CfgPanelApplet.prototype.getConfig = function cfgPanelApplet_getConfig() { return deep.clone(this.config) } return CfgPanelApplet }) define('cfg-panel', []) define(['jquery', 'module', 'app', 'basic-cfg-panel-applet', 'img/gear'], function($, Module, App, BasicCfgPanelApplet, imgGear) { function CfgPanel() { } CfgPanel.prototype = new Module() CfgPanel.prototype.attach = function cfgPanel_attach(config) { this._collectApplets() this._btn = $('<a href="#" class="tabun_fixes">').css({ background: 'url("' + imgGear + '") no-repeat 50% 50%', }).on('click', function() { if (this._dialog) { this._closeDialog() } else { this._openDialog() } return false }.bind(this)) $('#widemode').append(this._btn) } CfgPanel.prototype.detach = function cfgPanel_detach() { this._closeDialog() this._btn.remove() this._btn = null return true } CfgPanel.prototype._collectApplets = function cfgPanel_collectApplets() { this._columns = [] var curColIdx = 1 this.getApp().getModuleIds().forEach(function(id) { var module = this.getApp().getModule(id) , params = (module.getInstallParams() || {}).cfgPanel if (!params || params.skip) { return } curColIdx = params.column || curColIdx var col = this._columns[curColIdx] = this._columns[curColIdx] || [] , applet if (module.createCfgPanelApplet) { applet = module.createCfgPanelApplet() } else { applet = new BasicCfgPanelApplet(module.getLabel()) } col.push({ id: id, module: module, applet: applet, }) }, this) this._columns = this._columns.filter(function(c) { return c && c.length }) } CfgPanel.prototype._closeDialog = function cfgPanel_closeDialog() { this._dialog.remove() this._dialog = null } CfgPanel.prototype._openDialog = function cfgPanel_createUi() { this._dialog = $('<div>') this._applets = {} var tds = [] this._columns.forEach(function(col, colIdx) { col.forEach(function(applet) { td = tds[colIdx] = tds[colIdx] || $('<td>') this._applets[applet.id] = { id: applet.id, applet: applet.applet, module: applet.module, ui: $(applet.applet.build()) .css('margin-bottom', '10px') .css('background', applet.module.isDirty() ? '#FDD' : 'none') // TODO : theme .appendTo(td), } }.bind(this)) }.bind(this)) this._dialog = $('<div>').attr('id', this.getApp().getId() + '-cfg-panel').css({ position: 'fixed', right: 6, bottom: 30, width: 450 * tds.length, zIndex: 10000, background: 'White', // TODO : theme border: "1px solid Silver", // TODO : theme borderRadius: 6, padding: 10 }) $('<div>').text("Настройки userscript'а TabunFixes").css({ fontWeight: 'bold', textAlign: 'center', marginBottom: 15 }).appendTo(this._dialog) var table = $('<table>').css({ border: "none", width: "100%" }).appendTo(this._dialog) var tr = $('<tr>').appendTo(table) var tdCss = { padding: '5px', verticalAlign: 'top' } var tdWidth = Math.floor(100 / tds.length) + '%' tds.forEach(function(td, i) { td.attr('width', tdWidth).css(tdCss).appendTo(tr) if (i > 0) { td.css('border-left', '1px solid #EEE') // TODO : theme } }.bind(this)) var ctlPanel = $('<div>').appendTo(this._dialog) $('<a href="#">').text("Сохранить конфигурацию").on('click', function() { if (this._saveConfig()) { this._closeDialog() } return false }.bind(this)).appendTo(ctlPanel) $('<a href="#">').text("Отмена").css('float', 'right').on('click', function() { this._closeDialog() return false }.bind(this)).appendTo(ctlPanel) $(document.body).append(this._dialog) this._setAppletsData() } CfgPanel.prototype._setAppletsData = function cfgPanel_setAppletsData() { var app = this.getApp() for (id in this._applets) { var a = this._applets[id] a.applet.setData(app.isModuleEnabled(a.module), app.getModuleConfig(a.module)) } } CfgPanel.prototype._saveConfig = function cfgPanel_saveConfig() { var app = this.getApp() , res = true , errs = null , cfgs = {} , enab = {} for (id in this._applets) { var a = this._applets[id] try { cfgs[id] = a.applet.getConfig() enab[id] = a.applet.getEnabled() } catch (err) { res = false errs = (errs ? errs + "\n" : "") + err.message } } if (!res) { alert("Ошибка сохранения конфига" + (errs ? ":\n" + errs : "")) return false } for (id in this._applets) { var a = this._applets[id] app.setModuleConfig(a.module, cfgs[id]) app.setModuleEnabled(a.module, enab[id]) } return true } return CfgPanel }) define('fast-scroll-to-comment', []) define(['jquery', 'module', 'hook'], function($, Module, hook) { function FastScrollToCommentModule() { } FastScrollToCommentModule.prototype = new Module() FastScrollToCommentModule.prototype.attach = function fastScrollToComment_attach(config) { this._hook = this.onGoToComment.bind(this) hook.add('ls.comments.goToNextComment', this._hook) } FastScrollToCommentModule.prototype.detach = function fastScrollToComment_detach() { hook.remove('ls.comments.goToNextComment', this._hook) this._hook = null } FastScrollToCommentModule.prototype.onGoToComment = function fastScrollToComment_onGoToComment() { $(window).stop(true) } return FastScrollToCommentModule }) define('fav-as-icon', []) define(['jquery', 'module', 'img/star-big-checked', 'img/star-big-unchecked', 'img/star-small-checked', 'img/star-small-unchecked'], function($, Module, imgStarBigChecked, imgStarBigUnchecked, imgStarSmallChecked, imgStarSmallUnchecked) { function FavAsIconModule() { } FavAsIconModule.prototype = new Module() FavAsIconModule.prototype.getLabel = function favAsIcon_getLabel() { return 'Заменить "В избранное" на звёздочку' } FavAsIconModule.prototype.attach = function favAsIcon_attach(config) { this.style = $('<style>').text( '.comment-info .favourite:before { ' + genFavBeforeStyle(11, 11, imgStarSmallUnchecked) + ' }' + '.comment-info .favourite.active:before { ' + genFavBeforeStyle(11, 11, imgStarSmallChecked) + ' }' + '.topic-info .favourite:before { ' + genFavBeforeStyle(11, 11, imgStarSmallUnchecked) + ' }' + '.topic-info .favourite.active:before { ' + genFavBeforeStyle(11, 11, imgStarSmallChecked) + ' }' + '.table-talk .favourite:before { ' + genFavBeforeStyle(17, 17, imgStarBigUnchecked) + ' }' + '.table-talk .favourite.active:before { ' + genFavBeforeStyle(17, 17, imgStarBigChecked) + ' }' + '.comment-info .favourite { ' + genFavStyle(11, 11) + ' }' + '.topic-info .favourite { ' + genFavStyle(11, 11) + ' }' + '.table-talk .favourite { ' + genFavStyle(17, 17) + ' }' + '.topic-info-favourite.favourite { padding:0 !important; margin:6px 11px 6px 0px }' ).appendTo(document.head) } FavAsIconModule.prototype.detach = function favAsIcon_detach() { if (this.style) { this.style.remove() } this.style = null } function genFavBeforeStyle(w, h, img) { return 'width:'+w+'px; height:'+h+'px; display:inline-block; content:" "; background:url("'+img+'")' } function genFavStyle(w, h) { return 'width:'+w+'px; height:'+h+'px; display:inline-block; overflow:hidden' } return FavAsIconModule }) define('favicon-unread-count', []) define(['module', 'ls-hook', 'img/favicon'], function(Module, lsHook, imgFavicon) { function FaviconUnreadCountModule() { } FaviconUnreadCountModule.prototype = new Module() FaviconUnreadCountModule.prototype.getLabel = function faviconUnreadCount_getLabel() { return "В иконке сайта показывать кол-во непрочитанных комментов" } FaviconUnreadCountModule.prototype.attach = function faviconUnreadCount_attach(config) { this.onTick = this.updateFavicon.bind(this) this.data = this.prepareData() this.interval = setInterval(this.onTick, 1000) this.updateFavicon() } FaviconUnreadCountModule.prototype.detach = function faviconUnreadCount_detach() { if (this.interval) { clearInterval(this.interval) } delete this.interval delete this.onTick // revert favicon if (this.data) { this.data.eFavLink.setAttribute('href', this.data.bakHref) reAttachEl(this.data.eFavLink) } delete this.data } FaviconUnreadCountModule.prototype.prepareData = function faviconUnreadCount_prepareCanvas() { var eFavLink = document.head.querySelector('LINK[rel~="icon"]') , bakHref = eFavLink.getAttribute('href') , eFavicon = new Image() , curCnt = 0 , eCanvas = document.createElement('canvas') , ctx = eCanvas.getContext('2d') , dimen = 64 , pad = 4 , fontSizeNormal = -1 , fontSize100 = -1 , fontSizeMoreThan100 = -1 eCanvas.setAttribute('width', dimen) eCanvas.setAttribute('height', dimen) // calculate font sizes for (var s = 32; s > 0; s--) { setFontSize(ctx, s) if (fontSizeNormal == -1 && ctx.measureText("'00").width < dimen - 2*pad) { fontSizeNormal = s } if (fontSize100 == -1 && ctx.measureText("100").width < dimen - 2*pad) { fontSize100 = s } if (ctx.measureText(">100").width < dimen - 2*pad) { fontSizeMoreThan100 = s break } } eFavicon.onload = this.updateFavicon.bind(this) eFavicon.src = imgFavicon return { bakHref: bakHref, eFavLink: eFavLink, eFavicon: eFavicon, eCanvas: eCanvas, ctx: ctx, fontSizeNormal: fontSizeNormal, fontSize100: fontSize100, fontSizeMoreThan100: fontSizeMoreThan100, dimen: dimen, pad: pad, } } FaviconUnreadCountModule.prototype.updateFavicon = function faviconUnreadCount_updateFavicon() { if (!this.data) { return } var cnt = getCountToDisplay() if (cnt != this.data.curCnt) { this.data.curCnt = cnt this.redraw() } } FaviconUnreadCountModule.prototype.redraw = function faviconUnreadCount_redraw() { var w = this.data.eFavicon.width , h = this.data.eFavicon.height , dimen = this.data.dimen , curCnt = this.data.curCnt this.data.ctx.clearRect(0, 0, dimen, dimen) if (w > 0 && h > 0) { // draw favicon this.data.ctx.scale(dimen/w, dimen/h) this.data.ctx.drawImage(this.data.eFavicon, 0, 0) this.data.ctx.scale(w/dimen, h/dimen) } // draw text if (curCnt == 0) { // do nothing } else if (curCnt < 100) { this.drawCnt(curCnt, this.data.fontSizeNormal) } else if (curCnt == 100) { this.drawCnt(curCnt, this.data.fontSize100) } else { this.drawCnt(">100", this.data.fontSizeMoreThan100) } // force browser to redraw this.data.eFavLink.setAttribute('href', this.data.eCanvas.toDataURL()) reAttachEl(this.data.eFavLink) } FaviconUnreadCountModule.prototype.drawCnt = function faviconUnreadCount_drawCnt(sCnt, fontSize) { var ctx = this.data.ctx , dimen = this.data.dimen , pad = this.data.pad setFontSize(ctx, fontSize) var m = ctx.measureText(sCnt) ctx.fillStyle = "rgba(255,255,255,0.8)" ctx.fillRect(dimen - 2*pad - m.width, dimen - 2*pad - fontSize, m.width + 2*pad, fontSize + 2*pad) ctx.fillStyle = "black" ctx.shadowColor = "white" ctx.shadowOffsetX = -2 ctx.shadowOffsetY = -2 ctx.shadowBlur = 5 ctx.fillText(sCnt, dimen - pad - m.width, dimen - pad) } function setFontSize(ctx, size) { ctx.font = size + 'pt Sans' } function reAttachEl(e) { var eNext = e.nextSibling , eParent = e.parentNode eParent.removeChild(e) if (eNext) { eParent.insertBefore(e, eNext) } else { eParent.appendChild(e) } } function getCountToDisplay() { var el = document.getElementById('new_comments_counter') if (!el) { return 0 } if (el && el.offsetWidth) { return parseInt(el.textContent.trim()) } else { return 0 } } return FaviconUnreadCountModule }) define('fix-aside-toolbar', []) define(['jquery', 'module'], function($, Module) { function FixAsideToolbarModule() { } FixAsideToolbarModule.prototype = new Module() FixAsideToolbarModule.prototype.getLabel = function fixAsideToolbar_getLabel() { return "Починить расположение боковой панели" } FixAsideToolbarModule.prototype.attach = function fixAsideToolbar_attach(config) { this._style = $('<style>').text( 'ASIDE.toolbar { width: 1; height: 1; overflow: visible; } ' + 'ASIDE.toolbar SECTION { position: fixed; right: 0; top: 30%; }' ).appendTo(document.head) } FixAsideToolbarModule.prototype.detach = function fixAsideToolbar_detach() { if (this._style) { this._style.remove() } delete this._style } return FixAsideToolbarModule }) define('format-date', []) /** * Переформатирует дату/время, представленную в виде строки isoDateTime, например 2013-02-06T23:01:33+04:00 * в требуемый формат. Допустимые элементы формата: * - yyyy, yy - год (четыре или две цифры) * - M, MM, MMM, MMMM - месяц (одна/две цифры или сокращённое/полное название) * - d, dd - день * - H, HH - час * - m, mm - минуты * - s, ss - секунды * * @param strDate - дата в формате isoDateTime * @param strFormat - строка формата * @param bToLocal - конвертировать ли дату в локальную из той зоны, в которой она представлена * * @return переформатированные дату/время */ define(function() { var aMonthsLong = ['января','февраля','марта','апреля','мая','июня','июля','августа','сентября','октября','ноября','декабря'] , aMonthsShort = ['янв','фев','мар','апр','мая','июн','июл','авг','сен','окт','ноя','дек'] function padIntWithZero(x) { return x < 10 ? '0' + x : '' + x } return function formatDate(strDate, strFormat, bToLocalDate) { var arr if (!bToLocalDate) { arr = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})/.exec(strDate) } else { var d = new Date(strDate) arr = [ null, '' + d.getFullYear(), padIntWithZero(d.getMonth() + 1), padIntWithZero(d.getDate()), padIntWithZero(d.getHours()), padIntWithZero(d.getMinutes()), padIntWithZero(d.getSeconds()), padIntWithZero(d.getMilliseconds()), ] } return strFormat.replace(/(?:\\([\\yMdHms]))|(yyyy|yy|MMMM|MMM|MM|M|dd|d|HH|H|mm|m|ss|s)/g, function(whole, escaped, pattern) { if (escaped) { return escaped } switch (pattern) { case 'yyyy': return arr[1] case 'yy' : return arr[1].substring(2) case 'MMMM': return aMonthsLong[parseInt(arr[2], 10)-1] case 'MMM' : return aMonthsShort[parseInt(arr[2], 10)-1] case 'MM' : return arr[2] case 'M' : return parseInt(arr[2], 10) case 'dd' : return arr[3] case 'd' : return parseInt(arr[3], 10) case 'HH' : return arr[4] case 'H' : return parseInt(arr[4], 10) case 'mm' : return arr[5] case 'm' : return parseInt(arr[5], 10) case 'ss' : return arr[6] case 's' : return parseInt(arr[6], 10) } }) } }) define('img-alt-to-title', []) define(['jquery', 'module', 'ls-hook'], function($, Module, lsHook) { function ImgAltToTitleModule() { } ImgAltToTitleModule.prototype = new Module() ImgAltToTitleModule.prototype.init = function imgAltToTitle_init(config) { this.attrName = this.getApp() + '-' + this.getId() + '-data' return config } ImgAltToTitleModule.prototype.getLabel = function imgAltToTitle_getLabel() { return "Показывать атрибуты ALT у картинок всплывающими подсказками" } ImgAltToTitleModule.prototype.attach = function imgAltToTitle_attach(config) { this.processPage() this._hook = this.processPage.bind(this) lsHook.add('ls_comments_load_after', this._hook) lsHook.add('ls_userfeed_get_more_after', this._hook) } ImgAltToTitleModule.prototype.detach = function imgAltToTitle_detach() { lsHook.remove('ls_comments_load_after', this._hook) lsHook.remove('ls_userfeed_get_more_after', this._hook) delete this._hook this.unprocessPage() } ImgAltToTitleModule.prototype.processPage = function imgAltToTitle_processPage() { var self = this , cfg = self.getConfig() $('IMG[alt]').each(function() { var el = $(this) if (!el.data(self.attrName)) { var origTitle = el.attr('title') , alt = (el.attr('alt')||'').trim() , title = (origTitle||'').trim() el.data(self.attrName, { origTitle: origTitle, }) if (!alt) { return } if (title) { title = alt + '(' + title + ')' } else { title = alt } el.attr('title', title) } }) } ImgAltToTitleModule.prototype.unprocessPage = function imgAltToTitle_unprocessPage() { var self = this $('IMG[alt]').each(function() { var el = $(this) , data = el.data(self.attrName) if (data) { if (data.origTitle == null) { el.removeAttr('title') } else { el.attr('title', data.origTitle) } el.removeData(self.attrName) } }) } return ImgAltToTitleModule }) define('ls-hook', []) /** * Эмулируем несколько хуков livestreet CMS, нужных для работы скрипта, * но недоступных в новой версии табуна */ define(function() { var hooks = { ls_comments_load_after: [], ls_userfeed_get_more_after: [], } var interval , global = this , lastCommentsCount = getCommentsCount() , lastArticlesCount = getArticlesCount() function addLsHook(key, fn) { hooks[key].push(fn) if (!interval) { interval = setInterval(checkAndInvoke, 100) } } function removeLsHook(key, fn) { var idx = hooks[key].indexOf(fn) if (idx >= 0) { hooks[key].splice(idx, 1) } if (interval && !hasHooks()) { clearInterval(interval) interval = null } } function hasHooks() { var sum = 0 , key for (key in hooks) { sum += hooks[key].length } return sum > 0 } function checkAndInvoke() { if (hooks.ls_userfeed_get_more_after.length) { var articlesCount = getArticlesCount() if (articlesCount > lastArticlesCount) { lastArticlesCount = articlesCount invokeHook('ls_userfeed_get_more_after') } } if (hooks.ls_comments_load_after.length) { var commentCount = getCommentsCount() if (commentCount > lastCommentsCount) { lastCommentsCount = commentCount invokeHook('ls_comments_load_after') } } } function invokeHook(name) { (hooks[name]||[]).forEach(function(fn) { fn.call(global) }) } function getArticlesCount() { return document.getElementsByTagName('article').length } function getCommentsCount() { return document.getElementsByClassName('comment').length } return { add: addLsHook, remove: removeLsHook, } }) define('narrow-tree', []) define(['jquery', 'module', 'basic-cfg-panel-applet'], function($, Module, BasicCfgPanelApplet) { function NarrowTreeModule() { } NarrowTreeModule.prototype = new Module() NarrowTreeModule.prototype.getLabel = function narrowTree_getLabel() { return "Уменьшить ширину лесенки комментов" } NarrowTreeModule.prototype.attach = function narrowTree_attach(config) { config = this.ensureConfig(config) var style = '.comment-wrapper'; for (var i = 1; i < config.maxTreeWidth; i++) { style += ' .comment-wrapper'; } this._style = $('<style>').text( style + ' { padding-left: 0 !important } ' ).appendTo(document.head); } NarrowTreeModule.prototype.detach = function narrowTree_detach() { if (this._style) { this._style.remove() } this._style = null } NarrowTreeModule.prototype.ensureConfig = function narrowTree_ensureConfig(config) { config = config || {} config.maxTreeWidth = parseInt(config.maxTreeWidth, 10) if (isNaN(config.maxTreeWidth) || config.maxTreeWidth < 10 || config.maxTreeWidth > 1000) { config.maxTreeWidth = 60 } this.saveConfig(config) return config } NarrowTreeModule.prototype.createCfgPanelApplet = function narrowTree_createCfgPanelApplet() { var txtMax = $('<input>') .attr('type', 'number') .attr('name', 'maxTreeWidth') .css({ width: 40 }) return new BasicCfgPanelApplet(this.getLabel(), " до ", txtMax, " вложений") } return NarrowTreeModule }) define('open-nested-spoilers', []) define(['jquery', 'module', 'cfg-panel-applet'], function($, Module, CfgPanelApplet) { function OpenNestedSpoilersModule() { } OpenNestedSpoilersModule.prototype = new Module() OpenNestedSpoilersModule.prototype.init = function openNestedSpoilers_init(config) { config = config || { openOnLongClick: false, openOnShiftClick: false, alwaysOpen: false, } return config } OpenNestedSpoilersModule.prototype.getLabel = function openNestedSpoilers_getLabel() { return "Автоматически открывать вложенные спойлеры" } OpenNestedSpoilersModule.prototype.attach = function openNestedSpoilers_attach(config) { var self = this this.mouseDownHandler = function mouseDownHandler(ev) { return self.onMouseDown(this, ev) } $(document).on('mousedown', '.spoiler-title', this.mouseDownHandler) this.clickHandler = function clickHandler(ev) { return self.onClick(this, ev) } $(document).on('click', '.spoiler-title', this.clickHandler) } OpenNestedSpoilersModule.prototype.detach = function openNestedSpoilers_detach() { delete this._spoilerBodyIsVisibleOnMouseDown delete this._timeMouseDown if (this.mouseDownHandler) { $(document).off('mousedown', '.spoiler-title', this.mouseDownHandler) delete this.mouseDownHandler } if (this.clickHandler) { $(document).off('click', '.spoiler-title', this.clickHandler) delete this.clickHandler } } OpenNestedSpoilersModule.prototype.onMouseDown = function openNestedSpoilers_onMouseDown(elSpoilerTitle, ev) { this._spoilerBodyIsVisibleOnMouseDown = $(elSpoilerTitle).next('.spoiler-body').is(':visible') this._timeMouseDown = getNow() } OpenNestedSpoilersModule.prototype.onClick = function openNestedSpoilers_onClick(elSpoilerTitle, ev) { var cfg = this.getConfig() if ( cfg.alwaysOpen || cfg.openOnLongClick && this._timeMouseDown && (getNow() - this._timeMouseDown > 500) || cfg.openOnShiftClick && ev.shiftKey ) { this.processNestedSpoilers(elSpoilerTitle) } delete this._timeMouseDown } OpenNestedSpoilersModule.prototype.processNestedSpoilers = function openNestedSpoilers_processNestedSpoilers(elSpoilerTitle) { var elSpoilerBody = $(elSpoilerTitle).next('.spoiler-body') , opening = !this._spoilerBodyIsVisibleOnMouseDown // if body is not yet visible, we are probably opening it if (opening) { setAllSpoilersOpen(elSpoilerBody, true) } else { window.setTimeout(function() { setAllSpoilersOpen(elSpoilerBody, false) }, 400) } } OpenNestedSpoilersModule.prototype.createCfgPanelApplet = function openNestedSpoilers_createCfgPanelApplet() { return new OpenNestedSpoilersCfgPanelApplet() } function OpenNestedSpoilersCfgPanelApplet() { } OpenNestedSpoilersCfgPanelApplet.prototype = new CfgPanelApplet() OpenNestedSpoilersCfgPanelApplet.prototype.build = function openNestedSpoilersApplet_build() { this.chkOnLongClick = $('<input>') .attr('type', 'checkbox') .attr('name', 'openOnLongClick') this.chkOnShiftClick = $('<input>') .attr('type', 'checkbox') .attr('name', 'openOnShiftClick') this.chkAlways = $('<input>') .attr('type', 'checkbox') .attr('name', 'alwaysOpen') var div = $('<div>') , labelOnLongClick = $('<label>') .text("Открывать вложенные спойлеры при длинном клике") .prepend(this.chkOnLongClick) , labelOnShiftClick = $('<label>') .text("Открывать вложенные спойлеры при клике с Shift'ом") .prepend(this.chkOnShiftClick) , labelAlways = $('<label>') .text("Всегда открывать вложенные спойлеры") .prepend(this.chkAlways) div.append(labelOnLongClick, '<br/>', labelOnShiftClick, '<br/>', labelAlways) this.chkAlways.on('change', function() { if (this.chkAlways.is(':checked')) { this.chkOnLongClick.prop('checked', null) this.chkOnShiftClick.prop('checked', null) } }.bind(this)) this.chkOnLongClick.on('change', function() { if (this.chkOnLongClick.is(':checked')) { this.chkAlways.prop('checked', null) } }.bind(this)) this.chkOnShiftClick.on('change', function() { if (this.chkOnShiftClick.is(':checked')) { this.chkAlways.prop('checked', null) } }.bind(this)) return div } OpenNestedSpoilersCfgPanelApplet.prototype.setData = function openNestedSpoilersApplet_setData(enabled, config) { CfgPanelApplet.prototype.setData.apply(this, arguments) // call to super() config = config || {} this.chkOnLongClick.prop('checked', config.openOnLongClick ? 'checked' : null) this.chkOnShiftClick.prop('checked', config.openOnShiftClick ? 'checked' : null) this.chkAlways.prop('checked', config.alwaysOpen ? 'checked' : null) } OpenNestedSpoilersCfgPanelApplet.prototype.getEnabled = function openNestedSpoilersApplet_getEnabled() { return this.chkOnLongClick.is(':checked') || this.chkOnShiftClick.is(':checked') || this.chkAlways.is(':checked') } OpenNestedSpoilersCfgPanelApplet.prototype.getConfig = function openNestedSpoilersApplet_getConfig() { var cfg = CfgPanelApplet.prototype.getConfig.apply(this, arguments) // call to super() cfg = cfg || {} cfg.openOnLongClick = this.chkOnLongClick.is(':checked') cfg.openOnShiftClick = this.chkOnShiftClick.is(':checked') cfg.alwaysOpen = this.chkAlways.is(':checked') return cfg } function getNow() { return Date.now ? Date.now() : new Date().getTime() } function setAllSpoilersOpen(elBlock, open) { $('.spoiler-body', elBlock).css('display', open ? 'block' : 'none') } return OpenNestedSpoilersModule }) define('reformat-dates', []) define(['jquery', 'module', 'basic-cfg-panel-applet', 'format-date', 'ls-hook'], function($, Module, BasicCfgPanelApplet, formatDate, lsHook) { function ReformatDatesModule() { } ReformatDatesModule.prototype = new Module() ReformatDatesModule.prototype.init = function reformatDates_init(config) { config = config || { format: 'd MMM yyyy, H:mm:ss', } this.attrName = this.getApp() + '-' + this.getId() + '-data' return config } ReformatDatesModule.prototype.getLabel = function reformatDates_getLabel() { return "Сменить формат дат" } ReformatDatesModule.prototype.attach = function reformatDates_attach(config) { this.processPage() this._hook = this.processPage.bind(this) lsHook.add('ls_comments_load_after', this._hook) lsHook.add('ls_userfeed_get_more_after', this._hook) } ReformatDatesModule.prototype.detach = function reformatDates_detach() { lsHook.remove('ls_comments_load_after', this._hook) lsHook.remove('ls_userfeed_get_more_after', this._hook) delete this._hook this.unprocessPage() } ReformatDatesModule.prototype.processPage = function reformatDates_processPage() { var self = this , cfg = self.getConfig() $('[datetime]').each(function() { var el = $(this) if (!el.data(self.attrName) && !el.children().length) { el.data(self.attrName, { origText: el.text(), }) el.html(formatDate(el.attr('datetime'), cfg.format, false)) } }) } ReformatDatesModule.prototype.unprocessPage = function reformatDates_unprocessPage() { var self = this $('[datetime]').each(function() { var el = $(this) , data = el.data(self.attrName) if (data) { el.text(data.origText) el.removeData(self.attrName) } }) } ReformatDatesModule.prototype.createCfgPanelApplet = function reformatDates_createCfgPanelApplet() { var txtFormat = $('<input>') .attr('type', 'text') .attr('name', 'format') .css({ width: 150, }) return new BasicCfgPanelApplet(this.getLabel(), ": ", txtFormat, "<br/>", '<p style="padding-left: 20px">Формат — это строка вроде "d MMMM yyyy, HH:mm", где:<br/>' + 'yyyy, yy — год (2011 или 11)<br/>' + 'MMMM, MMM, MM, M — месяц (августа, авг, 08, 8)<br/>' + 'dd, d, HH, H, mm, m, ss, s — день, часы, минуты, секунды (09 или 9)<br/>' + 'Используйте \\ если нужны буквы y,M,d,H,m,s: \\M, \\s и т.д.</p>') } return ReformatDatesModule }) define('reveal-lite-spoilers', []) define(['module', 'cfg-panel-applet'], function(Module, CfgPanelApplet) { function RevealLiteSpoilersModule() { } RevealLiteSpoilersModule.prototype = new Module() RevealLiteSpoilersModule.prototype.init = function revealLiteSpoilers_init(config) { config = config || { revealOnHover: false, revealInCurrentComment: false, alwaysReveal: false, } return config } RevealLiteSpoilersModule.prototype.getLabel = function revealLiteSpoilers_getLabel() { return "Приоткрывать лайт-спойлеры" } RevealLiteSpoilersModule.prototype.attach = function revealLiteSpoilers_attach(config) { this._generateStyleSheet(config) if (this._style) { this._style.appendTo(document.head) } } RevealLiteSpoilersModule.prototype.detach = function revealLiteSpoilers_detach() { if (this._style) { this._style.remove() this._style = null } } RevealLiteSpoilersModule.prototype.createCfgPanelApplet = function revealLiteSpoilers_createCfgPanelApplet() { return new RevealLiteSpoilersCfgPanelApplet() } RevealLiteSpoilersModule.prototype._generateStyleSheet = function revealLiteSpoilers_generateStyleSheet(config) { // http://userstyles.org/styles/92211/night-tabun var nightTabun = getComputedStyle($('<span>').attr('class', 'spoiler-gray')[0]).backgroundColor == "rgb(63, 53, 61)" // always visible state var transBgColor = nightTabun ? '#2F252D' : '#EEE' var transTextColor = nightTabun ? '#8F8F8F' : '#999' var transATextColor = nightTabun ? '#7C89CA' : '#66AAFF' var transAVisitedTextColor = nightTabun ? '#7C89CA' : '#66AAFF' // hover state (fully visible) var hoverTextColor = nightTabun ? '#DFDFDF' : '#666' var hoverATextColor = nightTabun ? '#7C89CA' : '#0099FF' var hoverAVisitedTextColor = nightTabun ? '#7C89CA' : '#0099FF' var containers = ['.comment', '.comment-preview', '.topic', '.profile-info-about'] // селекторы для спойлеров в обычном состоянии , selectorSpoiler = containers.map(function(s) { return s + ' .spoiler-gray' }).join(', ') , selectorA = containers.map(function(s) { return s + ' .spoiler-gray A' }).join(', ') , selectorAVisited = containers.map(function(s) { return s + ' .spoiler-gray A:visited' }).join(', ') // селекторы для наведённого коммента/поста , selectorPostHoverSpoiler = containers.map(function(s) { return s + ':hover .spoiler-gray' }).join(', ') , selectorPostHoverA = containers.map(function(s) { return s + ':hover .spoiler-gray A' }).join(', ') , selectorPostHoverAVisited = containers.map(function(s) { return s + ':hover .spoiler-gray A:visited' }).join(', ') // селекторы для текущего коммента , selectorPostActiveSpoiler = '.comment.comment-current .spoiler-gray' , selectorPostActiveA = '.comment.comment-current .spoiler-gray A' , selectorPostActiveAVisited = '.comment.comment-current .spoiler-gray A:visited' // и более специфичные селекторы для оригинального лайтспойлера в наведённом состоянии (иначе эти стили не пробиваются через наши) , selectorHoverSpoiler = containers.map(function(s) { return s + ':hover .spoiler-gray:hover' }).join(', ') , selectorHoverA = containers.map(function(s) { return s + ':hover .spoiler-gray:hover A' }).join(', ') , selectorHoverAVisited = containers.map(function(s) { return s + ':hover .spoiler-gray:hover A:visited' }).join(', ') var css = '' if (config.alwaysReveal) { css += selectorSpoiler + ' { background-color: ' + transBgColor + ' !important; color: ' + transTextColor + ' !important; } ' + selectorA + ' { color: ' + transATextColor + ' !important; } ' + selectorAVisited + ' { color: ' + transAVisitedTextColor + ' !important; } ' } else { if (config.revealOnHover) { css += selectorPostHoverSpoiler + ' { background-color: ' + transBgColor + ' !important; color: ' + transTextColor + ' !important; } ' + selectorPostHoverA + ' { color: ' + transATextColor + ' !important; } ' + selectorPostHoverAVisited + ' { color: ' + transAVisitedTextColor + ' !important; } ' } if (config.revealInCurrentComment) { css += selectorPostActiveSpoiler + ' { background-color: ' + transBgColor + ' !important; color: ' + transTextColor + ' !important; } ' + selectorHoverA + ' { color: ' + transATextColor + ' !important; } ' + selectorHoverAVisited + ' { color: ' + transAVisitedTextColor + ' !important; } ' } } if (css) { css += // и более специфичные селекторы для оригинального лайтспойлера в наведённом состоянии (иначе эти стили не пробиваются через наши) selectorHoverSpoiler + ' { background-color: transparent !important; color: ' + hoverTextColor + ' !important; } ' + selectorHoverA + ' { background-color: transparent !important; color: ' + hoverATextColor + ' !important; } ' + selectorHoverAVisited + ' { background-color: transparent !important; color: ' + hoverAVisitedTextColor + ' !important; } ' } this._style = $('<style>').text(css) } function RevealLiteSpoilersCfgPanelApplet() { } RevealLiteSpoilersCfgPanelApplet.prototype = new CfgPanelApplet() RevealLiteSpoilersCfgPanelApplet.prototype.build = function revealLiteSpoilersApplet_build() { this.chkOnHover = $('<input>') .attr('type', 'checkbox') .attr('name', 'revealOnHover') this.chkInCurrent = $('<input>') .attr('type', 'checkbox') .attr('name', 'revealInCurrentComment') this.chkAlways = $('<input>') .attr('type', 'checkbox') .attr('name', 'alwaysReveal') var div = $('<div>') , labelOnHover = $('<label>') .text("Приоткрывать лайт-спойлеры при наведении на пост/коммент") .prepend(this.chkOnHover) , labelInCurrent = $('<label>') .text("Светить лайт-спойлеры в активном комменте") .prepend(this.chkInCurrent) , labelAlways = $('<label>') .text("Всегда светить лайт-спойлеры") .prepend(this.chkAlways) div.append(labelOnHover, '<br/>', labelInCurrent, '<br/>', labelAlways) this.chkAlways.on('change', function() { if (this.chkAlways.is(':checked')) { this.chkOnHover.prop('checked', null) this.chkInCurrent.prop('checked', null) } }.bind(this)) this.chkOnHover.on('change', function() { if (this.chkOnHover.is(':checked')) { this.chkAlways.prop('checked', null) } }.bind(this)) this.chkInCurrent.on('change', function() { if (this.chkInCurrent.is(':checked')) { this.chkAlways.prop('checked', null) } }.bind(this)) return div } RevealLiteSpoilersCfgPanelApplet.prototype.setData = function revealLiteSpoilersApplet_setData(enabled, config) { CfgPanelApplet.prototype.setData.apply(this, arguments) // call to super() config = config || {} this.chkOnHover.prop('checked', config.revealOnHover ? 'checked' : null) this.chkInCurrent.prop('checked', config.revealInCurrentComment ? 'checked' : null) this.chkAlways.prop('checked', config.alwaysReveal ? 'checked' : null) } RevealLiteSpoilersCfgPanelApplet.prototype.getEnabled = function revealLiteSpoilersApplet_getEnabled() { return this.chkOnHover.is(':checked') || this.chkAlways.is(':checked') || this.chkInCurrent.is(':checked') } RevealLiteSpoilersCfgPanelApplet.prototype.getConfig = function revealLiteSpoilersApplet_getConfig() { var cfg = CfgPanelApplet.prototype.getConfig.apply(this, arguments) // call to super() cfg = cfg || {} cfg.revealOnHover = this.chkOnHover.is(':checked') cfg.revealInCurrentComment = this.chkInCurrent.is(':checked') cfg.alwaysReveal = this.chkAlways.is(':checked') return cfg } return RevealLiteSpoilersModule }) define('shim', []) // Т.к. наша урезанная версия define не поддерживает // shim конфиг, нужные библиотеки объявим явно. // Просто возвращаем глобальные объекты define('jquery', function() { return jQuery }) define('spacebar-move-to-next', []) define(['jquery', 'module'], function($, Module) { function SpacebarMoveToNextModule() { } SpacebarMoveToNextModule.prototype = new Module() SpacebarMoveToNextModule.prototype.getLabel = function spacebarMoveToNext_getLabel() { return "По пробелу переходить на следующий пост/непрочитанный коммент" } SpacebarMoveToNextModule.prototype.attach = function spacebarMoveToNext_attach(config) { this.handler = this.onSpacebarPressed.bind(this) document.addEventListener('keypress', this.handler) } SpacebarMoveToNextModule.prototype.detach = function spacebarMoveToNext_detach() { document.removeEventListener('keypress', this.handler) this.handler = null } SpacebarMoveToNextModule.prototype.onSpacebarPressed = function spacebarMoveToNext_onSpacebarPressed(ev) { var el = ev.target if (el.tagName == 'INPUT' || el.tagName == 'SELECT' || el.tagName == 'TEXTAREA' || el.isContentEditable) { // ignore input fields (as in https://github.com/ccampbell/mousetrap/blob/master/mousetrap.js) return } if (ev.which == 32 /* space */) { if (this.goToNext()) { ev.preventDefault() } } } SpacebarMoveToNextModule.prototype.goToNext = function spacebarMoveToNext_goToNext() { $(window).stop(true) if ($('#update-comments').length) { // we are on comments return ls.comments.goToNextComment() } else { var article $('ARTICLE').each(function() { var el = $(this) /* 40px - небольшой запас на случай микроскроллов, не очень заметных пользователю */ if (el.offset().top > $(window).scrollTop() + 40) { article = el return false } }) if (article) { $.scrollTo(article, 300, {offset: -10}) return true } else { return false } } } return SpacebarMoveToNextModule }) define('sync-config-among-tabs', []) define(['module'], function(Module) { function SyncConfigAmongTabsModule() { } SyncConfigAmongTabsModule.prototype = new Module() SyncConfigAmongTabsModule.prototype.attach = function syncConfigAmongTabs_attach(config) { this.onWindowFocus = this.syncConfig.bind(this) window.addEventListener('focus', this.onWindowFocus) } SyncConfigAmongTabsModule.prototype.detach = function syncConfigAmongTabs_detach() { window.removeEventListener('focus', this.onWindowFocus) delete this.onWindowFocus } SyncConfigAmongTabsModule.prototype.syncConfig = function syncConfigAmongTabs_syncConfig() { this.getApp().reconfigure() } return SyncConfigAmongTabsModule }) define('whats-new', []) define(['jquery', 'module', 'app', 'cfg-panel-applet'], function($, Module, App, CfgPanelApplet) { function WhatsNewModule() { } WhatsNewModule.prototype = new Module() WhatsNewModule.prototype.init = function whatsNew_init(config, text) { config = config || {} if (config.installed && config.text != text) { this._alertText = "Юзерскрипт tabun-fixes обновился!\nЧто нового:\n" + $("<p>").html(text.replace(/\<br\/?\>/g, "\n")).text() } config.installed = true config.text = text return config } WhatsNewModule.prototype.attach = function whatsNew_attach(config) { if (this._alertText) { alert(this._alertText) } delete this._alertText } WhatsNewModule.prototype.detach = function whatsNew_detach() { return true } WhatsNewModule.prototype.createCfgPanelApplet = function whatsNew_createCfgPanelApplet() { return new WhatsNewCfgPanelApplet() } function WhatsNewCfgPanelApplet() { } WhatsNewCfgPanelApplet.prototype = new CfgPanelApplet() WhatsNewCfgPanelApplet.prototype.build = function whatsNewApplet_build() { return this.div = $('<div>') } WhatsNewCfgPanelApplet.prototype.setData = function whatsNewApplet_setData(enabled, config) { CfgPanelApplet.prototype.setData.apply(this, arguments) // call to super() this.div.html('<strong>Что нового:</strong><br/>' + config.text) } return WhatsNewModule }) define('img/favicon', [], 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAnklEQVQ4y6VTwQnDMAw8mS7RQQolUzR0zYRuUQIZJGNcHomIaisWjQ/8MNKdTpYs2EGS+AMiIgAgV8hWJIXksT8NkSRYw/BihBRWtHfHza1q+/4I2ygdvD8HuXtuR4U0diow9ttRskJF1IlxlH7ITgW3PZMn7hhzF98JWGa3QClgH0x7X+byjapT0CQVy+/5NrUsEsKMQKT5M0nrd14BXnj9gc+qGlMAAAAASUVORK5CYII=') define('img/gear', [], 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACHUlEQVQ4y62Sv0vVYRTGn3Pe9/3ebsEdcnBpqCiKK9wkIzKStMFF6AcW1VBkg2sN+g/k0uDQ1FZSBDokmYjrNYgMUfMamWlDoEuDi0P33u/7nvc0xA1Nb0ud6Zzn/ODw4QH+MehvzaGpUicRulV1oKe9eX23Gf5TeFJcyNbyxJr2jLO91phCTXtaXGio+8GzN6UHRNQdVa8R0UY2cc+d4c6qD4MVHwYIuEFMj2LUu3fOnxgBALv1QOLshjWclxhnCYTEmqw1DIX2gdDLRDkmKleDLNVl8HpuZYyJLimwScBDAOsKFADcI4ILEvuvnDo2WJeBqrYwk1fVtjTIcMWHbBAZIOA2gRCjduxgMDRVKhimR86axr0ZlwcwmXq5HFXXiNCoiuL11vyF8fnVNYnxQCUNZR9kSaJ2WQBwlg9lnO1IrIFhRhpkUzRaZ34xSIM0AECMWk6sARNlU8O5Suq3MxifXz3IRKU0CNIQDifWFpzh7mqQxyIxl3F22ll+39V8pHVXBhdPHv0mMY4YphwTTfsg+ys+DInEc8w0QQT4IJN1ffDi7ceWfXuSaWeMkxjhg0AVICY4wzDMqPpQ/lFNz9xqKyzu+MAw9zIRANxnorbE2WImsUisGWWiJgD9hska5s5dvT/87pMbnVnO1+qx2ZW+iQ9f9dXsl56a9nJmuXHrzjYn3jzb5AH8dpkPsghgLg3yuaZdPX38O/5n/ASdNN+15exi2wAAAABJRU5ErkJggg==') define('img/star-big-checked', [], 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAARCAYAAAA7bUf6AAACNklEQVQ4y52TPWtUQRSGn5k7dy9rNhvBTYpgDLqKElYiqE0aSSGx8ReIoE1WQ0BBQe0sBbWyun60YmNpYxcwhUJA4YpFEElwA5JNdpNsdu/XzFjsLiYkROPAA2fOHN7DzHkH9liBjzRzp64FPnKvOrmHgCiODBeFdJ4dLh4Z+C8RQCovcwNkLtfXezPwcfYtcrDQ1+M47lWQSJW5Arj7Egl8xMDgoUtCOv0IiZCqeOLM6YnAR/yzSP9gocdxM3fax22Ud+AWoHarF+bjwGOEcxEUQjojCOUiFG1c/sQdUCBUauE7OGDTd/LHfO2p0SwIKUZBulu7g2D7XoKQIIQSwj1p0vDrQvDpiTM5YWT1V+NDLp9VynXPg8PfkTrcWH0+PzfzMIladWfqMimQ1FcaXzzPrWS87AWsdLCSNg7dWBiNTZrherVyf/Hb51dAFWiKrjOBLFAYPjY0ne3J321fxYDVYOIOLRqN8P7PpeZrYAUIS2WMAiiVMYFPE1hVOhoyjSWwKVizYxIZwVGgBrRKZey2EXcShmjjrAlDTJRiYrMDtD0H6K7ADp/059SgTfRxE2tMrInD9H21Hl9PwnS2myPRo72ezO9qtsBHethxk2h0rCtrzXR6sZbcrrf0zEItmdxopvd0opdNolXeFeNbf/ZWByqhzVic8mI5ti8TwxKwBiRAZrll3ngRswVPTAkYA962X367iFiL7aN1zSawCTSApFTGBj4REEWGZqVlH/Q5ZDtOBOA3IsECgTKeLKoAAAAASUVORK5CYII=') define('img/star-big-unchecked', [], 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAARCAYAAAA7bUf6AAAB50lEQVQ4y52TMWhTURSG//vOPff1vbQJKi/JEFPqIiikYAcR6hAHBadM4qLQDs4VHNwcBBcXOwg6O7WKkkVah8apFVscah1E0MWhdhAt6dMk797joJQG8l7Us91zf77h/+4FhszC0urlYRmVdbm4vFZRSr0TkaOXLpzZTct5WRDDNBMGJs9MM1m5VMjjF684DPxrhXyIYMTM/heENdVzoV/JjwbIhX7t2crr6X+CLC6vsW/0zVzoIwwMcoEPn/VcarELS6t3AJz/c64BYNaEw4dGUY4KCEYM4h9dbO98w9fve0gSCwA9AJu/zdin2hh9i4kqylNXlFJQCvCNRmEsgGENpRQMa+THQjgRdHsJRMAiMuWczBd2HtzbV/zm/ccbWtNdTykQeTCsobUHpRREBL3EottN4JzAifSSxF4/dfzY/a2HoL538mn7y0Ui7wmAIEPGrrWuMVEutQYWO1EuPbfOzWXptNbNHgQMtBPHnZpzLg2AdtyZGqq4HXfqiR0MSaxDe+/n2UxI8+VGSUROHFg9AjAJoLm/ETndbK0HqRBNXt03DCLvg4icq0bR1WoUbVajqCEiDU3eZ2M0E3n1VAgzTbOm2+JkcrxY7CtvvFhsishJw3qeNaV+AbQ2to7gL2Zl/W1f7hdPXqRVE0AkoQAAAABJRU5ErkJggg==') define('img/star-small-checked', [], 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAYAAACprHcmAAABCklEQVQY04WQMUvDUBSFv5c2DtUO8kZxFEGyqaCL4mpnVxeHCCL1fxQ3B52yiT9Awd3FgF1EUKl0qjiJGltL8uK9Lq1gLPTAgXu5HxfOgYI0tr7G9oIR8v7BSg3Y1NhujIVRrQOI6G7xZDS2i4N5DrDA0WDPgQPgFWgBGLm258ZQY4xU9Ky0tdy/r075tuyZBUQZ5a+ui1oPHw0DcHfC/OxMZX+yUt4rfvzsusbzS/84CGl7AEHIo5fL03c/p+iS6G0Q0v7ThnGyMgDyrOeaQ9hzsjpkyr8BsnwpV1rvmR4mTpvTE2at6pu6Maz/6TmJsGkql52e7CROT4OQm7dMo05PttNUrpIIH+AHo7WPvp8+nRQAAAAASUVORK5CYII=') define('img/star-small-unchecked', [], 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAYAAACprHcmAAABFUlEQVQY04WQIUsEURSFv/vePEat+xvswiyowbBJmKDVtCKYNC2aBNMKGwTTRsGRMVgsgsUsswYtBou/YYvFYWbfu4bdLbOCJx0uH4dzDzTUzwrXz4on/pBpHkRIrSW9uC06/8IukqMlZ4isHCwE9bMimflVY2itxHYYO6GsQv1ThV4IjIEvgEiEc4RdmaYSOyGODKq4iddhjaIASi4AV/evl0bkNIqE5WkFaq+UVWDiFa96drK3MZB5n+vHt2MjDK0RREAVfFCC0j3caefNB7/n4GwVrBFUdbywRlmFjg8KUAPPME0ua+0swF51HXhXaHfTZBvYUvj0oQEP7kYtK3ITlM39NPkA6KbJiyprVngY5CMH8Avqkmope7TXOQAAAABJRU5ErkJggg==') define('start', []) define(['app'], function(App) { new App('tabun-fixes') .add('cfg-panel', { defaultEnabled:true }) .add('add-onclick-to-spoilers', { defaultEnabled:true }) .add('fast-scroll-to-comment', { defaultEnabled:true }) .add('sync-config-among-tabs', { defaultEnabled:true }) .add('alter-same-page-links', { defaultEnabled:true, cfgPanel:{column:1} }) .add('alter-links-to-mirrors', { defaultEnabled:true, cfgPanel:{column:1} }) .add('reveal-lite-spoilers', { defaultEnabled:false, cfgPanel:{column:1} }) .add('open-nested-spoilers', { defaultEnabled:false, cfgPanel:{column:1} }) .add('reformat-dates', { defaultEnabled:false, cfgPanel:{column:1} }) .add('spacebar-move-to-next', { defaultEnabled:false, cfgPanel:{column:2} }) .add('fav-as-icon', { defaultEnabled:false, cfgPanel:{column:2} }) .add('favicon-unread-count' , { defaultEnabled:true, cfgPanel:{column:2} }) .add('narrow-tree', { defaultEnabled:false, cfgPanel:{column:2} }) .add('img-alt-to-title', { defaultEnabled:false, cfgPanel:{column:2} }) .add('fix-aside-toolbar', { defaultEnabled:true, cfgPanel:{column:2} }) .add('autospoiler-images', { defaultEnabled:false, cfgPanel:{column:2} }) .add('whats-new', { defaultEnabled:true, cfgPanel:{column:2} }, "• Добавлено автоскрытие больших картинок" ) .start() }) })