From 8b836aec8dc8145325ea7c2737637706ecc793ff Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Sun, 10 Feb 2013 23:15:06 +0100 Subject: [PATCH] Add Notifications. Add error handling. I don't feel like I did a good job of it, might revisit later. --- 4chan_x.user.js | 459 ++++++++++++++++++++++---------------------- css/style.css | 75 ++++++-- lib/$.coffee | 5 +- src/config.coffee | 4 +- src/features.coffee | 92 +++++++-- src/main.coffee | 304 +++++++++-------------------- 6 files changed, 474 insertions(+), 465 deletions(-) diff --git a/4chan_x.user.js b/4chan_x.user.js index 1b332b27f..4661647a0 100644 --- a/4chan_x.user.js +++ b/4chan_x.user.js @@ -20,7 +20,7 @@ // @icon https://github.com/MayhemYDG/4chan-x/raw/stable/img/icon.gif // ==/UserScript== -/* 4chan X Alpha - Version 3.0.0 - 2013-02-09 +/* 4chan X Alpha - Version 3.0.0 - 2013-02-10 * http://mayhemydg.github.com/4chan-x/ * * Copyright (c) 2009-2011 James Campos @@ -43,7 +43,7 @@ */ (function() { - var $, $$, Anonymize, ArchiveLink, AutoGIF, Board, Build, Clone, Conf, Config, DeleteLink, DownloadLink, FileInfo, Filter, Get, Header, ImageHover, Main, Menu, Post, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, Quotify, Recursive, Redirect, ReplyHiding, ReportLink, RevealSpoilers, Sauce, Settings, Thread, ThreadHiding, ThreadUpdater, Time, UI, d, g, _base, + var $, $$, Anonymize, ArchiveLink, AutoGIF, Board, Build, Clone, Conf, Config, DeleteLink, DownloadLink, FileInfo, Filter, Get, Header, ImageHover, Main, Menu, Notification, Post, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, Quotify, Recursive, Redirect, ReplyHiding, ReportLink, RevealSpoilers, Sauce, Settings, Thread, ThreadHiding, ThreadUpdater, Time, UI, d, g, _base, __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }, __hasProp = {}.hasOwnProperty, __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; @@ -113,8 +113,8 @@ 'Quote Preview': [true, 'Show quoted post on hover.'], 'Quote Highlighting': [true, 'Highlight the previewed post.'], 'Resurrect Quotes': [true, 'Linkify dead quotes to archives.'], - 'Indicate OP Quotes': [true, 'Add \'(OP)\' to OP quotes.'], - 'Indicate Cross-thread Quotes': [true, 'Add \'(Cross-thread)\' to cross-threads quotes.'] + 'Mark OP Quotes': [true, 'Add \'(OP)\' to OP quotes.'], + 'Mark Cross-thread Quotes': [true, 'Add \'(Cross-thread)\' to cross-threads quotes.'] } }, filter: { @@ -597,8 +597,8 @@ return d.getElementById(id); }, ready: function(fc) { - var cb; - if (/interactive|complete/.test(d.readyState)) { + var cb, _ref; + if ((_ref = d.readyState) === 'interactive' || _ref === 'complete') { $.queueTask(fc); return; } @@ -935,7 +935,10 @@ try { this.setBoardList(); } catch (err) { - $.log(err, 'Header - board list'); + Main.handleErrors({ + message: '"Header (board list)" crashed.', + error: err + }); } return $.asap((function() { return d.body; @@ -981,6 +984,43 @@ } }; + Notification = (function() { + + function Notification(type, content, timeout) { + var el; + this.type = type; + this.el = $.el('div', { + className: "notification " + type, + innerHTML: '×
' + }); + $.on(this.el.firstElementChild, 'click', this.close.bind(this)); + if (typeof content === 'string') { + content = $.tn(content); + } + $.add(this.el.lastElementChild, content); + if (timeout) { + setTimeout(this.close.bind(this), timeout * $.SECOND); + } + el = this.el; + $.ready(function() { + return $.add($.id('notifications'), el); + }); + } + + Notification.prototype.setType = function(type) { + $.rmClass(this.el, this.type); + $.addClass(this.el, type); + return this.type = type; + }; + + Notification.prototype.close = function() { + return $.rm(this.el); + }; + + return Notification; + + })(); + Settings = { init: function() { var link, settings; @@ -1026,6 +1066,9 @@ filters: {}, init: function() { var boards, filter, hl, key, op, regexp, stub, top, _i, _len, _ref, _ref1, _ref2, _ref3, _ref4, _ref5; + if (g.VIEW === 'catalog' || !Conf['Filter']) { + return; + } for (key in Config.filter) { this.filters[key] = []; _ref = Conf[key].split('\n'); @@ -1048,7 +1091,7 @@ try { regexp = RegExp(regexp[1], regexp[2]); } catch (err) { - alert(err.message); + new Notification('warning', err.message, 60); continue; } } @@ -1219,6 +1262,9 @@ menu: { init: function() { var div, entry, type, _i, _len, _ref; + if (g.VIEW === 'catalog' || !Conf['Menu'] || !Conf['Filter']) { + return; + } div = $.el('div', { textContent: 'Filter' }); @@ -1280,7 +1326,7 @@ ThreadHiding = { init: function() { - if (g.VIEW !== 'index') { + if (g.VIEW !== 'index' || !Conf['Thread Hiding']) { return; } this.getHiddenThreads(); @@ -1366,7 +1412,7 @@ menu: { init: function() { var apply, div, makeStub; - if (g.VIEW !== 'index') { + if (g.VIEW !== 'index' || !Conf['Menu'] || !Conf['Thread Hiding']) { return; } div = $.el('div', { @@ -1492,6 +1538,9 @@ ReplyHiding = { init: function() { + if (g.VIEW === 'catalog' || !Conf['Reply Hiding']) { + return; + } this.getHiddenPosts(); this.clean(); return Post.prototype.callbacks.push({ @@ -1565,6 +1614,9 @@ menu: { init: function() { var apply, div, makeStub, replies, thisPost; + if (g.VIEW === 'catalog' || !Conf['Menu'] || !Conf['Reply Hiding']) { + return; + } div = $.el('div', { className: 'hide-reply-link', textContent: 'Hide reply' @@ -1770,6 +1822,9 @@ Menu = { init: function() { + if (g.VIEW === 'catalog' || !Conf['Menu']) { + return; + } this.menu = new UI.Menu('post'); return Post.prototype.callbacks.push({ name: 'Menu', @@ -1809,6 +1864,9 @@ ReportLink = { init: function() { var a; + if (g.VIEW === 'catalog' || !Conf['Menu'] || !Conf['Report Link']) { + return; + } a = $.el('a', { className: 'report-link', href: 'javascript:;', @@ -1836,6 +1894,9 @@ DeleteLink = { init: function() { var div, fileEl, fileEntry, postEl, postEntry; + if (g.VIEW === 'catalog' || !Conf['Menu'] || !Conf['Delete Link']) { + return; + } div = $.el('div', { className: 'delete-link', textContent: 'Delete' @@ -1964,6 +2025,9 @@ DownloadLink = { init: function() { var a; + if (g.VIEW === 'catalog' || !Conf['Menu'] || !Conf['Download Link']) { + return; + } if ($.el('a').download === void 0) { return; } @@ -1988,6 +2052,9 @@ ArchiveLink = { init: function() { var div, entry, type, _i, _len, _ref; + if (g.VIEW === 'catalog' || !Conf['Menu'] || !Conf['Archive Link']) { + return; + } div = $.el('div', { textContent: 'Archive' }); @@ -2636,6 +2703,9 @@ Quotify = { init: function() { + if (g.VIEW === 'catalog' || !Conf['Resurrect Quotes']) { + return; + } return Post.prototype.callbacks.push({ name: 'Resurrect Quotes', cb: this.node @@ -2715,6 +2785,9 @@ QuoteInline = { init: function() { + if (g.VIEW === 'catalog' || !Conf['Quote Inline']) { + return; + } return Post.prototype.callbacks.push({ name: 'Quote Inline', cb: this.node @@ -2800,6 +2873,9 @@ QuotePreview = { init: function() { + if (g.VIEW === 'catalog' || !Conf['Quote Preview']) { + return; + } return Post.prototype.callbacks.push({ name: 'Quote Preview', cb: this.node @@ -2881,6 +2957,9 @@ QuoteBacklink = { init: function() { var format; + if (g.VIEW === 'catalog' || !Conf['Quote Backlinks']) { + return; + } format = Conf['backlink'].replace(/%id/g, "' + id + '"); this.funk = Function('id', "return '" + format + "'"); this.containers = {}; @@ -2950,9 +3029,12 @@ QuoteOP = { init: function() { + if (g.VIEW === 'catalog' || !Conf['Mark OP Quotes']) { + return; + } this.text = '\u00A0(OP)'; return Post.prototype.callbacks.push({ - name: 'Indicate OP Quotes', + name: 'Mark OP Quotes', cb: this.node }); }, @@ -2987,9 +3069,12 @@ QuoteCT = { init: function() { + if (g.VIEW === 'catalog' || !Conf['Mark Cross-thread Quotes']) { + return; + } this.text = '\u00A0(Cross-thread)'; return Post.prototype.callbacks.push({ - name: 'Indicate Cross-thread Quotes', + name: 'Mark Cross-thread Quotes', cb: this.node }); }, @@ -3021,6 +3106,9 @@ Anonymize = { init: function() { + if (g.VIEW === 'catalog' || !Conf['Anonymize']) { + return; + } return Post.prototype.callbacks.push({ name: 'Anonymize', cb: this.node @@ -3052,6 +3140,9 @@ Time = { init: function() { + if (g.VIEW === 'catalog' || !Conf['Time Formatting']) { + return; + } this.funk = this.createFunc(Conf['time']); return Post.prototype.callbacks.push({ name: 'Time Formatting', @@ -3146,6 +3237,9 @@ FileInfo = { init: function() { + if (g.VIEW === 'catalog' || !Conf['File Info Formatting']) { + return; + } this.funk = this.createFunc(Conf['fileInfo']); return Post.prototype.callbacks.push({ name: 'File Info Formatting', @@ -3244,6 +3338,9 @@ Sauce = { init: function() { var link, links, _i, _len, _ref; + if (g.VIEW === 'catalog' || !Conf['Sauce']) { + return; + } links = []; _ref = Conf['sauces'].split('\n'); for (_i = 0, _len = _ref.length; _i < _len; _i++) { @@ -3302,6 +3399,9 @@ RevealSpoilers = { init: function() { + if (g.VIEW === 'catalog' || !Conf['Reveal Spoilers']) { + return; + } return Post.prototype.callbacks.push({ name: 'Reveal Spoilers', cb: this.node @@ -3321,7 +3421,7 @@ AutoGIF = { init: function() { var _ref; - if ((_ref = g.BOARD.ID) === 'gif' || _ref === 'wsg') { + if (g.VIEW === 'catalog' || !Conf['Auto-GIF'] || ((_ref = g.BOARD.ID) === 'gif' || _ref === 'wsg')) { return; } return Post.prototype.callbacks.push({ @@ -3352,8 +3452,7 @@ ImageHover = { init: function() { - var _ref; - if ((_ref = g.BOARD.ID) === 'gif' || _ref === 'wsg') { + if (g.VIEW === 'catalog' || !Conf['Image Hover']) { return; } return Post.prototype.callbacks.push({ @@ -3429,7 +3528,7 @@ ThreadUpdater = { init: function() { - if (g.VIEW !== 'thread') { + if (g.VIEW !== 'thread' || !Conf['Thread Updater']) { return; } return Thread.prototype.callbacks.push({ @@ -4018,7 +4117,7 @@ Main = { init: function() { - var flatten, key, pathname, val; + var flatten, initFeature, key, pathname, val; flatten = function(parent, obj) { var key, val; if (obj instanceof Array) { @@ -4067,211 +4166,57 @@ }); return; } - if (g.VIEW === 'catalog') { - return; - } $.addStyle(Main.css); - $.asap((function() { - return d.body; - }), (function() { - $.addClass(d.body, $.engine); - return $.addClass(d.body, 'fourchan_x'); - })); - try { - Header.init(); - } catch (err) { - $.log(err, 'Header'); - } - try { - Settings.init(); - } catch (err) { - $.log(err, 'Settings'); - } - if (Conf['Resurrect Quotes']) { + $.addClass(d.documentElement, $.engine); + $.addClass(d.documentElement, 'fourchan_x'); + initFeature = function(name, module) { + console.time("" + name + " initialization"); try { - Quotify.init(); + module.init(); } catch (err) { - $.log(err, 'Resurrect Quotes'); + Main.handleErrors({ + message: "\"" + name + "\" initialization crashed.", + error: err + }); } - } - if (Conf['Filter']) { - try { - Filter.init(); - } catch (err) { - $.log(err, 'Filter'); - } - } - if (Conf['Thread Hiding']) { - try { - ThreadHiding.init(); - } catch (err) { - $.log(err, 'Thread Hiding'); - } - } - if (Conf['Reply Hiding']) { - try { - ReplyHiding.init(); - } catch (err) { - $.log(err, 'Reply Hiding'); - } - } - try { - Recursive.init(); - } catch (err) { - $.log(err, 'Recursive'); - } - if (Conf['Menu']) { - try { - Menu.init(); - } catch (err) { - $.log(err, 'Menu'); - } - if (Conf['Report Link']) { - try { - ReportLink.init(); - } catch (err) { - $.log(err, 'Report Link'); - } - } - if (Conf['Thread Hiding']) { - try { - ThreadHiding.menu.init(); - } catch (err) { - $.log(err, 'Thread Hiding - Menu'); - } - } - if (Conf['Reply Hiding']) { - try { - ReplyHiding.menu.init(); - } catch (err) { - $.log(err, 'Reply Hiding - Menu'); - } - } - if (Conf['Delete Link']) { - try { - DeleteLink.init(); - } catch (err) { - $.log(err, 'Delete Link'); - } - } - if (Conf['Filter']) { - try { - Filter.menu.init(); - } catch (err) { - $.log(err, 'Filter - Menu'); - } - } - if (Conf['Download Link']) { - try { - DownloadLink.init(); - } catch (err) { - $.log(err, 'Download Link'); - } - } - if (Conf['Archive Link']) { - try { - ArchiveLink.init(); - } catch (err) { - $.log(err, 'Archive Link'); - } - } - } - if (Conf['Quote Inline']) { - try { - QuoteInline.init(); - } catch (err) { - $.log(err, 'Quote Inline'); - } - } - if (Conf['Quote Preview']) { - try { - QuotePreview.init(); - } catch (err) { - $.log(err, 'Quote Preview'); - } - } - if (Conf['Quote Backlinks']) { - try { - QuoteBacklink.init(); - } catch (err) { - $.log(err, 'Quote Backlinks'); - } - } - if (Conf['Indicate OP Quotes']) { - try { - QuoteOP.init(); - } catch (err) { - $.log(err, 'Indicate OP Quotes'); - } - } - if (Conf['Indicate Cross-thread Quotes']) { - try { - QuoteCT.init(); - } catch (err) { - $.log(err, 'Indicate Cross-thread Quotes'); - } - } - if (Conf['Anonymize']) { - try { - Anonymize.init(); - } catch (e) { - $.log(err, 'Anonymize'); - } - } - if (Conf['Time Formatting']) { - try { - Time.init(); - } catch (err) { - $.log(err, 'Time Formatting'); - } - } - if (Conf['File Info Formatting']) { - try { - FileInfo.init(); - } catch (err) { - $.log(err, 'File Info Formatting'); - } - } - if (Conf['Sauce']) { - try { - Sauce.init(); - } catch (err) { - $.log(err, 'Sauce'); - } - } - if (Conf['Reveal Spoilers']) { - try { - RevealSpoilers.init(); - } catch (err) { - $.log(err, 'Reveal Spoilers'); - } - } - if (Conf['Auto-GIF']) { - try { - AutoGIF.init(); - } catch (err) { - $.log(err, 'Auto-GIF'); - } - } - if (Conf['Image Hover']) { - try { - ImageHover.init(); - } catch (err) { - $.log(err, 'Image Hover'); - } - } - if (Conf['Thread Updater']) { - try { - ThreadUpdater.init(); - } catch (err) { - $.log(err, 'Thread Updater'); - } - } + return console.timeEnd("" + name + " initialization"); + }; + console.time('All initializations'); + initFeature('Header', Header); + initFeature('Settings', Settings); + initFeature('Resurrect Quotes', Quotify); + initFeature('Filter', Filter); + initFeature('Thread Hiding', ThreadHiding); + initFeature('Reply Hiding', ReplyHiding); + initFeature('Recursive', Recursive); + initFeature('Menu', Menu); + initFeature('Report Link', ReportLink); + initFeature('Thread Hiding (Menu)', ThreadHiding.menu); + initFeature('Reply Hiding (Menu)', ReplyHiding.menu); + initFeature('Delete Link', DeleteLink); + initFeature('Filter (Menu)', Filter.menu); + initFeature('Download Link', DownloadLink); + initFeature('Archive Link', ArchiveLink); + initFeature('Quote Inline', QuoteInline); + initFeature('Quote Preview', QuotePreview); + initFeature('Quote Backlinks', QuoteBacklink); + initFeature('Mark OP Quotes', QuoteOP); + initFeature('Mark Cross-thread Quotes', QuoteCT); + initFeature('Anonymize', Anonymize); + initFeature('Time Formatting', Time); + initFeature('File Info Formatting', FileInfo); + initFeature('Sauce', Sauce); + initFeature('Reveal Spoilers', RevealSpoilers); + initFeature('Auto-GIF', AutoGIF); + initFeature('Image Hover', ImageHover); + initFeature('Thread Updater', ThreadUpdater); + console.timeEnd('All initializations'); return $.ready(Main.initReady); }, initReady: function() { - var boardChild, posts, thread, threadChild, threads, _i, _j, _len, _len1, _ref, _ref1, _ref2; + var boardChild, errors, posts, thread, threadChild, threads, _i, _j, _len, _len1, _ref, _ref1, _ref2; if (d.title === '4chan - 404 Not Found') { + $.rmClass(d.documentElement, 'fourchan_x'); if (Conf['404 Redirect'] && g.VIEW === 'thread') { location.href = Redirect.to({ board: g.BOARD, @@ -4303,30 +4248,92 @@ try { posts.push(new Post(threadChild, thread, g.BOARD)); } catch (err) { - $.log(threadChild, err); + if (!errors) { + errors = []; + } + errors.push({ + message: "Parsing of Post No." + (threadChild.id.match(/\d+/)) + " failed. Post will be skipped.", + error: err + }); } } } - Main.callbackNodes(Thread, threads, true); - return Main.callbackNodes(Post, posts, true); + if (errors) { + Main.handleErrors(errors); + } + Main.callbackNodes(Thread, threads); + return Main.callbackNodes(Post, posts); }, - callbackNodes: function(klass, nodes, notify) { - var callback, i, len, _i, _j, _len, _ref; + callbackNodes: function(klass, nodes) { + var callback, errors, i, len, node, _i, _j, _len, _ref; len = nodes.length; _ref = klass.prototype.callbacks; for (_i = 0, _len = _ref.length; _i < _len; _i++) { callback = _ref[_i]; - try { - for (i = _j = 0; 0 <= len ? _j < len : _j > len; i = 0 <= len ? ++_j : --_j) { - callback.cb.call(nodes[i]); + for (i = _j = 0; 0 <= len ? _j < len : _j > len; i = 0 <= len ? ++_j : --_j) { + node = nodes[i]; + try { + callback.cb.call(node); + } catch (err) { + if (!errors) { + errors = []; + } + errors.push({ + message: "\"" + callback.name + "\" crashed on " + klass.name + " No." + node + " (/" + node.board + "/).", + error: err + }); } - } catch (err) { - $.log(callback.name, 'crashed. error:', err.message, nodes[i]); - $.log(err.stack); } } + if (errors) { + return Main.handleErrors(errors); + } }, - css: "/* general */\n.dialog.reply {\n display: block;\n border: 1px solid rgba(0, 0, 0, .25);\n padding: 0;\n}\n.move {\n cursor: move;\n}\nlabel {\n cursor: pointer;\n}\na[href=\"javascript:;\"] {\n text-decoration: none;\n}\n.warning {\n color: red;\n}\n\n/* 4chan style fixes */\n.opContainer, .op {\n display: block !important;\n}\n.post {\n overflow: visible !important;\n}\n[hidden] {\n display: none !important;\n}\n\n/* fixed, z-index */\n#qp, #ihover,\n#updater, #stats,\n#header,\n#qr, #watcher {\n position: fixed;\n}\n#qp, #ihover {\n z-index: 100;\n}\n#menu {\n z-index: 95;\n}\n#updater, #stats {\n z-index: 90;\n}\n#header:hover {\n z-index: 80;\n}\n#qr {\n z-index: 50;\n}\n#watcher {\n z-index: 30;\n}\n#header {\n z-index: 10;\n}\n\n/* XXX support different styles */\n#header-bar {\n font-size: 9pt;\n color: #89A;\n background-color: #D6DAF0;\n border-color: #B7C5D9;\n border-width: 0 0 1px;\n border-style: solid;\n}\n\n/* header */\nbody.fourchan_x {\n margin-top: 2em;\n}\n#header {\n top: 0;\n right: 0;\n left: 0;\n}\n#header-bar {\n padding: 4px;\n position: relative;\n transition: all .1s ease-in-out;\n -o-transition: all .1s ease-in-out;\n -moz-transition: all .1s ease-in-out;\n -webkit-transition: all .1s ease-in-out;\n}\n#header-bar.autohide:not(:hover) {\n transform: translateY(-100%);\n -o-transform: translateY(-100%);\n -moz-transform: translateY(-100%);\n -webkit-transform: translateY(-100%);\n transition: all .75s .25s ease-in-out;\n -o-transition: all .75s .25s ease-in-out;\n -moz-transition: all .75s .25s ease-in-out;\n -webkit-transition: all .75s .25s ease-in-out;\n}\n#toggle-header-bar {\n cursor: n-resize;\n left: 0;\n right: 0;\n bottom: -8px;\n height: 10px;\n position: absolute;\n}\n#header-bar.autohide #toggle-header-bar {\n cursor: s-resize;\n}\n#header-bar a {\n text-decoration: none;\n padding: 1px;\n}\n#header-bar > .menu-button {\n float: right;\n padding: 0;\n}\nbody > #boardNavDesktop,\n#navtopright,\n#boardNavDesktopFoot {\n display: none !important;\n}\n\n/* thread updater */\n#updater {\n text-align: right;\n}\n#updater:not(:hover) {\n background: none;\n border: none;\n}\n#updater input[type=number] {\n width: 4em;\n}\n#updater:not(:hover) > div:not(.move) {\n display: none;\n}\n.new {\n color: limegreen;\n}\n\n/* quote */\n.quotelink.deadlink {\n text-decoration: underline !important;\n}\n.deadlink:not(.quotelink) {\n text-decoration: none !important;\n}\n.inlined {\n opacity: .5;\n}\n#qp input, .forwarded {\n display: none;\n}\n.quotelink.forwardlink,\n.backlink.forwardlink {\n text-decoration: none;\n border-bottom: 1px dashed;\n}\n.filtered {\n text-decoration: underline line-through;\n}\n.inline {\n border: 1px solid rgba(128, 128, 128, .5);\n display: table;\n margin: 2px 0;\n}\n.inline .post {\n border: 0 !important;\n display: table !important;\n margin: 0 !important;\n padding: 1px 2px !important;\n}\n#qp {\n padding: 2px 2px 5px;\n}\n#qp .post {\n border: none;\n margin: 0;\n padding: 0;\n}\n#qp img {\n max-height: 300px;\n max-width: 500px;\n}\n.qphl {\n box-shadow: 0 0 0 2px rgba(216, 94, 49, .7);\n}\n\n/* file */\n.fileText:hover .fntrunc,\n.fileText:not(:hover) .fnfull {\n display: none;\n}\n#ihover {\n box-sizing: border-box;\n -moz-box-sizing: border-box;\n max-height: 100%;\n max-width: 75%;\n padding-bottom: 16px;\n}\n\n/* Filter */\n.opContainer.filter-highlight {\n box-shadow: inset 5px 0 rgba(255, 0, 0, .5);\n}\n.opContainer.filter-highlight.qphl {\n box-shadow: inset 5px 0 rgba(255, 0, 0, .5),\n 0 0 0 2px rgba(216, 94, 49, .7);\n}\n.filter-highlight > .reply {\n box-shadow: -5px 0 rgba(255, 0, 0, .5);\n}\n.filter-highlight > .reply.qphl {\n box-shadow: -5px 0 rgba(255, 0, 0, .5),\n 0 0 0 2px rgba(216, 94, 49, .7)\n}\n\n/* Thread & Reply Hiding */\n.hide-thread-button,\n.hide-reply-button {\n float: left;\n margin-right: 2px;\n}\n.stub ~ .sideArrows,\n.stub ~ .hide-reply-button,\n.stub ~ .post {\n display: none !important;\n}\n\n/* Menu */\n.menu-button {\n display: inline-block;\n}\n.menu-button > span {\n border-top: 6px solid;\n border-right: 4px solid transparent;\n border-left: 4px solid transparent;\n display: inline-block;\n margin: 2px;\n vertical-align: middle;\n}\n#menu {\n position: absolute;\n outline: none;\n}\n.entry {\n border-bottom: 1px solid rgba(0, 0, 0, .25);\n cursor: pointer;\n display: block;\n outline: none;\n padding: 3px 7px;\n position: relative;\n text-decoration: none;\n white-space: nowrap;\n}\n.entry:last-child {\n border: none;\n}\n.focused.entry {\n background: rgba(255, 255, 255, .33);\n}\n.entry.has-submenu {\n padding-right: 20px;\n}\n.has-submenu::after {\n content: \"\";\n border-left: 6px solid;\n border-top: 4px solid transparent;\n border-bottom: 4px solid transparent;\n display: inline-block;\n margin: 4px;\n position: absolute;\n right: 3px;\n}\n.has-submenu:not(.focused) > .submenu {\n display: none;\n}\n.submenu {\n position: absolute;\n margin: -1px 0;\n}\n.entry input {\n margin: 0;\n}" + handleErrors: function(errors) { + var div, error, logs, _i, _len; + if (!('length' in errors)) { + error = errors; + } else if (errors.length === 1) { + error = errors[0]; + } + if (error) { + new Notification('error', Main.parseError(error), 15); + return; + } + div = $.el('div', { + innerHTML: "" + errors.length + " errors occured. [show]" + }); + $.on(div.lastElementChild, 'click', function() { + if (this.textContent === 'show') { + this.textContent = 'hide'; + return logs.hidden = false; + } else { + this.textContent = 'show'; + return logs.hidden = true; + } + }); + logs = $.el('div', { + hidden: true + }); + for (_i = 0, _len = errors.length; _i < _len; _i++) { + error = errors[_i]; + $.add(logs, Main.parseError(error)); + } + return new Notification('error', [div, logs], 30); + }, + parseError: function(data) { + var error, message; + message = data.message, error = data.error; + $.log(message, error.stack); + message = $.el('div', { + textContent: message + }); + error = $.el('div', { + textContent: error + }); + return [message, error]; + }, + css: "/* general */\n.dialog.reply {\n display: block;\n border: 1px solid rgba(0, 0, 0, .25);\n padding: 0;\n}\n.move {\n cursor: move;\n}\nlabel {\n cursor: pointer;\n}\na[href=\"javascript:;\"] {\n text-decoration: none;\n}\n.warning {\n color: red;\n}\n\n/* 4chan style fixes */\n.opContainer, .op {\n display: block !important;\n}\n.post {\n overflow: visible !important;\n}\n[hidden] {\n display: none !important;\n}\n\n/* fixed, z-index */\n#qp, #ihover,\n#updater, #stats,\n#header,\n#qr, #watcher {\n position: fixed;\n}\n#notifications {\n z-index: 80;\n}\n#qp, #ihover {\n z-index: 70;\n}\n#menu {\n z-index: 60;\n}\n#updater, #stats {\n z-index: 50;\n}\n#header:hover {\n z-index: 40;\n}\n#qr {\n z-index: 30;\n}\n#header {\n z-index: 20;\n}\n#watcher {\n z-index: 10;\n}\n\n/* XXX support different styles */\n#header-bar {\n font-size: 9pt;\n color: #89A;\n background-color: #D6DAF0;\n border-color: #B7C5D9;\n border-width: 0 0 1px;\n border-style: solid;\n}\n\n/* header */\n.fourchan_x body {\n margin-top: 2em;\n}\n#header {\n top: 0;\n right: 0;\n left: 0;\n}\n#header-bar {\n padding: 4px;\n position: relative;\n transition: all .1s ease-in-out;\n -o-transition: all .1s ease-in-out;\n -moz-transition: all .1s ease-in-out;\n -webkit-transition: all .1s ease-in-out;\n}\n#header-bar.autohide:not(:hover) {\n margin-bottom: -1em;\n transform: translateY(-100%);\n -o-transform: translateY(-100%);\n -moz-transform: translateY(-100%);\n -webkit-transform: translateY(-100%);\n transition: all .75s .25s ease-in-out;\n -o-transition: all .75s .25s ease-in-out;\n -moz-transition: all .75s .25s ease-in-out;\n -webkit-transition: all .75s .25s ease-in-out;\n}\n#toggle-header-bar {\n cursor: n-resize;\n left: 0;\n right: 0;\n bottom: -8px;\n height: 10px;\n position: absolute;\n}\n#header-bar.autohide #toggle-header-bar {\n cursor: s-resize;\n}\n#header-bar a {\n text-decoration: none;\n padding: 1px;\n}\n#header-bar > .menu-button {\n float: right;\n padding: 0;\n}\nbody > #boardNavDesktop,\n#navtopright,\n#boardNavDesktopFoot {\n display: none !important;\n}\n\n/* notifications */\n#notifications {\n text-align: center;\n}\n.notification {\n color: #FFF;\n font-weight: 700;\n text-shadow: 0 1px 2px rgba(0, 0, 0, .5);\n border-radius: 2px;\n margin: 1px auto;\n width: 500px;\n max-width: 100%;\n position: relative;\n transition: all .25s ease-in-out;\n -o-transition: all .25s ease-in-out;\n -moz-transition: all .25s ease-in-out;\n -webkit-transition: all .25s ease-in-out;\n}\n.notification.error {\n background-color: hsl(0, 100%, 40%);\n}\n.notification.warning {\n background-color: hsl(36, 100%, 40%);\n}\n.notification.info {\n background-color: hsl(200, 100%, 40%);\n}\n.notification.success {\n background-color: hsl(104, 100%, 40%);\n}\n.notification > .close {\n color: white;\n padding: 4px 6px;\n top: 0;\n right: 0;\n position: absolute;\n}\n.message {\n box-sizing: border-box;\n padding: 4px 20px;\n max-height: 200px;\n width: 100%;\n overflow: auto;\n}\n\n/* thread updater */\n#updater {\n text-align: right;\n}\n#updater:not(:hover) {\n background: none;\n border: none;\n}\n#updater input[type=number] {\n width: 4em;\n}\n#updater:not(:hover) > div:not(.move) {\n display: none;\n}\n.new {\n color: limegreen;\n}\n\n/* quote */\n.quotelink.deadlink {\n text-decoration: underline !important;\n}\n.deadlink:not(.quotelink) {\n text-decoration: none !important;\n}\n.inlined {\n opacity: .5;\n}\n#qp input, .forwarded {\n display: none;\n}\n.quotelink.forwardlink,\n.backlink.forwardlink {\n text-decoration: none;\n border-bottom: 1px dashed;\n}\n.filtered {\n text-decoration: underline line-through;\n}\n.inline {\n border: 1px solid rgba(128, 128, 128, .5);\n display: table;\n margin: 2px 0;\n}\n.inline .post {\n border: 0 !important;\n display: table !important;\n margin: 0 !important;\n padding: 1px 2px !important;\n}\n#qp {\n padding: 2px 2px 5px;\n}\n#qp .post {\n border: none;\n margin: 0;\n padding: 0;\n}\n#qp img {\n max-height: 300px;\n max-width: 500px;\n}\n.qphl {\n box-shadow: 0 0 0 2px rgba(216, 94, 49, .7);\n}\n\n/* file */\n.fileText:hover .fntrunc,\n.fileText:not(:hover) .fnfull {\n display: none;\n}\n#ihover {\n box-sizing: border-box;\n -moz-box-sizing: border-box;\n max-height: 100%;\n max-width: 75%;\n padding-bottom: 16px;\n}\n\n/* Filter */\n.opContainer.filter-highlight {\n box-shadow: inset 5px 0 rgba(255, 0, 0, .5);\n}\n.opContainer.filter-highlight.qphl {\n box-shadow: inset 5px 0 rgba(255, 0, 0, .5),\n 0 0 0 2px rgba(216, 94, 49, .7);\n}\n.filter-highlight > .reply {\n box-shadow: -5px 0 rgba(255, 0, 0, .5);\n}\n.filter-highlight > .reply.qphl {\n box-shadow: -5px 0 rgba(255, 0, 0, .5),\n 0 0 0 2px rgba(216, 94, 49, .7)\n}\n\n/* Thread & Reply Hiding */\n.hide-thread-button,\n.hide-reply-button {\n float: left;\n margin-right: 2px;\n}\n.stub ~ .sideArrows,\n.stub ~ .hide-reply-button,\n.stub ~ .post {\n display: none !important;\n}\n\n/* Menu */\n.menu-button {\n display: inline-block;\n}\n.menu-button > span {\n border-top: 6px solid;\n border-right: 4px solid transparent;\n border-left: 4px solid transparent;\n display: inline-block;\n margin: 2px;\n vertical-align: middle;\n}\n#menu {\n position: absolute;\n outline: none;\n}\n.entry {\n border-bottom: 1px solid rgba(0, 0, 0, .25);\n cursor: pointer;\n display: block;\n outline: none;\n padding: 3px 7px;\n position: relative;\n text-decoration: none;\n white-space: nowrap;\n}\n.entry:last-child {\n border: none;\n}\n.focused.entry {\n background: rgba(255, 255, 255, .33);\n}\n.entry.has-submenu {\n padding-right: 20px;\n}\n.has-submenu::after {\n content: \"\";\n border-left: 6px solid;\n border-top: 4px solid transparent;\n border-bottom: 4px solid transparent;\n display: inline-block;\n margin: 4px;\n position: absolute;\n right: 3px;\n}\n.has-submenu:not(.focused) > .submenu {\n display: none;\n}\n.submenu {\n position: absolute;\n margin: -1px 0;\n}\n.entry input {\n margin: 0;\n}" }; Main.init(); diff --git a/css/style.css b/css/style.css index e7f7f150a..a9472404b 100644 --- a/css/style.css +++ b/css/style.css @@ -35,25 +35,28 @@ a[href="javascript:;"] { #qr, #watcher { position: fixed; } -#qp, #ihover { - z-index: 100; -} -#menu { - z-index: 95; -} -#updater, #stats { - z-index: 90; -} -#header:hover { +#notifications { z-index: 80; } -#qr { +#qp, #ihover { + z-index: 70; +} +#menu { + z-index: 60; +} +#updater, #stats { z-index: 50; } -#watcher { +#header:hover { + z-index: 40; +} +#qr { z-index: 30; } #header { + z-index: 20; +} +#watcher { z-index: 10; } @@ -68,7 +71,7 @@ a[href="javascript:;"] { } /* header */ -body.fourchan_x { +.fourchan_x body { margin-top: 2em; } #header { @@ -85,6 +88,7 @@ body.fourchan_x { -webkit-transition: all .1s ease-in-out; } #header-bar.autohide:not(:hover) { + margin-bottom: -1em; transform: translateY(-100%); -o-transform: translateY(-100%); -moz-transform: translateY(-100%); @@ -119,6 +123,51 @@ body > #boardNavDesktop, display: none !important; } +/* notifications */ +#notifications { + text-align: center; +} +.notification { + color: #FFF; + font-weight: 700; + text-shadow: 0 1px 2px rgba(0, 0, 0, .5); + border-radius: 2px; + margin: 1px auto; + width: 500px; + max-width: 100%; + position: relative; + transition: all .25s ease-in-out; + -o-transition: all .25s ease-in-out; + -moz-transition: all .25s ease-in-out; + -webkit-transition: all .25s ease-in-out; +} +.notification.error { + background-color: hsl(0, 100%, 40%); +} +.notification.warning { + background-color: hsl(36, 100%, 40%); +} +.notification.info { + background-color: hsl(200, 100%, 40%); +} +.notification.success { + background-color: hsl(104, 100%, 40%); +} +.notification > .close { + color: white; + padding: 4px 6px; + top: 0; + right: 0; + position: absolute; +} +.message { + box-sizing: border-box; + padding: 4px 20px; + max-height: 200px; + width: 100%; + overflow: auto; +} + /* thread updater */ #updater { text-align: right; diff --git a/lib/$.coffee b/lib/$.coffee index c1d923466..4ff3a0715 100644 --- a/lib/$.coffee +++ b/lib/$.coffee @@ -22,7 +22,7 @@ $.extend $, id: (id) -> d.getElementById id ready: (fc) -> - if /interactive|complete/.test d.readyState + if d.readyState in ['interactive', 'complete'] $.queueTask fc return cb = -> @@ -106,9 +106,6 @@ $.extend $, tn: (s) -> d.createTextNode s nodes: (nodes) -> - # In (at least) Chrome, elements created inside different - # scripts/window contexts inherit from unequal prototypes. - # window_context1.Node !== window_context2.Node unless nodes instanceof Array return nodes frag = d.createDocumentFragment() diff --git a/src/config.coffee b/src/config.coffee index e8aabbaa0..84e55d97d 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -57,8 +57,8 @@ Config = 'Quote Preview': [true, 'Show quoted post on hover.'] 'Quote Highlighting': [true, 'Highlight the previewed post.'] 'Resurrect Quotes': [true, 'Linkify dead quotes to archives.'] - 'Indicate OP Quotes': [true, 'Add \'(OP)\' to OP quotes.'] - 'Indicate Cross-thread Quotes': [true, 'Add \'(Cross-thread)\' to cross-threads quotes.'] + 'Mark OP Quotes': [true, 'Add \'(OP)\' to OP quotes.'] + 'Mark Cross-thread Quotes': [true, 'Add \'(Cross-thread)\' to cross-threads quotes.'] filter: name: [ '# Filter any namefags:' diff --git a/src/features.coffee b/src/features.coffee index b092153ba..5d09d0d0a 100644 --- a/src/features.coffee +++ b/src/features.coffee @@ -40,8 +40,9 @@ Header = try @setBoardList() catch err - # XXX handle error - $.log err, 'Header - board list' + Main.handleErrors + message: '"Header (board list)" crashed.' + error: err $.asap (-> d.body), -> $.prepend d.body, Header.headerEl @@ -74,6 +75,31 @@ Header = menuToggle: (e) -> Header.menu.toggle e, @, g +class Notification + constructor: (@type, content, timeout) -> + @el = $.el 'div', + className: "notification #{type}" + innerHTML: '×
' + $.on @el.firstElementChild, 'click', @close.bind @ + if typeof content is 'string' + content = $.tn content + $.add @el.lastElementChild, content + + if timeout + setTimeout @close.bind(@), timeout * $.SECOND + + el = @el + $.ready -> + $.add $.id('notifications'), el + + setType: (type) -> + $.rmClass @el, @type + $.addClass @el, type + @type = type + + close: -> + $.rm @el + Settings = init: -> # 4chan X settings link @@ -107,6 +133,8 @@ Settings = Filter = filters: {} init: -> + return if g.VIEW is 'catalog' or !Conf['Filter'] + for key of Config.filter @filters[key] = [] for filter in Conf[key].split '\n' @@ -134,8 +162,7 @@ Filter = regexp = RegExp regexp[1], regexp[2] catch err # I warned you, bro. - # XXX handle error - alert err.message + new Notification 'warning', err.message, 60 continue # Filter OPs along with their threads, replies only, or both. @@ -274,6 +301,8 @@ Filter = menu: init: -> + return if g.VIEW is 'catalog' or !Conf['Menu'] or !Conf['Filter'] + div = $.el 'div', textContent: 'Filter' @@ -376,7 +405,8 @@ Filter = ThreadHiding = init: -> - return if g.VIEW isnt 'index' + return if g.VIEW isnt 'index' or !Conf['Thread Hiding'] + @getHiddenThreads() @syncFromCatalog() @clean() @@ -438,7 +468,8 @@ ThreadHiding = menu: init: -> - return if g.VIEW isnt 'index' + return if g.VIEW isnt 'index' or !Conf['Menu'] or !Conf['Thread Hiding'] + div = $.el 'div', className: 'hide-thread-link' textContent: 'Hide thread' @@ -535,6 +566,8 @@ ThreadHiding = ReplyHiding = init: -> + return if g.VIEW is 'catalog' or !Conf['Reply Hiding'] + @getHiddenPosts() @clean() Post::callbacks.push @@ -583,6 +616,8 @@ ReplyHiding = menu: init: -> + return if g.VIEW is 'catalog' or !Conf['Menu'] or !Conf['Reply Hiding'] + div = $.el 'div', className: 'hide-reply-link' textContent: 'Hide reply' @@ -726,6 +761,8 @@ Recursive = Menu = init: -> + return if g.VIEW is 'catalog' or !Conf['Menu'] + @menu = new UI.Menu 'post' Post::callbacks.push name: 'Menu' @@ -758,6 +795,8 @@ Menu = ReportLink = init: -> + return if g.VIEW is 'catalog' or !Conf['Menu'] or !Conf['Report Link'] + a = $.el 'a', className: 'report-link' href: 'javascript:;' @@ -777,6 +816,8 @@ ReportLink = DeleteLink = init: -> + return if g.VIEW is 'catalog' or !Conf['Menu'] or !Conf['Delete Link'] + div = $.el 'div', className: 'delete-link' textContent: 'Delete' @@ -881,6 +922,8 @@ DeleteLink = DownloadLink = init: -> + return if g.VIEW is 'catalog' or !Conf['Menu'] or !Conf['Download Link'] + # Test for download feature support. return if $.el('a').download is undefined a = $.el 'a', @@ -896,6 +939,8 @@ DownloadLink = ArchiveLink = init: -> + return if g.VIEW is 'catalog' or !Conf['Menu'] or !Conf['Archive Link'] + div = $.el 'div', textContent: 'Archive' @@ -1554,6 +1599,8 @@ Get = Quotify = init: -> + return if g.VIEW is 'catalog' or !Conf['Resurrect Quotes'] + Post::callbacks.push name: 'Resurrect Quotes' cb: @node @@ -1621,6 +1668,8 @@ Quotify = QuoteInline = init: -> + return if g.VIEW is 'catalog' or !Conf['Quote Inline'] + Post::callbacks.push name: 'Quote Inline' cb: @node @@ -1701,6 +1750,8 @@ QuoteInline = QuotePreview = init: -> + return if g.VIEW is 'catalog' or !Conf['Quote Preview'] + Post::callbacks.push name: 'Quote Preview' cb: @node @@ -1767,6 +1818,8 @@ QuoteBacklink = # This is is so that fetched posts can get their backlinks, # and that as much backlinks are appended in the background as possible. init: -> + return if g.VIEW is 'catalog' or !Conf['Quote Backlinks'] + format = Conf['backlink'].replace /%id/g, "' + id + '" @funk = Function 'id', "return '#{format}'" @containers = {} @@ -1810,10 +1863,12 @@ QuoteBacklink = QuoteOP = init: -> + return if g.VIEW is 'catalog' or !Conf['Mark OP Quotes'] + # \u00A0 is nbsp @text = '\u00A0(OP)' Post::callbacks.push - name: 'Indicate OP Quotes' + name: 'Mark OP Quotes' cb: @node node: -> # Stop there if it's a clone of a post in the same thread. @@ -1838,10 +1893,12 @@ QuoteOP = QuoteCT = init: -> + return if g.VIEW is 'catalog' or !Conf['Mark Cross-thread Quotes'] + # \u00A0 is nbsp @text = '\u00A0(Cross-thread)' Post::callbacks.push - name: 'Indicate Cross-thread Quotes' + name: 'Mark Cross-thread Quotes' cb: @node node: -> # Stop there if it's a clone of a post in the same thread. @@ -1862,6 +1919,8 @@ QuoteCT = Anonymize = init: -> + return if g.VIEW is 'catalog' or !Conf['Anonymize'] + Post::callbacks.push name: 'Anonymize' cb: @node @@ -1882,6 +1941,8 @@ Anonymize = Time = init: -> + return if g.VIEW is 'catalog' or !Conf['Time Formatting'] + @funk = @createFunc Conf['time'] Post::callbacks.push name: 'Time Formatting' @@ -1940,6 +2001,8 @@ Time = FileInfo = init: -> + return if g.VIEW is 'catalog' or !Conf['File Info Formatting'] + @funk = @createFunc Conf['fileInfo'] Post::callbacks.push name: 'File Info Formatting' @@ -1990,6 +2053,8 @@ FileInfo = Sauce = init: -> + return if g.VIEW is 'catalog' or !Conf['Sauce'] + links = [] for link in Conf['sauces'].split '\n' continue if link[0] is '#' @@ -2031,6 +2096,8 @@ Sauce = RevealSpoilers = init: -> + return if g.VIEW is 'catalog' or !Conf['Reveal Spoilers'] + Post::callbacks.push name: 'Reveal Spoilers' cb: @node @@ -2042,7 +2109,8 @@ RevealSpoilers = AutoGIF = init: -> - return if g.BOARD.ID in ['gif', 'wsg'] + return if g.VIEW is 'catalog' or !Conf['Auto-GIF'] or g.BOARD.ID in ['gif', 'wsg'] + Post::callbacks.push name: 'Auto-GIF' cb: @node @@ -2062,7 +2130,8 @@ AutoGIF = ImageHover = init: -> - return if g.BOARD.ID in ['gif', 'wsg'] + return if g.VIEW is 'catalog' or !Conf['Image Hover'] + Post::callbacks.push name: 'Auto-GIF' cb: @node @@ -2101,7 +2170,8 @@ ImageHover = ThreadUpdater = init: -> - return if g.VIEW isnt 'thread' + return if g.VIEW isnt 'thread' or !Conf['Thread Updater'] + Thread::callbacks.push name: 'Thread Updater' cb: @node diff --git a/src/main.coffee b/src/main.coffee index e29798627..d7566620e 100644 --- a/src/main.coffee +++ b/src/main.coffee @@ -277,211 +277,56 @@ Main = location.href = url if url return - return if g.VIEW is 'catalog' - $.addStyle Main.css - $.asap (-> d.body), (-> - $.addClass d.body, $.engine - $.addClass d.body, 'fourchan_x' - ) + $.addClass d.documentElement, $.engine + $.addClass d.documentElement, 'fourchan_x' - try - Header.init() - catch err - # XXX handle error - $.log err, 'Header' - - try - Settings.init() - catch err - # XXX handle error - $.log err, 'Settings' - - if Conf['Resurrect Quotes'] + initFeature = (name, module) -> + console.time "#{name} initialization" try - Quotify.init() + module.init() catch err - # XXX handle error - $.log err, 'Resurrect Quotes' + Main.handleErrors + message: "\"#{name}\" initialization crashed." + error: err + console.timeEnd "#{name} initialization" - if Conf['Filter'] - try - Filter.init() - catch err - # XXX handle error - $.log err, 'Filter' - - if Conf['Thread Hiding'] - try - ThreadHiding.init() - catch err - # XXX handle error - $.log err, 'Thread Hiding' - - if Conf['Reply Hiding'] - try - ReplyHiding.init() - catch err - # XXX handle error - $.log err, 'Reply Hiding' - - try - Recursive.init() - catch err - # XXX handle error - $.log err, 'Recursive' - - if Conf['Menu'] - try - Menu.init() - catch err - # XXX handle error - $.log err, 'Menu' - - if Conf['Report Link'] - try - ReportLink.init() - catch err - # XXX handle error - $.log err, 'Report Link' - - if Conf['Thread Hiding'] - try - ThreadHiding.menu.init() - catch err - # XXX handle error - $.log err, 'Thread Hiding - Menu' - - if Conf['Reply Hiding'] - try - ReplyHiding.menu.init() - catch err - # XXX handle error - $.log err, 'Reply Hiding - Menu' - - if Conf['Delete Link'] - try - DeleteLink.init() - catch err - # XXX handle error - $.log err, 'Delete Link' - - if Conf['Filter'] - try - Filter.menu.init() - catch err - # XXX handle error - $.log err, 'Filter - Menu' - - if Conf['Download Link'] - try - DownloadLink.init() - catch err - # XXX handle error - $.log err, 'Download Link' - - if Conf['Archive Link'] - try - ArchiveLink.init() - catch err - # XXX handle error - $.log err, 'Archive Link' - - if Conf['Quote Inline'] - try - QuoteInline.init() - catch err - # XXX handle error - $.log err, 'Quote Inline' - - if Conf['Quote Preview'] - try - QuotePreview.init() - catch err - # XXX handle error - $.log err, 'Quote Preview' - - if Conf['Quote Backlinks'] - try - QuoteBacklink.init() - catch err - # XXX handle error - $.log err, 'Quote Backlinks' - - if Conf['Indicate OP Quotes'] - try - QuoteOP.init() - catch err - # XXX handle error - $.log err, 'Indicate OP Quotes' - - if Conf['Indicate Cross-thread Quotes'] - try - QuoteCT.init() - catch err - # XXX handle error - $.log err, 'Indicate Cross-thread Quotes' - - if Conf['Anonymize'] - try - Anonymize.init() - catch e - # XXX handle error - $.log err, 'Anonymize' - - if Conf['Time Formatting'] - try - Time.init() - catch err - # XXX handle error - $.log err, 'Time Formatting' - - if Conf['File Info Formatting'] - try - FileInfo.init() - catch err - # XXX handle error - $.log err, 'File Info Formatting' - - if Conf['Sauce'] - try - Sauce.init() - catch err - # XXX handle error - $.log err, 'Sauce' - - if Conf['Reveal Spoilers'] - try - RevealSpoilers.init() - catch err - # XXX handle error - $.log err, 'Reveal Spoilers' - - if Conf['Auto-GIF'] - try - AutoGIF.init() - catch err - # XXX handle error - $.log err, 'Auto-GIF' - - if Conf['Image Hover'] - try - ImageHover.init() - catch err - # XXX handle error - $.log err, 'Image Hover' - - if Conf['Thread Updater'] - try - ThreadUpdater.init() - catch err - # XXX handle error - $.log err, 'Thread Updater' + console.time 'All initializations' + initFeature 'Header', Header + initFeature 'Settings', Settings + initFeature 'Resurrect Quotes', Quotify + initFeature 'Filter', Filter + initFeature 'Thread Hiding', ThreadHiding + initFeature 'Reply Hiding', ReplyHiding + initFeature 'Recursive', Recursive + initFeature 'Menu', Menu + initFeature 'Report Link', ReportLink + initFeature 'Thread Hiding (Menu)', ThreadHiding.menu + initFeature 'Reply Hiding (Menu)', ReplyHiding.menu + initFeature 'Delete Link', DeleteLink + initFeature 'Filter (Menu)', Filter.menu + initFeature 'Download Link', DownloadLink + initFeature 'Archive Link', ArchiveLink + initFeature 'Quote Inline', QuoteInline + initFeature 'Quote Preview', QuotePreview + initFeature 'Quote Backlinks', QuoteBacklink + initFeature 'Mark OP Quotes', QuoteOP + initFeature 'Mark Cross-thread Quotes', QuoteCT + initFeature 'Anonymize', Anonymize + initFeature 'Time Formatting', Time + initFeature 'File Info Formatting', FileInfo + initFeature 'Sauce', Sauce + initFeature 'Reveal Spoilers', RevealSpoilers + initFeature 'Auto-GIF', AutoGIF + initFeature 'Image Hover', ImageHover + initFeature 'Thread Updater', ThreadUpdater + console.timeEnd 'All initializations' $.ready Main.initReady initReady: -> if d.title is '4chan - 404 Not Found' + $.rmClass d.documentElement, 'fourchan_x' if Conf['404 Redirect'] and g.VIEW is 'thread' location.href = Redirect.to board: g.BOARD @@ -505,25 +350,66 @@ Main = posts.push new Post threadChild, thread, g.BOARD catch err # Skip posts that we failed to parse. - # XXX handle error - # Post parser crashed for post No.#{threadChild.id[2..]} - $.log threadChild, err + unless errors + errors = [] + errors.push + message: "Parsing of Post No.#{threadChild.id.match(/\d+/)} failed. Post will be skipped." + error: err + Main.handleErrors errors if errors - Main.callbackNodes Thread, threads, true - Main.callbackNodes Post, posts, true + Main.callbackNodes Thread, threads + Main.callbackNodes Post, posts - callbackNodes: (klass, nodes, notify) -> + callbackNodes: (klass, nodes) -> # get the nodes' length only once len = nodes.length for callback in klass::callbacks - try - for i in [0...len] - callback.cb.call nodes[i] - catch err - # XXX handle error if notify - $.log callback.name, 'crashed. error:', err.message, nodes[i] - $.log err.stack - return + for i in [0...len] + node = nodes[i] + try + callback.cb.call node + catch err + unless errors + errors = [] + errors.push + message: "\"#{callback.name}\" crashed on #{klass.name} No.#{node} (/#{node.board}/)." + error: err + Main.handleErrors errors if errors + + handleErrors: (errors) -> + unless 'length' of errors + error = errors + else if errors.length is 1 + error = errors[0] + if error + new Notification 'error', Main.parseError(error), 15 + return + + div = $.el 'div', + innerHTML: "#{errors.length} errors occured. [show]" + $.on div.lastElementChild, 'click', -> + if @textContent is 'show' + @textContent = 'hide' + logs.hidden = false + else + @textContent = 'show' + logs.hidden = true + + logs = $.el 'div', + hidden: true + for error in errors + $.add logs, Main.parseError error + + new Notification 'error', [div, logs], 30 + + parseError: (data) -> + {message, error} = data + $.log message, error.stack + message = $.el 'div', + textContent: message + error = $.el 'div', + textContent: error + [message, error] css: """<%= grunt.file.read('css/style.css') %>"""