This commit is contained in:
Kabir Sala 2014-02-24 23:35:11 +01:00
commit 180844b376
82 changed files with 4965 additions and 2395 deletions

View File

@ -1,8 +1,17 @@
### v1.3.10 ## v1.4.0
*2014-02-20* *2014-02-24*
**Spittie** **ParrotParrot**:
- I keep breaking and fixing stuff. But mostly breaking. - Added `Original filename` variable to Sauce panel.
**MayhemYDG**:
- Added a Reset Settings button in the settings.
- More stability update.
- Stability update.
**Zixaphir**:
- Merge changes from Mayhem fork
### v1.3.9 ### v1.3.9
*2014-02-20* *2014-02-20*

48
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,48 @@
## Reporting bugs and suggestions
Reporting bugs:
1. Make sure both your **browser** and **4chan X** are up to date.<br>
Only **Chrome**, **Firefox** and **Opera** are supported.<br>
**SRWare Iron**, **Firefox ESR**, **Pale Moon**, **Waterfox**, and other derivatives are not supported, use them at your own risk.
2. Look at the list of [known problems and solutions](https://github.com/MayhemYDG/4chan-x/wiki/FAQ#known-problems).
3. Disable your other extensions & scripts to identify conflicts.
4. If your issue persists, open a [new issue](https://github.com/MayhemYDG/4chan-x/issues) with the following information:
1. Precise steps to reproduce the problem, with the expected and actual results.
2. [Console errors](https://github.com/MayhemYDG/4chan-x/wiki/FAQ#console-errors), if any.
3. 4chan X version, browser variant, browser version, and Greasemonkey version if you are using it.
4. Your exported settings. If your settings contains sensible information (e.g. personas), edit the text file manually.
Respect these guidelines:
- Describe the issue clearly, put some effort into it. A one-liner isn't a good enough description.
- If you want to get your suggestion implemented sooner, make it convincing.
- If you want to criticize, make it convincing and constructive.
- Be mature. Act like an idiot and you will be blocked without warning.
## Development & Contribution
### Get started
- Install [node.js](http://nodejs.org/).
- Install [Grunt's CLI](http://gruntjs.com/) with `npm install -g grunt-cli`.
- Clone 4chan X.
- `cd` into it.
- Install/Update 4chan X dependencies with `npm install`.
### Build
- Build with `grunt`.
- Continuously build with `grunt watch`.
### Release
- Update the version with `grunt patch`, `grunt minor` or `grunt major`.
- Release with `grunt release`.
Note: this is only used to release new 4chan X versions, and is **not** needed or wanted in pull requests.
### Contribute
- Edit the sources.
- If the edits affect regular users, edit the changelog.
- Open a pull request.

View File

@ -36,6 +36,7 @@ module.exports = (grunt) ->
'src/Monitoring/**/*' 'src/Monitoring/**/*'
'src/Archive/**/*' 'src/Archive/**/*'
'src/Miscellaneous/**/*' 'src/Miscellaneous/**/*'
'src/General/Navigate.coffee'
'src/General/Settings.coffee' 'src/General/Settings.coffee'
'src/General/Main.coffee' 'src/General/Main.coffee'
] ]

View File

@ -1,5 +1,5 @@
/* /*
* 4chan X - Version 1.3.10 - 2014-02-24 * 4chan X - Version 1.4.0 - 2014-02-24
* *
* Licensed under the MIT license. * Licensed under the MIT license.
* https://github.com/Spittie/4chan-x/blob/master/LICENSE * https://github.com/Spittie/4chan-x/blob/master/LICENSE

View File

@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name 4chan X // @name 4chan X
// @version 1.3.10 // @version 1.4.0
// @minGMVer 1.13 // @minGMVer 1.14
// @minFFVer 26 // @minFFVer 26
// @namespace 4chan-X // @namespace 4chan-X
// @description Cross-browser userscript for maximum lurking on 4chan. // @description Cross-browser userscript for maximum lurking on 4chan.
@ -13,6 +13,7 @@
// @grant GM_getValue // @grant GM_getValue
// @grant GM_setValue // @grant GM_setValue
// @grant GM_deleteValue // @grant GM_deleteValue
// @grant GM_listValues
// @grant GM_openInTab // @grant GM_openInTab
// @grant GM_xmlhttpRequest // @grant GM_xmlhttpRequest
// @run-at document-start // @run-at document-start

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{ {
"name": "4chan X", "name": "4chan X",
"version": "1.3.10", "version": "1.4.0",
"manifest_version": 2, "manifest_version": 2,
"description": "Cross-browser userscript for maximum lurking on 4chan.", "description": "Cross-browser userscript for maximum lurking on 4chan.",
"icons": { "icons": {

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
postMessage({version:'1.3.10'},'*') postMessage({version:'1.4.0'},'*')

View File

@ -1,6 +1,6 @@
{ {
"name": "4chan-X", "name": "4chan-X",
"version": "1.3.10", "version": "1.4.0",
"description": "Cross-browser userscript for maximum lurking on 4chan.", "description": "Cross-browser userscript for maximum lurking on 4chan.",
"meta": { "meta": {
"name": "4chan X", "name": "4chan X",
@ -21,22 +21,22 @@
"min": { "min": {
"chrome": "31", "chrome": "31",
"firefox": "26", "firefox": "26",
"greasemonkey": "1.13" "greasemonkey": "1.14"
} }
}, },
"devDependencies": { "devDependencies": {
"font-awesome": "https://github.com/FortAwesome/Font-Awesome/archive/v4.0.3.tar.gz", "font-awesome": "~4.0.3",
"grunt": "~0.4.1", "grunt": "~0.4.2",
"grunt-bump": "~0.0.11", "grunt-bump": "~0.0.13",
"grunt-concurrent": "~0.4.0", "grunt-concurrent": "~0.4.3",
"grunt-contrib-clean": "~0.5.0", "grunt-contrib-clean": "~0.5.0",
"grunt-contrib-coffee": "~0.8.0", "grunt-contrib-coffee": "~0.8.2",
"grunt-contrib-compress": "~0.5.2", "grunt-contrib-compress": "~0.6.0",
"grunt-contrib-concat": "~0.3.0", "grunt-contrib-concat": "~0.3.0",
"grunt-contrib-copy": "~0.5.0", "grunt-contrib-copy": "~0.5.0",
"grunt-contrib-watch": "~0.5.3", "grunt-contrib-watch": "~0.5.3",
"grunt-shell": "~0.6.0", "grunt-shell": "~0.6.4",
"load-grunt-tasks": "~0.2.0" "load-grunt-tasks": "~0.2.1"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -32,7 +32,6 @@ Recursive =
apply: (recursive, post, args...) -> apply: (recursive, post, args...) ->
{fullID} = post {fullID} = post
for ID, post of g.posts g.posts.forEach (post) ->
if fullID in post.quotes if fullID in post.quotes
recursive post, args... recursive post, args...
return

View File

@ -58,7 +58,7 @@ ThreadHiding =
$.cache "//a.4cdn.org/#{g.BOARD}/threads.json", -> $.cache "//a.4cdn.org/#{g.BOARD}/threads.json", ->
return unless @status is 200 return unless @status is 200
threads = {} threads = {}
for page in JSON.parse @response for page in @response
for thread in page.threads for thread in page.threads
if thread.no of hiddenThreadsOnCatalog if thread.no of hiddenThreadsOnCatalog
threads[thread.no] = hiddenThreadsOnCatalog[thread.no] threads[thread.no] = hiddenThreadsOnCatalog[thread.no]

View File

@ -208,7 +208,8 @@ Build =
className: 'summary' className: 'summary'
textContent: text.join ' ' textContent: text.join ' '
href: "/#{boardID}/res/#{threadID}" href: "/#{boardID}/res/#{threadID}"
thread: (board, data) ->
thread: (board, data, full) ->
Build.spoilerRange[board] = data.custom_spoiler Build.spoilerRange[board] = data.custom_spoiler
if (OP = board.posts[data.no]) and root = OP.nodes.root.parentNode if (OP = board.posts[data.no]) and root = OP.nodes.root.parentNode
@ -218,6 +219,10 @@ Build =
className: 'thread' className: 'thread'
id: "t#{data.no}" id: "t#{data.no}"
$.add root, Build[if full then 'fullThread' else 'excerptThread'] board, data, OP
root
excerptThread: (board, data, OP) ->
nodes = [if OP then OP.nodes.root else Build.postFromObject data, board.ID] nodes = [if OP then OP.nodes.root else Build.postFromObject data, board.ID]
if data.omitted_posts or !Conf['Show Replies'] and data.replies if data.omitted_posts or !Conf['Show Replies'] and data.replies
[posts, files] = if Conf['Show Replies'] [posts, files] = if Conf['Show Replies']
@ -226,6 +231,6 @@ Build =
# XXX data.images is not accurate. # XXX data.images is not accurate.
[data.replies, data.omitted_images + data.last_replies.filter((data) -> !!data.ext).length] [data.replies, data.omitted_images + data.last_replies.filter((data) -> !!data.ext).length]
nodes.push Build.summary board.ID, data.no, posts, files nodes.push Build.summary board.ID, data.no, posts, files
nodes
$.add root, nodes fullThread: (board, data) -> Build.postFromObject data, board.ID
root

View File

@ -1,9 +1,11 @@
# I am bad at JavaScript and if you reuse this, so are you. # I am bad at JavaScript and if you reuse this, so are you.
Array::indexOf = (val) -> Array::indexOf = (val, i) ->
i = @length i or= 0
while i-- len = @length
while i < len
return i if @[i] is val return i if @[i] is val
return i i++
return -1
# Update CoffeeScript's reference to [].indexOf # Update CoffeeScript's reference to [].indexOf
# Reserved keywords are ignored in embedded javascript. # Reserved keywords are ignored in embedded javascript.

View File

@ -1,6 +1,10 @@
Config = Config =
main: main:
'Miscellaneous': 'Miscellaneous':
'JSON Navigation' : [
true
'Use JSON for loading the Board Index and Threads. Also allows searching and sorting the board index and infinite scolling.'
]
'Catalog Links': [ 'Catalog Links': [
true true
'Add toggle link in header menu to turn Navigation links into links to each board\'s catalog.' 'Add toggle link in header menu to turn Navigation links into links to each board\'s catalog.'

View File

@ -1,13 +1,13 @@
Get = Get =
threadExcerpt: (thread) -> threadExcerpt: (thread) ->
{OP} = thread {OP} = thread
excerpt = OP.info.subject?.trim() or excerpt = "/#{thread.board}/ - " + (
OP.info.subject?.trim() or
OP.info.comment.replace(/\n+/g, ' // ') or OP.info.comment.replace(/\n+/g, ' // ') or
Conf['Anonymize'] and 'Anonymous' or Conf['Anonymize'] and 'Anonymous' or
$('.nameBlock', OP.nodes.info).textContent.trim() $('.nameBlock', OP.nodes.info).textContent.trim())
if excerpt.length > 70 return "#{excerpt[...70]}..." if excerpt.length > 73
excerpt = "#{excerpt[...67]}..." excerpt
"/#{thread.board}/ - #{excerpt}"
threadFromRoot: (root) -> threadFromRoot: (root) ->
g.threads["#{g.BOARD}.#{root.id[1..]}"] g.threads["#{g.BOARD}.#{root.id[1..]}"]
threadFromNode: (node) -> threadFromNode: (node) ->
@ -40,24 +40,28 @@ Get =
allQuotelinksLinkingTo: (post) -> allQuotelinksLinkingTo: (post) ->
# Get quotelinks & backlinks linking to the given post. # Get quotelinks & backlinks linking to the given post.
quotelinks = [] quotelinks = []
{posts} = g
fullID = {post}
handleQuotes = (qPost, type) ->
quotelinks.push qPost.nodes[type]...
quotelinks.push clone.nodes[type]... for clone in qPost.clones
return
# First: # First:
# In every posts, # In every posts,
# if it did quote this post, # if it did quote this post,
# get all their backlinks. # get all their backlinks.
for ID, quoterPost of g.posts posts.forEach (qPost) ->
if post.fullID in quoterPost.quotes if fullID in qPost.quotes
for quoterPost in [quoterPost].concat quoterPost.clones handleQuotes qPost, 'quotelinks'
quotelinks.push.apply quotelinks, quoterPost.nodes.quotelinks
# Second: # Second:
# If we have quote backlinks: # If we have quote backlinks:
# in all posts this post quoted # in all posts this post quoted
# and their clones, # and their clones,
# get all of their backlinks. # get all of their backlinks.
if Conf['Quote Backlinks'] if Conf['Quote Backlinks']
for quote in post.quotes handleQuotes qPost, 'backlinks' for quote in post.quotes when qPost = posts[quote]
continue unless quotedPost = g.posts[quote]
for quotedPost in [quotedPost].concat quotedPost.clones
quotelinks.push.apply quotelinks, [quotedPost.nodes.backlinks...]
# Third: # Third:
# Filter out irrelevant quotelinks. # Filter out irrelevant quotelinks.
quotelinks.filter (quotelink) -> quotelinks.filter (quotelink) ->
@ -76,6 +80,7 @@ Get =
$.cache url, $.cache url,
-> Get.archivedPost @, boardID, postID, root, context -> Get.archivedPost @, boardID, postID, root, context
, ,
responseType: 'json'
withCredentials: url.archive.withCredentials withCredentials: url.archive.withCredentials
insert: (post, root, context) -> insert: (post, root, context) ->
# Stop here if the container has been removed while loading. # Stop here if the container has been removed while loading.
@ -114,7 +119,7 @@ Get =
"Error #{req.statusText} (#{req.status})." "Error #{req.statusText} (#{req.status})."
return return
posts = JSON.parse(req.response).posts {posts} = req.response
Build.spoilerRange[boardID] = posts[0].custom_spoiler Build.spoilerRange[boardID] = posts[0].custom_spoiler
for post in posts for post in posts
break if post.no is postID # we found it! break if post.no is postID # we found it!
@ -145,7 +150,7 @@ Get =
Get.insert post, root, context Get.insert post, root, context
return return
data = JSON.parse req.response data = req.response
if data.error if data.error
$.addClass root, 'warning' $.addClass root, 'warning'
root.textContent = data.error root.textContent = data.error
@ -211,28 +216,16 @@ Get =
Main.callbackNodes Post, [post] Main.callbackNodes Post, [post]
Get.insert post, root, context Get.insert post, root, context
parseMarkup: (text) -> parseMarkup: (text) ->
switch text {
when '\n' '\n': '<br>'
'<br>' '[b]': '<b>'
when '[b]' '[/b]': '</b>'
'<b>' '[spoiler]': '<s>'
when '[/b]' '[/spoiler]': '</s>'
'</b>' '[code]': '<pre class=prettyprint>'
when '[spoiler]' '[/code]': '</pre>'
'<s>' '[moot]': '<div style="padding:5px;margin-left:.5em;border-color:#faa;border:2px dashed rgba(255,0,0,.1);border-radius:2px">'
when '[/spoiler]' '[/moot]': '</div>'
'</s>' '[banned]': '<strong style="color: red;">'
when '[code]' '[/banned]': '</strong>'
'<pre class=prettyprint>' }[text] or text.replace ':lit', ''
when '[/code]'
'</pre>'
when '[moot]'
'<div style="padding:5px;margin-left:.5em;border-color:#faa;border:2px dashed rgba(255,0,0,.1);border-radius:2px">'
when '[/moot]'
'</div>'
when '[banned]'
'<strong style="color: red;">'
when '[/banned]'
'</strong>'
else
text.replace ':lit', ''

View File

@ -5,6 +5,4 @@ doc = d.documentElement
g = g =
VERSION: '<%= version %>' VERSION: '<%= version %>'
NAMESPACE: '<%= meta.name %>.' NAMESPACE: '<%= meta.name %>.'
boards: {} boards: {}
threads: {}
posts: {}

View File

@ -102,9 +102,10 @@ Header =
$.ready => $.ready =>
@footer = footer = $.id 'boardNavDesktopFoot' @footer = footer = $.id 'boardNavDesktopFoot'
if Conf['JSON Navigation']
$.on a, 'click', Navigate.navigate for a in $$ 'a', footer
if a = $ "a[href*='/#{g.BOARD}/']", footer if a = $ "a[href*='/#{g.BOARD}/']", footer
a.className = 'current' a.className = 'current'
$.on a, 'click', Index.cb.link
cs = $.el 'a', cs = $.el 'a',
id: 'settingsWindowLink' id: 'settingsWindowLink'
@ -133,21 +134,27 @@ Header =
toggle: $.el 'div', toggle: $.el 'div',
id: 'scroll-marker' id: 'scroll-marker'
initReady: ->
Header.setBoardList()
Header.addNav()
setBoardList: -> setBoardList: ->
fourchannav = $.id 'boardNavDesktop' fourchannav = $.id 'boardNavDesktop'
boardList = $.el 'span', Header.boardList = boardList = $.el 'span',
id: 'board-list' id: 'board-list'
innerHTML: "<span id=custom-board-list></span><span id=full-board-list hidden><span class='hide-board-list-container brackets-wrap'><a href=javascript:; class='hide-board-list-button'>&nbsp;-&nbsp;</a></span> #{fourchannav.innerHTML}</span>" innerHTML: "<span id=custom-board-list></span><span id=full-board-list hidden><span class='hide-board-list-container brackets-wrap'><a href=javascript:; class='hide-board-list-button'>&nbsp;-&nbsp;</a></span> #{fourchannav.innerHTML}</span>"
if a = $ "a[href*='/#{g.BOARD}/']", boardList for a in $$ 'a', boardList
a.className = 'current' if Conf['JSON Navigation']
$.on a, 'click', Index.cb.link $.on a, 'click', Navigate.navigate
if a.pathname.split('/')[1] is g.BOARD.ID
a.className = 'current'
fullBoardList = $ '#full-board-list', boardList fullBoardList = $ '#full-board-list', boardList
btn = $ '.hide-board-list-button', fullBoardList btn = $ '.hide-board-list-button', fullBoardList
$.on btn, 'click', Header.toggleBoardList $.on btn, 'click', Header.toggleBoardList
$.rm $ '#navtopright', fullBoardList $.rm $ '#navtopright', fullBoardList
$.add boardList, fullBoardList $.add boardList, fullBoardList
$.add Header.bar, [boardList, Header.shortcuts, Header.noticesRoot, Header.toggle] $.add Header.bar, [Header.boardList, Header.shortcuts, Header.noticesRoot, Header.toggle]
Header.setCustomNav Conf['Custom Board Navigation'] Header.setCustomNav Conf['Custom Board Navigation']
Header.generateBoardList Conf['boardnav'].replace /(\r\n|\n|\r)/g, ' ' Header.generateBoardList Conf['boardnav'].replace /(\r\n|\n|\r)/g, ' '
@ -156,10 +163,10 @@ Header =
$.sync 'boardnav', Header.generateBoardList $.sync 'boardnav', Header.generateBoardList
generateBoardList: (text) -> generateBoardList: (text) ->
list = $ '#custom-board-list', Header.bar list = $ '#custom-board-list', Header.boardList
$.rmAll list $.rmAll list
return unless text return unless text
as = $$ '#full-board-list a[title]', Header.bar as = $$ '#full-board-list a[title]', Header.boardList
nodes = text.match(/[\w@]+((-(all|title|replace|full|index|catalog|url:"[^"]+[^"]"|text:"[^"]+")|\,"[^"]+[^"]"))*|[^\w@]+/g).map (t) -> nodes = text.match(/[\w@]+((-(all|title|replace|full|index|catalog|url:"[^"]+[^"]"|text:"[^"]+")|\,"[^"]+[^"]"))*|[^\w@]+/g).map (t) ->
if /^[^\w@]/.test t if /^[^\w@]/.test t
return $.tn t return $.tn t
@ -184,11 +191,10 @@ Header =
if a.textContent is board if a.textContent is board
a = a.cloneNode true a = a.cloneNode true
current = $.hasClass a, 'current' if Conf['JSON Navigation']
if current $.on a, 'click', Navigate.navigate
$.on a, 'click', Index.cb.link
a.textContent = if /-title/.test(t) or /-replace/.test(t) and current a.textContent = if /-title/.test(t) or /-replace/.test(t) and $.hasClass a, 'current'
a.title a.title
else if /-full/.test t else if /-full/.test t
"/#{board}/ - #{a.title}" "/#{board}/ - #{a.title}"
@ -299,17 +305,15 @@ Header =
toggleHideBarOnScroll: (e) -> toggleHideBarOnScroll: (e) ->
hide = @checked hide = @checked
$.set 'Header auto-hide on scroll', hide $.cb.checked.call @
Header.setHideBarOnScroll hide Header.setHideBarOnScroll hide
hideBarOnScroll: -> hideBarOnScroll: ->
offsetY = window.pageYOffset offsetY = window.pageYOffset
if offsetY > (Header.previousOffset or 0) if offsetY > (Header.previousOffset or 0)
$.addClass Header.bar, 'autohide' $.addClass Header.bar, 'autohide', 'scroll'
$.addClass Header.bar, 'scroll'
else else
$.rmClass Header.bar, 'autohide' $.rmClass Header.bar, 'autohide', 'scroll'
$.rmClass Header.bar, 'scroll'
Header.previousOffset = offsetY Header.previousOffset = offsetY
setBarPosition: (bottom) -> setBarPosition: (bottom) ->
@ -329,7 +333,7 @@ Header =
$.addClass doc, args[0] $.addClass doc, args[0]
$.rmClass doc, args[1] $.rmClass doc, args[1]
Header.bar.parentNode.className = args[2] Header.bar.parentNode.className = args[2]
$[args[3]] Header.bar, Header.noticesRoot $[args[3]] Header.bar, Header.noticesRoot
toggleBarPosition: -> toggleBarPosition: ->
@ -379,21 +383,37 @@ Header =
return if (Get.postFromRoot post).isHidden return if (Get.postFromRoot post).isHidden
Header.scrollTo post Header.scrollTo post
scrollTo: (root, down, needed) -> scrollTo: (root, down, needed) ->
if down if down
x = Header.getBottomOf root x = Header.getBottomOf root
if Conf['Header auto-hide on scroll'] and Conf['Bottom header']
{height} = Header.bar.getBoundingClientRect()
if x <= 0
x += height if !Header.isHidden()
else
x -= height if Header.isHidden()
window.scrollBy 0, -x unless needed and x >= 0 window.scrollBy 0, -x unless needed and x >= 0
else else
x = Header.getTopOf root x = Header.getTopOf root
if Conf['Header auto-hide on scroll'] and !Conf['Bottom header']
{height} = Header.bar.getBoundingClientRect()
if x >= 0
x += height if !Header.isHidden()
else
x -= height if Header.isHidden()
window.scrollBy 0, x unless needed and x >= 0 window.scrollBy 0, x unless needed and x >= 0
scrollToIfNeeded: (root, down) -> scrollToIfNeeded: (root, down) ->
Header.scrollTo root, down, true Header.scrollTo root, down, true
getTopOf: (root) -> getTopOf: (root) ->
{top} = root.getBoundingClientRect() {top} = root.getBoundingClientRect()
if Conf['Fixed Header'] and not Conf['Bottom Header'] if Conf['Fixed Header'] and not Conf['Bottom Header']
headRect = Header.toggle.getBoundingClientRect() headRect = Header.toggle.getBoundingClientRect()
top -= headRect.top + headRect.height top -= headRect.top + headRect.height
top top
getBottomOf: (root) -> getBottomOf: (root) ->
{clientHeight} = doc {clientHeight} = doc
bottom = clientHeight - root.getBoundingClientRect().bottom bottom = clientHeight - root.getBoundingClientRect().bottom
@ -401,6 +421,12 @@ Header =
headRect = Header.toggle.getBoundingClientRect() headRect = Header.toggle.getBoundingClientRect()
bottom -= clientHeight - headRect.bottom + headRect.height bottom -= clientHeight - headRect.bottom + headRect.height
bottom bottom
isHidden: ->
{top} = Header.bar.getBoundingClientRect()
if Conf['Bottom header']
top is doc.clientHeight
else
top < 0
addShortcut: (el) -> addShortcut: (el) ->
shortcut = $.el 'span', shortcut = $.el 'span',
@ -408,6 +434,8 @@ Header =
$.add shortcut, el $.add shortcut, el
$.prepend Header.shortcuts, shortcut $.prepend Header.shortcuts, shortcut
rmShortcut: (el) ->
$.rm el.parentElement
menuToggle: (e) -> menuToggle: (e) ->
Header.menu.toggle e, @, g Header.menu.toggle e, @, g

View File

@ -1,10 +1,12 @@
Index = Index =
init: -> init: ->
return if g.VIEW isnt 'index' or g.BOARD.ID is 'f' return if g.BOARD.ID is 'f' or g.VIEW is 'catalog' or !Conf['JSON Navigation']
@board = "#{g.BOARD}"
@button = $.el 'a', @button = $.el 'a',
className: 'index-refresh-shortcut fa fa-refresh' className: 'index-refresh-shortcut fa fa-refresh'
title: 'Refresh Index' title: 'Refresh'
href: 'javascript:;' href: 'javascript:;'
textContent: 'Refresh Index' textContent: 'Refresh Index'
$.on @button, 'click', @update $.on @button, 'click', @update
@ -68,7 +70,6 @@ Index =
subEntries: [repliesEntry, anchorEntry, refNavEntry, modeEntry, sortEntry] subEntries: [repliesEntry, anchorEntry, refNavEntry, modeEntry, sortEntry]
$.addClass doc, 'index-loading' $.addClass doc, 'index-loading'
@update()
@root = $.el 'div', className: 'board' @root = $.el 'div', className: 'board'
@pagelist = $.el 'div', @pagelist = $.el 'div',
className: 'pagelist' className: 'pagelist'
@ -79,40 +80,49 @@ Index =
innerHTML: <%= importHTML('Features/Index-navlinks') %> innerHTML: <%= importHTML('Features/Index-navlinks') %>
@searchInput = $ '#index-search', @navLinks @searchInput = $ '#index-search', @navLinks
@currentPage = @getCurrentPage() @currentPage = @getCurrentPage()
$.on window, 'popstate', @cb.popstate
$.on d, 'scroll', Index.scroll $.on d, 'scroll', Index.scroll
$.on @pagelist, 'click', @cb.pageNav $.on @pagelist, 'click', @cb.pageNav
$.on @searchInput, 'input', @onSearchInput $.on @searchInput, 'input', @onSearchInput
$.on $('#index-search-clear', @navLinks), 'click', @clearSearch $.on $('#index-search-clear', @navLinks), 'click', @clearSearch
$.asap (-> $('.board', doc) or d.readyState isnt 'loading'), -> $.on $('#returnlink a', @navLinks), 'click', Navigate.navigate
board = $ '.board' $.on $('#cataloglink a', @navLinks), 'click', -> window.location = "//boards.4chan.org/#{g.BOARD}/catalog"
$.replace board, Index.root
# Hacks:
# - When removing an element from the document during page load,
# its ancestors will still be correctly created inside of it.
# - Creating loadable elements inside of an origin-less document
# will not download them.
# - Combine the two and you get a download canceller!
# Does not work on Firefox unfortunately. bugzil.la/939713
d.implementation.createDocument(null, null, null).appendChild board
for navLink in $$ '.navLinks' @update() if g.VIEW is 'index'
$.rm navLink $.asap (-> $('.board', doc) or d.readyState isnt 'loading'), ->
if g.VIEW is 'index'
board = $ '.board'
$.replace board, Index.root
# Hacks:
# - When removing an element from the document during page load,
# its ancestors will still be correctly created inside of it.
# - Creating loadable elements inside of an origin-less document
# will not download them.
# - Combine the two and you get a download canceller!
# Does not work on Firefox unfortunately. bugzil.la/939713
d.implementation.createDocument(null, null, null).appendChild board
$.rm navLink for navLink in $$ '.navLinks'
$.after $.x('child::form/preceding-sibling::hr[1]'), Index.navLinks $.after $.x('child::form/preceding-sibling::hr[1]'), Index.navLinks
$.rmClass doc, 'index-loading' $.rmClass doc, 'index-loading'
$.asap (-> $('.pagelist') or d.readyState isnt 'loading'), ->
$.replace $('.pagelist'), Index.pagelist $.asap (-> $('.pagelist', doc) or d.readyState isnt 'loading'), ->
if pagelist = $('.pagelist')
$.replace pagelist, Index.pagelist
else
$.after $.id('delform'), Index.pagelist
scroll: $.debounce 100, -> scroll: $.debounce 100, ->
return if Index.req or Conf['Index Mode'] isnt 'infinite' or ((d.body.scrollTop or doc.scrollTop) <= doc.scrollHeight - (300 + window.innerHeight)) return if Index.req or Conf['Index Mode'] isnt 'infinite' or (doc.scrollTop <= doc.scrollHeight - (300 + window.innerHeight)) or g.VIEW is 'thread'
pageNum = Index.getCurrentPage() + 1 Index.pageNum = Index.getCurrentPage() unless Index.pageNum? # Avoid having to pushState to keep track of the current page
pageNum = Index.pageNum++
return Index.endNotice() if pageNum >= Index.pagesNum return Index.endNotice() if pageNum >= Index.pagesNum
nodesPerPage = Index.threadsNumPerPage * 2
history.pushState null, '', "/#{g.BOARD}/#{pageNum}" nodes = Index.buildSinglePage pageNum
nodes = Index.sortedNodes[nodesPerPage * pageNum ... nodesPerPage * (pageNum + 1)] Index.buildReplies nodes if Conf['Show Replies']
Index.buildReplies nodes if Conf['Show Replies'] Index.buildStructure nodes
$.add Index.root, nodes Index.setPage pageNum
Index.setPage()
endNotice: do -> endNotice: do ->
notify = false notify = false
@ -134,9 +144,6 @@ Index =
Index.buildThreads() Index.buildThreads()
Index.sort() Index.sort()
Index.buildIndex() Index.buildIndex()
popstate: (e) ->
pageNum = Index.getCurrentPage()
Index.pageLoad pageNum if Index.currentPage isnt pageNum
pageNav: (e) -> pageNav: (e) ->
return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or e.button isnt 0 return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or e.button isnt 0
switch e.target.nodeName switch e.target.nodeName
@ -149,11 +156,6 @@ Index =
return if a.textContent is 'Catalog' return if a.textContent is 'Catalog'
e.preventDefault() e.preventDefault()
Index.userPageNav +a.pathname.split('/')[2] Index.userPageNav +a.pathname.split('/')[2]
link: (e) ->
return if g.VIEW isnt 'index' or /catalog/.test @href
e.preventDefault()
history.pushState null, '', @pathname
Index.update()
scrollToIndex: -> scrollToIndex: ->
Header.scrollToIfNeeded Index.root Header.scrollToIfNeeded Index.root
@ -198,8 +200,8 @@ Index =
$.rmAll pagesRoot $.rmAll pagesRoot
$.add pagesRoot, nodes $.add pagesRoot, nodes
Index.togglePagelist() Index.togglePagelist()
setPage: -> setPage: (pageNum) ->
pageNum = Index.getCurrentPage() pageNum or= Index.getCurrentPage()
maxPageNum = Index.getMaxPageNum() maxPageNum = Index.getMaxPageNum()
pagesRoot = $ '.pages', Index.pagelist pagesRoot = $ '.pages', Index.pagelist
# Previous/Next buttons # Previous/Next buttons
@ -223,19 +225,37 @@ Index =
update: (pageNum) -> update: (pageNum) ->
return unless navigator.onLine return unless navigator.onLine
if g.VIEW is 'thread'
return ThreadUpdater.update() if Conf['Thread Updater']
return
unless d.readyState is 'loading' or Index.root.parentElement
$.replace $('.board'), Index.root
delete Index.pageNum
Index.req?.abort() Index.req?.abort()
Index.notice?.close() Index.notice?.close()
# This notice only displays if Index Refresh is taking too long
now = Date.now()
$.ready ->
Index.nTimeout = setTimeout (->
if Index.req and !Index.notice
Index.notice = new Notice 'info', 'Refreshing index...', 2
), 3 * $.SECOND - (Date.now() - now)
pageNum = null if typeof pageNum isnt 'number' # event pageNum = null if typeof pageNum isnt 'number' # event
onload = (e) -> Index.load e, pageNum onload = (e) -> Index.load e, pageNum
Index.req = $.ajax "//a.4cdn.org/#{g.BOARD}/catalog.json", Index.req = $.ajax "//a.4cdn.org/#{g.BOARD}/catalog.json",
onabort: onload onabort: onload
onloadend: onload onloadend: onload
, ,
whenModified: true whenModified: Index.board is "#{g.BOARD}"
$.addClass Index.button, 'fa-spin' $.addClass Index.button, 'fa-spin'
load: (e, pageNum) -> load: (e, pageNum) ->
$.rmClass Index.button, 'fa-spin' $.rmClass Index.button, 'fa-spin'
{req, notice} = Index {req, notice, nTimeout} = Index
clearTimeout nTimeout if nTimeout
delete Index.nTimeout
delete Index.req delete Index.req
delete Index.notice delete Index.notice
@ -244,26 +264,40 @@ Index =
notice.close() notice.close()
return return
if req.status not in [200, 304]
err = "Index refresh failed. Error #{req.statusText} (#{req.status})"
if notice
notice.setType 'warning'
notice.el.lastElementChild.textContent = err
setTimeout notice.close, $.SECOND
else
new Notice 'warning', err, 1
return
Navigate.title()
Index.board = "#{g.BOARD}"
try try
if req.status is 200 if req.status is 200
Index.parse JSON.parse(req.response), pageNum Index.parse req.response, pageNum
else if req.status is 304 and pageNum? else if req.status is 304 and pageNum?
Index.pageNav pageNum Index.pageNav pageNum
catch err catch err
c.error 'Index failure:', err.stack c.error "Index failure: #{err.message}", err.stack
# network error or non-JSON content for example. # network error or non-JSON content for example.
if notice if notice
notice.setType 'error' notice.setType 'error'
notice.el.lastElementChild.textContent = 'Index refresh failed.' notice.el.lastElementChild.textContent = 'Index refresh failed.'
setTimeout notice.close, 2 * $.SECOND setTimeout notice.close, $.SECOND
else else
new Notice 'error', 'Index refresh failed.', 2 new Notice 'error', 'Index refresh failed.', 1
return return
timeEl = $ '#index-last-refresh', Index.navLinks timeEl = $ '#index-last-refresh time', Index.navLinks
timeEl.dataset.utc = Date.parse req.getResponseHeader 'Last-Modified' timeEl.dataset.utc = Date.parse req.getResponseHeader 'Last-Modified'
RelativeDates.update timeEl RelativeDates.update timeEl
Index.scrollToIndex() Index.scrollToIndex()
parse: (pages, pageNum) -> parse: (pages, pageNum) ->
Index.parseThreadList pages Index.parseThreadList pages
Index.buildThreads() Index.buildThreads()
@ -274,36 +308,38 @@ Index =
return return
Index.buildIndex() Index.buildIndex()
Index.setPage() Index.setPage()
parseThreadList: (pages) -> parseThreadList: (pages) ->
Index.pagesNum = pages.length Index.pagesNum = pages.length
Index.threadsNumPerPage = pages[0].threads.length Index.threadsNumPerPage = pages[0].threads.length
Index.liveThreadData = pages.reduce ((arr, next) -> arr.concat next.threads), [] Index.liveThreadData = pages.reduce ((arr, next) -> arr.concat next.threads), []
Index.liveThreadIDs = Index.liveThreadData.map (data) -> data.no Index.liveThreadIDs = Index.liveThreadData.map (data) -> data.no
for threadID, thread of g.BOARD.threads when thread.ID not in Index.liveThreadIDs g.BOARD.threads.forEach (thread) ->
thread.collect() thread.collect() unless thread.ID in Index.liveThreadIDs
return return
buildThreads: -> buildThreads: ->
Index.nodes = [] Index.nodes = []
threads = [] threads = []
posts = [] posts = []
for threadData, i in Index.liveThreadData for threadData, i in Index.liveThreadData
threadRoot = Build.thread g.BOARD, threadData
Index.nodes.push threadRoot, $.el 'hr'
if thread = g.BOARD.threads[threadData.no]
thread.setPage Math.floor i / Index.threadsNumPerPage
thread.setStatus 'Sticky', !!threadData.sticky
thread.setStatus 'Closed', !!threadData.closed
else
thread = new Thread threadData.no, g.BOARD
threads.push thread
continue if thread.ID of thread.posts
try try
threadRoot = Build.thread g.BOARD, threadData
if thread = g.BOARD.threads[threadData.no]
thread.setPage Math.floor i / Index.threadsNumPerPage
thread.setStatus 'Sticky', !!threadData.sticky
thread.setStatus 'Closed', !!threadData.closed
else
thread = new Thread threadData.no, g.BOARD
threads.push thread
Index.nodes.push threadRoot
continue if thread.ID of thread.posts
posts.push new Post $('.opContainer', threadRoot), thread, g.BOARD posts.push new Post $('.opContainer', threadRoot), thread, g.BOARD
catch err catch err
# Skip posts that we failed to parse. # Skip posts that we failed to parse.
errors = [] unless errors errors = [] unless errors
errors.push errors.push
message: "Parsing of Post No.#{thread} failed. Post will be skipped." message: "Parsing of Thread No.#{thread} failed. Thread will be skipped."
error: err error: err
Main.handleErrors errors if errors Main.handleErrors errors if errors
@ -312,9 +348,10 @@ Index =
Main.callbackNodes Thread, threads Main.callbackNodes Thread, threads
Main.callbackNodes Post, posts Main.callbackNodes Post, posts
$.event 'IndexRefresh' $.event 'IndexRefresh'
buildReplies: (threadRoots) -> buildReplies: (threadRoots) ->
posts = [] posts = []
for threadRoot in threadRoots by 2 for threadRoot in threadRoots
thread = Get.threadFromRoot threadRoot thread = Get.threadFromRoot threadRoot
i = Index.liveThreadIDs.indexOf thread.ID i = Index.liveThreadIDs.indexOf thread.ID
continue unless lastReplies = Index.liveThreadData[i].last_replies continue unless lastReplies = Index.liveThreadData[i].last_replies
@ -336,57 +373,99 @@ Index =
Main.handleErrors errors if errors Main.handleErrors errors if errors
Main.callbackNodes Post, posts Main.callbackNodes Post, posts
sort: -> sort: ->
switch Conf['Index Sort'] {liveThreadIDs, liveThreadData} = Index
when 'bump' sortedThreadIDs = {
sortedThreadIDs = Index.liveThreadIDs lastreply:
when 'lastreply' [liveThreadData...].sort((a, b) ->
sortedThreadIDs = [Index.liveThreadData...].sort((a, b) -> a = num[num.length - 1] if (num = a.last_replies)
a = a.last_replies[a.last_replies.length - 1] if 'last_replies' of a b = num[num.length - 1] if (num = b.last_replies)
b = b.last_replies[b.last_replies.length - 1] if 'last_replies' of b
b.no - a.no b.no - a.no
).map (data) -> data.no ).map (post) -> post.no
when 'birth' bump: liveThreadIDs
sortedThreadIDs = [Index.liveThreadIDs...].sort (a, b) -> b - a birth: [liveThreadIDs... ].sort (a, b) -> b - a
when 'replycount' replycount: [liveThreadData...].sort((a, b) -> b.replies - a.replies).map (post) -> post.no
sortedThreadIDs = [Index.liveThreadData...].sort((a, b) -> b.replies - a.replies).map (data) -> data.no filecount: [liveThreadData...].sort((a, b) -> b.images - a.images ).map (post) -> post.no
when 'filecount' }[Conf['Index Sort']]
sortedThreadIDs = [Index.liveThreadData...].sort((a, b) -> b.images - a.images).map (data) -> data.no Index.sortedNodes = sortedNodes = new RandomAccessList
Index.sortedNodes = [] {nodes} = Index
for threadID in sortedThreadIDs for threadID in sortedThreadIDs
i = Index.liveThreadIDs.indexOf(threadID) * 2 sortedNodes.push nodes[Index.liveThreadIDs.indexOf(threadID)]
Index.sortedNodes.push Index.nodes[i], Index.nodes[i + 1] if Index.isSearching and nodes = Index.querySearch(Index.searchInput.value)
if Index.isSearching Index.sortedNodes = new RandomAccessList nodes
Index.sortedNodes = Index.querySearch(Index.searchInput.value) or Index.sortedNodes items = [
# Sticky threads # Sticky threads
Index.sortOnTop (thread) -> thread.isSticky fn: (thread) -> thread.isSticky
# Highlighted threads cnd: true
Index.sortOnTop((thread) -> thread.isOnTop) if Conf['Filter'] , # Highlighted threads
# Non-hidden threads fn: (thread) -> thread.isOnTop
Index.sortOnTop((thread) -> !thread.isHidden) if Conf['Anchor Hidden Threads'] cnd: Conf['Filter']
, # Non-hidden threads
fn: (thread) -> !thread.isHidden
cnd: Conf['Anchor Hidden Threads']
]
i = 0
while item = items[i++]
{fn, cnd} = item
Index.sortOnTop fn if cnd
return
sortOnTop: (match) -> sortOnTop: (match) ->
offset = 0 offset = 0
for threadRoot, i in Index.sortedNodes by 2 when match Get.threadFromRoot threadRoot {sortedNodes} = Index
Index.sortedNodes.splice offset++ * 2, 0, Index.sortedNodes.splice(i, 2)... threadRoot = sortedNodes.first
while threadRoot
if match Get.threadFromRoot threadRoot.data
target = sortedNodes.first
j = 0
while j++ < offset
target = target.next
unless threadRoot is target
offset++
sortedNodes.before target, threadRoot
threadRoot = threadRoot.next
return return
buildIndex: -> buildIndex: ->
if Conf['Index Mode'] isnt 'all pages' if Conf['Index Mode'] isnt 'all pages'
pageNum = Index.getCurrentPage() nodes = Index.buildSinglePage Index.getCurrentPage()
nodesPerPage = Index.threadsNumPerPage * 2
nodes = Index.sortedNodes[nodesPerPage * pageNum ... nodesPerPage * (pageNum + 1)]
else else
nodes = Index.sortedNodes nodes = [(target = Index.sortedNodes.first).data]
while target = target.next
nodes.push target.data
$.rmAll Index.root $.rmAll Index.root
$.rmAll Header.hover $.rmAll Header.hover
Index.buildReplies nodes if Conf['Show Replies'] Index.buildReplies nodes if Conf['Show Replies']
$.event 'IndexBuild', nodes Index.buildStructure nodes
$.add Index.root, nodes
buildSinglePage: (pageNum) ->
nodes = []
nodesPerPage = Index.threadsNumPerPage
offset = nodesPerPage * pageNum
end = offset + nodesPerPage
target = Index.sortedNodes.order()[offset]
Index.sortedNodes
while (offset++ <= end) and target
nodes.push target.data
target = target.next
nodes
buildStructure: (nodes) ->
result = $.frag()
i = 0
$.add result, [node, $.el 'hr'] while node = nodes[i++]
$.add Index.root, result
$.rm hr for hr in $$ 'hr + hr', Index.root # Temp fix until I figure out where I fucked up
$.event 'IndexBuild', result
isSearching: false isSearching: false
clearSearch: -> clearSearch: ->
Index.searchInput.value = null Index.searchInput.value = null
Index.onSearchInput() Index.onSearchInput()
Index.searchInput.focus() Index.searchInput.focus()
onSearchInput: -> onSearchInput: ->
if Index.isSearching = !!Index.searchInput.value.trim() if Index.isSearching = !!Index.searchInput.value.trim()
unless Index.searchInput.dataset.searching unless Index.searchInput.dataset.searching
@ -396,6 +475,7 @@ Index =
else else
pageNum = Index.getCurrentPage() pageNum = Index.getCurrentPage()
else else
return unless Index.searchInput.dataset.searching
pageNum = Index.pageBeforeSearch pageNum = Index.pageBeforeSearch
delete Index.pageBeforeSearch delete Index.pageBeforeSearch
<% if (type === 'userscript') { %> <% if (type === 'userscript') { %>
@ -413,15 +493,21 @@ Index =
Index.setPage() Index.setPage()
else else
Index.pageNav pageNum Index.pageNav pageNum
querySearch: (query) -> querySearch: (query) ->
return unless keywords = query.toLowerCase().match /\S+/g return unless keywords = query.toLowerCase().match /\S+/g
Index.search keywords Index.search keywords
search: (keywords) ->
found = [] search: (keywords) ->
for threadRoot, i in Index.sortedNodes by 2 found = []
if Index.searchMatch Get.threadFromRoot(threadRoot), keywords target = Index.sortedNodes.first
found.push Index.sortedNodes[i], Index.sortedNodes[i + 1] while target
{data} = target
if Index.searchMatch Get.threadFromRoot(data), keywords
found.push data
target = target.next
found found
searchMatch: (thread, keywords) -> searchMatch: (thread, keywords) ->
{info, file} = thread.OP {info, file} = thread.OP
text = [] text = []

View File

@ -1,5 +1,8 @@
Main = Main =
init: -> init: ->
g.threads = new SimpleDict
g.posts = new SimpleDict
pathname = location.pathname.split '/' pathname = location.pathname.split '/'
g.BOARD = new Board pathname[1] g.BOARD = new Board pathname[1]
return if g.BOARD.ID in ['z', 'fk'] return if g.BOARD.ID in ['z', 'fk']
@ -54,81 +57,17 @@ Main =
location.replace URL if URL location.replace URL if URL
return return
init = (features) ->
for name, module of features
# c.time "#{name} initialization"
try
module.init()
catch err
Main.handleErrors
message: "\"#{name}\" initialization crashed."
error: err
# finally
# c.timeEnd "#{name} initialization"
return
# c.time 'All initializations' # c.time 'All initializations'
init for [name, feature] in Main.features
'Polyfill': Polyfill # c.time "#{name} initialization"
'Redirect': Redirect try
'Header': Header feature.init()
'Catalog Links': CatalogLinks catch err
'Settings': Settings Main.handleErrors
'Index Generator': Index message: "\"#{name}\" initialization crashed."
'Announcement Hiding': PSAHiding error: err
'Fourchan thingies': Fourchan # finally
'Emoji': Emoji # c.timeEnd "#{name} initialization"
'Color User IDs': IDColor
'Custom CSS': CustomCSS
'Linkify': Linkify
'Reveal Spoilers': RemoveSpoilers
'Resurrect Quotes': Quotify
'Filter': Filter
'Thread Hiding Buttons': ThreadHiding
'Reply Hiding Buttons': PostHiding
'Recursive': Recursive
'Strike-through Quotes': QuoteStrikeThrough
'Quick Reply': QR
'Menu': Menu
'Report Link': ReportLink
'Thread Hiding (Menu)': ThreadHiding.menu
'Reply Hiding (Menu)': PostHiding.menu
'Delete Link': DeleteLink
'Filter (Menu)': Filter.menu
'Download Link': DownloadLink
'Archive Link': ArchiveLink
'Quote Inlining': QuoteInline
'Quote Previewing': QuotePreview
'Quote Backlinks': QuoteBacklink
'Mark Quotes of You': QuoteYou
'Mark OP Quotes': QuoteOP
'Mark Cross-thread Quotes': QuoteCT
'Anonymize': Anonymize
'Time Formatting': Time
'Relative Post Dates': RelativeDates
'File Info Formatting': FileInfo
'Fappe Tyme': FappeTyme
'Gallery': Gallery
'Gallery (menu)': Gallery.menu
'Sauce': Sauce
'Image Expansion': ImageExpand
'Image Expansion (Menu)': ImageExpand.menu
'Reveal Spoiler Thumbnails': RevealSpoilers
'Image Loading': ImageLoader
'Image Hover': ImageHover
'Thread Expansion': ExpandThread
'Thread Excerpt': ThreadExcerpt
'Favicon': Favicon
'Unread': Unread
'Quote Threading': QuoteThreading
'Thread Stats': ThreadStats
'Thread Updater': ThreadUpdater
'Thread Watcher': ThreadWatcher
'Thread Watcher (Menu)': ThreadWatcher.menu
'Index Navigation': Nav
'Keybinds': Keybinds
'Show Dice Roll': Dice
'Banner': Banner
# c.timeEnd 'All initializations' # c.timeEnd 'All initializations'
$.on d, 'AddCallback', Main.addCallback $.on d, 'AddCallback', Main.addCallback
@ -139,16 +78,12 @@ Main =
return if !Main.isThisPageLegit() or $.hasClass doc, 'fourchan-x' return if !Main.isThisPageLegit() or $.hasClass doc, 'fourchan-x'
# disable the mobile layout # disable the mobile layout
$('link[href*=mobile]', d.head)?.disabled = true $('link[href*=mobile]', d.head)?.disabled = true
<% if (type === 'crx') { %> $.addClass doc, 'fourchan-x', 'seaweedchan', g.VIEW, '<% if (type === 'crx') { %>blink<% } else { %>gecko<% } %>'
$.addClass doc, 'blink'
<% } else { %>
$.addClass doc, 'gecko'
<% } %>
$.addClass doc, 'fourchan-x'
$.addClass doc, 'seaweedchan'
$.addClass doc, g.VIEW
$.addStyle Main.css $.addStyle Main.css
Main.setClass()
setClass: ->
if g.VIEW is 'catalog' if g.VIEW is 'catalog'
$.addClass doc, $.id('base-css').href.match(/catalog_(\w+)/)[1].replace('_new', '').replace /_+/g, '-' $.addClass doc, $.id('base-css').href.match(/catalog_(\w+)/)[1].replace('_new', '').replace /_+/g, '-'
return return
@ -182,11 +117,7 @@ Main =
# Something might have gone wrong! # Something might have gone wrong!
Main.initStyle() Main.initStyle()
if g.VIEW is 'thread' # 4chan Pass Link
Main.initThread()
else
$.event '4chanXInitFinished'
if styleSelector = $.id 'styleSelector' if styleSelector = $.id 'styleSelector'
passLink = $.el 'a', passLink = $.el 'a',
textContent: '4chan Pass' textContent: '4chan Pass'
@ -197,7 +128,18 @@ Main =
'left=0,top=0,width=500,height=255,toolbar=0,resizable=0' 'left=0,top=0,width=500,height=255,toolbar=0,resizable=0'
$.before styleSelector.previousSibling, [$.tn '['; passLink, $.tn ']\u00A0\u00A0'] $.before styleSelector.previousSibling, [$.tn '['; passLink, $.tn ']\u00A0\u00A0']
# Parse HTML or skip it and start building from JSON.
unless Conf['JSON Navigation'] and g.VIEW is 'index'
Main.initThread()
else
$.event '4chanXInitFinished'
<% if (type === 'userscript') { %> <% if (type === 'userscript') { %>
test = $.el 'span'
test.classList.add 'a', 'b'
if test.className isnt 'a b'
new Notice 'warning', "Your version of Firefox is outdated (v<%= meta.min.firefox %> minimum) and <%= meta.name %> may not operate correctly.", 30
GMver = GM_info.version.split '.' GMver = GM_info.version.split '.'
for v, i in "<%= meta.min.greasemonkey %>".split '.' for v, i in "<%= meta.min.greasemonkey %>".split '.'
continue if v is GMver[i] continue if v is GMver[i]
@ -211,23 +153,39 @@ Main =
new Notice 'warning', 'Cookies need to be enabled on 4chan for <%= meta.name %> to operate properly.', 30 new Notice 'warning', 'Cookies need to be enabled on 4chan for <%= meta.name %> to operate properly.', 30
initThread: -> initThread: ->
return unless threadRoot = $ '.thread' if board = $ '.board'
thread = new Thread +threadRoot.id[1..], g.BOARD threads = []
posts = [] posts = []
for postRoot in $$ '.thread > .postContainer', threadRoot
try
posts.push new Post postRoot, thread, g.BOARD, {isOriginalMarkup: true}
catch err
# Skip posts that we failed to parse.
errors = [] unless errors
errors.push
message: "Parsing of Post No.#{postRoot.id.match /\d+/} failed. Post will be skipped."
error: err
Main.handleErrors errors if errors
Main.callbackNodes Thread, [thread] for threadRoot in $$ '.board > .thread', board
Main.callbackNodesDB Post, posts, -> thread = new Thread +threadRoot.id[1..], g.BOARD
$.event '4chanXInitFinished' threads.push thread
for postRoot in $$ '.thread > .postContainer', threadRoot
try
posts.push new Post postRoot, thread, g.BOARD
catch err
# Skip posts that we failed to parse.
unless errors
errors = []
errors.push
message: "Parsing of Post No.#{postRoot.id.match(/\d+/)} failed. Post will be skipped."
error: err
Main.handleErrors errors if errors
Main.callbackNodes Thread, threads
Main.callbackNodesDB Post, posts, ->
$.event '4chanXInitFinished'
$.get 'previousversion', null, ({previousversion}) ->
return if previousversion is g.VERSION
if previousversion
changelog = '<%= meta.repo %>blob/<%= meta.mainBranch %>/CHANGELOG.md'
el = $.el 'span',
innerHTML: "<%= meta.name %> has been updated to <a href='#{changelog}' target=_blank>version #{g.VERSION}</a>."
new Notice 'info', el, 15
else
Settings.open()
$.set 'previousversion', g.VERSION
callbackNodes: (klass, nodes) -> callbackNodes: (klass, nodes) ->
i = 0 i = 0
@ -237,24 +195,21 @@ Main =
return return
callbackNodesDB: (klass, nodes, cb) -> callbackNodesDB: (klass, nodes, cb) ->
errors = null i = 0
len = 0
i = 0
cbs = klass.callbacks cbs = klass.callbacks
fn = -> fn = ->
node = nodes[i++] return false unless node = nodes[i]
cbs.execute node cbs.execute node
i % 25 ++i % 25
softTask = -> softTask = ->
while fn() while fn()
if len is i continue
cb() if cb unless nodes[i]
return cb() if cb
return
setTimeout softTask, 0 setTimeout softTask, 0
len = nodes.length
softTask() softTask()
addCallback: (e) -> addCallback: (e) ->
@ -322,4 +277,68 @@ Main =
<%= grunt.file.read('src/General/css/photon.css').replace(/\s+/g, ' ').trim() %> <%= grunt.file.read('src/General/css/photon.css').replace(/\s+/g, ' ').trim() %>
""" """
features: [
['Polyfill', Polyfill]
['Redirect', Redirect]
['Header', Header]
['Catalog Links', CatalogLinks]
['Settings', Settings]
['Index Generator', Index]
['Announcement Hiding', PSAHiding]
['Fourchan thingies', Fourchan]
['Emoji', Emoji]
['Color User IDs', IDColor]
['Custom CSS', CustomCSS]
['Linkify', Linkify]
['Reveal Spoilers', RemoveSpoilers]
['Resurrect Quotes', Quotify]
['Filter', Filter]
['Thread Hiding Buttons', ThreadHiding]
['Reply Hiding Buttons', PostHiding]
['Recursive', Recursive]
['Strike-through Quotes', QuoteStrikeThrough]
['Quick Reply', QR]
['Menu', Menu]
['Report Link', ReportLink]
['Thread Hiding (Menu)', ThreadHiding.menu]
['Reply Hiding (Menu)', PostHiding.menu]
['Delete Link', DeleteLink]
['Filter (Menu)', Filter.menu]
['Download Link', DownloadLink]
['Archive Link', ArchiveLink]
['Quote Inlining', QuoteInline]
['Quote Previewing', QuotePreview]
['Quote Backlinks', QuoteBacklink]
['Mark Quotes of You', QuoteYou]
['Mark OP Quotes', QuoteOP]
['Mark Cross-thread Quotes', QuoteCT]
['Anonymize', Anonymize]
['Time Formatting', Time]
['Relative Post Dates', RelativeDates]
['File Info Formatting', FileInfo]
['Fappe Tyme', FappeTyme]
['Gallery', Gallery]
['Gallery (menu)', Gallery.menu]
['Sauce', Sauce]
['Image Expansion', ImageExpand]
['Image Expansion (Menu)', ImageExpand.menu]
['Reveal Spoiler Thumbnails', RevealSpoilers]
['Image Loading', ImageLoader]
['Image Hover', ImageHover]
['Thread Expansion', ExpandThread]
['Thread Excerpt', ThreadExcerpt]
['Favicon', Favicon]
['Unread', Unread]
['Quote Threading', QuoteThreading]
['Thread Stats', ThreadStats]
['Thread Updater', ThreadUpdater]
['Thread Watcher', ThreadWatcher]
['Thread Watcher (Menu)', ThreadWatcher.menu]
['Index Navigation', Nav]
['Keybinds', Keybinds]
['Show Dice Roll', Dice]
['Banner', Banner]
['Navigate', Navigate]
]
Main.init() Main.init()

310
src/General/Navigate.coffee Normal file
View File

@ -0,0 +1,310 @@
Navigate =
path: window.location.pathname
init: ->
return if g.VIEW is 'catalog' or g.BOARD.ID is 'f' or !Conf['JSON Navigation']
# blink/webkit throw a popstate on page load. Not what we want.
$.ready -> $.on window, 'popstate', Navigate.popstate
@title = -> return
Thread.callbacks.push
name: 'Navigate'
cb: @thread
Post.callbacks.push
name: 'Navigate'
cb: @post
thread: ->
return if g.VIEW is 'thread' # The reply link only exists in index view
replyLink = $ 'a.replylink', @OP.nodes.info
$.on replyLink, 'click', Navigate.navigate
post: ->
# We don't need to reload the thread inside the thread
return if g.VIEW is 'thread' and @thread.ID is g.THREADID
postlink = $ 'a[title="Highlight this post"]', @nodes.info
$.on postlink, 'click', Navigate.navigate
return unless Conf['Quote Hash Navigation']
for hashlink in $$ '.hashlink', @nodes.comment
$.on hashlink, 'click', Navigate.navigate
return
clean: ->
# Garbage collection
g.threads.forEach (thread) -> thread.collect()
QuoteBacklink.containers = {}
$.rmAll $('.board')
features: [
['Thread Excerpt', ThreadExcerpt]
['Unread Count', Unread]
['Quote Threading', QuoteThreading]
['Thread Stats', ThreadStats]
['Thread Updater', ThreadUpdater]
['Thread Expansion', ExpandThread]
]
disconnect: ->
for [name, feature] in Navigate.features
try
feature.disconnect()
catch err
errors = [] unless errors
errors.push
message: "Failed to disconnect feature #{name}."
error: err
Main.handleErrors errors if errors
return
reconnect: ->
for [name, feature] in Navigate.features
try
feature.init()
catch err
errors = [] unless errors
errors.push
message: "Failed to reconnect feature #{name}."
error: err
Main.handleErrors errors if errors
return
ready: (name, feature, condition) ->
try
feature() if condition
catch err
error = [
message: "#{name} Failed."
error: err
]
Main.handleErrors error if error
QR.generatePostableThreadsList()
updateContext: (view) ->
g.DEAD = false
unless view is g.VIEW
$.rmClass doc, g.VIEW
$.addClass doc, view
oldView = g.VIEW
g.VIEW = view
{
index: ->
return if oldView is g.VIEW
delete g.THREADID
QR.link.textContent = 'Start a Thread'
$.off d, 'ThreadUpdate', QR.statusCheck
$.on d, 'IndexRefresh', QR.generatePostableThreadsList
thread: ->
g.THREADID = +window.location.pathname.split('/')[3]
return if oldView is g.VIEW
QR.link.textContent = 'Reply to Thread'
$.on d, 'ThreadUpdate', QR.statusCheck
$.off d, 'IndexRefresh', QR.generatePostableThreadsList
}[g.VIEW]()
updateBoard: (boardID) ->
fullBoardList = $ '#full-board-list', Header.boardList
$.rmClass $('.current', fullBoardList), 'current'
$.addClass $("a[href*='/#{boardID}/']", fullBoardList), 'current'
Header.generateBoardList Conf['boardnav'].replace /(\r\n|\n|\r)/g, ' '
QR.flagsInput()
onload = (e) ->
if e.type is 'abort'
req.onloadend = null
return
return unless req.status is 200
try
for aboard in req.response.boards when aboard.board is boardID
board = aboard
break
catch err
Main.handleErrors [
message: "Navigation failed to update board name."
error: err
]
return false
return unless board
Navigate.updateTitle board
Navigate.updateSFW !!board.ws_board
req = $.ajax '//a.4cdn.org/boards.json',
onabort: onload
onloadend: onload
updateSFW: (sfw) ->
# TODO: think of a better name for this. Changes style, too.
Favicon.el.href = "//s.4cdn.org/image/favicon#{if sfw then '-ws' else ''}.ico"
$.add d.head, Favicon.el # Changing the href alone doesn't update the icon on Firefox
return if Favicon.SFW is sfw # Board SFW status hasn't changed
Favicon.SFW = sfw
Favicon.update()
findStyle = (type, base) ->
style = d.cookie.match new RegExp "\b#{type}\_style\=([^;]+);\b"
return ["#{type}_style", (if style then style[1] else base)]
style = findStyle (if sfw
['ws', 'Yotsuba B New']
else
['nws', 'Yotsuba New'])...
$.globalEval "var style_group = '#{style[0]}'"
$('link[title=switch]', d.head).href = $("link[title='#{style[1]}']", d.head).href
Main.setClass()
updateTitle: ({board, title}) ->
$.rm subtitle if subtitle = $ '.boardSubtitle'
$('.boardTitle').textContent = d.title = "/#{board}/ - #{title}"
navigate: (e) ->
return if @hostname isnt 'boards.4chan.org' or window.location.hostname is 'rs.4chan.org' or
(e and (e.shiftKey or (e.type is 'click' and e.button isnt 0))) # Not simply a left click
$.addClass Index.button, 'fa-spin'
path = @pathname.split '/'
path.shift() if path[0] is ''
[boardID, view, threadID] = path
return if view is 'catalog' or 'f' in [boardID, g.BOARD.ID]
e.preventDefault() if e
Navigate.title = -> return
delete Index.pageNum
path = @pathname
path += @hash if @hash
history.pushState null, '', path unless @id is 'popState'
Navigate.path = @pathname
if threadID
view = 'thread'
else
pageNum = view
view = 'index' # path is "/boardID/". See the problem?
if view is g.VIEW and boardID is g.BOARD.ID
Navigate.updateContext view
else # We've navigated somewhere we weren't before!
Navigate.disconnect()
Navigate.updateContext view
Navigate.clean()
Navigate.reconnect()
if boardID is g.BOARD.ID
Navigate.title = -> d.title = $('.boardTitle').textContent if view is 'index'
else
g.BOARD = new Board boardID
Navigate.title = -> Navigate.updateBoard boardID
if view is 'index'
Index.update pageNum
# Moving from index to thread or thread to thread
else
Navigate.updateSFW Favicon.SFW
{load} = Navigate
Navigate.req = $.ajax "//a.4cdn.org/#{boardID}/res/#{threadID}.json",
onabort: load
onloadend: load
setTimeout (->
if Navigate.req and !Navigate.notice
Navigate.notice = new Notice 'info', 'Loading thread...'
), 3 * $.SECOND
load: (e) ->
$.rmClass Index.button, 'fa-spin'
{req, notice} = Navigate
notice?.close()
delete Navigate.req
delete Navigate.notice
if e.type is 'abort' or req.status isnt 200
req.onloadend = null
new Notice 'warning', "Failed to load thread.#{if req.status then " #{req.status}" else ''}"
return
Navigate.title()
try
Navigate.parse req.response.posts
catch err
console.error 'Navigate failure:'
console.log err
# network error or non-JSON content for example.
if notice
notice.setType 'error'
notice.el.lastElementChild.textContent = 'Navigation Failed.'
setTimeout notice.close, 2 * $.SECOND
else
new Notice 'error', 'Navigation Failed.', 2
return
parse: (data) ->
board = g.BOARD
Navigate.threadRoot = threadRoot = Build.thread board, OP = data.shift(), true
thread = new Thread OP.no, board
posts = []
errors = null
makePost = (postNode) ->
try
posts.push new Post postNode, thread, board
catch err
# Skip posts that we failed to parse.
errors = [] unless errors
errors.push
message: "Parsing of Post No.#{thread.ID} failed. Post will be skipped."
error: err
makePost $('.opContainer', threadRoot)
for obj in data
post = Build.postFromObject obj, board
makePost post
$.add threadRoot, post
Main.handleErrors errors if errors
Main.callbackNodes Thread, [thread]
Main.callbackNodes Post, posts
Navigate.ready 'Quote Threading', QuoteThreading.force, Conf['Quote Threading'] and not Conf['Unread Count']
Navigate.buildThread()
Header.hashScroll.call window
buildThread: ->
board = $ '.board'
$.rmAll board
$.add board, [Navigate.threadRoot, $.el 'hr']
if Conf['Unread Count']
Navigate.ready 'Unread Count', Unread.ready, Conf['Unread Count']
popstate: ->
return if window.location.pathname is Navigate.path
a = $.el 'a',
href: window.location
id: 'popState'
Navigate.navigate.call a

View File

@ -9,19 +9,6 @@ Settings =
Header.addShortcut link Header.addShortcut link
$.get 'previousversion', null, (item) ->
if previous = item['previousversion']
return if previous is g.VERSION
changelog = '<%= meta.repo %>blob/<%= meta.mainBranch %>/CHANGELOG.md'
el = $.el 'span',
innerHTML: "<%= meta.name %> has been updated to <a href='#{changelog}' target=_blank>version #{g.VERSION}</a>."
if Conf['Show Updated Notifications']
new Notice 'info', el, 30
else
$.on d, '4chanXInitFinished', Settings.open
$.set 'previousversion', g.VERSION
Settings.addSection 'Main', Settings.main Settings.addSection 'Main', Settings.main
Settings.addSection 'Filter', Settings.filter Settings.addSection 'Filter', Settings.filter
Settings.addSection 'Sauce', Settings.sauce Settings.addSection 'Sauce', Settings.sauce
@ -37,7 +24,6 @@ Settings =
localStorage.setItem '4chan-settings', JSON.stringify settings localStorage.setItem '4chan-settings', JSON.stringify settings
open: (openSection) -> open: (openSection) ->
$.off d, '4chanXInitFinished', Settings.open
return if Settings.dialog return if Settings.dialog
$.event 'CloseMenu' $.event 'CloseMenu'
@ -53,6 +39,7 @@ Settings =
$.on $('.export', Settings.dialog), 'click', Settings.export $.on $('.export', Settings.dialog), 'click', Settings.export
$.on $('.import', Settings.dialog), 'click', Settings.import $.on $('.import', Settings.dialog), 'click', Settings.import
$.on $('.reset', Settings.dialog), 'click', Settings.reset
$.on $('input', Settings.dialog), 'change', Settings.onImport $.on $('input', Settings.dialog), 'change', Settings.onImport
links = [] links = []
@ -124,56 +111,39 @@ Settings =
div = $.el 'div', div = $.el 'div',
innerHTML: "<button></button><span class=description>: Clear manually-hidden threads and posts on all boards. Reload the page to apply." innerHTML: "<button></button><span class=description>: Clear manually-hidden threads and posts on all boards. Reload the page to apply."
button = $ 'button', div button = $ 'button', div
hiddenNum = 0 $.get {hiddenThreads: {}, hiddenPosts: {}}, ({hiddenThreads, hiddenPosts}) ->
$.get 'hiddenThreads', boards: {}, (item) -> hiddenNum = 0
for ID, board of item.hiddenThreads.boards for ID, board of hiddenThreads.boards
hiddenNum += Object.keys(board).length
for ID, board of hiddenPosts.boards
for ID, thread of board for ID, thread of board
hiddenNum++ hiddenNum += Object.keys(thread).length
button.textContent = "Hidden: #{hiddenNum}"
$.get 'hiddenPosts', boards: {}, (item) ->
for ID, board of item.hiddenPosts.boards
for ID, thread of board
for ID, post of thread
hiddenNum++
button.textContent = "Hidden: #{hiddenNum}" button.textContent = "Hidden: #{hiddenNum}"
$.on button, 'click', -> $.on button, 'click', ->
@textContent = 'Hidden: 0' @textContent = 'Hidden: 0'
$.get 'hiddenThreads', boards: {}, (item) -> $.get 'hiddenThreads', {}, ({hiddenThreads}) ->
for boardID of item.hiddenThreads.boards for boardID of hiddenThreads.boards
localStorage.removeItem "4chan-hide-t-#{boardID}" localStorage.removeItem "4chan-hide-t-#{boardID}"
$.delete ['hiddenThreads', 'hiddenPosts'] $.delete ['hiddenThreads', 'hiddenPosts']
$.after $('input[name="Stubs"]', section).parentNode.parentNode, div $.after $('input[name="Stubs"]', section).parentNode.parentNode, div
export: (now, data) -> export: ->
unless typeof now is 'number' # Make sure to export the most recent data.
now = Date.now() $.get Conf, (Conf) ->
data = # XXX don't export archives.
version: g.VERSION delete Conf['archives']
date: now Settings.downloadExport {version: g.VERSION, date: Date.now(), Conf}
for db in DataBoard.keys downloadExport: (data) ->
Conf[db] = boards: {}
# Make sure to export the most recent data.
$.get Conf, (Conf) ->
# XXX don't export archives.
delete Conf['archives']
data.Conf = Conf
Settings.export now, data
return
a = $.el 'a', a = $.el 'a',
className: 'warning' download: "<%= meta.name %> v#{g.VERSION}-#{data.date}.json"
textContent: 'Save me!'
download: "<%= meta.name %> v#{g.VERSION}-#{now}.json"
href: "data:application/json;base64,#{btoa unescape encodeURIComponent JSON.stringify data, null, 2}" href: "data:application/json;base64,#{btoa unescape encodeURIComponent JSON.stringify data, null, 2}"
target: '_blank'
<% if (type === 'userscript') { %> <% if (type === 'userscript') { %>
# XXX Firefox won't let us download automatically.
p = $ '.imp-exp-result', Settings.dialog p = $ '.imp-exp-result', Settings.dialog
$.rmAll p $.rmAll p
$.add p, a $.add p, a
<% } else { %>
a.click()
<% } %> <% } %>
a.click()
import: -> import: ->
@nextElementSibling.click() $('input', @parentNode).click()
onImport: -> onImport: ->
return unless file = @files[0] return unless file = @files[0]
output = $('.imp-exp-result') output = $('.imp-exp-result')
@ -183,8 +153,7 @@ Settings =
reader = new FileReader() reader = new FileReader()
reader.onload = (e) -> reader.onload = (e) ->
try try
data = JSON.parse e.target.result Settings.loadSettings JSON.parse e.target.result
Settings.loadSettings data
if confirm 'Import successful. Reload now?' if confirm 'Import successful. Reload now?'
window.location.reload() window.location.reload()
catch err catch err
@ -194,6 +163,11 @@ Settings =
loadSettings: (data) -> loadSettings: (data) ->
version = data.version.split '.' version = data.version.split '.'
if version[0] is '2' if version[0] is '2'
convertSettings = (data, map) ->
for prevKey, newKey of map
data.Conf[newKey] = data.Conf[prevKey] if newKey
delete data.Conf[prevKey]
data
data = Settings.convertSettings data, data = Settings.convertSettings data,
# General confs # General confs
'Disable 4chan\'s extension': '' 'Disable 4chan\'s extension': ''
@ -265,11 +239,9 @@ Settings =
data.Conf['watchedThreads'] = boards: ThreadWatcher.convert data.Conf['WatchedThreads'] data.Conf['watchedThreads'] = boards: ThreadWatcher.convert data.Conf['WatchedThreads']
delete data.Conf['WatchedThreads'] delete data.Conf['WatchedThreads']
$.set data.Conf $.set data.Conf
convertSettings: (data, map) -> reset: ->
for prevKey, newKey of map if confirm 'Your current settings will be entirely wiped, are you sure?'
data.Conf[newKey] = data.Conf[prevKey] if newKey $.clear -> window.location.reload() if confirm 'Reset successful. Reload now?'
delete data.Conf[prevKey]
data
filter: (section) -> filter: (section) ->
section.innerHTML = <%= importHTML('Settings/Filter-select') %> section.innerHTML = <%= importHTML('Settings/Filter-select') %>
@ -333,20 +305,20 @@ Settings =
$.on $('input[name="Custom CSS"]', section), 'change', Settings.togglecss $.on $('input[name="Custom CSS"]', section), 'change', Settings.togglecss
$.on $.id('apply-css'), 'click', Settings.usercss $.on $.id('apply-css'), 'click', Settings.usercss
boards = {} archBoards = {}
for name, archive of Redirect.archives for {name, boards, files, data} in Redirect.archives
for boardID in archive.boards for boardID in boards
data = boards[boardID] or= o = archBoards[boardID] or=
thread: [] thread: []
post: [] post: []
file: [] file: []
data.thread.push name o.thread.push name
data.post.push name if archive.software is 'foolfuuka' o.post.push name if data.software is 'foolfuuka'
data.file.push name if boardID in archive.files o.file.push name if boardID in files
rows = [] rows = []
boardOptions = [] boardOptions = []
for boardID in Object.keys(boards).sort() # Alphabetical order for boardID in Object.keys(archBoards).sort() # Alphabetical order
row = $.el 'tr', row = $.el 'tr',
className: "board-#{boardID}" className: "board-#{boardID}"
row.hidden = boardID isnt g.BOARD.ID row.hidden = boardID isnt g.BOARD.ID
@ -356,8 +328,8 @@ Settings =
value: "board-#{boardID}" value: "board-#{boardID}"
selected: boardID is g.BOARD.ID selected: boardID is g.BOARD.ID
data = boards[boardID] o = archBoards[boardID]
$.add row, Settings.addArchiveCell boardID, data, item for item in ['thread', 'post', 'file'] $.add row, Settings.addArchiveCell boardID, o, item for item in ['thread', 'post', 'file']
rows.push row rows.push row
$.add $('tbody', section), rows $.add $('tbody', section), rows

View File

@ -24,6 +24,7 @@ UI = do ->
constructor: (@type) -> constructor: (@type) ->
# Doc here: https://github.com/MayhemYDG/4chan-x/wiki/Menu-API # Doc here: https://github.com/MayhemYDG/4chan-x/wiki/Menu-API
$.on d, 'AddMenuEntry', @addEntry $.on d, 'AddMenuEntry', @addEntry
$.on d, 'rmMenuEntry', @rmEntry
@entries = [] @entries = []
makeMenu: -> makeMenu: ->
@ -156,6 +157,9 @@ UI = do ->
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
onFocus: (e) =>
e.stopPropagation()
@focus e.target
focus: (entry) -> focus: (entry) ->
while focused = $.x 'parent::*/child::*[contains(@class,"focused")]', entry while focused = $.x 'parent::*/child::*[contains(@class,"focused")]', entry
$.rmClass focused, 'focused' $.rmClass focused, 'focused'
@ -189,13 +193,16 @@ UI = do ->
@parseEntry entry @parseEntry entry
@entries.push entry @entries.push entry
rmEntry: (e) =>
entry = e.detail
return if entry.type isnt @type
index = @entries.indexOf entry
@entries.splice index, 1
parseEntry: (entry) -> parseEntry: (entry) ->
{el, subEntries} = entry {el, subEntries} = entry
$.addClass el, 'entry' $.addClass el, 'entry'
$.on el, 'focus mouseover', ((e) -> $.on el, 'focus mouseover', @onFocus
e.stopPropagation()
@focus el
).bind @
el.style.order = entry.order or 100 el.style.order = entry.order or 100
return unless subEntries return unless subEntries
$.addClass el, 'has-submenu' $.addClass el, 'has-submenu'

View File

@ -90,6 +90,11 @@ div.navLinks {
.reply > .file > .fileText { .reply > .file > .fileText {
margin: 0 20px; margin: 0 20px;
} }
.hashlink::before {
content: ' ';
visibility: hidden;
}
.inline + .hashlink,
[hidden] { [hidden] {
display: none !important; display: none !important;
} }
@ -273,12 +278,12 @@ div.center:not(.ad-cnt) {
font-weight: bold; font-weight: bold;
} }
/* 4chan X link brackets */ /* 4chan X link brackets */
.brackets-wrap::after {
content: "]";
}
.brackets-wrap::before { .brackets-wrap::before {
content: "["; content: "[";
} }
.brackets-wrap::after {
content: "]";
}
/* Notifications */ /* Notifications */
#notifications { #notifications {
position: fixed; position: fixed;
@ -501,7 +506,13 @@ div.center:not(.ad-cnt) {
.summary { .summary {
text-decoration: none; text-decoration: none;
} }
.index #returnlink,
.index #bottomlink,
.thread #index-last-refresh,
.thread #index-search-clear,
.thread #index-search {
display: none;
}
/* Announcement Hiding */ /* Announcement Hiding */
:root.hide-announcement #globalMessage { :root.hide-announcement #globalMessage {
@ -642,6 +653,15 @@ span.hide-announcement {
text-decoration: none; text-decoration: none;
border-bottom: 1px dashed; border-bottom: 1px dashed;
} }
@supports (text-decoration-style: dashed) or (-moz-text-decoration-style: dashed) {
.quotelink.forwardlink,
.backlink.forwardlink {
text-decoration: underline;
-moz-text-decoration-style: dashed;
text-decoration-style: dashed;
border-bottom: none;
}
}
.filtered { .filtered {
text-decoration: underline line-through; text-decoration: underline line-through;
} }
@ -777,7 +797,7 @@ span.hide-announcement {
:root.hide-original-post-form .postingMode, :root.hide-original-post-form .postingMode,
:root.hide-original-post-form #togglePostForm, :root.hide-original-post-form #togglePostForm,
#qr.autohide:not(.has-focus):not(:hover) > form, #qr.autohide:not(.has-focus):not(:hover) > form,
.postingMode ~ #qr select[data-name=thread], .thread #qr select[data-name=thread],
#file-n-submit:not(.has-file) #qr-filerm { #file-n-submit:not(.has-file) #qr-filerm {
display: none; display: none;
} }
@ -1187,7 +1207,7 @@ a:only-of-type > .remove {
left: 0px; left: 0px;
width: 200px; width: 200px;
} }
.export, .import { .export, .import, .reset {
cursor: pointer; cursor: pointer;
text-decoration: none !important; text-decoration: none !important;
} }

View File

@ -1,4 +1,6 @@
[<a href="./catalog">Catalog</a>]&nbsp; <span class=brackets-wrap id=returnlink><a href=.././>Return</a></span>
[<time id="index-last-refresh" title="Last index refresh">...</time>]&nbsp; <span class=brackets-wrap id=cataloglink><a href=javascript:;>Catalog</a></span>
<span class=brackets-wrap id=bottomlink><a href="#bottom">Bottom</a></span>
<span class=brackets-wrap id="index-last-refresh"><time title="Last index refresh">...</time></span>
<input type="search" id="index-search" class="field" placeholder="Search"> <input type="search" id="index-search" class="field" placeholder="Search">
<a id="index-search-clear" href="javascript:;" title="Clear search">×</a> <a id="index-search-clear" href="javascript:;" title="Clear search">×</a>

View File

@ -65,7 +65,7 @@
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>Quick Reply Personas <span class="warning" #{if Conf['Quick Reply'] then 'hidden' else ''}>is disabled.</span></legend> <legend>Quick Reply Personas</legend>
<textarea class=personafield name="QR.personas" class="field" spellcheck="false"></textarea> <textarea class=personafield name="QR.personas" class="field" spellcheck="false"></textarea>
<p> <p>
One item per line.<br> One item per line.<br>
@ -86,6 +86,7 @@
<select name=favicon> <select name=favicon>
<option value=ferongr>ferongr</option> <option value=ferongr>ferongr</option>
<option value=xat->xat-</option> <option value=xat->xat-</option>
<option value=4chanJS>4chanJS</option>
<option value=Mayhem>Mayhem</option> <option value=Mayhem>Mayhem</option>
<option value=Original>Original</option> <option value=Original>Original</option>
<option value=Metro>Metro</option> <option value=Metro>Metro</option>

View File

@ -5,6 +5,7 @@
<li><code>%TURL</code>: Thumbnail URL.</li> <li><code>%TURL</code>: Thumbnail URL.</li>
<li><code>%URL</code>: Full image URL.</li> <li><code>%URL</code>: Full image URL.</li>
<li><code>%MD5</code>: MD5 hash.</li> <li><code>%MD5</code>: MD5 hash.</li>
<li><code>%name</code>: Original file name.</li>
<li><code>%board</code>: Current board.</li> <li><code>%board</code>: Current board.</li>
</ul> </ul>
<textarea name=sauces class=field spellcheck=false></textarea> <textarea name=sauces class=field spellcheck=false></textarea>

View File

@ -2,13 +2,14 @@
<div class=sections-list></div> <div class=sections-list></div>
<p class='imp-exp-result warning'></p> <p class='imp-exp-result warning'></p>
<div class=credits> <div class=credits>
<a class=export>Export</a> | <a class=export>Export</a>&nbsp|&nbsp
<a class=import>Import</a> | <a class=import>Import</a>&nbsp|&nbsp
<input type=file style='display: none;'> <a class=reset>Reset Settings</a>&nbsp|&nbsp
<input type=file hidden>
<a href='<%= meta.page %>' target=_blank><%= meta.name %></a> | <a href='<%= meta.page %>' target=_blank><%= meta.name %></a> |
<a href='<%= meta.repo %>blob/<%= meta.mainBranch %>/CHANGELOG.md' target=_blank>#{g.VERSION}</a> | <a href='<%= meta.repo %>blob/<%= meta.mainBranch %>/CHANGELOG.md' target=_blank>#{g.VERSION}</a> |
<a href='<%= meta.repo %>blob/<%= meta.mainBranch %>/README.md#reporting-bugs-and-suggestions' target=_blank>Issues</a> | <a href='<%= meta.repo %>blob/<%= meta.mainBranch %>/README.md#reporting-bugs-and-suggestions' target=_blank>Issues</a> |
<a href=javascript:; class=close title=Close>×</a> <a href=javascript:; class='close fa fa-times' title=Close></a>
</div> </div>
</nav> </nav>
<div class=section-container><section></section></div> <div class=section-container><section></section></div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 254 B

After

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 300 B

After

Width:  |  Height:  |  Size: 311 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 263 B

After

Width:  |  Height:  |  Size: 252 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 311 B

After

Width:  |  Height:  |  Size: 310 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 B

After

Width:  |  Height:  |  Size: 256 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 311 B

After

Width:  |  Height:  |  Size: 310 B

View File

@ -54,6 +54,8 @@ $.ajax = do ->
if whenModified if whenModified
r.setRequestHeader 'If-Modified-Since', lastModified[url] if url of lastModified r.setRequestHeader 'If-Modified-Since', lastModified[url] if url of lastModified
$.on r, 'load', -> lastModified[url] = r.getResponseHeader 'Last-Modified' $.on r, 'load', -> lastModified[url] = r.getResponseHeader 'Last-Modified'
if /\.json$/.test url
r.responseType = 'json'
$.extend r, options $.extend r, options
$.extend r.upload, upCallbacks $.extend r.upload, upCallbacks
r.send form r.send form
@ -113,11 +115,11 @@ $.X = (path, root) ->
# XPathResult.ORDERED_NODE_SNAPSHOT_TYPE === 7 # XPathResult.ORDERED_NODE_SNAPSHOT_TYPE === 7
d.evaluate path, root, null, 7, null d.evaluate path, root, null, 7, null
$.addClass = (el, className) -> $.addClass = (el, className...) ->
el.classList.add className el.classList.add className...
$.rmClass = (el, className) -> $.rmClass = (el, className...) ->
el.classList.remove className el.classList.remove className...
$.toggleClass = (el, className) -> $.toggleClass = (el, className) ->
el.classList.toggle className el.classList.toggle className
@ -125,18 +127,12 @@ $.toggleClass = (el, className) ->
$.hasClass = (el, className) -> $.hasClass = (el, className) ->
className in el.classList className in el.classList
$.rm = do -> $.rm = (el) ->
if 'remove' of Element:: el.remove()
(el) -> el.remove()
else
(el) -> el.parentNode?.removeChild el
$.rmAll = (root) -> $.rmAll = (root) ->
# jsperf.com/emptify-element # https://gist.github.com/MayhemYDG/8646194
for node in [root.childNodes...] root.textContent = null
# HTMLSelectElement.remove !== Element.remove
root.removeChild node
return
$.tn = (s) -> $.tn = (s) ->
d.createTextNode s d.createTextNode s
@ -268,6 +264,7 @@ $.item = (key, val) ->
item item
$.syncing = {} $.syncing = {}
<% if (type === 'crx') { %> <% if (type === 'crx') { %>
$.sync = do -> $.sync = do ->
chrome.storage.onChanged.addListener (changes) -> chrome.storage.onChanged.addListener (changes) ->
@ -277,6 +274,8 @@ $.sync = do ->
return return
(key, cb) -> $.syncing[key] = cb (key, cb) -> $.syncing[key] = cb
$.desync = (key) -> delete $.syncing[key]
$.localKeys = [ $.localKeys = [
# filters # filters
'name', 'name',
@ -329,29 +328,50 @@ $.get = (key, val, cb) ->
chrome.storage.sync.get syncItems, done chrome.storage.sync.get syncItems, done
$.set = do -> $.set = do ->
items = {} items =
localItems = {} sync: {}
local: {}
timeout = {}
set = $.debounce $.SECOND, -> setArea = (area) ->
data = items[area]
return if !Object.keys(data).length or timeout[area]
items[area] = {}
chrome.storage[area].set data, ->
if chrome.runtime.lastError
c.error chrome.runtime.lastError.message
for key, val of data when key not of items[area]
items[area][key] = val
timeout[area] = setTimeout setArea, $.MINUTE, area
return
delete timeout[area]
setAll = $.debounce $.SECOND, ->
for key in $.localKeys for key in $.localKeys
if key of items if key of items.sync
(localItems or= {})[key] = items[key] items.local[key] = items.sync[key]
delete items[key] delete items.sync[key]
try try
chrome.storage.local.set localItems setArea 'local'
chrome.storage.sync.set items setArea 'sync'
items = {}
localItems = {}
catch err catch err
c.error err.stack c.error err.stack
(key, val) -> (key, val) ->
if typeof key is 'string' if typeof key is 'string'
items[key] = val items.sync[key] = val
else else
$.extend items, key $.extend items.sync, key
set() setAll()
$.clear = (cb) ->
count = 2
done = ->
if chrome.runtime.lastError
c.error chrome.runtime.lastError.message
return
cb?() unless --count
chrome.storage.local.clear done
chrome.storage.sync.clear done
<% } else { %> <% } else { %>
# http://wiki.greasespot.net/Main_Page # http://wiki.greasespot.net/Main_Page
@ -361,6 +381,8 @@ $.sync = do ->
cb JSON.parse(newValue), key cb JSON.parse(newValue), key
(key, cb) -> $.syncing[g.NAMESPACE + key] = cb (key, cb) -> $.syncing[g.NAMESPACE + key] = cb
$.desync = (key) -> delete $.syncing[g.NAMESPACE + key]
$.delete = (keys) -> $.delete = (keys) ->
unless keys instanceof Array unless keys instanceof Array
keys = [keys] keys = [keys]
@ -397,6 +419,9 @@ $.set = do ->
for key, val of keys for key, val of keys
set key, val set key, val
return return
$.clear = (cb) ->
$.delete GM_listValues().map (key) -> key.replace g.NAMESPACE, ''
cb?()
<% } %> <% } %>
$$ = (selector, root=d.body) -> $$ = (selector, root=d.body) ->

View File

@ -2,7 +2,7 @@ class Board
toString: -> @ID toString: -> @ID
constructor: (@ID) -> constructor: (@ID) ->
@threads = {} @threads = new SimpleDict
@posts = {} @posts = new SimpleDict
g.boards[@] = @ g.boards[@] = @

View File

@ -1,20 +1,23 @@
class Callbacks class Callbacks
push: ({name, cb}) -> @[name] = cb constructor: (@type) ->
@keys = []
clean: -> push: ({name, cb}) ->
@rm name for name of @ when @hasOwnProperty name @connect name if @[name]
return @keys.push name unless @[name]
@[name] = cb
rm: (name) -> delete @[name] connect: (name) -> delete @[name].disconnected if @[name].disconnected
disconnect: (name) -> @[name].disconnected = true if @[name]
execute: (node) -> execute: (node) ->
for name of @ when @hasOwnProperty name for name in @keys
try try
@[name].call node @[name].call node unless @[name].disconnected
catch err catch err
errors = [] unless errors errors = [] unless errors
errors.push errors.push
message: ['"', name, '" crashed on node No.', node, ' (', node.board, ').'].join('') message: ['"', name, '" crashed on node ', @type, ' No.', node.ID, ' (', node.board, ').'].join('')
error: err error: err
Main.handleErrors errors if errors Main.handleErrors errors if errors

View File

@ -5,4 +5,5 @@
<%= grunt.file.read('src/General/lib/clone.class') %> <%= grunt.file.read('src/General/lib/clone.class') %>
<%= grunt.file.read('src/General/lib/databoard.class') %> <%= grunt.file.read('src/General/lib/databoard.class') %>
<%= grunt.file.read('src/General/lib/notice.class') %> <%= grunt.file.read('src/General/lib/notice.class') %>
<%= grunt.file.read('src/General/lib/randomaccesslist.class') %> <%= grunt.file.read('src/General/lib/randomaccesslist.class') %>
<%= grunt.file.read('src/General/lib/simpledict.class') %>

View File

@ -13,8 +13,7 @@ class DataBoard
@sync = sync @sync = sync
$.on d, '4chanXInitFinished', init $.on d, '4chanXInitFinished', init
save: -> save: -> $.set @key, @data
$.set @key, @data
delete: ({boardID, threadID, postID}) -> delete: ({boardID, threadID, postID}) ->
if postID if postID
@ -79,7 +78,7 @@ class DataBoard
return return
board = @data.boards[boardID] board = @data.boards[boardID]
threads = {} threads = {}
for page in JSON.parse e.target.response for page in e.target.response
for thread in page.threads for thread in page.threads
if thread.no of board if thread.no of board
threads[thread.no] = board[thread.no] threads[thread.no] = board[thread.no]
@ -90,3 +89,8 @@ class DataBoard
onSync: (data) => onSync: (data) =>
@data = data or boards: {} @data = data or boards: {}
@sync?() @sync?()
disconnect: ->
$.desync @key
delete @sync
delete @data

View File

@ -1,5 +1,5 @@
class Post class Post
@callbacks = new Callbacks() @callbacks = new Callbacks 'Post'
toString: -> @ID toString: -> @ID
constructor: (root, @thread, @board, that={}) -> constructor: (root, @thread, @board, that={}) ->
@ -48,18 +48,13 @@ class Post
if date = $ '.dateTime', info if date = $ '.dateTime', info
@nodes.date = date @nodes.date = date
@info.date = new Date date.dataset.utc * 1000 @info.date = new Date date.dataset.utc * 1000
if Conf['Quick Reply']
@info.yours = QR.db.get
boardID: @board
threadID: @thread
postID: @ID
@parseComment() @parseComment()
@parseQuotes() @parseQuotes()
@parseFile that @parseFile that
@clones = [] @clones = []
g.posts[@fullID] = thread.posts[@] = board.posts[@] = @ g.posts.push @fullID, thread.posts.push @, board.posts.push @, @
@kill() if that.isArchived @kill() if that.isArchived
parseComment: -> parseComment: ->
@ -185,7 +180,7 @@ class Post
# Get quotelinks/backlinks to this post # Get quotelinks/backlinks to this post
# and paint them (Dead). # and paint them (Dead).
for quotelink in Get.allQuotelinksLinkingTo @ when not $.hasClass quotelink, 'deadlink' for quotelink in Get.allQuotelinksLinkingTo @ when not $.hasClass quotelink, 'deadlink'
$.add quotelink, $.tn '\u00A0(Dead)' quotelink.textContent = quotelink.textContent + '\u00A0(Dead)'
$.addClass quotelink, 'deadlink' $.addClass quotelink, 'deadlink'
return return
# XXX tmp fix for 4chan's racing condition # XXX tmp fix for 4chan's racing condition
@ -213,9 +208,9 @@ class Post
collect: -> collect: ->
@kill() @kill()
delete g.posts[@fullID] g.posts.rm @fullID
delete @thread.posts[@] @thread.posts.rm @
delete @board.posts[@] @board.posts.rm @
addClone: (context) -> addClone: (context) ->
new Clone @, context new Clone @, context

View File

@ -1,19 +1,36 @@
class RandomAccessList class RandomAccessList
constructor: -> constructor: (items) ->
@length = 0 @length = 0
@push item for item in items if items
push: (item) -> push: (data) ->
{ID} = item {ID} = data
ID or= data.id
return if @[ID] return if @[ID]
{last} = @ {last} = @
@[ID] = item =
prev: last
next: null
data: data
ID: ID
item.prev = last item.prev = last
@[ID] = item
@last = if last @last = if last
last.next = item last.next = item
else else
@first = item @first = item
@length++ @length++
before: (root, item) ->
return if item.next is root
@rmi item
{prev} = root
root.prev = item
item.next = root
item.prev = prev
prev.next = item if prev
after: (root, item) -> after: (root, item) ->
return if item.prev is root return if item.prev is root
@ -23,8 +40,8 @@ class RandomAccessList
root.next = item root.next = item
item.prev = root item.prev = root
item.next = next item.next = next
next.prev = item next.prev = item if next
prepend: (item) -> prepend: (item) ->
{first} = @ {first} = @
return if item is first or not @[item.ID] return if item is first or not @[item.ID]
@ -36,6 +53,11 @@ class RandomAccessList
shift: -> shift: ->
@rm @first.ID @rm @first.ID
order: ->
order = [item = @first]
order.push item while item = item.next
order
rm: (ID) -> rm: (ID) ->
item = @[ID] item = @[ID]

View File

@ -0,0 +1,16 @@
class SimpleDict
constructor: ->
@keys = []
push: (key, data) ->
key = "#{key}"
@keys.push key unless @[key]
@[key] = data
rm: (key) ->
key = "#{key}"
if (i = @keys.indexOf key) isnt -1
@keys.splice i, 1
delete @[key]
forEach: (fn) -> fn @[key] for key in [@keys...]

View File

@ -1,16 +1,16 @@
class Thread class Thread
@callbacks = new Callbacks() @callbacks = new Callbacks 'Thread'
toString: -> @ID toString: -> @ID
constructor: (@ID, @board) -> constructor: (@ID, @board) ->
@fullID = "#{@board}.#{@ID}" @fullID = "#{@board}.#{@ID}"
@posts = {} @posts = new SimpleDict
@isSticky = false @isSticky = false
@isClosed = false @isClosed = false
@postLimit = false @postLimit = false
@fileLimit = false @fileLimit = false
g.threads[@fullID] = board.threads[@] = @ g.threads.push @fullID, board.threads.push @, @
setPage: (pageNum) -> setPage: (pageNum) ->
icon = $ '.page-num', @OP.nodes.post icon = $ '.page-num', @OP.nodes.post
@ -44,7 +44,6 @@ class Thread
@timeOfDeath = Date.now() @timeOfDeath = Date.now()
collect: -> collect: ->
for postID, post in @posts @posts.forEach (post) -> post.collect()
post.collect() g.threads.rm @fullID
delete g.threads[@fullID] @board.threads.rm @
delete @board.threads[@]

View File

@ -14,6 +14,7 @@
// @grant GM_getValue // @grant GM_getValue
// @grant GM_setValue // @grant GM_setValue
// @grant GM_deleteValue // @grant GM_deleteValue
// @grant GM_listValues
// @grant GM_openInTab // @grant GM_openInTab
// @grant GM_xmlhttpRequest // @grant GM_xmlhttpRequest
// @run-at document-start // @run-at document-start

View File

@ -201,7 +201,8 @@ Gallery =
$.ajax "//api.4chan.org/#{post.board}/res/#{post.thread}.json", onload: -> $.ajax "//api.4chan.org/#{post.board}/res/#{post.thread}.json", onload: ->
return if @status isnt 200 return if @status isnt 200
i = 0 i = 0
while postObj = JSON.parse(@response).posts[i++] {posts} = @response
while postObj = posts[i++]
break if postObj.no is post.ID break if postObj.no is post.ID
unless postObj.no unless postObj.no
return post.kill() return post.kill()

View File

@ -30,8 +30,18 @@ ImageExpand =
return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or e.button isnt 0 return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or e.button isnt 0
e.preventDefault() e.preventDefault()
ImageExpand.toggle Get.postFromNode @ ImageExpand.toggle Get.postFromNode @
toggleAll: -> toggleAll: ->
$.event 'CloseMenu' $.event 'CloseMenu'
toggle = (post) ->
{file} = post
return unless file and file.isImage and doc.contains post.nodes.root
if ImageExpand.on and
(!Conf['Expand spoilers'] and file.isSpoiler or
Conf['Expand from here'] and Header.getTopOf(file.thumb) < 0)
return
$.queueTask func, post
if ImageExpand.on = $.hasClass ImageExpand.EAI, 'expand-all-shortcut' if ImageExpand.on = $.hasClass ImageExpand.EAI, 'expand-all-shortcut'
ImageExpand.EAI.className = 'contract-all-shortcut fa fa-compress' ImageExpand.EAI.className = 'contract-all-shortcut fa fa-compress'
ImageExpand.EAI.title = 'Contract All Images' ImageExpand.EAI.title = 'Contract All Images'
@ -40,16 +50,12 @@ ImageExpand =
ImageExpand.EAI.className = 'expand-all-shortcut fa fa-expand' ImageExpand.EAI.className = 'expand-all-shortcut fa fa-expand'
ImageExpand.EAI.title = 'Expand All Images' ImageExpand.EAI.title = 'Expand All Images'
func = ImageExpand.contract func = ImageExpand.contract
for ID, post of g.posts
for post in [post].concat post.clones g.posts.forEach (post) ->
{file} = post toggle post
continue unless file and file.isImage and doc.contains post.nodes.root toggle post for post in post.clones
if ImageExpand.on and return
(!Conf['Expand spoilers'] and file.isSpoiler or
Conf['Expand from here'] and Header.getTopOf(file.thumb) < 0)
continue
$.queueTask func, post
return
setFitness: -> setFitness: ->
(if @checked then $.addClass else $.rmClass) doc, @name.toLowerCase().replace /\s+/g, '-' (if @checked then $.addClass else $.rmClass) doc, @name.toLowerCase().replace /\s+/g, '-'
@ -147,10 +153,19 @@ ImageExpand =
return return
timeoutID = setTimeout ImageExpand.expand, 10000, post timeoutID = setTimeout ImageExpand.expand, 10000, post
<% if (type === 'crx') { %>
$.ajax @src,
onloadend: ->
return if @status isnt 404
clearTimeout timeoutID
post.kill true
,
type: 'head'
<% } else { %>
# XXX CORS for i.4cdn.org WHEN? # XXX CORS for i.4cdn.org WHEN?
$.ajax "//a.4cdn.org/#{post.board}/res/#{post.thread}.json", onload: -> $.ajax "//a.4cdn.org/#{post.board}/res/#{post.thread}.json", onload: ->
return if @status isnt 200 return if @status isnt 200
for postObj in JSON.parse(@response).posts for postObj in @response.posts
break if postObj.no is post.ID break if postObj.no is post.ID
if postObj.no isnt post.ID if postObj.no isnt post.ID
clearTimeout timeoutID clearTimeout timeoutID
@ -158,6 +173,7 @@ ImageExpand =
else if postObj.filedeleted else if postObj.filedeleted
clearTimeout timeoutID clearTimeout timeoutID
post.kill true post.kill true
<% } %>
menu: menu:
init: -> init: ->

View File

@ -38,10 +38,19 @@ ImageHover =
return return
timeoutID = setTimeout (=> @src = post.file.URL + '?' + Date.now()), 3000 timeoutID = setTimeout (=> @src = post.file.URL + '?' + Date.now()), 3000
<% if (type === 'crx') { %>
$.ajax @src,
onloadend: ->
return if @status isnt 404
clearTimeout timeoutID
post.kill true
,
type: 'head'
<% } else { %>
# XXX CORS for i.4cdn.org WHEN? # XXX CORS for i.4cdn.org WHEN?
$.ajax "//a.4cdn.org/#{post.board}/res/#{post.thread}.json", onload: -> $.ajax "//a.4cdn.org/#{post.board}/res/#{post.thread}.json", onload: ->
return if @status isnt 200 return if @status isnt 200
for postObj in JSON.parse(@response).posts for postObj in @response.posts
break if postObj.no is post.ID break if postObj.no is post.ID
if postObj.no isnt post.ID if postObj.no isnt post.ID
clearTimeout timeoutID clearTimeout timeoutID
@ -49,3 +58,4 @@ ImageHover =
else if postObj.filedeleted else if postObj.filedeleted
clearTimeout timeoutID clearTimeout timeoutID
post.kill true post.kill true
<% } %>

View File

@ -7,6 +7,10 @@ ImageLoader =
name: 'Image Replace' name: 'Image Replace'
cb: @node cb: @node
Thread.callbacks.push
name: 'Image Replace'
cb: @thread
return unless Conf['Image Prefetching'] and g.VIEW is 'thread' return unless Conf['Image Prefetching'] and g.VIEW is 'thread'
prefetch = $.el 'label', prefetch = $.el 'label',
@ -19,6 +23,9 @@ ImageLoader =
type: 'header' type: 'header'
el: prefetch el: prefetch
order: 104 order: 104
thread: ->
ImageLoader.thread = @
node: -> node: ->
return if @isClone or @isHidden or @thread.isHidden or !@file?.isImage return if @isClone or @isHidden or @thread.isHidden or !@file?.isImage
@ -38,5 +45,5 @@ ImageLoader =
toggle: -> toggle: ->
enabled = Conf['prefetch'] = @checked enabled = Conf['prefetch'] = @checked
if enabled if enabled
ImageLoader.node.call post for id, post of g.threads["#{g.BOARD.ID}.#{g.THREADID}"].posts ImageLoader.thread.posts.forEach ImageLoader.node.call
return return

View File

@ -15,19 +15,17 @@ Sauce =
name: 'Sauce' name: 'Sauce'
cb: @node cb: @node
createSauceLink: (link) -> createSauceLink: (link) ->
link = link.replace /%(T?URL|MD5|board)/ig, (parameter) -> link = link.replace /%(T?URL|MD5|board|name)/g, (parameter) ->
switch parameter return (if type = {
'%TURL': 'post.file.thumbURL'
when '%TURL' '%URL': 'post.file.URL'
"' + encodeURIComponent(post.file.thumbURL) + '" '%MD5': 'post.file.MD5'
when '%URL' '%board': 'post.board'
"' + encodeURIComponent(post.file.URL) + '" '%name': 'post.file.name'
when '%MD5' }[parameter]
"' + encodeURIComponent(post.file.MD5) + '" "' + encodeURIComponent(#{type}) + '"
when '%board' else
"' + encodeURIComponent(post.board) + '" parameter)
else
parameter
text = if m = link.match(/;text:(.+)$/) then m[1] else link.match(/(\w+)\.\w+\//)[1] text = if m = link.match(/;text:(.+)$/) then m[1] else link.match(/(\w+)\.\w+\//)[1]
link = link.replace /;text:.+$/, '' link = link.replace /;text:.+$/, ''
Function 'post', 'a', """ Function 'post', 'a', """

View File

@ -189,8 +189,10 @@ Linkify =
embed.dataset.title = title[0] embed.dataset.title = title[0]
else else
try try
$.cache service.api(uid), -> $.cache service.api(uid),
title = Linkify.cb.title @, data -> title = Linkify.cb.title @, data
,
responseType: 'json'
catch err catch err
if link if link
link.innerHTML = "[#{key}] <span class=warning>Title Link Blocked</span> (are you using NoScript?)</a>" link.innerHTML = "[#{key}] <span class=warning>Title Link Blocked</span> (are you using NoScript?)</a>"
@ -241,7 +243,7 @@ Linkify =
service = Linkify.types[key].title service = Linkify.types[key].title
switch response.status switch response.status
when 200, 304 when 200, 304
text = "#{service.text JSON.parse response.responseText}" text = "#{service.text response.response}"
if Conf['Embedding'] if Conf['Embedding']
embed.dataset.title = text embed.dataset.title = text
when 404 when 404
@ -310,7 +312,7 @@ Linkify =
$.cache "https://mediacru.sh/#{a.dataset.uid}.json", -> $.cache "https://mediacru.sh/#{a.dataset.uid}.json", ->
{status} = @ {status} = @
return div.innerHTML = "ERROR #{status}" unless status in [200, 304] return div.innerHTML = "ERROR #{status}" unless status in [200, 304]
{files} = JSON.parse @response {files} = @response
for type in ['video/mp4', 'video/ogv', 'image/svg+xml', 'image/png', 'image/gif', 'image/jpeg', 'image/svg', 'audio/mpeg'] for type in ['video/mp4', 'video/ogv', 'image/svg+xml', 'image/png', 'image/gif', 'image/jpeg', 'image/svg', 'audio/mpeg']
for file in files for file in files
if file.type is type if file.type is type

View File

@ -1,10 +1,9 @@
PSAHiding = PSAHiding =
init: -> init: ->
return if !Conf['Announcement Hiding'] return if !Conf['Announcement Hiding']
$.addClass doc, 'hide-announcement' $.addClass doc, 'hide-announcement'
$.on d, '4chanXInitFinished', @setup $.on d, '4chanXInitFinished', @setup
setup: -> setup: ->
$.off d, '4chanXInitFinished', PSAHiding.setup $.off d, '4chanXInitFinished', PSAHiding.setup

View File

@ -1,21 +1,26 @@
ExpandThread = ExpandThread =
statuses: {}
init: -> init: ->
return if g.VIEW isnt 'index' or !Conf['Thread Expansion'] return if g.VIEW is 'thread' or !Conf['Thread Expansion']
@statuses = {}
$.on d, 'IndexRefresh', @onIndexRefresh $.on d, 'IndexRefresh', @onIndexRefresh
setButton: (thread) -> setButton: (thread) ->
return unless a = $.x 'following-sibling::a[contains(@class,"summary")][1]', thread.OP.nodes.root return unless a = $.x 'following-sibling::a[contains(@class,"summary")][1]', thread.OP.nodes.root
a.textContent = ExpandThread.text '+', a.textContent.match(/\d+/g)... a.textContent = ExpandThread.text '+', a.textContent.match(/\d+/g)...
$.on a, 'click', ExpandThread.cbToggle $.on a, 'click', ExpandThread.cbToggle
onIndexRefresh: -> disconnect: (refresh) ->
return if g.VIEW is 'thread' or !Conf['Thread Expansion']
for threadID, status of ExpandThread.statuses for threadID, status of ExpandThread.statuses
status.req?.abort() status.req?.abort()
delete ExpandThread.statuses[threadID] delete ExpandThread.statuses[threadID]
for threadID, thread of g.BOARD.threads
$.off d, 'IndexRefresh', @onIndexRefresh unless refresh
onIndexRefresh: ->
ExpandThread.disconnect true
g.BOARD.threads.forEach (thread) ->
ExpandThread.setButton thread ExpandThread.setButton thread
return
text: (status, posts, files) -> text: (status, posts, files) ->
"#{status} #{posts} post#{if posts > 1 then 's' else ''}" + "#{status} #{posts} post#{if posts > 1 then 's' else ''}" +
@ -72,13 +77,13 @@ ExpandThread =
a.textContent = "Error #{req.statusText} (#{req.status})" a.textContent = "Error #{req.statusText} (#{req.status})"
return return
data = JSON.parse(req.response).posts Build.spoilerRange[thread.board] = req.response.posts[0].custom_spoiler
Build.spoilerRange[thread.board] = data.shift().custom_spoiler
posts = [] posts = []
postsRoot = [] postsRoot = []
filesCount = 0 filesCount = 0
for postData in data for postData in req.response.posts
continue if postData.no is thread.ID
if post = thread.posts[postData.no] if post = thread.posts[postData.no]
filesCount++ if 'file' of post filesCount++ if 'file' of post
postsRoot.push post.nodes.root postsRoot.push post.nodes.root

View File

@ -1,64 +1,82 @@
Favicon = Favicon =
init: -> init: ->
$.ready -> $.asap (-> Favicon.el = $ 'link[rel="shortcut icon"]', d.head), Favicon.initAsap
Favicon.el = $ 'link[rel="shortcut icon"]', d.head
Favicon.el.type = 'image/x-icon' initAsap: ->
{href} = Favicon.el Favicon.el.type = 'image/x-icon'
Favicon.SFW = /ws\.ico$/.test href {href} = Favicon.el
Favicon.default = href Favicon.SFW = /ws\.ico$/.test href
Favicon.switch() Favicon.default = href
Favicon.switch()
switch: -> switch: ->
if Favicon.SFW items = {
Favicon.default = 'https://s.4cdn.org/image/favicon-ws.ico' ferongr: [
'<%= grunt.file.read("src/General/img/favicons/ferongr/unreadDead.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/ferongr/unreadDeadY.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/ferongr/unreadSFW.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/ferongr/unreadSFWY.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/ferongr/unreadNSFW.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/ferongr/unreadNSFWY.png", {encoding: "base64"}) %>'
]
'xat-': [
'<%= grunt.file.read("src/General/img/favicons/xat-/unreadDead.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/xat-/unreadDeadY.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/xat-/unreadSFW.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/xat-/unreadSFWY.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/xat-/unreadNSFW.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/xat-/unreadNSFWY.png", {encoding: "base64"}) %>'
]
Mayhem: [
'<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadDead.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadDeadY.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadSFW.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadSFWY.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadNSFW.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadNSFWY.png", {encoding: "base64"}) %>'
]
'4chanJS': [
'<%= grunt.file.read("src/General/img/favicons/4chanJS/unreadDead.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/4chanJS/unreadDeadY.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/4chanJS/unreadSFW.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/4chanJS/unreadSFWY.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/4chanJS/unreadNSFW.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/4chanJS/unreadNSFWY.png", {encoding: "base64"}) %>'
]
Original: [
'<%= grunt.file.read("src/General/img/favicons/Original/unreadDead.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/Original/unreadDeadY.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/Original/unreadSFW.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/Original/unreadSFWY.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/Original/unreadNSFW.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/Original/unreadNSFWY.png", {encoding: "base64"}) %>'
]
'Metro': [
'<%= grunt.file.read("src/General/img/favicons/Metro/unreadDead.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/Metro/unreadDeadY.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/Metro/unreadSFW.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/Metro/unreadSFWY.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/Metro/unreadNSFW.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/Metro/unreadNSFWY.png", {encoding: "base64"}) %>'
]
}[Conf['favicon']]
f = Favicon
t = 'data:image/png;base64,'
i = 0
while items[i]
items[i] = t + items[i++]
[f.unreadDead, funreadDeadY, f.unreadSFW, f.unreadSFWY, f.unreadNSFW, f.unreadNSFWY] = items
f.update()
update: ->
if @SFW
@unread = @unreadSFW
@unreadY = @unreadSFWY
else else
Favicon.default = 'https://s.4cdn.org/image/favicon.ico' @unread = @unreadNSFW
switch Conf['favicon'] @unreadY = @unreadNSFWY
when 'ferongr'
Favicon.unreadDead = 'data:image/gif;base64,<%= grunt.file.read("src/General/img/favicons/ferongr/unreadDead.gif", {encoding: "base64"}) %>'
Favicon.unreadDeadY = 'data:image/png;base64,<%= grunt.file.read("src/General/img/favicons/ferongr/unreadDeadY.png", {encoding: "base64"}) %>'
Favicon.unreadSFW = 'data:image/gif;base64,<%= grunt.file.read("src/General/img/favicons/ferongr/unreadSFW.gif", {encoding: "base64"}) %>'
Favicon.unreadSFWY = 'data:image/png;base64,<%= grunt.file.read("src/General/img/favicons/ferongr/unreadSFWY.png", {encoding: "base64"}) %>'
Favicon.unreadNSFW = 'data:image/gif;base64,<%= grunt.file.read("src/General/img/favicons/ferongr/unreadNSFW.gif", {encoding: "base64"}) %>'
Favicon.unreadNSFWY = 'data:image/png;base64,<%= grunt.file.read("src/General/img/favicons/ferongr/unreadNSFWY.png", {encoding: "base64"}) %>'
when 'xat-'
Favicon.unreadDead = 'data:image/png;base64,<%= grunt.file.read("src/General/img/favicons/xat-/unreadDead.png", {encoding: "base64"}) %>'
Favicon.unreadDeadY = 'data:image/png;base64,<%= grunt.file.read("src/General/img/favicons/xat-/unreadDeadY.png", {encoding: "base64"}) %>'
Favicon.unreadSFW = 'data:image/png;base64,<%= grunt.file.read("src/General/img/favicons/xat-/unreadSFW.png", {encoding: "base64"}) %>'
Favicon.unreadSFWY = 'data:image/png;base64,<%= grunt.file.read("src/General/img/favicons/xat-/unreadSFWY.png", {encoding: "base64"}) %>'
Favicon.unreadNSFW = 'data:image/png;base64,<%= grunt.file.read("src/General/img/favicons/xat-/unreadNSFW.png", {encoding: "base64"}) %>'
Favicon.unreadNSFWY = 'data:image/png;base64,<%= grunt.file.read("src/General/img/favicons/xat-/unreadNSFWY.png", {encoding: "base64"}) %>'
when 'Mayhem'
Favicon.unreadDead = 'data:image/png;base64,<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadDead.png", {encoding: "base64"}) %>'
Favicon.unreadDeadY = 'data:image/png;base64,<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadDeadY.png", {encoding: "base64"}) %>'
Favicon.unreadSFW = 'data:image/png;base64,<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadSFW.png", {encoding: "base64"}) %>'
Favicon.unreadSFWY = 'data:image/png;base64,<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadSFWY.png", {encoding: "base64"}) %>'
Favicon.unreadNSFW = 'data:image/png;base64,<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadNSFW.png", {encoding: "base64"}) %>'
Favicon.unreadNSFWY = 'data:image/png;base64,<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadNSFWY.png", {encoding: "base64"}) %>'
when 'Original'
Favicon.unreadDead = 'data:image/gif;base64,<%= grunt.file.read("src/General/img/favicons/Original/unreadDead.gif", {encoding: "base64"}) %>'
Favicon.unreadDeadY = 'data:image/png;base64,<%= grunt.file.read("src/General/img/favicons/Original/unreadDeadY.png", {encoding: "base64"}) %>'
Favicon.unreadSFW = 'data:image/gif;base64,<%= grunt.file.read("src/General/img/favicons/Original/unreadSFW.gif", {encoding: "base64"}) %>'
Favicon.unreadSFWY = 'data:image/png;base64,<%= grunt.file.read("src/General/img/favicons/Original/unreadSFWY.png", {encoding: "base64"}) %>'
Favicon.unreadNSFW = 'data:image/gif;base64,<%= grunt.file.read("src/General/img/favicons/Original/unreadNSFW.gif", {encoding: "base64"}) %>'
Favicon.unreadNSFWY = 'data:image/png;base64,<%= grunt.file.read("src/General/img/favicons/Original/unreadNSFWY.png", {encoding: "base64"}) %>'
when 'Metro'
Favicon.unreadDead = 'data:image/gif;base64,<%= grunt.file.read("src/General/img/favicons/Metro/unreadDead.png", {encoding: "base64"}) %>'
Favicon.unreadDeadY = 'data:image/png;base64,<%= grunt.file.read("src/General/img/favicons/Metro/unreadDeadY.png", {encoding: "base64"}) %>'
Favicon.unreadSFW = 'data:image/gif;base64,<%= grunt.file.read("src/General/img/favicons/Metro/unreadSFW.png", {encoding: "base64"}) %>'
Favicon.unreadSFWY = 'data:image/png;base64,<%= grunt.file.read("src/General/img/favicons/Metro/unreadSFWY.png", {encoding: "base64"}) %>'
Favicon.unreadNSFW = 'data:image/gif;base64,<%= grunt.file.read("src/General/img/favicons/Metro/unreadNSFW.png", {encoding: "base64"}) %>'
Favicon.unreadNSFWY = 'data:image/png;base64,<%= grunt.file.read("src/General/img/favicons/Metro/unreadNSFWY.png", {encoding: "base64"}) %>'
if Favicon.SFW
Favicon.default = 'data:image/png;base64,<%= grunt.file.read("src/General/img/favicons/Metro/readSFW.png", {encoding: "base64"}) %>'
else
Favicon.default = 'data:image/png;base64,<%= grunt.file.read("src/General/img/favicons/Metro/readNSFW.png", {encoding: "base64"}) %>'
if Favicon.SFW
Favicon.unread = Favicon.unreadSFW
Favicon.unreadY = Favicon.unreadSFWY
else
Favicon.unread = Favicon.unreadNSFW
Favicon.unreadY = Favicon.unreadNSFWY
dead: 'data:image/gif;base64,<%= grunt.file.read("src/General/img/favicons/dead.gif", {encoding: "base64"}) %>' dead: 'data:image/gif;base64,<%= grunt.file.read("src/General/img/favicons/dead.gif", {encoding: "base64"}) %>'
logo: 'data:image/png;base64,<%= grunt.file.read("src/General/img/icon128.png", {encoding: "base64"}) %>' logo: 'data:image/png;base64,<%= grunt.file.read("src/General/img/icon128.png", {encoding: "base64"}) %>'

View File

@ -5,5 +5,7 @@ ThreadExcerpt =
Thread.callbacks.push Thread.callbacks.push
name: 'Thread Excerpt' name: 'Thread Excerpt'
cb: @node cb: @node
node: -> node: -> d.title = Get.threadExcerpt @
d.title = Get.threadExcerpt @ disconnect: ->
return if g.VIEW isnt 'thread' or !Conf['Thread Excerpt']
Thread.callbacks.disconnect 'Thread Excerpt'

View File

@ -9,11 +9,11 @@ ThreadStats =
title: 'Post Count / File Count' + (if Conf["Page Count in Stats"] then " / Page Count" else "") title: 'Post Count / File Count' + (if Conf["Page Count in Stats"] then " / Page Count" else "")
$.ready -> $.ready ->
Header.addShortcut sc Header.addShortcut sc
else else
@dialog = sc = UI.dialog 'thread-stats', 'bottom: 0px; right: 0px;', @dialog = sc = UI.dialog 'thread-stats', 'bottom: 0px; right: 0px;',
"<div class=move title='Post Count / File Count#{if Conf["Page Count in Stats"] then " / Page Count" else ""}'><span id=post-count>0</span> / <span id=file-count>0</span>#{if Conf["Page Count in Stats"] then " / <span id=page-count>0</span>" else ""}</div>" "<div class=move title='Post Count / File Count#{if Conf["Page Count in Stats"] then " / Page Count" else ""}'><span id=post-count>0</span> / <span id=file-count>0</span>#{if Conf["Page Count in Stats"] then " / <span id=page-count>0</span>" else ""}</div>"
$.ready => $.ready =>
$.add d.body, sc $.add d.body, sc
@postCountEl = $ '#post-count', sc @postCountEl = $ '#post-count', sc
@fileCountEl = $ '#file-count', sc @fileCountEl = $ '#file-count', sc
@ -26,7 +26,7 @@ ThreadStats =
node: -> node: ->
postCount = 0 postCount = 0
fileCount = 0 fileCount = 0
for ID, post of @posts @posts.forEach (post) ->
postCount++ postCount++
fileCount++ if post.file fileCount++ if post.file
ThreadStats.thread = @ ThreadStats.thread = @
@ -34,6 +34,25 @@ ThreadStats =
ThreadStats.update postCount, fileCount ThreadStats.update postCount, fileCount
$.on d, 'ThreadUpdate', ThreadStats.onUpdate $.on d, 'ThreadUpdate', ThreadStats.onUpdate
disconnect: ->
return if g.VIEW isnt 'thread' or !Conf['Thread Stats']
if Conf['Updater and Stats in Header']
Header.rmShortcut @dialog
else
$.rm d.body, sc
clearTimeout @timeout # a possible race condition might be that this won't clear in time, but the resulting error will prevent issues anyways.
delete @timeout
delete @thread
delete @postCountEl
delete @fileCountEl
delete @pageCountEl
Thread.callbacks.disconnect 'Thread Stats'
$.off d, 'ThreadUpdate', ThreadStats.onUpdate
onUpdate: (e) -> onUpdate: (e) ->
return if e.detail[404] return if e.detail[404]
{postCount, fileCount} = e.detail {postCount, fileCount} = e.detail
@ -48,20 +67,18 @@ ThreadStats =
fetchPage: -> fetchPage: ->
return if !Conf["Page Count in Stats"] return if !Conf["Page Count in Stats"]
if ThreadStats.thread.isDead if ThreadStats.thread.isDead
ThreadStats.pageCountEl.textContent = 'Dead' ThreadStats.pageCountEl.textContent = 'Dead'
$.addClass ThreadStats.pageCountEl, 'warning' $.addClass ThreadStats.pageCountEl, 'warning'
return return
setTimeout ThreadStats.fetchPage, 2 * $.MINUTE ThreadStats.timeout = setTimeout ThreadStats.fetchPage, 2 * $.MINUTE
$.ajax "//a.4cdn.org/#{ThreadStats.thread.board}/threads.json", onload: ThreadStats.onThreadsLoad, $.ajax "//a.4cdn.org/#{ThreadStats.thread.board}/threads.json", onload: ThreadStats.onThreadsLoad,
whenModified: true whenModified: true
onThreadsLoad: -> onThreadsLoad: ->
return unless Conf["Page Count in Stats"] and @status is 200 return unless Conf["Page Count in Stats"] and @status is 200
pages = JSON.parse @response for page in @response
for page in pages for thread in page.threads when thread.no is ThreadStats.thread.ID
for thread in page.threads ThreadStats.pageCountEl.textContent = page.page
if thread.no is ThreadStats.thread.ID (if page.page is @response.length - 1 then $.addClass else $.rmClass) ThreadStats.pageCountEl, 'warning'
ThreadStats.pageCountEl.textContent = page.page return
(if page.page is pages.length - 1 then $.addClass else $.rmClass) ThreadStats.pageCountEl, 'warning'
return

View File

@ -22,8 +22,8 @@ ThreadUpdater =
@status = $ '#update-status', sc @status = $ '#update-status', sc
@isUpdating = Conf['Auto Update'] @isUpdating = Conf['Auto Update']
$.on @timer, 'click', ThreadUpdater.update $.on @timer, 'click', @update
$.on @status, 'click', ThreadUpdater.update $.on @status, 'click', @update
subEntries = [] subEntries = []
for name, conf of Config.updater.checkbox for name, conf of Config.updater.checkbox
@ -34,20 +34,20 @@ ThreadUpdater =
input = el.firstElementChild input = el.firstElementChild
$.on input, 'change', $.cb.checked $.on input, 'change', $.cb.checked
if input.name is 'Scroll BG' if input.name is 'Scroll BG'
$.on input, 'change', ThreadUpdater.cb.scrollBG $.on input, 'change', @cb.scrollBG
ThreadUpdater.cb.scrollBG() @cb.scrollBG()
else if input.name is 'Auto Update' else if input.name is 'Auto Update'
$.on input, 'change', ThreadUpdater.cb.update $.on input, 'change', @cb.update
subEntries.push el: el subEntries.push el: el
settings = $.el 'span', @settings = $.el 'span',
innerHTML: '<a href=javascript:;>Interval</a>' innerHTML: '<a href=javascript:;>Interval</a>'
$.on settings, 'click', @intervalShortcut $.on @settings, 'click', @intervalShortcut
subEntries.push el: settings subEntries.push el: @settings
$.event 'AddMenuEntry', $.event 'AddMenuEntry', @entry =
type: 'header' type: 'header'
el: $.el 'span', el: $.el 'span',
textContent: 'Updater' textContent: 'Updater'
@ -57,6 +57,40 @@ ThreadUpdater =
Thread.callbacks.push Thread.callbacks.push
name: 'Thread Updater' name: 'Thread Updater'
cb: @node cb: @node
disconnect: ->
return if g.VIEW isnt 'thread' or !Conf['Thread Updater']
$.off @timer, 'click', @update
$.off @status, 'click', @update
clearTimeout @timeoutID if @timeoutID
for entry in @entry.subEntries
{el} = entry
input = el.firstElementChild
$.off input, 'change', $.cb.checked
$.off input, 'change', @cb.scrollBG
$.off input, 'change', @cb.update
$.off @settings, 'click', @intervalShortcut
$.off window, 'online offline', @cb.online
$.off d, 'QRPostSuccessful', @cb.checkpost
$.off d, 'visibilitychange', @cb.visibility
@set 'timer', null
@set 'status', 'Offline'
$.event 'rmMenuEntry', @entry
if Conf['Updater and Stats in Header']
Header.rmShortcut @dialog
else
$.rmClass doc, 'float'
$.rm @dialog
delete @[name] for name in ['checkPostCount', 'timer', 'status', 'isUpdating', 'entry', 'dialog', 'thread', 'root', 'lastPost', 'outdateCount', 'online', 'seconds', 'timeoutID']
Thread.callbacks.disconnect 'Thread Updater'
node: -> node: ->
ThreadUpdater.thread = @ ThreadUpdater.thread = @
@ -124,7 +158,7 @@ ThreadUpdater =
switch req.status switch req.status
when 200 when 200
g.DEAD = false g.DEAD = false
ThreadUpdater.parse JSON.parse(req.response).posts ThreadUpdater.parse req.response.posts
ThreadUpdater.setInterval() ThreadUpdater.setInterval()
when 404 when 404
g.DEAD = true g.DEAD = true
@ -256,11 +290,11 @@ ThreadUpdater =
deletedFiles = [] deletedFiles = []
# Check for deleted posts/files. # Check for deleted posts/files.
for ID, post of ThreadUpdater.thread.posts ThreadUpdater.thread.posts.forEach (post) ->
# XXX tmp fix for 4chan's racing condition # XXX tmp fix for 4chan's racing condition
# giving us false-positive dead posts. # giving us false-positive dead posts.
# continue if post.isDead # continue if post.isDead
ID = +ID ID = +post.ID
unless ID in index unless ID in index
post.kill() post.kill()
@ -271,6 +305,7 @@ ThreadUpdater =
post.kill true post.kill true
deletedFiles.push post deletedFiles.push post
# Fetching your own posts after posting
if ThreadUpdater.postID and ThreadUpdater.postID is ID if ThreadUpdater.postID and ThreadUpdater.postID is ID
ThreadUpdater.foundPost = true ThreadUpdater.foundPost = true
@ -291,8 +326,7 @@ ThreadUpdater =
scroll = Conf['Auto Scroll'] and ThreadUpdater.scrollBG() and scroll = Conf['Auto Scroll'] and ThreadUpdater.scrollBG() and
ThreadUpdater.root.getBoundingClientRect().bottom - doc.clientHeight < 25 ThreadUpdater.root.getBoundingClientRect().bottom - doc.clientHeight < 25
for key, post of posts for post in posts
continue unless posts.hasOwnProperty key
root = post.nodes.root root = post.nodes.root
if post.cb if post.cb
unless post.cb() unless post.cb()

View File

@ -180,7 +180,9 @@ ThreadWatcher =
$.rmAll list $.rmAll list
$.add list, nodes $.add list, nodes
for threadID, thread of g.BOARD.threads {threads} = g.BOARD
for threadID in threads.keys
thread = threads[threadID]
toggler = $ '.watch-thread-link', thread.OP.nodes.post toggler = $ '.watch-thread-link', thread.OP.nodes.post
watched = ThreadWatcher.db.get {boardID: thread.board.ID, threadID} watched = ThreadWatcher.db.get {boardID: thread.board.ID, threadID}
helper = if watched then ['addClass', 'Unwatch'] else ['rmClass', 'Watch'] helper = if watched then ['addClass', 'Unwatch'] else ['rmClass', 'Watch']

View File

@ -12,6 +12,21 @@ Unread =
name: 'Unread' name: 'Unread'
cb: @node cb: @node
disconnect: ->
return if g.VIEW isnt 'thread' or !Conf['Unread Count'] and !Conf['Unread Favicon'] and !Conf['Desktop Notifications']
Unread.db.disconnect()
$.rm hr if {hr} = Unread
delete @[name] for name in ['db', 'hr', 'posts', 'postsQuotingYou', 'thread', 'title', 'lastReadPost']
$.off d, '4chanXInitFinished', @ready
$.off d, 'ThreadUpdate', @onUpdate
$.off d, 'scroll visibilitychange', @read
$.off d, 'visibilitychange', @setLine if Conf['Unread Line']
Thread.callbacks.disconnect 'Unread'
node: -> node: ->
Unread.thread = @ Unread.thread = @
Unread.title = d.title Unread.title = d.title
@ -26,9 +41,10 @@ Unread =
ready: -> ready: ->
$.off d, '4chanXInitFinished', Unread.ready $.off d, '4chanXInitFinished', Unread.ready
posts = [] unless Conf['Quote Threading']
posts.push post for ID, post of Unread.thread.posts when post.isReply posts = []
Unread.addPosts posts Unread.thread.posts.forEach (post) -> posts.push post if post.isReply
Unread.addPosts posts
QuoteThreading.force() if Conf['Quote Threading'] QuoteThreading.force() if Conf['Quote Threading']
Unread.scroll() if Conf['Scroll to Last Read Post'] Unread.scroll() if Conf['Scroll to Last Read Post']
@ -37,14 +53,15 @@ Unread =
return if (hash = location.hash.match /\d+/) and hash[0] of Unread.thread.posts return if (hash = location.hash.match /\d+/) and hash[0] of Unread.thread.posts
if post = Unread.posts.first if post = Unread.posts.first
# Scroll to a non-hidden, non-OP post that's before the first unread post. # Scroll to a non-hidden, non-OP post that's before the first unread post.
while root = $.x 'preceding-sibling::div[contains(@class,"replyContainer")][1]', post.nodes.root while root = $.x 'preceding-sibling::div[contains(@class,"replyContainer")][1]', post.data.nodes.root
break unless (post = Get.postFromRoot root).isHidden break unless (post = Get.postFromRoot root).isHidden
return unless root return unless root
down = true down = true
else else
# Scroll to the last read post. # Scroll to the last read post.
posts = Object.keys Unread.thread.posts {posts} = Unread.thread
{root} = Unread.thread.posts[posts[posts.length - 1]].nodes {keys} = posts
{root} = posts[keys[keys.length - 1]].nodes
# Scroll to the target unless we scrolled past it. # Scroll to the target unless we scrolled past it.
Header.scrollTo root, down if Header.getBottomOf(root) < 0 Header.scrollTo root, down if Header.getBottomOf(root) < 0
@ -75,16 +92,15 @@ Unread =
threadID: post.thread.ID threadID: post.thread.ID
postID: ID postID: ID
} }
Unread.posts.push post unless post.prev or post.next Unread.posts.push post
Unread.addPostQuotingYou post Unread.addPostQuotingYou post
if Conf['Unread Line'] if Conf['Unread Line']
# Force line on visible threads if there were no unread posts previously. # Force line on visible threads if there were no unread posts previously.
Unread.setLine Unread.posts.first in posts Unread.setLine Unread.posts.first?.data in posts
Unread.read() Unread.read()
Unread.update() Unread.update()
addPostQuotingYou: (post) -> addPostQuotingYou: (post) ->
return unless QR.db
for quotelink in post.nodes.quotelinks when QR.db.get Get.postDataFromLink quotelink for quotelink in post.nodes.quotelinks when QR.db.get Get.postDataFromLink quotelink
Unread.postsQuotingYou.push post Unread.postsQuotingYou.push post
Unread.openNotification post Unread.openNotification post
@ -110,16 +126,20 @@ Unread =
onUpdate: (e) -> onUpdate: (e) ->
if e.detail[404] if e.detail[404]
Unread.update() Unread.update()
else else if !Conf['Quote Threading']
Unread.addPosts e.detail.newPosts Unread.addPosts e.detail.newPosts
else
Unread.read()
Unread.update()
readSinglePost: (post) -> readSinglePost: (post) ->
{ID} = post {ID} = post
return unless Unread.posts[ID] {posts} = Unread
if post is Unread.posts.first return unless posts[ID]
if post is posts.first
Unread.lastReadPost = ID Unread.lastReadPost = ID
Unread.saveLastReadPost() Unread.saveLastReadPost()
Unread.posts.rm ID posts.rm ID
if (i = Unread.postsQuotingYou.indexOf post) isnt -1 if (i = Unread.postsQuotingYou.indexOf post) isnt -1
Unread.postsQuotingYou.splice i, 1 Unread.postsQuotingYou.splice i, 1
Unread.update() Unread.update()
@ -135,12 +155,16 @@ Unread =
{posts} = Unread {posts} = Unread
while post = posts.first while post = posts.first
break unless Header.getBottomOf(post.nodes.root) > -1 # post is not completely read break unless Header.getBottomOf(post.data.nodes.root) > -1 # post is not completely read
{ID} = post {ID, data} = post
posts.rm ID posts.rm ID
if Conf['Mark Quotes of You'] and post.info.yours if Conf['Mark Quotes of You'] and QR.db.get {
QuoteYou.lastRead = post.nodes.root boardID: data.board.ID
threadID: data.thread.ID
postID: ID
}
QuoteYou.lastRead = data.nodes.root
return unless ID return unless ID
@ -159,8 +183,8 @@ Unread =
setLine: (force) -> setLine: (force) ->
return unless d.hidden or force is true return unless d.hidden or force is true
return $.rm Unread.hr unless post = Unread.posts.first return $.rm Unread.hr unless post = Unread.posts.first
if $.x 'preceding-sibling::div[contains(@class,"replyContainer")]', post.nodes.root # not the first reply if $.x 'preceding-sibling::div[contains(@class,"replyContainer")]', post.data.nodes.root # not the first reply
$.before post.nodes.root, Unread.hr $.before post.data.nodes.root, Unread.hr
update: <% if (type === 'crx') { %>(dontrepeat) <% } %>-> update: <% if (type === 'crx') { %>(dontrepeat) <% } %>->
count = Unread.posts.length count = Unread.posts.length

View File

@ -1,5 +1,6 @@
QR = QR =
mimeTypes: ['image/jpeg', 'image/png', 'image/gif', 'application/pdf', 'application/x-shockwave-flash', ''] mimeTypes: ['image/jpeg', 'image/png', 'image/gif', 'application/pdf', 'application/x-shockwave-flash', '']
init: -> init: ->
return if !Conf['Quick Reply'] return if !Conf['Quick Reply']
@ -46,6 +47,8 @@ QR =
link = $.el 'h1', link = $.el 'h1',
innerHTML: "<a href=javascript:; class='qr-link'>#{if g.VIEW is 'thread' then 'Reply to Thread' else 'Start a Thread'}</a>" innerHTML: "<a href=javascript:; class='qr-link'>#{if g.VIEW is 'thread' then 'Reply to Thread' else 'Start a Thread'}</a>"
className: "qr-link-container" className: "qr-link-container"
QR.link = link.firstElementChild
$.on link.firstChild, 'click', -> $.on link.firstChild, 'click', ->
$.event 'CloseMenu' $.event 'CloseMenu'
@ -67,15 +70,20 @@ QR =
$.on d, 'dragover', QR.dragOver $.on d, 'dragover', QR.dragOver
$.on d, 'drop', QR.dropFile $.on d, 'drop', QR.dropFile
$.on d, 'dragstart dragend', QR.drag $.on d, 'dragstart dragend', QR.drag
switch g.VIEW {
when 'index' catalog: ->
QR.open() if Conf["Persistent QR"]
index: ->
$.on d, 'IndexRefresh', QR.generatePostableThreadsList $.on d, 'IndexRefresh', QR.generatePostableThreadsList
when 'thread' thread: ->
$.on d, 'ThreadUpdate', -> $.on d, 'ThreadUpdate', QR.statusCheck
if g.DEAD }[g.VIEW]()
QR.abort()
else statusCheck: ->
QR.status() if g.DEAD
QR.abort()
else
QR.status()
node: -> node: ->
$.on $('a[title="Quote this post"]', @nodes.info), 'click', QR.quote $.on $('a[title="Quote this post"]', @nodes.info), 'click', QR.quote
@ -385,7 +393,7 @@ QR =
return unless QR.nodes return unless QR.nodes
list = QR.nodes.thread list = QR.nodes.thread
options = [list.firstChild] options = [list.firstChild]
for thread of g.BOARD.threads for thread in g.BOARD.threads.keys
options.push $.el 'option', options.push $.el 'option',
value: thread value: thread
textContent: "Thread No.#{thread}" textContent: "Thread No.#{thread}"
@ -430,8 +438,7 @@ QR =
status: '[type=submit]' status: '[type=submit]'
fileInput: '[type=file]' fileInput: '[type=file]'
} }
# Allow only this board's supported files.
nodes.fileInput.max = $('input[name=MAX_FILE_SIZE]').value nodes.fileInput.max = $('input[name=MAX_FILE_SIZE]').value
QR.spoiler = !!$ 'input[name=spoiler]' QR.spoiler = !!$ 'input[name=spoiler]'
@ -454,11 +461,8 @@ QR =
""" """
nodes.flashTag.dataset.default = '4' nodes.flashTag.dataset.default = '4'
$.add nodes.form, nodes.flashTag $.add nodes.form, nodes.flashTag
if flagSelector = $ '.flagSelector'
nodes.flag = flagSelector.cloneNode true QR.flagsInput()
nodes.flag.dataset.name = 'flag'
nodes.flag.dataset.default = '0'
$.add nodes.form, nodes.flag
$.on nodes.filename.parentNode, 'click keydown', QR.openFileInput $.on nodes.filename.parentNode, 'click keydown', QR.openFileInput
@ -509,6 +513,54 @@ QR =
# Use it to extend the QR's functionalities, or for XTRM RICE. # Use it to extend the QR's functionalities, or for XTRM RICE.
$.event 'QRDialogCreation', null, dialog $.event 'QRDialogCreation', null, dialog
flags: ->
fn = (val) -> $.el 'option',
value: val[0]
textContent: val[1]
select = $.el 'select',
name: 'flag'
className: 'flagSelector'
$.add select, fn flag for flag in [
['0', 'None']
['US', 'American']
['KP', 'Best Korean']
['BL', 'Black Nationalist']
['CM', 'Communist']
['CF', 'Confederate']
['RE', 'Conservative']
['EU', 'European']
['GY', 'Gay']
['PC', 'Hippie']
['IL', 'Israeli']
['DM', 'Liberal']
['RP', 'Libertarian']
['MF', 'Muslim']
['NZ', 'Nazi']
['OB', 'Obama']
['PR', 'Pirate']
['RB', 'Rebel']
['TP', 'Tea Partier']
['TX', 'Texan']
['TR', 'Tree Hugger']
['WP', 'White Supremacist']
]
select
flagsInput: ->
{nodes} = QR
if nodes.flagSelector
$.rm nodes.flagSelector
delete nodes.flagSelector
if g.BOARD.ID is 'pol'
flag = QR.flags()
flag.dataset.name = 'flag'
flag.dataset.default = '0'
nodes.flag = flag
$.add nodes.form, flag
preSubmitHooks: [] preSubmitHooks: []
submit: (e) -> submit: (e) ->
@ -589,13 +641,15 @@ QR =
responseType: 'document' responseType: 'document'
withCredentials: true withCredentials: true
onload: QR.response onload: QR.response
onerror: -> onerror: (err, url, line) ->
# Connection error, or # Connection error, or www.4chan.org/banned
# www.4chan.org/banned
delete QR.req delete QR.req
post.unlock() post.unlock()
QR.cooldown.auto = false QR.cooldown.auto = false
QR.status() QR.status()
console.log err
console.log url
console.log line
QR.error $.el 'span', QR.error $.el 'span',
innerHTML: """ innerHTML: """
4chan X encountered an error while posting. 4chan X encountered an error while posting.
@ -615,7 +669,7 @@ QR =
QR.req.progress = "#{Math.round e.loaded / e.total * 100}%" QR.req.progress = "#{Math.round e.loaded / e.total * 100}%"
QR.status() QR.status()
QR.req = $.ajax $.id('postForm').parentNode.action, options, extra QR.req = $.ajax "https://sys.4chan.org/#{g.BOARD}/post", options, extra
# Starting to upload might take some time. # Starting to upload might take some time.
# Provide some feedback that we're starting to submit. # Provide some feedback that we're starting to submit.
QR.req.uploadStartTime = Date.now() QR.req.uploadStartTime = Date.now()
@ -667,7 +721,7 @@ QR =
# Too many frequent mistyped captchas will auto-ban you! # Too many frequent mistyped captchas will auto-ban you!
# On connection error, the post most likely didn't go through. # On connection error, the post most likely didn't go through.
QR.cooldown.set delay: 2 QR.cooldown.set delay: 2
else if err.textContent and m = err.textContent.match /wait\s(\d+)\ssecond/i else if err.textContent and m = err.textContent.match /wait\s+(\d+)\s+second/i
QR.cooldown.auto = if QR.captcha.isEnabled QR.cooldown.auto = if QR.captcha.isEnabled
!!QR.captcha.captchas.length !!QR.captcha.captchas.length
else else
@ -700,8 +754,6 @@ QR =
ThreadUpdater.postID = postID ThreadUpdater.postID = postID
# Post/upload confirmed as successful. # Post/upload confirmed as successful.
$.event 'QRPostSuccessful', { $.event 'QRPostSuccessful', {
board: g.BOARD board: g.BOARD

View File

@ -10,12 +10,12 @@ QuoteBacklink =
# Second callback adds relevant containers into posts. # Second callback adds relevant containers into posts.
# This is is so that fetched posts can get their backlinks, # This is is so that fetched posts can get their backlinks,
# and that as much backlinks are appended in the background as possible. # and that as much backlinks are appended in the background as possible.
containers: {}
init: -> init: ->
return if g.VIEW is 'catalog' or !Conf['Quote Backlinks'] return if g.VIEW is 'catalog' or !Conf['Quote Backlinks']
format = Conf['backlink'].replace /%id/g, "' + id + '" format = Conf['backlink'].replace /%id/g, "' + id + '"
@funk = Function 'id', "return '#{format}'" @funk = Function 'id', "return '#{format}'"
@containers = {}
Post.callbacks.push Post.callbacks.push
name: 'Quote Backlinking Part 1' name: 'Quote Backlinking Part 1'
cb: @firstNode cb: @firstNode
@ -36,13 +36,13 @@ QuoteBacklink =
for clone in post.clones for clone in post.clones
containers.push clone.nodes.backlinkContainer containers.push clone.nodes.backlinkContainer
for container in containers for container in containers
frag = [$.tn(' '), link = a.cloneNode true] nodes = [$.tn(' '), link = a.cloneNode true]
if Conf['Quote Previewing'] if Conf['Quote Previewing']
$.on link, 'mouseover', QuotePreview.mouseover $.on link, 'mouseover', QuotePreview.mouseover
if Conf['Quote Inlining'] if Conf['Quote Inlining']
$.on link, 'click', QuoteInline.toggle $.on link, 'click', QuoteInline.toggle
frag.push.apply frag, QuoteInline.qiQuote link, $.hasClass link, 'filtered' if Conf['Quote Hash Navigation'] nodes.push QuoteInline.qiQuote link, $.hasClass link, 'filtered' if Conf['Quote Hash Navigation']
$.add container, frag $.add container, nodes
return return
secondNode: -> secondNode: ->
if @isClone and (@origin.isReply or Conf['OP Backlinks']) if @isClone and (@origin.isReply or Conf['OP Backlinks'])

View File

@ -2,18 +2,14 @@ QuoteInline =
init: -> init: ->
return if g.VIEW is 'catalog' or !Conf['Quote Inlining'] return if g.VIEW is 'catalog' or !Conf['Quote Inlining']
if Conf['Quote Hash Navigation'] @process = if Conf['Quote Hash Navigation']
@node = -> (link, clone) ->
for link in @nodes.quotelinks.concat [@nodes.backlinks...] $.after link, QuoteInline.qiQuote link, $.hasClass link, 'filtered' unless clone
$.after link, QuoteInline.qiQuote link, $.hasClass link, 'filtered' unless @isClone $.on link, 'click', QuoteInline.toggle
$.on link, 'click', QuoteInline.toggle
return
else else
@node = -> (link) ->
for link in @nodes.quotelinks.concat [@nodes.backlinks...] $.on link, 'click', QuoteInline.toggle
$.on link, 'click', QuoteInline.toggle
return
if Conf['Comment Expansion'] if Conf['Comment Expansion']
ExpandComment.callbacks.push @node ExpandComment.callbacks.push @node
@ -22,14 +18,18 @@ QuoteInline =
name: 'Quote Inlining' name: 'Quote Inlining'
cb: @node cb: @node
node: ->
{process} = QuoteInline
{isClone} = @
process link, isClone for link in @nodes.quotelinks
process link, isClone for link in @nodes.backlinks
return
qiQuote: (link, hidden) -> qiQuote: (link, hidden) ->
[ $.el 'a',
$.tn(' ') className: "hashlink#{if hidden then ' filtered' else ''}"
$.el 'a', textContent: '#'
className: if hidden then 'hashlink filtered' else 'hashlink' href: link.href
textContent: '#'
href: link.href
]
toggle: (e) -> toggle: (e) ->
return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or e.button isnt 0 return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or e.button isnt 0

View File

@ -13,31 +13,48 @@ QuoteThreading =
input = $ 'input', @controls input = $ 'input', @controls
$.on input, 'change', @toggle $.on input, 'change', @toggle
$.event 'AddMenuEntry', $.event 'AddMenuEntry', @entry =
type: 'header' type: 'header'
el: @controls el: @controls
order: 98 order: 98
$.on d, '4chanXInitFinished', @setup unless Conf['Unread Count'] $.on d, '4chanXInitFinished', @ready unless Conf['Unread Count']
Post.callbacks.push Post.callbacks.push
name: 'Quote Threading' name: 'Quote Threading'
cb: @node cb: @node
setup: -> disconnect: ->
$.off d, '4chanXInitFinished', QuoteThreading.setup return unless Conf['Quote Threading'] and g.VIEW is 'thread'
input = $ 'input', @controls
$.off input, 'change', @toggle
$.event 'rmMenuEntry', @entry
delete @enabled
delete @controls
delete @entry
Post.callbacks.disconnect 'Quote Threading'
ready: ->
$.off d, '4chanXInitFinished', QuoteThreading.ready
QuoteThreading.force() QuoteThreading.force()
force: -> force: ->
post.cb true for ID, post of g.posts when post.cb g.posts.forEach (post) ->
return post.cb true if post.cb
if Conf['Unread Count'] and Unread.thread.OP.nodes.root.parentElement.parentElement
Unread.read()
Unread.update()
node: -> node: ->
{posts} = g {posts} = g
return if @isClone or not QuoteThreading.enabled return if @isClone or not QuoteThreading.enabled
Unread.posts.push @ if Conf['Unread Count']
return if @thread.OP is @ or !(post = posts[@fullID]) or post.isHidden # Filtered Unread.posts.push @ if Conf['Unread Count']
return if @thread.OP is @ or @isHidden # Filtered
keys = [] keys = []
len = g.BOARD.ID.length + 1 len = g.BOARD.ID.length + 1
@ -77,11 +94,11 @@ QuoteThreading =
return true unless Conf['Unread Count'] return true unless Conf['Unread Count']
if posts[post.ID] if post = posts[post.ID]
posts.after post, @ posts.after post, posts[@ID]
else else
posts.prepend @ posts.prepend posts[@ID]
return true return true
@ -93,8 +110,10 @@ QuoteThreading =
thread = $('.thread') thread = $('.thread')
posts = [] posts = []
nodes = [] nodes = []
g.posts.forEach (post) ->
posts.push post unless post is post.thread.OP or post.isClone
posts.push post for ID, post of g.posts when not (post is post.thread.OP or post.isClone)
posts.sort (a, b) -> a.ID - b.ID posts.sort (a, b) -> a.ID - b.ID
nodes.push post.nodes.root for post in posts nodes.push post.nodes.root for post in posts
@ -103,7 +122,7 @@ QuoteThreading =
containers = $$ '.threadContainer', thread containers = $$ '.threadContainer', thread
$.rm container for container in containers $.rm container for container in containers
$.rmClass post, 'threadOP' for post in $$ '.threadOP' $.rmClass post, 'threadOP' for post in $$ '.threadOP'
return return
kb: -> kb: ->

View File

@ -20,7 +20,7 @@ QuoteYou =
node: -> node: ->
return if @isClone return if @isClone
if @info.yours if QR.db.get {boardID: @board.ID, threadID: @thread.ID, postID: @ID}
$.addClass @nodes.root, 'yourPost' $.addClass @nodes.root, 'yourPost'
# Stop there if there's no quotes in that post. # Stop there if there's no quotes in that post.

View File

@ -8,6 +8,7 @@ Quotify =
Post.callbacks.push Post.callbacks.push
name: 'Resurrect Quotes' name: 'Resurrect Quotes'
cb: @node cb: @node
node: -> node: ->
for deadlink in $$ '.deadlink', @nodes.comment for deadlink in $$ '.deadlink', @nodes.comment
if @isClone if @isClone