Merge
17
CHANGELOG.md
@ -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
@ -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.
|
||||||
@ -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'
|
||||||
]
|
]
|
||||||
|
|||||||
2
LICENSE
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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": {
|
||||||
|
|||||||
2679
builds/crx/script.js
@ -1 +1 @@
|
|||||||
postMessage({version:'1.3.10'},'*')
|
postMessage({version:'1.4.0'},'*')
|
||||||
|
|||||||
20
package.json
@ -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",
|
||||||
|
|||||||
@ -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
|
|
||||||
|
|||||||
@ -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]
|
||||||
|
|||||||
@ -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
|
|
||||||
@ -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.
|
||||||
|
|||||||
@ -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.'
|
||||||
|
|||||||
@ -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', ''
|
|
||||||
|
|||||||
@ -5,6 +5,4 @@ doc = d.documentElement
|
|||||||
g =
|
g =
|
||||||
VERSION: '<%= version %>'
|
VERSION: '<%= version %>'
|
||||||
NAMESPACE: '<%= meta.name %>.'
|
NAMESPACE: '<%= meta.name %>.'
|
||||||
boards: {}
|
boards: {}
|
||||||
threads: {}
|
|
||||||
posts: {}
|
|
||||||
@ -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'> - </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'> - </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
|
||||||
|
|||||||
@ -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 = []
|
||||||
|
|||||||
@ -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
@ -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
|
||||||
@ -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
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
[<a href="./catalog">Catalog</a>]
|
<span class=brackets-wrap id=returnlink><a href=.././>Return</a></span>
|
||||||
[<time id="index-last-refresh" title="Last index refresh">...</time>]
|
<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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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> | 
|
||||||
<a class=import>Import</a> |
|
<a class=import>Import</a> | 
|
||||||
<input type=file style='display: none;'>
|
<a class=reset>Reset Settings</a> | 
|
||||||
|
<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>
|
||||||
BIN
src/General/img/favicons/4chanJS/unreadDead.png
Normal file
|
After Width: | Height: | Size: 162 B |
BIN
src/General/img/favicons/4chanJS/unreadDeadY.png
Normal file
|
After Width: | Height: | Size: 161 B |
BIN
src/General/img/favicons/4chanJS/unreadNSFW.png
Normal file
|
After Width: | Height: | Size: 168 B |
BIN
src/General/img/favicons/4chanJS/unreadNSFWY.png
Normal file
|
After Width: | Height: | Size: 167 B |
BIN
src/General/img/favicons/4chanJS/unreadSFW.png
Normal file
|
After Width: | Height: | Size: 162 B |
BIN
src/General/img/favicons/4chanJS/unreadSFWY.png
Normal file
|
After Width: | Height: | Size: 161 B |
|
Before Width: | Height: | Size: 110 B |
BIN
src/General/img/favicons/Original/unreadDead.png
Normal file
|
After Width: | Height: | Size: 170 B |
|
Before Width: | Height: | Size: 110 B |
BIN
src/General/img/favicons/Original/unreadNSFW.png
Normal file
|
After Width: | Height: | Size: 170 B |
|
Before Width: | Height: | Size: 110 B |
BIN
src/General/img/favicons/Original/unreadSFW.png
Normal file
|
After Width: | Height: | Size: 170 B |
|
Before Width: | Height: | Size: 133 B |
BIN
src/General/img/favicons/ferongr/unreadDead.png
Normal file
|
After Width: | Height: | Size: 201 B |
|
Before Width: | Height: | Size: 133 B |
BIN
src/General/img/favicons/ferongr/unreadNSFW.png
Normal file
|
After Width: | Height: | Size: 201 B |
|
Before Width: | Height: | Size: 133 B |
BIN
src/General/img/favicons/ferongr/unreadSFW.png
Normal file
|
After Width: | Height: | Size: 201 B |
|
Before Width: | Height: | Size: 254 B After Width: | Height: | Size: 247 B |
|
Before Width: | Height: | Size: 300 B After Width: | Height: | Size: 311 B |
|
Before Width: | Height: | Size: 263 B After Width: | Height: | Size: 252 B |
|
Before Width: | Height: | Size: 311 B After Width: | Height: | Size: 310 B |
|
Before Width: | Height: | Size: 262 B After Width: | Height: | Size: 256 B |
|
Before Width: | Height: | Size: 311 B After Width: | Height: | Size: 310 B |
@ -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) ->
|
||||||
|
|||||||
@ -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[@] = @
|
||||||
@ -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
|
||||||
|
|||||||
@ -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') %>
|
||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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]
|
||||||
|
|||||||
16
src/General/lib/simpledict.class
Normal 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...]
|
||||||
@ -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[@]
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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: ->
|
||||||
|
|||||||
@ -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
|
||||||
|
<% } %>
|
||||||
|
|||||||
@ -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
|
||||||
@ -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', """
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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"}) %>'
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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
|
|
||||||
@ -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()
|
||||||
|
|||||||
@ -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']
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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'])
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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: ->
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||