diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 42fa67ad2..66cdd1973 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,10 +3,10 @@ 1. Make sure both your **browser** and **4chan X** are up to date. 2. Disable your other extensions & scripts to identify conflicts. 3. If your issue persists, open a [new issue](https://github.com/MayhemYDG/4chan-x/issues) with the following information: - 1. Report precise steps to reproduce the problem. - 2. Report console errors, if any. - 3. Report browser version. - 4. Include your exported settings. + 1. Precise steps to reproduce the problem. + 2. Console errors, if any. + 3. Browser version. + 4. Your exported settings. Open your console with: - `Ctrl + Shift + J` on Chrome. diff --git a/Gruntfile.coffee b/Gruntfile.coffee index cc031785f..a21e7644e 100644 --- a/Gruntfile.coffee +++ b/Gruntfile.coffee @@ -23,31 +23,18 @@ module.exports = (grunt) -> ] dest: 'tmp/script.coffee' - manifest: - options: - process: - data: pkg - src: 'src/manifest.json', - dest: 'builds/crx/manifest.json' - - metadata: - options: - process: - data: pkg - src: 'src/metadata.js', - dest: '<%= pkg.name %>.meta.js' - crx: options: process: data: pkg - src: [ - 'src/banner.js' - 'tmp/script.js' - ] - dest: 'builds/crx/script.js' + files: + 'builds/crx/manifest.json': 'src/manifest.json' + 'builds/crx/script.js': [ + 'src/banner.js' + 'tmp/script.js' + ] - userscript: + userjs: options: process: data: pkg @@ -56,13 +43,27 @@ module.exports = (grunt) -> 'src/banner.js' 'tmp/script.js' ] - dest: '<%= pkg.name %>.user.js' - - userjs: - # Lazily copy the userscript - src: '<%= pkg.name %>.user.js' dest: 'builds/<%= pkg.name %>.js' + userscript: + options: + process: + data: pkg + files: + 'builds/<%= pkg.name %>.meta.js': 'src/metadata.js' + 'builds/<%= pkg.name %>.user.js': [ + 'src/metadata.js' + 'src/banner.js' + 'tmp/script.js' + ] + + copy: + crx: + src: 'img/*.png' + dest: 'builds/crx/' + expand: true + flatten: true + coffee: script: src: 'tmp/script.coffee' @@ -89,35 +90,73 @@ module.exports = (grunt) -> options: interrupt: true files: [ - 'Gruntfile.js' + 'Gruntfile.coffee' 'package.json' - 'lib/**/*.coffee' - 'src/**/*.coffee' - 'src/**/*.js' - 'css/**/*.css' - 'img/*' + 'lib/**/*' + 'src/**/*' + 'css/**/*' + 'img/**/*' ] - tasks: 'default' + tasks: 'build' + + compress: + crx: + options: + archive: 'builds/4chan-X.zip' + level: 9 + pretty: true + expand: true + cwd: 'builds/crx/' + src: '**' clean: - tmp: 'tmp' + builds: 'builds' + tmp: 'tmp' grunt.loadNpmTasks 'grunt-bump' grunt.loadNpmTasks 'grunt-contrib-clean' grunt.loadNpmTasks 'grunt-contrib-coffee' + grunt.loadNpmTasks 'grunt-contrib-compress' grunt.loadNpmTasks 'grunt-contrib-concat' + grunt.loadNpmTasks 'grunt-contrib-copy' grunt.loadNpmTasks 'grunt-contrib-watch' grunt.loadNpmTasks 'grunt-exec' - grunt.registerTask 'default', [ - 'concat:coffee', - 'coffee:script', - 'concat:manifest', - 'concat:crx', - 'concat:userscript', - 'concat:userjs', - 'concat:metadata', - 'clean' + grunt.registerTask 'default', ['build'] + + grunt.registerTask 'set-build', 'Set the build type variable', (type) -> + pkg.type = type; + grunt.log.ok 'pkg.type = %s', type + + grunt.registerTask 'build', [ + 'build-crx' + 'build-userjs' + 'build-userscript' + ] + + grunt.registerTask 'build-crx', [ + 'set-build:crx' + 'concat:coffee' + 'coffee:script' + 'concat:crx' + 'copy:crx' + 'clean:tmp' + ] + + grunt.registerTask 'build-userjs', [ + 'set-build:userjs' + 'concat:coffee' + 'coffee:script' + 'concat:userjs' + 'clean:tmp' + ] + + grunt.registerTask 'build-userscript', [ + 'set-build:userscript' + 'concat:coffee' + 'coffee:script' + 'concat:userscript' + 'clean:tmp' ] grunt.registerTask 'release', [ @@ -140,6 +179,7 @@ module.exports = (grunt) -> 'bump:major' 'updcl:1' ] + grunt.registerTask 'updcl', 'Update the changelog', (i) -> # Update the `pkg` object with the new version. pkg = grunt.file.readJSON('package.json'); diff --git a/README.md b/README.md index d377b6372..c8c176c3d 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # 4chan X -Get it [here](http://mayhemydg.github.com/4chan-x/). +Get it [here](https://4chan-x.just-believe.in/). *** -### [MIT License](/4chan-x/blob/master/LICENSE) -### [Contribute](/4chan-x/blob/master/CONTRIBUTING.md) +### [MIT License](/LICENSE) +### [Contribute](/CONTRIBUTING.md) diff --git a/changelog b/changelog-old similarity index 100% rename from changelog rename to changelog-old diff --git a/css/style.css b/css/style.css index 3f21f4327..ae66b6610 100644 --- a/css/style.css +++ b/css/style.css @@ -60,7 +60,7 @@ a[href="javascript:;"] { #qp, #ihover, #updater, #thread-stats, #navlinks, #header, -#qr, #watcher { +#qr { position: fixed; } #overlay { @@ -81,12 +81,15 @@ a[href="javascript:;"] { #qr { z-index: 30; } -#watcher { +#watcher:hover { z-index: 20; } #header { z-index: 10; } +#watcher { + z-index: 5; +} /* Header */ .fourchan-x body { @@ -104,8 +107,8 @@ a[href="javascript:;"] { display: flex; padding: 3px 4px 4px; position: relative; - -webkit-transition: all .1s ease-in-out; - transition: all .1s ease-in-out; + -webkit-transition: all .1s .05s ease-in-out; + transition: all .1s .05s ease-in-out; } #board-list { -webkit-flex: 1; @@ -252,6 +255,9 @@ a[href="javascript:;"] { -webkit-flex: 1; flex: 1; } +.tab-selected { + font-weight: 700; +} .section-container { -webkit-flex: 1; flex: 1; @@ -421,7 +427,7 @@ a[href="javascript:;"] { max-height: 300px; max-width: 500px; } -.qphl { +.qphl > .post { outline: 2px solid rgba(216, 94, 49, .7); } @@ -435,6 +441,9 @@ a[href="javascript:;"] { .expanding { opacity: .5; } +.expanded-image { + clear: both; +} .expanded-image > .op > .file::after { content: ''; clear: both; @@ -736,6 +745,11 @@ a[href="javascript:;"] { #file-n-submit > #qr-file-spoiler { margin: 0 2px; } +#file-n-submit input[type='submit'] { + min-width: 40px; + -webkit-order: 1; + order: 1; +} #qr input[type='file'] { position: absolute; visibility: hidden; diff --git a/img/favicons/Mayhem/unreadNSFWY.png b/img/favicons/Mayhem/unreadNSFWY.png index a4b95550d..16180b91c 100644 Binary files a/img/favicons/Mayhem/unreadNSFWY.png and b/img/favicons/Mayhem/unreadNSFWY.png differ diff --git a/img/favicons/Mayhem/unreadSFWY.png b/img/favicons/Mayhem/unreadSFWY.png index 7b5b0ee60..3f48cf5c2 100644 Binary files a/img/favicons/Mayhem/unreadSFWY.png and b/img/favicons/Mayhem/unreadSFWY.png differ diff --git a/img/favicons/Original/unreadDeadY.png b/img/favicons/Original/unreadDeadY.png index f0a1bdab4..f5f41b903 100644 Binary files a/img/favicons/Original/unreadDeadY.png and b/img/favicons/Original/unreadDeadY.png differ diff --git a/img/favicons/Original/unreadNSFWY.png b/img/favicons/Original/unreadNSFWY.png index f005e187c..fb562d564 100644 Binary files a/img/favicons/Original/unreadNSFWY.png and b/img/favicons/Original/unreadNSFWY.png differ diff --git a/img/favicons/Original/unreadSFWY.png b/img/favicons/Original/unreadSFWY.png index 881821f9f..eaf49a8e3 100644 Binary files a/img/favicons/Original/unreadSFWY.png and b/img/favicons/Original/unreadSFWY.png differ diff --git a/img/favicons/ferongr/unreadDeadY.png b/img/favicons/ferongr/unreadDeadY.png index 39c27dcec..e8008c9c6 100644 Binary files a/img/favicons/ferongr/unreadDeadY.png and b/img/favicons/ferongr/unreadDeadY.png differ diff --git a/img/favicons/ferongr/unreadNSFWY.png b/img/favicons/ferongr/unreadNSFWY.png index 4f4fa0979..35c59ddc2 100644 Binary files a/img/favicons/ferongr/unreadNSFWY.png and b/img/favicons/ferongr/unreadNSFWY.png differ diff --git a/img/favicons/ferongr/unreadSFWY.png b/img/favicons/ferongr/unreadSFWY.png index f4397364b..4ab87b0c4 100644 Binary files a/img/favicons/ferongr/unreadSFWY.png and b/img/favicons/ferongr/unreadSFWY.png differ diff --git a/img/favicons/xat-/unreadDeadY.png b/img/favicons/xat-/unreadDeadY.png index dab4041c2..b18f8140c 100644 Binary files a/img/favicons/xat-/unreadDeadY.png and b/img/favicons/xat-/unreadDeadY.png differ diff --git a/img/favicons/xat-/unreadNSFWY.png b/img/favicons/xat-/unreadNSFWY.png index 6e5802597..4ee06355c 100644 Binary files a/img/favicons/xat-/unreadNSFWY.png and b/img/favicons/xat-/unreadNSFWY.png differ diff --git a/img/favicons/xat-/unreadSFWY.png b/img/favicons/xat-/unreadSFWY.png index aa0b53c66..4e6c1b4be 100644 Binary files a/img/favicons/xat-/unreadSFWY.png and b/img/favicons/xat-/unreadSFWY.png differ diff --git a/img/icon128.png b/img/icon128.png new file mode 100644 index 000000000..e75a8f86b Binary files /dev/null and b/img/icon128.png differ diff --git a/img/icon16.png b/img/icon16.png new file mode 100644 index 000000000..a1d2a71e8 Binary files /dev/null and b/img/icon16.png differ diff --git a/img/icon48.png b/img/icon48.png new file mode 100644 index 000000000..227f681ed Binary files /dev/null and b/img/icon48.png differ diff --git a/lib/$.coffee b/lib/$.coffee index 447bb1c3d..d23fa6a8e 100644 --- a/lib/$.coffee +++ b/lib/$.coffee @@ -51,7 +51,7 @@ $.extend String::, $.DAY = 24 * ($.HOUR = 60 * ($.MINUTE = 60 * ($.SECOND = 1000))) $.extend $, - engine: /WebKit|Presto|Gecko/.exec(navigator.userAgent)[0].toLowerCase() + engine: '<% if (type === 'crx') { %>webkit<% } else if (type === 'userjs') { %>presto<% } else { %>gecko<% } %>' id: (id) -> d.getElementById id ready: (fc) -> @@ -226,19 +226,8 @@ $.extend $, globalEval: (code) -> script = $.el 'script', textContent: code - $.add d.head, script + $.add (d.head or doc), script $.rm script - # http://mths.be/unsafewindow - unsafeWindow: - if window.opera # Opera - window - else if unsafeWindow? # Firefox - unsafeWindow - else # Chrome - do -> - p = d.createElement 'p' - p.setAttribute 'onclick', 'return window' - p.onclick() bytesToString: (size) -> unit = 0 # Bytes while size >= 1024 @@ -255,11 +244,30 @@ $.extend $, Math.round size "#{size} #{['B', 'KB', 'MB', 'GB'][unit]}" -if GM_deleteValue? +<% if (type === 'crx') { %> + delete: (name) -> + localStorage.removeItem g.NAMESPACE + name + get: (name, defaultValue) -> + if value = localStorage.getItem g.NAMESPACE + name + JSON.parse value + else + defaultValue + set: (name, value) -> + localStorage.setItem g.NAMESPACE + name, JSON.stringify value +<% } else if (type === 'userjs') { %> +do -> + # http://www.opera.com/docs/userjs/specs/#scriptstorage + # http://www.opera.com/docs/userjs/using/#securepages + # The scriptStorage object is available only during + # the main User JavaScript thread, being therefore + # accessible only in the main body of the user script. + # To access the storage object later, keep a reference + # to the object. + {scriptStorage} = opera $.delete = (name) -> - GM_deleteValue g.NAMESPACE + name + delete scriptStorage[g.NAMESPACE + name] $.get = (name, defaultValue) -> - if value = GM_getValue g.NAMESPACE + name + if value = scriptStorage[g.NAMESPACE + name] JSON.parse value else defaultValue @@ -268,37 +276,19 @@ if GM_deleteValue? value = JSON.stringify value # for `storage` events localStorage.setItem name, value - GM_setValue name, value -else if window.opera - do -> - # http://www.opera.com/docs/userjs/specs/#scriptstorage - # http://www.opera.com/docs/userjs/using/#securepages - # >The scriptStorage object is available only during - # the main User JavaScript thread, being therefore - # accessible only in the main body of the user script. - # To access the storage object later, keep a reference - # to the object. - {scriptStorage} = opera - $.delete = (name) -> - delete scriptStorage[g.NAMESPACE + name] - $.get = (name, defaultValue) -> - if value = scriptStorage[g.NAMESPACE + name] - JSON.parse value - else - defaultValue - $.set = (name, value) -> - name = g.NAMESPACE + name - value = JSON.stringify value - # for `storage` events - localStorage.setItem name, value - scriptStorage[name] = value -else - $.delete = (name) -> - localStorage.removeItem g.NAMESPACE + name - $.get = (name, defaultValue) -> - if value = localStorage.getItem g.NAMESPACE + name + scriptStorage[name] = value +<% } else { %> + delete: (name) -> + GM_deleteValue g.NAMESPACE + name + get: (name, defaultValue) -> + if value = GM_getValue g.NAMESPACE + name JSON.parse value else defaultValue - $.set = (name, value) -> - localStorage.setItem g.NAMESPACE + name, JSON.stringify value + set: (name, value) -> + name = g.NAMESPACE + name + value = JSON.stringify value + # for `storage` events + localStorage.setItem name, value + GM_setValue name, value +<% } %> diff --git a/package.json b/package.json index a099124d4..32095fc07 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "meta": { "name": "4chan X Beta", "repo": "https://github.com/MayhemYDG/4chan-x/", - "page": "http://mayhemydg.github.com/4chan-x/", + "page": "https://4chan-x.just-believe.in/", "mainBranch": "v3", "matches": [ "*://api.4chan.org/*", @@ -15,12 +15,14 @@ ] }, "devDependencies": { - "grunt": "~0.4.0", + "grunt": "~0.4.1", "grunt-bump": "~0.0.0", "grunt-contrib-clean": "~0.4.0", - "grunt-contrib-coffee": "~0.6.2", - "grunt-contrib-concat": "~0.1.0", - "grunt-contrib-watch": "~0.3.0", + "grunt-contrib-coffee": "~0.6.4", + "grunt-contrib-compress": "~0.4.5", + "grunt-contrib-concat": "~0.1.3", + "grunt-contrib-copy": "~0.4.0", + "grunt-contrib-watch": "~0.3.1", "grunt-exec": "~0.4.0" }, "repository": { diff --git a/src/config.coffee b/src/config.coffee index 31eb11526..bee11d02a 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -15,7 +15,7 @@ Config = ] 'Custom Board Navigation': [ true - 'Disable this to always display the full board list.' + 'Show custom links instead of the full board list.' ] '404 Redirect': [ true @@ -27,7 +27,7 @@ Config = ] 'Time Formatting': [ true - 'Localize and format timestamps arbitrarily.' + 'Localize and format timestamps.' ] 'Relative Post Dates': [ false @@ -39,19 +39,15 @@ Config = ] 'Comment Expansion': [ true - 'Can expand too long comments.' + 'Add buttons to expand long comments.' ] 'Thread Expansion': [ true - 'Can expand threads to view all replies.' + 'Add buttons to expand threads.' ] 'Index Navigation': [ false - 'Navigate to previous / next thread.' - ] - 'Custom CSS': [ - false - 'Apply custom CSS to 4chan.' + 'Add buttons to navigate between threads.' ] 'Check for Updates': [ true @@ -75,7 +71,7 @@ Config = 'Filtering': 'Anonymize': [ false - 'Turn everyone Anonymous.' + 'Make everyone Anonymous.' ] 'Filter': [ true @@ -87,19 +83,19 @@ Config = ] 'Thread Hiding': [ true - 'Hide entire threads.' + 'Add buttons to hide entire threads.' ] 'Reply Hiding': [ true - 'Hide single replies.' + 'Add buttons to hide single replies.' ] 'Hiding Buttons': [ true - 'Make buttons to hide threads / replies, in addition to menu links.' + 'Add buttons to hide threads / replies, in addition to menu links.' ] 'Stubs': [ true - 'Make stubs of hidden threads / replies.' + 'Show stubs of hidden threads / replies.' ] 'Images': @@ -135,8 +131,16 @@ Config = 'Menu': 'Menu': [ true - 'Add a drop-down menu in posts.' + 'Add a drop-down menu to posts.' ] + 'Thread Hiding Link': [ + true + 'Add a link to hide entire threads.' + ] + 'Reply Hiding Link': [ + true + 'Add a link to hide single replies.' + ] 'Report Link': [ true 'Add a report link to the menu.' @@ -222,7 +226,7 @@ Config = 'Hide the normal post form.' ] - 'Quote links': + 'Quote Links': 'Quote Backlinks': [ true 'Add quote backlinks.' @@ -331,21 +335,23 @@ Config = MD5: '' sauces: """ -http://iqdb.org/?url=%turl -http://www.google.com/searchbyimage?image_url=%turl -#http://tineye.com/search?url=%turl -#http://saucenao.com/search.php?db=999&url=%turl -#http://3d.iqdb.org/?url=%turl -#http://regex.info/exif.cgi?imgurl=%url +http://iqdb.org/?url=%TURL +https://www.google.com/searchbyimage?image_url=%TURL +#//tineye.com/search?url=%TURL +#http://saucenao.com/search.php?url=%TURL +#http://3d.iqdb.org/?url=%TURL +#http://regex.info/exif.cgi?imgurl=%URL # uploaders: -#http://imgur.com/upload?url=%url;text:Upload to imgur -#http://omploader.org/upload?url1=%url;text:Upload to omploader +#http://imgur.com/upload?url=%URL;text:Upload to imgur +#http://ompldr.org/upload?url1=%URL;text:Upload to ompldr # "View Same" in archives: #//archive.foolz.us/_/search/image/%MD5/;text:View same on foolz #//archive.foolz.us/%board/search/image/%MD5/;text:View same on foolz /%board/ #//archive.installgentoo.net/%board/image/%MD5;text:View same on installgentoo /%board/ """ + 'Custom CSS': false + 'Header auto-hide': false 'Header catalog links': false diff --git a/src/features.coffee b/src/features.coffee index 665e07294..a448820a2 100644 --- a/src/features.coffee +++ b/src/features.coffee @@ -31,7 +31,7 @@ Header = className: 'hide-board-list-button brackets-wrap' innerHTML: ' - ' $.on btn, 'click', Header.toggleBoardList - $.prepend fullBoardList, btn + $.add fullBoardList, btn else $.rm $ '#custom-board-list', nav fullBoardList.hidden = false @@ -40,8 +40,8 @@ Header = list = $ '#custom-board-list', Header.nav list.innerHTML = null return unless text - as = $$('#full-board-list a', Header.nav) - nodes = text.match(/[\w@]+(-(all|title|full|text:"[^"]+"))?|[^\w@]+/g).map (t) -> + as = $$('#full-board-list a', Header.nav)[0...-2] # ignore the Settings and Home links + nodes = text.match(/[\w@]+(-(all|title|full|index|catalog|text:"[^"]+"))*|[^\w@]+/g).map (t) -> if /^[^\w@]/.test t return $.tn t if t is 'toggle-all' @@ -58,12 +58,17 @@ Header = for a in as if a.textContent is board a = a.cloneNode true - if /-title$/.test t + if /-title/.test t a.textContent = a.title - else if /-full$/.test t + else if /-full/.test t a.textContent = "/#{board}/ - #{a.title}" - else if m = t.match /-text:"(.+)"$/ - a.textContent = m[1] + else if /-(index|catalog|text)/.test t + if m = t.match /-(index|catalog)/ + a.setAttribute 'data-only', m[1] + a.href = "//boards.4chan.org/#{board}/" + a.href += 'catalog' if m[1] is 'catalog' + if m = t.match /-text:"(.+)"/ + a.textContent = m[1] else if board is '@' $.addClass a, 'navSmall' return a @@ -224,17 +229,15 @@ Settings = links = [] for section in Settings.sections link = $.el 'a', + className: "tab-#{section.hyphenatedTitle}" textContent: section.title href: 'javascript:;' $.on link, 'click', Settings.openSection.bind section links.push link, $.tn ' | ' sectionToOpen = link if section.title is openSection links.pop() - if sectionToOpen - sectionToOpen.click() - else - links[0].click() $.add $('.sections-list', overlay), links + (if sectionToOpen then sectionToOpen else links[0]).click() $.on $('.close', overlay), 'click', Settings.close $.on overlay, 'click', Settings.close @@ -254,11 +257,15 @@ Settings = addSection: (title, open) -> if typeof title isnt 'string' {title, open} = title.detail - Settings.sections.push {title, open} + hyphenatedTitle = title.toLowerCase().replace /\s+/g, '-' + Settings.sections.push {title, hyphenatedTitle, open} openSection: -> + if selected = $ '.tab-selected', Settings.dialog + $.rmClass selected, 'tab-selected' + $.addClass $(".tab-#{@hyphenatedTitle}", Settings.dialog), 'tab-selected' section = $ 'section', Settings.dialog section.innerHTML = null - section.className = "section-#{@title.toLowerCase().replace /\s+/g, '-'}" + section.className = "section-#{@hyphenatedTitle}" @open section, g section.scrollTop = 0 @@ -283,7 +290,7 @@ Settings = description = arr[1] div = $.el 'div', innerHTML: ": #{description}" - $.on $('input', div), 'click', $.cb.checked + $.on $('input', div), 'change', $.cb.checked $.add fs, div $.add section, fs @@ -311,7 +318,7 @@ Settings = className: 'warning' textContent: 'Save me!' download: "<%= meta.name %> v#{g.VERSION}-#{now}.json" - href: "data:application/json;base64,#{btoa unescape encodeURIComponent JSON.stringify data}" + href: "data:application/json;base64,#{btoa unescape encodeURIComponent JSON.stringify data, null, 2}" target: '_blank' if $.engine isnt 'gecko' a.click() @@ -451,7 +458,7 @@ Settings = $.add div, ta return div.innerHTML = """ -
Filter is disabled.
+
Filter is disabled.

Use regular expressions, one per line.
Lines starting with a # will be ignored.
@@ -509,6 +516,9 @@ Settings =

Title link: board-title
Full text link: board-full
Custom text link: board-text:"VIP Board"
+
Index-only link: board-index
+
Catalog-only link: board-catalog
+
Combinations are possible: board-index-text:"VIP Index"
Full board list toggle: toggle-all
@@ -551,9 +561,9 @@ Settings =
- Custom CSS is disabled. + Custom CSS - +
""" for name in ['boardnav', 'time', 'backlink', 'fileInfo', 'favicon', 'usercss'] @@ -567,6 +577,7 @@ Settings = unless 'usercss' is name $.on input, event, Settings[name] Settings[name].call input + $.on $('input[name="Custom CSS"]', section), 'change', Settings.togglecss $.on $.id('apply-css'), 'click', Settings.usercss boardnav: -> Header.generateBoardList @value @@ -591,12 +602,20 @@ Settings = favicon: -> Favicon.switch() Unread.update() if g.VIEW is 'thread' and Conf['Unread Tab Icon'] - @nextElementSibling.innerHTML = " " - usercss: -> - if Conf['Custom CSS'] - CustomCSS.update() - else + @nextElementSibling.innerHTML = """ + + + + + """ + togglecss: -> + if $('textarea', @parentNode.parentNode).disabled = !@checked CustomCSS.rmStyle() + else + CustomCSS.addStyle() + $.cb.checked.call @ + usercss: -> + CustomCSS.update() keybinds: (section) -> section.innerHTML = """ @@ -632,33 +651,40 @@ Fourchan = board = g.BOARD.ID if board is 'g' + $.globalEval """ + window.addEventListener('prettyprint', function(e) { + var pre = e.detail; + pre.innerHTML = prettyPrintOne(pre.innerHTML); + }, false); + """ Post::callbacks.push name: 'Parse /g/ code' cb: @code if board is 'sci' + # https://github.com/MayhemYDG/4chan-x/issues/645#issuecomment-13704562 + $.globalEval """ + window.addEventListener('jsmath', function(e) { + if (jsMath.loaded) { + // process one post + jsMath.ProcessBeforeShowing(e.detail); + } else { + // load jsMath and process whole document + jsMath.Autoload.Script.Push('ProcessBeforeShowing', [null]); + jsMath.Autoload.LoadJsMath(); + } + }, false); + """ Post::callbacks.push name: 'Parse /sci/ math' cb: @math code: -> return if @isClone for pre in $$ '.prettyprint', @nodes.comment - pre.innerHTML = $.unsafeWindow.prettyPrintOne pre.innerHTML + $.event 'prettyprint', pre, window return math: -> return if @isClone or !$ '.math', @nodes.comment - # https://github.com/MayhemYDG/4chan-x/issues/645#issuecomment-13704562 - {jsMath} = $.unsafeWindow - if jsMath - if jsMath.loaded - # process one post - jsMath.ProcessBeforeShowing @nodes.post - else - # load jsMath and process whole document - # Yes this requires to be globalEval'd, don't ask me why. - $.globalEval """ - jsMath.Autoload.Script.Push('ProcessBeforeShowing', [null]); - jsMath.Autoload.LoadJsMath(); - """ + $.event 'jsmath', @nodes.post, window parseThread: (threadID, offset, limit) -> # Fix /sci/ # Fix /g/ @@ -959,7 +985,7 @@ Filter = ThreadHiding = init: -> - return if g.VIEW isnt 'index' or !Conf['Thread Hiding'] + return if g.VIEW isnt 'index' or !Conf['Thread Hiding'] and !Conf['Thread Hiding Link'] Misc.clearThreads "hiddenThreads.#{g.BOARD}" @getHiddenThreads() @@ -971,7 +997,7 @@ ThreadHiding = node: -> if data = ThreadHiding.hiddenThreads.threads[@] ThreadHiding.hide @, data.makeStub - return unless Conf['Hiding Buttons'] + return unless Conf['Thread Hiding'] $.prepend @OP.nodes.root, ThreadHiding.makeButton @, 'hide' getHiddenThreads: -> @@ -999,7 +1025,7 @@ ThreadHiding = menu: init: -> - return if g.VIEW isnt 'index' or !Conf['Menu'] or !Conf['Thread Hiding'] + return if g.VIEW isnt 'index' or !Conf['Menu'] or !Conf['Thread Hiding Link'] div = $.el 'div', className: 'hide-thread-link' @@ -1101,7 +1127,7 @@ ThreadHiding = ReplyHiding = init: -> - return if g.VIEW is 'catalog' or !Conf['Reply Hiding'] + return if g.VIEW is 'catalog' or !Conf['Reply Hiding'] and !Conf['Reply Hiding Link'] Misc.clearThreads "hiddenPosts.#{g.BOARD}" @getHiddenPosts() @@ -1118,7 +1144,7 @@ ReplyHiding = else Recursive.apply ReplyHiding.hide, @, data.makeStub, true Recursive.add ReplyHiding.hide, @, data.makeStub, true - return unless Conf['Hiding Buttons'] + return unless Conf['Reply Hiding'] $.replace $('.sideArrows', @nodes.root), ReplyHiding.makeButton @, 'hide' getHiddenPosts: -> @@ -1126,7 +1152,7 @@ ReplyHiding = menu: init: -> - return if g.VIEW is 'catalog' or !Conf['Menu'] or !Conf['Reply Hiding'] + return if g.VIEW is 'catalog' or !Conf['Menu'] or !Conf['Reply Hiding Link'] # Hide div = $.el 'div', @@ -1336,7 +1362,7 @@ Recursive = QuoteStrikeThrough = init: -> - return if g.VIEW is 'catalog' or !Conf['Reply Hiding'] and !Conf['Filter'] + return if g.VIEW is 'catalog' or !Conf['Reply Hiding'] and !Conf['Reply Hiding Link'] and !Conf['Filter'] Post::callbacks.push name: 'Strike-through Quotes' @@ -1624,7 +1650,7 @@ Keybinds = when Conf['Open settings'] Settings.open() when Conf['Close'] - if $.id 'settings' + if $.id 'fourchanx-settings' Settings.close() else if (notifications = $$ '.notification').length for notification in notifications @@ -1791,7 +1817,6 @@ Keybinds = focus: (post) -> $.addClass post, 'highlight' - $('a[title="Highlight this post"]', post).focus() Nav = init: -> @@ -2419,10 +2444,10 @@ Misc = # super semantic return if data.lastChecked > Date.now() - 12 * $.HOUR - $.ajax "//api.4chan.org/#{g.BOARD}/catalog.json", onload: -> + $.ajax "//api.4chan.org/#{g.BOARD}/threads.json", onload: -> threads = {} - for obj in JSON.parse @response - for thread in obj.threads + for page in JSON.parse @response + for thread in page.threads if thread.no of data.threads threads[thread.no] = data.threads[thread.no] unless Object.keys(threads).length @@ -2630,7 +2655,7 @@ QuotePreview = # Remove the clone that's in the qp from the array. posts.pop() for post in posts - $.addClass post.nodes.post, 'qphl' + $.addClass post.nodes.root, 'qphl' quoterID = $.x('ancestor::*[@id][1]', @).id.match(/\d+$/)[0] clone = Get.postFromRoot qp.firstChild @@ -2651,7 +2676,7 @@ QuotePreview = return unless Conf['Quote Highlighting'] for post in [post].concat post.clones - $.rmClass post.nodes.post, 'qphl' + $.rmClass post.nodes.root, 'qphl' return QuoteBacklink = @@ -3055,14 +3080,15 @@ Sauce = createSauceLink: (link) -> link = link.replace /%(T?URL|MD5|board)/ig, (parameter) -> switch parameter - when '%TURL', '%turl' - "' + post.file.thumbURL + '" - when '%URL', '%url' - "' + post.file.URL + '" - when '%MD5', '%md5' + + when '%TURL' + "' + encodeURIComponent(post.file.thumbURL) + '" + when '%URL' + "' + encodeURIComponent(post.file.URL) + '" + when '%MD5' "' + encodeURIComponent(post.file.MD5) + '" when '%board' - "' + post.board + '" + "' + encodeURIComponent(post.board) + '" else parameter text = if m = link.match(/;text:(.+)$/) then m[1] else link.match(/(\w+)\.\w+\//)[1] @@ -3171,7 +3197,7 @@ ImageExpand = # Scroll back to the thumbnail when contracting the image # to avoid being left miles away from the relevant post. postRect = post.nodes.root.getBoundingClientRect() - headRect = Header.bar.getBoundingClientRect() + headRect = Header.toggle.getBoundingClientRect() top = postRect.top - headRect.top - headRect.height - 2 root = if $.engine is 'webkit' d.body @@ -3222,7 +3248,10 @@ ImageExpand = post = Get.postFromNode @ $.rm @ delete post.file.fullImage - unless $.hasClass post.file.thumb, 'expanding' + # Images can error: + # - before the image started loading. + # - after the image started loading. + unless $.hasClass(post.file.thumb, 'expanding') or $.hasClass post.nodes.root, 'expanded-image' # Don't try to re-expend if it was already contracted. return ImageExpand.contract post @@ -3445,7 +3474,7 @@ ExpandThread = #goddamit moot num = if thread.isSticky 1 - else switch g.BOARD + else switch g.BOARD.ID # XXX boards config when 'b', 'vg', 'q' then 3 when 't' then 1 @@ -3493,7 +3522,7 @@ ExpandThread = # Enable 4chan features. if Conf['Enable 4chan\'s Extension'] - $.unsafeWindow.Parser.parseThread thread.ID, 1, nodes.length + $.globalEval "Parser.parseThread(#{thread.ID}, 1, #{nodes.length})" else Fourchan.parseThread thread.ID, 1, nodes.length @@ -3505,7 +3534,10 @@ ThreadExcerpt = name: 'Thread Excerpt' cb: @node node: -> - d.title = Get.threadExcerpt @ + d.title = if (excerpt = Get.threadExcerpt @).length > 80 + "#{excerpt[...77]}..." + else + excerpt Unread = init: -> @@ -3523,6 +3555,7 @@ Unread = Unread.lastReadPost = $.get("lastReadPosts.#{@board}", threads: {}).threads[@] or 0 Unread.posts = [] Unread.postsQuotingYou = [] + Unread.titleEl = $ 'title', d.head Unread.title = d.title posts = [] for ID, post of @posts @@ -3604,10 +3637,17 @@ Unread = count = Unread.posts.length if Conf['Unread Count'] - d.title = if g.DEAD - "(#{Unread.posts.length}) /#{g.BOARD}/ - 404" + prefix = if count + "(#{count})" else - "(#{Unread.posts.length}) #{Unread.title}" + '' + # XXX Chrome bug where it doesn't always update the tab title. + # crbug.com/16650 + # crbug.com/124381 + Unread.titleEl.textContent = if g.DEAD + "#{prefix} /#{g.BOARD}/ - 404" + else + "#{prefix} #{Unread.title}" return unless Conf['Unread Tab Icon'] @@ -3997,9 +4037,9 @@ ThreadUpdater = $.queueTask -> # Enable 4chan features. threadID = ThreadUpdater.thread.ID - {length} = ThreadUpdater.root.children + {length} = $$ '.thread > .postContainer', ThreadUpdater.root if Conf['Enable 4chan\'s Extension'] - $.unsafeWindow.Parser.parseThread threadID, -count + $.globalEval "Parser.parseThread(#{threadID}, #{-count})" else Fourchan.parseThread threadID, length - count, length @@ -4038,6 +4078,7 @@ ThreadWatcher = $.delete 'AutoWatch' ready: -> + return unless Main.isThisPageLegit() ThreadWatcher.refresh() $.add d.body, ThreadWatcher.dialog diff --git a/src/globals.coffee b/src/globals.coffee index a17a6f7a0..ad48281e4 100644 --- a/src/globals.coffee +++ b/src/globals.coffee @@ -1,12 +1,14 @@ +<% if (type === 'userjs') { %> # Opera doesn't support the @match metadata key, # return 4chan X here if we're not on 4chan. return unless /^(boards|images|sys)\.4chan\.org$/.test location.hostname +<% } %> Conf = {} c = console d = document -doc = null -g = +doc = d.documentElement +g = VERSION: '<%= version %>' NAMESPACE: '<%= meta.name %>.' boards: {} diff --git a/src/main.coffee b/src/main.coffee index aab6f9545..a355587d2 100644 --- a/src/main.coffee +++ b/src/main.coffee @@ -41,28 +41,28 @@ class Post backlinks: info.getElementsByClassName 'backlink' @info = {} - if subject = $ '.subject', info + if subject = $ '.subject', info @nodes.subject = subject @info.subject = subject.textContent - if name = $ '.name', info + if name = $ '.name', info @nodes.name = name @info.name = name.textContent - if email = $ '.useremail', info + if email = $ '.useremail', info @nodes.email = email @info.email = decodeURIComponent email.href[7..] - if tripcode = $ '.postertrip', info + if tripcode = $ '.postertrip', info @nodes.tripcode = tripcode @info.tripcode = tripcode.textContent - if uniqueID = $ '.posteruid', info + if uniqueID = $ '.posteruid', info @nodes.uniqueID = uniqueID @info.uniqueID = uniqueID.firstElementChild.textContent - if capcode = $ '.capcode', info + if capcode = $ '.capcode.hand', info @nodes.capcode = capcode - @info.capcode = capcode.textContent - if flag = $ '.countryFlag', info + @info.capcode = capcode.textContent.replace '## ', '' + if flag = $ '.countryFlag', info @nodes.flag = flag @info.flag = flag.title - if date = $ '.dateTime', info + if date = $ '.dateTime', info @nodes.date = date @info.date = new Date date.dataset.utc * 1000 if Conf['Quick Reply'] @@ -284,9 +284,6 @@ class Clone extends Post Main = init: -> - $.asap (-> d.documentElement), -> - doc = d.documentElement - # flatten Config into Conf # and get saved or default values flatten = (parent, obj) -> @@ -486,7 +483,9 @@ Main = Main.handleErrors errors if errors addCallback: (e) -> - obj = e.detail + obj = e.detail + unless typeof obj.callback.name is 'string' + throw new Error "Invalid callback name: #{obj.callback.name}" Klass = if obj.type is 'Post' Post else @@ -504,7 +503,7 @@ Main = return div = $.el 'div', - innerHTML: "#{errors.length} errors occured. [show]" + innerHTML: "#{errors.length} errors occurred. [show]" $.on div.lastElementChild, 'click', -> if @textContent is 'show' @textContent = 'hide' @@ -533,7 +532,9 @@ Main = isThisPageLegit: -> # 404 error page or similar. unless 'thisPageIsLegit' of Main - Main.thisPageIsLegit = !$('link[href*="favicon-status.ico"]', d.head) and d.title isnt '4chan - Temporarily Offline' + Main.thisPageIsLegit = location.hostname is 'boards.4chan.org' and + !$('link[href*="favicon-status.ico"]', d.head) and + d.title not in ['4chan - Temporarily Offline', '4chan - Error'] Main.thisPageIsLegit css: """ diff --git a/src/manifest.json b/src/manifest.json index f125f6cb5..53e48aeda 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -3,6 +3,11 @@ "version": "<%= version %>", "manifest_version": 2, "description": "<%= description %>", + "icons": { + "16": "icon16.png", + "48": "icon48.png", + "128": "icon128.png" + }, "content_scripts": [{ "js": ["script.js"], "matches": <%= JSON.stringify(meta.matches) %>, diff --git a/src/metadata.js b/src/metadata.js index 415133169..810a371bc 100644 --- a/src/metadata.js +++ b/src/metadata.js @@ -16,7 +16,7 @@ // @grant GM_deleteValue // @grant GM_openInTab // @run-at document-start -// @updateURL <%= meta.repo %>raw/<%= meta.mainBranch %>/<%= name %>.meta.js -// @downloadURL <%= meta.repo %>raw/<%= meta.mainBranch %>/<%= name %>.user.js -// @icon data:image/gif;base64,<%= grunt.file.read('img/icon.gif', {encoding: 'base64'}) %> +// @updateURL <%= meta.page %>builds/<%= name %>.meta.js +// @downloadURL <%= meta.page %>builds/<%= name %>.user.js +// @icon data:image/png;base64,<%= grunt.file.read('img/icon48.png', {encoding: 'base64'}) %> // ==/UserScript== diff --git a/src/qr.coffee b/src/qr.coffee index 3950ff973..42a1061c8 100644 --- a/src/qr.coffee +++ b/src/qr.coffee @@ -486,6 +486,7 @@ QR = # so we generate thumbnails `s` times bigger then expected # to avoid crappy resized quality. s = 90*2 + s *= 3 if @file.type is 'image/gif' # let them animate {height, width} = img if height < s or width < s @URL = fileURL if window.URL @@ -581,6 +582,12 @@ QR = return unless @isEnabled = !!$.id 'captchaFormPart' $.asap (-> $.id 'recaptcha_challenge_field_holder'), @ready.bind @ ready: -> + setLifetime = (e) => @lifetime = e.detail + $.on window, 'captcha:timeout', setLifetime + $.globalEval 'window.dispatchEvent(new CustomEvent("captcha:timeout", {detail: RecaptchaState.timeout}))' + $.off window, 'captcha:timeout', setLifetime + c.log @lifetime + imgContainer = $.el 'div', className: 'captcha-img' title: 'Reload' @@ -648,7 +655,7 @@ QR = load: -> return unless @nodes.challenge.firstChild # -1 minute to give upload some time. - @timeout = Date.now() + $.unsafeWindow.RecaptchaState.timeout * $.SECOND - $.MINUTE + @timeout = Date.now() + @lifetime * $.SECOND - $.MINUTE challenge = @nodes.challenge.firstChild.value @nodes.img.alt = challenge @nodes.img.src = "//www.google.com/recaptcha/api/image?c=#{challenge}" @@ -666,7 +673,7 @@ QR = @nodes.input.alt = count # For XTRM RICE. reload: (focus) -> # the 't' argument prevents the input from being focused - $.unsafeWindow.Recaptcha.reload 't' + $.globalEval 'Recaptcha.reload("t")' # Focus if we meant to. @nodes.input.focus() if focus keydown: (e) -> @@ -704,14 +711,14 @@ QR =
+ No selected file - × - - + × +
@@ -877,12 +884,14 @@ QR = callbacks = onload: QR.response onerror: -> + # Connection error, or + # www.4chan.org/banned delete QR.req post.unlock() QR.cooldown.auto = false QR.status() - # Connection error. - QR.error 'Network error.' + QR.error $.el 'span', + innerHTML: 'Connection error. You may have been banned.' opts = form: $.formData postData upCallbacks: @@ -966,6 +975,7 @@ QR = [_, threadID, postID] = h1.nextSibling.textContent.match /thread:(\d+),no:(\d+)/ postID = +postID threadID = +threadID or postID + isReply = threadID isnt postID (QR.yourPosts.threads[threadID] or= []).push postID $.set "yourPosts.#{g.BOARD}", QR.yourPosts @@ -980,18 +990,15 @@ QR = }, QR.nodes.el # Enable auto-posting if we have stuff to post, disable it otherwise. - QR.cooldown.auto = QR.posts.length > 1 + QR.cooldown.auto = QR.posts.length > 1 and isReply post.rm() - QR.cooldown.set - req: req - post: post - isReply: !!threadID + QR.cooldown.set {req, post, isReply} if threadID is postID # new thread URL = "/#{g.BOARD}/res/#{threadID}" - else if g.VIEW is 'index' and !QR.cooldown.auto # posting from the index + else if g.VIEW is 'index' and !QR.cooldown.auto and Conf['Open Post in New Tab'] # replying from the index URL = "/#{g.BOARD}/res/#{threadID}#p#{postID}" if URL if Conf['Open Post in New Tab'] diff --git a/src/report.coffee b/src/report.coffee index 42ae19bd4..eea172ddc 100644 --- a/src/report.coffee +++ b/src/report.coffee @@ -6,7 +6,7 @@ Report = form = $ 'form' field = $.id 'recaptcha_response_field' $.on field, 'keydown', (e) -> - $.unsafeWindow.Recaptcha.reload 't' if e.keyCode is 8 and not field.value + $.globalEval 'Recaptcha.reload("t")' if e.keyCode is 8 and not field.value $.on form, 'submit', (e) -> e.preventDefault() response = field.value.trim()