Merge
17
CHANGELOG.md
@ -1,8 +1,17 @@
|
||||
### v1.3.10
|
||||
*2014-02-20*
|
||||
## v1.4.0
|
||||
*2014-02-24*
|
||||
|
||||
**Spittie**
|
||||
- I keep breaking and fixing stuff. But mostly breaking.
|
||||
**ParrotParrot**:
|
||||
- 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
|
||||
*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/Archive/**/*'
|
||||
'src/Miscellaneous/**/*'
|
||||
'src/General/Navigate.coffee'
|
||||
'src/General/Settings.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.
|
||||
* https://github.com/Spittie/4chan-x/blob/master/LICENSE
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// ==UserScript==
|
||||
// @name 4chan X
|
||||
// @version 1.3.10
|
||||
// @minGMVer 1.13
|
||||
// @version 1.4.0
|
||||
// @minGMVer 1.14
|
||||
// @minFFVer 26
|
||||
// @namespace 4chan-X
|
||||
// @description Cross-browser userscript for maximum lurking on 4chan.
|
||||
@ -13,6 +13,7 @@
|
||||
// @grant GM_getValue
|
||||
// @grant GM_setValue
|
||||
// @grant GM_deleteValue
|
||||
// @grant GM_listValues
|
||||
// @grant GM_openInTab
|
||||
// @grant GM_xmlhttpRequest
|
||||
// @run-at document-start
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "4chan X",
|
||||
"version": "1.3.10",
|
||||
"version": "1.4.0",
|
||||
"manifest_version": 2,
|
||||
"description": "Cross-browser userscript for maximum lurking on 4chan.",
|
||||
"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",
|
||||
"version": "1.3.10",
|
||||
"version": "1.4.0",
|
||||
"description": "Cross-browser userscript for maximum lurking on 4chan.",
|
||||
"meta": {
|
||||
"name": "4chan X",
|
||||
@ -21,22 +21,22 @@
|
||||
"min": {
|
||||
"chrome": "31",
|
||||
"firefox": "26",
|
||||
"greasemonkey": "1.13"
|
||||
"greasemonkey": "1.14"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"font-awesome": "https://github.com/FortAwesome/Font-Awesome/archive/v4.0.3.tar.gz",
|
||||
"grunt": "~0.4.1",
|
||||
"grunt-bump": "~0.0.11",
|
||||
"grunt-concurrent": "~0.4.0",
|
||||
"font-awesome": "~4.0.3",
|
||||
"grunt": "~0.4.2",
|
||||
"grunt-bump": "~0.0.13",
|
||||
"grunt-concurrent": "~0.4.3",
|
||||
"grunt-contrib-clean": "~0.5.0",
|
||||
"grunt-contrib-coffee": "~0.8.0",
|
||||
"grunt-contrib-compress": "~0.5.2",
|
||||
"grunt-contrib-coffee": "~0.8.2",
|
||||
"grunt-contrib-compress": "~0.6.0",
|
||||
"grunt-contrib-concat": "~0.3.0",
|
||||
"grunt-contrib-copy": "~0.5.0",
|
||||
"grunt-contrib-watch": "~0.5.3",
|
||||
"grunt-shell": "~0.6.0",
|
||||
"load-grunt-tasks": "~0.2.0"
|
||||
"grunt-shell": "~0.6.4",
|
||||
"load-grunt-tasks": "~0.2.1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@ -32,7 +32,6 @@ Recursive =
|
||||
|
||||
apply: (recursive, post, args...) ->
|
||||
{fullID} = post
|
||||
for ID, post of g.posts
|
||||
g.posts.forEach (post) ->
|
||||
if fullID in post.quotes
|
||||
recursive post, args...
|
||||
return
|
||||
|
||||
@ -58,7 +58,7 @@ ThreadHiding =
|
||||
$.cache "//a.4cdn.org/#{g.BOARD}/threads.json", ->
|
||||
return unless @status is 200
|
||||
threads = {}
|
||||
for page in JSON.parse @response
|
||||
for page in @response
|
||||
for thread in page.threads
|
||||
if thread.no of hiddenThreadsOnCatalog
|
||||
threads[thread.no] = hiddenThreadsOnCatalog[thread.no]
|
||||
|
||||
@ -208,7 +208,8 @@ Build =
|
||||
className: 'summary'
|
||||
textContent: text.join ' '
|
||||
href: "/#{boardID}/res/#{threadID}"
|
||||
thread: (board, data) ->
|
||||
|
||||
thread: (board, data, full) ->
|
||||
Build.spoilerRange[board] = data.custom_spoiler
|
||||
|
||||
if (OP = board.posts[data.no]) and root = OP.nodes.root.parentNode
|
||||
@ -218,6 +219,10 @@ Build =
|
||||
className: 'thread'
|
||||
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]
|
||||
if data.omitted_posts or !Conf['Show Replies'] and data.replies
|
||||
[posts, files] = if Conf['Show Replies']
|
||||
@ -226,6 +231,6 @@ Build =
|
||||
# XXX data.images is not accurate.
|
||||
[data.replies, data.omitted_images + data.last_replies.filter((data) -> !!data.ext).length]
|
||||
nodes.push Build.summary board.ID, data.no, posts, files
|
||||
nodes
|
||||
|
||||
$.add root, nodes
|
||||
root
|
||||
fullThread: (board, data) -> Build.postFromObject data, board.ID
|
||||
@ -1,9 +1,11 @@
|
||||
# I am bad at JavaScript and if you reuse this, so are you.
|
||||
Array::indexOf = (val) ->
|
||||
i = @length
|
||||
while i--
|
||||
Array::indexOf = (val, i) ->
|
||||
i or= 0
|
||||
len = @length
|
||||
while i < len
|
||||
return i if @[i] is val
|
||||
return i
|
||||
i++
|
||||
return -1
|
||||
|
||||
# Update CoffeeScript's reference to [].indexOf
|
||||
# Reserved keywords are ignored in embedded javascript.
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
Config =
|
||||
main:
|
||||
'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': [
|
||||
true
|
||||
'Add toggle link in header menu to turn Navigation links into links to each board\'s catalog.'
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
Get =
|
||||
threadExcerpt: (thread) ->
|
||||
{OP} = thread
|
||||
excerpt = OP.info.subject?.trim() or
|
||||
excerpt = "/#{thread.board}/ - " + (
|
||||
OP.info.subject?.trim() or
|
||||
OP.info.comment.replace(/\n+/g, ' // ') or
|
||||
Conf['Anonymize'] and 'Anonymous' or
|
||||
$('.nameBlock', OP.nodes.info).textContent.trim()
|
||||
if excerpt.length > 70
|
||||
excerpt = "#{excerpt[...67]}..."
|
||||
"/#{thread.board}/ - #{excerpt}"
|
||||
$('.nameBlock', OP.nodes.info).textContent.trim())
|
||||
return "#{excerpt[...70]}..." if excerpt.length > 73
|
||||
excerpt
|
||||
threadFromRoot: (root) ->
|
||||
g.threads["#{g.BOARD}.#{root.id[1..]}"]
|
||||
threadFromNode: (node) ->
|
||||
@ -40,24 +40,28 @@ Get =
|
||||
allQuotelinksLinkingTo: (post) ->
|
||||
# Get quotelinks & backlinks linking to the given post.
|
||||
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:
|
||||
# In every posts,
|
||||
# if it did quote this post,
|
||||
# get all their backlinks.
|
||||
for ID, quoterPost of g.posts
|
||||
if post.fullID in quoterPost.quotes
|
||||
for quoterPost in [quoterPost].concat quoterPost.clones
|
||||
quotelinks.push.apply quotelinks, quoterPost.nodes.quotelinks
|
||||
posts.forEach (qPost) ->
|
||||
if fullID in qPost.quotes
|
||||
handleQuotes qPost, 'quotelinks'
|
||||
|
||||
# Second:
|
||||
# If we have quote backlinks:
|
||||
# in all posts this post quoted
|
||||
# and their clones,
|
||||
# get all of their backlinks.
|
||||
if Conf['Quote Backlinks']
|
||||
for quote in post.quotes
|
||||
continue unless quotedPost = g.posts[quote]
|
||||
for quotedPost in [quotedPost].concat quotedPost.clones
|
||||
quotelinks.push.apply quotelinks, [quotedPost.nodes.backlinks...]
|
||||
handleQuotes qPost, 'backlinks' for quote in post.quotes when qPost = posts[quote]
|
||||
|
||||
# Third:
|
||||
# Filter out irrelevant quotelinks.
|
||||
quotelinks.filter (quotelink) ->
|
||||
@ -76,6 +80,7 @@ Get =
|
||||
$.cache url,
|
||||
-> Get.archivedPost @, boardID, postID, root, context
|
||||
,
|
||||
responseType: 'json'
|
||||
withCredentials: url.archive.withCredentials
|
||||
insert: (post, root, context) ->
|
||||
# Stop here if the container has been removed while loading.
|
||||
@ -114,7 +119,7 @@ Get =
|
||||
"Error #{req.statusText} (#{req.status})."
|
||||
return
|
||||
|
||||
posts = JSON.parse(req.response).posts
|
||||
{posts} = req.response
|
||||
Build.spoilerRange[boardID] = posts[0].custom_spoiler
|
||||
for post in posts
|
||||
break if post.no is postID # we found it!
|
||||
@ -145,7 +150,7 @@ Get =
|
||||
Get.insert post, root, context
|
||||
return
|
||||
|
||||
data = JSON.parse req.response
|
||||
data = req.response
|
||||
if data.error
|
||||
$.addClass root, 'warning'
|
||||
root.textContent = data.error
|
||||
@ -211,28 +216,16 @@ Get =
|
||||
Main.callbackNodes Post, [post]
|
||||
Get.insert post, root, context
|
||||
parseMarkup: (text) ->
|
||||
switch text
|
||||
when '\n'
|
||||
'<br>'
|
||||
when '[b]'
|
||||
'<b>'
|
||||
when '[/b]'
|
||||
'</b>'
|
||||
when '[spoiler]'
|
||||
'<s>'
|
||||
when '[/spoiler]'
|
||||
'</s>'
|
||||
when '[code]'
|
||||
'<pre class=prettyprint>'
|
||||
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', ''
|
||||
{
|
||||
'\n': '<br>'
|
||||
'[b]': '<b>'
|
||||
'[/b]': '</b>'
|
||||
'[spoiler]': '<s>'
|
||||
'[/spoiler]': '</s>'
|
||||
'[code]': '<pre class=prettyprint>'
|
||||
'[/code]': '</pre>'
|
||||
'[moot]': '<div style="padding:5px;margin-left:.5em;border-color:#faa;border:2px dashed rgba(255,0,0,.1);border-radius:2px">'
|
||||
'[/moot]': '</div>'
|
||||
'[banned]': '<strong style="color: red;">'
|
||||
'[/banned]': '</strong>'
|
||||
}[text] or text.replace ':lit', ''
|
||||
|
||||
@ -5,6 +5,4 @@ doc = d.documentElement
|
||||
g =
|
||||
VERSION: '<%= version %>'
|
||||
NAMESPACE: '<%= meta.name %>.'
|
||||
boards: {}
|
||||
threads: {}
|
||||
posts: {}
|
||||
boards: {}
|
||||
@ -102,9 +102,10 @@ Header =
|
||||
|
||||
$.ready =>
|
||||
@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
|
||||
a.className = 'current'
|
||||
$.on a, 'click', Index.cb.link
|
||||
|
||||
cs = $.el 'a',
|
||||
id: 'settingsWindowLink'
|
||||
@ -133,21 +134,27 @@ Header =
|
||||
toggle: $.el 'div',
|
||||
id: 'scroll-marker'
|
||||
|
||||
initReady: ->
|
||||
Header.setBoardList()
|
||||
Header.addNav()
|
||||
|
||||
setBoardList: ->
|
||||
fourchannav = $.id 'boardNavDesktop'
|
||||
boardList = $.el 'span',
|
||||
Header.boardList = boardList = $.el 'span',
|
||||
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>"
|
||||
if a = $ "a[href*='/#{g.BOARD}/']", boardList
|
||||
a.className = 'current'
|
||||
$.on a, 'click', Index.cb.link
|
||||
for a in $$ 'a', boardList
|
||||
if Conf['JSON Navigation']
|
||||
$.on a, 'click', Navigate.navigate
|
||||
if a.pathname.split('/')[1] is g.BOARD.ID
|
||||
a.className = 'current'
|
||||
fullBoardList = $ '#full-board-list', boardList
|
||||
btn = $ '.hide-board-list-button', fullBoardList
|
||||
$.on btn, 'click', Header.toggleBoardList
|
||||
|
||||
$.rm $ '#navtopright', 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.generateBoardList Conf['boardnav'].replace /(\r\n|\n|\r)/g, ' '
|
||||
@ -156,10 +163,10 @@ Header =
|
||||
$.sync 'boardnav', Header.generateBoardList
|
||||
|
||||
generateBoardList: (text) ->
|
||||
list = $ '#custom-board-list', Header.bar
|
||||
list = $ '#custom-board-list', Header.boardList
|
||||
$.rmAll list
|
||||
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) ->
|
||||
if /^[^\w@]/.test t
|
||||
return $.tn t
|
||||
@ -184,11 +191,10 @@ Header =
|
||||
if a.textContent is board
|
||||
a = a.cloneNode true
|
||||
|
||||
current = $.hasClass a, 'current'
|
||||
if current
|
||||
$.on a, 'click', Index.cb.link
|
||||
if Conf['JSON Navigation']
|
||||
$.on a, 'click', Navigate.navigate
|
||||
|
||||
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
|
||||
else if /-full/.test t
|
||||
"/#{board}/ - #{a.title}"
|
||||
@ -299,17 +305,15 @@ Header =
|
||||
|
||||
toggleHideBarOnScroll: (e) ->
|
||||
hide = @checked
|
||||
$.set 'Header auto-hide on scroll', hide
|
||||
$.cb.checked.call @
|
||||
Header.setHideBarOnScroll hide
|
||||
|
||||
hideBarOnScroll: ->
|
||||
offsetY = window.pageYOffset
|
||||
if offsetY > (Header.previousOffset or 0)
|
||||
$.addClass Header.bar, 'autohide'
|
||||
$.addClass Header.bar, 'scroll'
|
||||
$.addClass Header.bar, 'autohide', 'scroll'
|
||||
else
|
||||
$.rmClass Header.bar, 'autohide'
|
||||
$.rmClass Header.bar, 'scroll'
|
||||
$.rmClass Header.bar, 'autohide', 'scroll'
|
||||
Header.previousOffset = offsetY
|
||||
|
||||
setBarPosition: (bottom) ->
|
||||
@ -329,7 +333,7 @@ Header =
|
||||
|
||||
$.addClass doc, args[0]
|
||||
$.rmClass doc, args[1]
|
||||
Header.bar.parentNode.className = args[2]
|
||||
Header.bar.parentNode.className = args[2]
|
||||
$[args[3]] Header.bar, Header.noticesRoot
|
||||
|
||||
toggleBarPosition: ->
|
||||
@ -379,21 +383,37 @@ Header =
|
||||
return if (Get.postFromRoot post).isHidden
|
||||
|
||||
Header.scrollTo post
|
||||
|
||||
scrollTo: (root, down, needed) ->
|
||||
if down
|
||||
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
|
||||
else
|
||||
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
|
||||
|
||||
scrollToIfNeeded: (root, down) ->
|
||||
Header.scrollTo root, down, true
|
||||
|
||||
getTopOf: (root) ->
|
||||
{top} = root.getBoundingClientRect()
|
||||
if Conf['Fixed Header'] and not Conf['Bottom Header']
|
||||
headRect = Header.toggle.getBoundingClientRect()
|
||||
top -= headRect.top + headRect.height
|
||||
top
|
||||
|
||||
getBottomOf: (root) ->
|
||||
{clientHeight} = doc
|
||||
bottom = clientHeight - root.getBoundingClientRect().bottom
|
||||
@ -401,6 +421,12 @@ Header =
|
||||
headRect = Header.toggle.getBoundingClientRect()
|
||||
bottom -= clientHeight - headRect.bottom + headRect.height
|
||||
bottom
|
||||
isHidden: ->
|
||||
{top} = Header.bar.getBoundingClientRect()
|
||||
if Conf['Bottom header']
|
||||
top is doc.clientHeight
|
||||
else
|
||||
top < 0
|
||||
|
||||
addShortcut: (el) ->
|
||||
shortcut = $.el 'span',
|
||||
@ -408,6 +434,8 @@ Header =
|
||||
$.add shortcut, el
|
||||
$.prepend Header.shortcuts, shortcut
|
||||
|
||||
rmShortcut: (el) ->
|
||||
$.rm el.parentElement
|
||||
|
||||
menuToggle: (e) ->
|
||||
Header.menu.toggle e, @, g
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
Index =
|
||||
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',
|
||||
className: 'index-refresh-shortcut fa fa-refresh'
|
||||
title: 'Refresh Index'
|
||||
title: 'Refresh'
|
||||
href: 'javascript:;'
|
||||
textContent: 'Refresh Index'
|
||||
$.on @button, 'click', @update
|
||||
@ -68,7 +70,6 @@ Index =
|
||||
subEntries: [repliesEntry, anchorEntry, refNavEntry, modeEntry, sortEntry]
|
||||
|
||||
$.addClass doc, 'index-loading'
|
||||
@update()
|
||||
@root = $.el 'div', className: 'board'
|
||||
@pagelist = $.el 'div',
|
||||
className: 'pagelist'
|
||||
@ -79,40 +80,49 @@ Index =
|
||||
innerHTML: <%= importHTML('Features/Index-navlinks') %>
|
||||
@searchInput = $ '#index-search', @navLinks
|
||||
@currentPage = @getCurrentPage()
|
||||
$.on window, 'popstate', @cb.popstate
|
||||
|
||||
$.on d, 'scroll', Index.scroll
|
||||
$.on @pagelist, 'click', @cb.pageNav
|
||||
$.on @searchInput, 'input', @onSearchInput
|
||||
$.on $('#index-search-clear', @navLinks), 'click', @clearSearch
|
||||
$.asap (-> $('.board', doc) or d.readyState isnt 'loading'), ->
|
||||
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
|
||||
$.on $('#returnlink a', @navLinks), 'click', Navigate.navigate
|
||||
$.on $('#cataloglink a', @navLinks), 'click', -> window.location = "//boards.4chan.org/#{g.BOARD}/catalog"
|
||||
|
||||
for navLink in $$ '.navLinks'
|
||||
$.rm navLink
|
||||
@update() if g.VIEW is 'index'
|
||||
$.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
|
||||
$.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, ->
|
||||
return if Index.req or Conf['Index Mode'] isnt 'infinite' or ((d.body.scrollTop or doc.scrollTop) <= doc.scrollHeight - (300 + window.innerHeight))
|
||||
pageNum = Index.getCurrentPage() + 1
|
||||
return if Index.req or Conf['Index Mode'] isnt 'infinite' or (doc.scrollTop <= doc.scrollHeight - (300 + window.innerHeight)) or g.VIEW is 'thread'
|
||||
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
|
||||
nodesPerPage = Index.threadsNumPerPage * 2
|
||||
history.pushState null, '', "/#{g.BOARD}/#{pageNum}"
|
||||
nodes = Index.sortedNodes[nodesPerPage * pageNum ... nodesPerPage * (pageNum + 1)]
|
||||
Index.buildReplies nodes if Conf['Show Replies']
|
||||
$.add Index.root, nodes
|
||||
Index.setPage()
|
||||
|
||||
nodes = Index.buildSinglePage pageNum
|
||||
Index.buildReplies nodes if Conf['Show Replies']
|
||||
Index.buildStructure nodes
|
||||
Index.setPage pageNum
|
||||
|
||||
endNotice: do ->
|
||||
notify = false
|
||||
@ -134,9 +144,6 @@ Index =
|
||||
Index.buildThreads()
|
||||
Index.sort()
|
||||
Index.buildIndex()
|
||||
popstate: (e) ->
|
||||
pageNum = Index.getCurrentPage()
|
||||
Index.pageLoad pageNum if Index.currentPage isnt pageNum
|
||||
pageNav: (e) ->
|
||||
return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or e.button isnt 0
|
||||
switch e.target.nodeName
|
||||
@ -149,11 +156,6 @@ Index =
|
||||
return if a.textContent is 'Catalog'
|
||||
e.preventDefault()
|
||||
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: ->
|
||||
Header.scrollToIfNeeded Index.root
|
||||
@ -198,8 +200,8 @@ Index =
|
||||
$.rmAll pagesRoot
|
||||
$.add pagesRoot, nodes
|
||||
Index.togglePagelist()
|
||||
setPage: ->
|
||||
pageNum = Index.getCurrentPage()
|
||||
setPage: (pageNum) ->
|
||||
pageNum or= Index.getCurrentPage()
|
||||
maxPageNum = Index.getMaxPageNum()
|
||||
pagesRoot = $ '.pages', Index.pagelist
|
||||
# Previous/Next buttons
|
||||
@ -223,19 +225,37 @@ Index =
|
||||
|
||||
update: (pageNum) ->
|
||||
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.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
|
||||
onload = (e) -> Index.load e, pageNum
|
||||
Index.req = $.ajax "//a.4cdn.org/#{g.BOARD}/catalog.json",
|
||||
onabort: onload
|
||||
onloadend: onload
|
||||
,
|
||||
whenModified: true
|
||||
whenModified: Index.board is "#{g.BOARD}"
|
||||
$.addClass Index.button, 'fa-spin'
|
||||
|
||||
load: (e, pageNum) ->
|
||||
$.rmClass Index.button, 'fa-spin'
|
||||
{req, notice} = Index
|
||||
{req, notice, nTimeout} = Index
|
||||
clearTimeout nTimeout if nTimeout
|
||||
delete Index.nTimeout
|
||||
delete Index.req
|
||||
delete Index.notice
|
||||
|
||||
@ -244,26 +264,40 @@ Index =
|
||||
notice.close()
|
||||
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
|
||||
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?
|
||||
Index.pageNav pageNum
|
||||
catch err
|
||||
c.error 'Index failure:', err.stack
|
||||
c.error "Index failure: #{err.message}", err.stack
|
||||
# network error or non-JSON content for example.
|
||||
if notice
|
||||
notice.setType 'error'
|
||||
notice.el.lastElementChild.textContent = 'Index refresh failed.'
|
||||
setTimeout notice.close, 2 * $.SECOND
|
||||
setTimeout notice.close, $.SECOND
|
||||
else
|
||||
new Notice 'error', 'Index refresh failed.', 2
|
||||
new Notice 'error', 'Index refresh failed.', 1
|
||||
return
|
||||
|
||||
timeEl = $ '#index-last-refresh', Index.navLinks
|
||||
timeEl = $ '#index-last-refresh time', Index.navLinks
|
||||
timeEl.dataset.utc = Date.parse req.getResponseHeader 'Last-Modified'
|
||||
RelativeDates.update timeEl
|
||||
Index.scrollToIndex()
|
||||
|
||||
parse: (pages, pageNum) ->
|
||||
Index.parseThreadList pages
|
||||
Index.buildThreads()
|
||||
@ -274,36 +308,38 @@ Index =
|
||||
return
|
||||
Index.buildIndex()
|
||||
Index.setPage()
|
||||
|
||||
parseThreadList: (pages) ->
|
||||
Index.pagesNum = pages.length
|
||||
Index.threadsNumPerPage = pages[0].threads.length
|
||||
Index.liveThreadData = pages.reduce ((arr, next) -> arr.concat next.threads), []
|
||||
Index.liveThreadIDs = Index.liveThreadData.map (data) -> data.no
|
||||
for threadID, thread of g.BOARD.threads when thread.ID not in Index.liveThreadIDs
|
||||
thread.collect()
|
||||
g.BOARD.threads.forEach (thread) ->
|
||||
thread.collect() unless thread.ID in Index.liveThreadIDs
|
||||
return
|
||||
|
||||
buildThreads: ->
|
||||
Index.nodes = []
|
||||
threads = []
|
||||
posts = []
|
||||
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
|
||||
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
|
||||
catch err
|
||||
# Skip posts that we failed to parse.
|
||||
errors = [] unless errors
|
||||
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
|
||||
Main.handleErrors errors if errors
|
||||
|
||||
@ -312,9 +348,10 @@ Index =
|
||||
Main.callbackNodes Thread, threads
|
||||
Main.callbackNodes Post, posts
|
||||
$.event 'IndexRefresh'
|
||||
|
||||
buildReplies: (threadRoots) ->
|
||||
posts = []
|
||||
for threadRoot in threadRoots by 2
|
||||
for threadRoot in threadRoots
|
||||
thread = Get.threadFromRoot threadRoot
|
||||
i = Index.liveThreadIDs.indexOf thread.ID
|
||||
continue unless lastReplies = Index.liveThreadData[i].last_replies
|
||||
@ -336,57 +373,99 @@ Index =
|
||||
|
||||
Main.handleErrors errors if errors
|
||||
Main.callbackNodes Post, posts
|
||||
|
||||
sort: ->
|
||||
switch Conf['Index Sort']
|
||||
when 'bump'
|
||||
sortedThreadIDs = Index.liveThreadIDs
|
||||
when 'lastreply'
|
||||
sortedThreadIDs = [Index.liveThreadData...].sort((a, b) ->
|
||||
a = a.last_replies[a.last_replies.length - 1] if 'last_replies' of a
|
||||
b = b.last_replies[b.last_replies.length - 1] if 'last_replies' of b
|
||||
{liveThreadIDs, liveThreadData} = Index
|
||||
sortedThreadIDs = {
|
||||
lastreply:
|
||||
[liveThreadData...].sort((a, b) ->
|
||||
a = num[num.length - 1] if (num = a.last_replies)
|
||||
b = num[num.length - 1] if (num = b.last_replies)
|
||||
b.no - a.no
|
||||
).map (data) -> data.no
|
||||
when 'birth'
|
||||
sortedThreadIDs = [Index.liveThreadIDs...].sort (a, b) -> b - a
|
||||
when 'replycount'
|
||||
sortedThreadIDs = [Index.liveThreadData...].sort((a, b) -> b.replies - a.replies).map (data) -> data.no
|
||||
when 'filecount'
|
||||
sortedThreadIDs = [Index.liveThreadData...].sort((a, b) -> b.images - a.images).map (data) -> data.no
|
||||
Index.sortedNodes = []
|
||||
).map (post) -> post.no
|
||||
bump: liveThreadIDs
|
||||
birth: [liveThreadIDs... ].sort (a, b) -> b - a
|
||||
replycount: [liveThreadData...].sort((a, b) -> b.replies - a.replies).map (post) -> post.no
|
||||
filecount: [liveThreadData...].sort((a, b) -> b.images - a.images ).map (post) -> post.no
|
||||
}[Conf['Index Sort']]
|
||||
Index.sortedNodes = sortedNodes = new RandomAccessList
|
||||
{nodes} = Index
|
||||
for threadID in sortedThreadIDs
|
||||
i = Index.liveThreadIDs.indexOf(threadID) * 2
|
||||
Index.sortedNodes.push Index.nodes[i], Index.nodes[i + 1]
|
||||
if Index.isSearching
|
||||
Index.sortedNodes = Index.querySearch(Index.searchInput.value) or Index.sortedNodes
|
||||
# Sticky threads
|
||||
Index.sortOnTop (thread) -> thread.isSticky
|
||||
# Highlighted threads
|
||||
Index.sortOnTop((thread) -> thread.isOnTop) if Conf['Filter']
|
||||
# Non-hidden threads
|
||||
Index.sortOnTop((thread) -> !thread.isHidden) if Conf['Anchor Hidden Threads']
|
||||
sortedNodes.push nodes[Index.liveThreadIDs.indexOf(threadID)]
|
||||
if Index.isSearching and nodes = Index.querySearch(Index.searchInput.value)
|
||||
Index.sortedNodes = new RandomAccessList nodes
|
||||
items = [
|
||||
# Sticky threads
|
||||
fn: (thread) -> thread.isSticky
|
||||
cnd: true
|
||||
, # Highlighted threads
|
||||
fn: (thread) -> thread.isOnTop
|
||||
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) ->
|
||||
offset = 0
|
||||
for threadRoot, i in Index.sortedNodes by 2 when match Get.threadFromRoot threadRoot
|
||||
Index.sortedNodes.splice offset++ * 2, 0, Index.sortedNodes.splice(i, 2)...
|
||||
{sortedNodes} = Index
|
||||
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
|
||||
|
||||
buildIndex: ->
|
||||
if Conf['Index Mode'] isnt 'all pages'
|
||||
pageNum = Index.getCurrentPage()
|
||||
nodesPerPage = Index.threadsNumPerPage * 2
|
||||
nodes = Index.sortedNodes[nodesPerPage * pageNum ... nodesPerPage * (pageNum + 1)]
|
||||
nodes = Index.buildSinglePage Index.getCurrentPage()
|
||||
else
|
||||
nodes = Index.sortedNodes
|
||||
nodes = [(target = Index.sortedNodes.first).data]
|
||||
while target = target.next
|
||||
nodes.push target.data
|
||||
$.rmAll Index.root
|
||||
$.rmAll Header.hover
|
||||
Index.buildReplies nodes if Conf['Show Replies']
|
||||
$.event 'IndexBuild', nodes
|
||||
$.add Index.root, nodes
|
||||
Index.buildStructure 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
|
||||
|
||||
clearSearch: ->
|
||||
Index.searchInput.value = null
|
||||
Index.onSearchInput()
|
||||
Index.searchInput.focus()
|
||||
|
||||
onSearchInput: ->
|
||||
if Index.isSearching = !!Index.searchInput.value.trim()
|
||||
unless Index.searchInput.dataset.searching
|
||||
@ -396,6 +475,7 @@ Index =
|
||||
else
|
||||
pageNum = Index.getCurrentPage()
|
||||
else
|
||||
return unless Index.searchInput.dataset.searching
|
||||
pageNum = Index.pageBeforeSearch
|
||||
delete Index.pageBeforeSearch
|
||||
<% if (type === 'userscript') { %>
|
||||
@ -413,15 +493,21 @@ Index =
|
||||
Index.setPage()
|
||||
else
|
||||
Index.pageNav pageNum
|
||||
|
||||
querySearch: (query) ->
|
||||
return unless keywords = query.toLowerCase().match /\S+/g
|
||||
Index.search keywords
|
||||
search: (keywords) ->
|
||||
found = []
|
||||
for threadRoot, i in Index.sortedNodes by 2
|
||||
if Index.searchMatch Get.threadFromRoot(threadRoot), keywords
|
||||
found.push Index.sortedNodes[i], Index.sortedNodes[i + 1]
|
||||
|
||||
search: (keywords) ->
|
||||
found = []
|
||||
target = Index.sortedNodes.first
|
||||
while target
|
||||
{data} = target
|
||||
if Index.searchMatch Get.threadFromRoot(data), keywords
|
||||
found.push data
|
||||
target = target.next
|
||||
found
|
||||
|
||||
searchMatch: (thread, keywords) ->
|
||||
{info, file} = thread.OP
|
||||
text = []
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
Main =
|
||||
init: ->
|
||||
g.threads = new SimpleDict
|
||||
g.posts = new SimpleDict
|
||||
|
||||
pathname = location.pathname.split '/'
|
||||
g.BOARD = new Board pathname[1]
|
||||
return if g.BOARD.ID in ['z', 'fk']
|
||||
@ -54,81 +57,17 @@ Main =
|
||||
location.replace URL if URL
|
||||
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'
|
||||
init
|
||||
'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
|
||||
for [name, feature] in Main.features
|
||||
# c.time "#{name} initialization"
|
||||
try
|
||||
feature.init()
|
||||
catch err
|
||||
Main.handleErrors
|
||||
message: "\"#{name}\" initialization crashed."
|
||||
error: err
|
||||
# finally
|
||||
# c.timeEnd "#{name} initialization"
|
||||
# c.timeEnd 'All initializations'
|
||||
|
||||
$.on d, 'AddCallback', Main.addCallback
|
||||
@ -139,16 +78,12 @@ Main =
|
||||
return if !Main.isThisPageLegit() or $.hasClass doc, 'fourchan-x'
|
||||
# disable the mobile layout
|
||||
$('link[href*=mobile]', d.head)?.disabled = true
|
||||
<% if (type === 'crx') { %>
|
||||
$.addClass doc, 'blink'
|
||||
<% } else { %>
|
||||
$.addClass doc, 'gecko'
|
||||
<% } %>
|
||||
$.addClass doc, 'fourchan-x'
|
||||
$.addClass doc, 'seaweedchan'
|
||||
$.addClass doc, g.VIEW
|
||||
$.addClass doc, 'fourchan-x', 'seaweedchan', g.VIEW, '<% if (type === 'crx') { %>blink<% } else { %>gecko<% } %>'
|
||||
$.addStyle Main.css
|
||||
|
||||
Main.setClass()
|
||||
|
||||
setClass: ->
|
||||
if g.VIEW is 'catalog'
|
||||
$.addClass doc, $.id('base-css').href.match(/catalog_(\w+)/)[1].replace('_new', '').replace /_+/g, '-'
|
||||
return
|
||||
@ -182,11 +117,7 @@ Main =
|
||||
# Something might have gone wrong!
|
||||
Main.initStyle()
|
||||
|
||||
if g.VIEW is 'thread'
|
||||
Main.initThread()
|
||||
else
|
||||
$.event '4chanXInitFinished'
|
||||
|
||||
# 4chan Pass Link
|
||||
if styleSelector = $.id 'styleSelector'
|
||||
passLink = $.el 'a',
|
||||
textContent: '4chan Pass'
|
||||
@ -197,7 +128,18 @@ Main =
|
||||
'left=0,top=0,width=500,height=255,toolbar=0,resizable=0'
|
||||
$.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') { %>
|
||||
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 '.'
|
||||
for v, i in "<%= meta.min.greasemonkey %>".split '.'
|
||||
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
|
||||
|
||||
initThread: ->
|
||||
return unless threadRoot = $ '.thread'
|
||||
thread = new Thread +threadRoot.id[1..], g.BOARD
|
||||
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
|
||||
if board = $ '.board'
|
||||
threads = []
|
||||
posts = []
|
||||
|
||||
Main.callbackNodes Thread, [thread]
|
||||
Main.callbackNodesDB Post, posts, ->
|
||||
$.event '4chanXInitFinished'
|
||||
for threadRoot in $$ '.board > .thread', board
|
||||
thread = new Thread +threadRoot.id[1..], g.BOARD
|
||||
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) ->
|
||||
i = 0
|
||||
@ -237,24 +195,21 @@ Main =
|
||||
return
|
||||
|
||||
callbackNodesDB: (klass, nodes, cb) ->
|
||||
errors = null
|
||||
len = 0
|
||||
i = 0
|
||||
|
||||
i = 0
|
||||
cbs = klass.callbacks
|
||||
fn = ->
|
||||
node = nodes[i++]
|
||||
fn = ->
|
||||
return false unless node = nodes[i]
|
||||
cbs.execute node
|
||||
i % 25
|
||||
++i % 25
|
||||
|
||||
softTask = ->
|
||||
while fn()
|
||||
if len is i
|
||||
cb() if cb
|
||||
return
|
||||
continue
|
||||
unless nodes[i]
|
||||
cb() if cb
|
||||
return
|
||||
setTimeout softTask, 0
|
||||
|
||||
len = nodes.length
|
||||
softTask()
|
||||
|
||||
addCallback: (e) ->
|
||||
@ -322,4 +277,68 @@ Main =
|
||||
<%= 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()
|
||||
|
||||
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
|
||||
|
||||
$.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 'Filter', Settings.filter
|
||||
Settings.addSection 'Sauce', Settings.sauce
|
||||
@ -37,7 +24,6 @@ Settings =
|
||||
localStorage.setItem '4chan-settings', JSON.stringify settings
|
||||
|
||||
open: (openSection) ->
|
||||
$.off d, '4chanXInitFinished', Settings.open
|
||||
return if Settings.dialog
|
||||
$.event 'CloseMenu'
|
||||
|
||||
@ -53,6 +39,7 @@ Settings =
|
||||
|
||||
$.on $('.export', Settings.dialog), 'click', Settings.export
|
||||
$.on $('.import', Settings.dialog), 'click', Settings.import
|
||||
$.on $('.reset', Settings.dialog), 'click', Settings.reset
|
||||
$.on $('input', Settings.dialog), 'change', Settings.onImport
|
||||
|
||||
links = []
|
||||
@ -124,56 +111,39 @@ Settings =
|
||||
div = $.el 'div',
|
||||
innerHTML: "<button></button><span class=description>: Clear manually-hidden threads and posts on all boards. Reload the page to apply."
|
||||
button = $ 'button', div
|
||||
hiddenNum = 0
|
||||
$.get 'hiddenThreads', boards: {}, (item) ->
|
||||
for ID, board of item.hiddenThreads.boards
|
||||
$.get {hiddenThreads: {}, hiddenPosts: {}}, ({hiddenThreads, hiddenPosts}) ->
|
||||
hiddenNum = 0
|
||||
for ID, board of hiddenThreads.boards
|
||||
hiddenNum += Object.keys(board).length
|
||||
for ID, board of hiddenPosts.boards
|
||||
for ID, thread of board
|
||||
hiddenNum++
|
||||
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++
|
||||
hiddenNum += Object.keys(thread).length
|
||||
button.textContent = "Hidden: #{hiddenNum}"
|
||||
$.on button, 'click', ->
|
||||
@textContent = 'Hidden: 0'
|
||||
$.get 'hiddenThreads', boards: {}, (item) ->
|
||||
for boardID of item.hiddenThreads.boards
|
||||
$.get 'hiddenThreads', {}, ({hiddenThreads}) ->
|
||||
for boardID of hiddenThreads.boards
|
||||
localStorage.removeItem "4chan-hide-t-#{boardID}"
|
||||
$.delete ['hiddenThreads', 'hiddenPosts']
|
||||
$.after $('input[name="Stubs"]', section).parentNode.parentNode, div
|
||||
export: (now, data) ->
|
||||
unless typeof now is 'number'
|
||||
now = Date.now()
|
||||
data =
|
||||
version: g.VERSION
|
||||
date: now
|
||||
for db in DataBoard.keys
|
||||
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
|
||||
export: ->
|
||||
# Make sure to export the most recent data.
|
||||
$.get Conf, (Conf) ->
|
||||
# XXX don't export archives.
|
||||
delete Conf['archives']
|
||||
Settings.downloadExport {version: g.VERSION, date: Date.now(), Conf}
|
||||
downloadExport: (data) ->
|
||||
a = $.el 'a',
|
||||
className: 'warning'
|
||||
textContent: 'Save me!'
|
||||
download: "<%= meta.name %> v#{g.VERSION}-#{now}.json"
|
||||
download: "<%= meta.name %> v#{g.VERSION}-#{data.date}.json"
|
||||
href: "data:application/json;base64,#{btoa unescape encodeURIComponent JSON.stringify data, null, 2}"
|
||||
target: '_blank'
|
||||
<% if (type === 'userscript') { %>
|
||||
# XXX Firefox won't let us download automatically.
|
||||
p = $ '.imp-exp-result', Settings.dialog
|
||||
$.rmAll p
|
||||
$.add p, a
|
||||
<% } else { %>
|
||||
a.click()
|
||||
<% } %>
|
||||
a.click()
|
||||
import: ->
|
||||
@nextElementSibling.click()
|
||||
$('input', @parentNode).click()
|
||||
onImport: ->
|
||||
return unless file = @files[0]
|
||||
output = $('.imp-exp-result')
|
||||
@ -183,8 +153,7 @@ Settings =
|
||||
reader = new FileReader()
|
||||
reader.onload = (e) ->
|
||||
try
|
||||
data = JSON.parse e.target.result
|
||||
Settings.loadSettings data
|
||||
Settings.loadSettings JSON.parse e.target.result
|
||||
if confirm 'Import successful. Reload now?'
|
||||
window.location.reload()
|
||||
catch err
|
||||
@ -194,6 +163,11 @@ Settings =
|
||||
loadSettings: (data) ->
|
||||
version = data.version.split '.'
|
||||
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,
|
||||
# General confs
|
||||
'Disable 4chan\'s extension': ''
|
||||
@ -265,11 +239,9 @@ Settings =
|
||||
data.Conf['watchedThreads'] = boards: ThreadWatcher.convert data.Conf['WatchedThreads']
|
||||
delete data.Conf['WatchedThreads']
|
||||
$.set data.Conf
|
||||
convertSettings: (data, map) ->
|
||||
for prevKey, newKey of map
|
||||
data.Conf[newKey] = data.Conf[prevKey] if newKey
|
||||
delete data.Conf[prevKey]
|
||||
data
|
||||
reset: ->
|
||||
if confirm 'Your current settings will be entirely wiped, are you sure?'
|
||||
$.clear -> window.location.reload() if confirm 'Reset successful. Reload now?'
|
||||
|
||||
filter: (section) ->
|
||||
section.innerHTML = <%= importHTML('Settings/Filter-select') %>
|
||||
@ -333,20 +305,20 @@ Settings =
|
||||
$.on $('input[name="Custom CSS"]', section), 'change', Settings.togglecss
|
||||
$.on $.id('apply-css'), 'click', Settings.usercss
|
||||
|
||||
boards = {}
|
||||
for name, archive of Redirect.archives
|
||||
for boardID in archive.boards
|
||||
data = boards[boardID] or=
|
||||
archBoards = {}
|
||||
for {name, boards, files, data} in Redirect.archives
|
||||
for boardID in boards
|
||||
o = archBoards[boardID] or=
|
||||
thread: []
|
||||
post: []
|
||||
file: []
|
||||
data.thread.push name
|
||||
data.post.push name if archive.software is 'foolfuuka'
|
||||
data.file.push name if boardID in archive.files
|
||||
o.thread.push name
|
||||
o.post.push name if data.software is 'foolfuuka'
|
||||
o.file.push name if boardID in files
|
||||
|
||||
rows = []
|
||||
boardOptions = []
|
||||
for boardID in Object.keys(boards).sort() # Alphabetical order
|
||||
for boardID in Object.keys(archBoards).sort() # Alphabetical order
|
||||
row = $.el 'tr',
|
||||
className: "board-#{boardID}"
|
||||
row.hidden = boardID isnt g.BOARD.ID
|
||||
@ -356,8 +328,8 @@ Settings =
|
||||
value: "board-#{boardID}"
|
||||
selected: boardID is g.BOARD.ID
|
||||
|
||||
data = boards[boardID]
|
||||
$.add row, Settings.addArchiveCell boardID, data, item for item in ['thread', 'post', 'file']
|
||||
o = archBoards[boardID]
|
||||
$.add row, Settings.addArchiveCell boardID, o, item for item in ['thread', 'post', 'file']
|
||||
rows.push row
|
||||
|
||||
$.add $('tbody', section), rows
|
||||
|
||||
@ -24,6 +24,7 @@ UI = do ->
|
||||
constructor: (@type) ->
|
||||
# Doc here: https://github.com/MayhemYDG/4chan-x/wiki/Menu-API
|
||||
$.on d, 'AddMenuEntry', @addEntry
|
||||
$.on d, 'rmMenuEntry', @rmEntry
|
||||
@entries = []
|
||||
|
||||
makeMenu: ->
|
||||
@ -156,6 +157,9 @@ UI = do ->
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
onFocus: (e) =>
|
||||
e.stopPropagation()
|
||||
@focus e.target
|
||||
focus: (entry) ->
|
||||
while focused = $.x 'parent::*/child::*[contains(@class,"focused")]', entry
|
||||
$.rmClass focused, 'focused'
|
||||
@ -189,13 +193,16 @@ UI = do ->
|
||||
@parseEntry 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) ->
|
||||
{el, subEntries} = entry
|
||||
$.addClass el, 'entry'
|
||||
$.on el, 'focus mouseover', ((e) ->
|
||||
e.stopPropagation()
|
||||
@focus el
|
||||
).bind @
|
||||
$.on el, 'focus mouseover', @onFocus
|
||||
el.style.order = entry.order or 100
|
||||
return unless subEntries
|
||||
$.addClass el, 'has-submenu'
|
||||
|
||||
@ -90,6 +90,11 @@ div.navLinks {
|
||||
.reply > .file > .fileText {
|
||||
margin: 0 20px;
|
||||
}
|
||||
.hashlink::before {
|
||||
content: ' ';
|
||||
visibility: hidden;
|
||||
}
|
||||
.inline + .hashlink,
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
@ -273,12 +278,12 @@ div.center:not(.ad-cnt) {
|
||||
font-weight: bold;
|
||||
}
|
||||
/* 4chan X link brackets */
|
||||
.brackets-wrap::after {
|
||||
content: "]";
|
||||
}
|
||||
.brackets-wrap::before {
|
||||
content: "[";
|
||||
}
|
||||
.brackets-wrap::after {
|
||||
content: "]";
|
||||
}
|
||||
/* Notifications */
|
||||
#notifications {
|
||||
position: fixed;
|
||||
@ -501,7 +506,13 @@ div.center:not(.ad-cnt) {
|
||||
.summary {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.index #returnlink,
|
||||
.index #bottomlink,
|
||||
.thread #index-last-refresh,
|
||||
.thread #index-search-clear,
|
||||
.thread #index-search {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Announcement Hiding */
|
||||
:root.hide-announcement #globalMessage {
|
||||
@ -642,6 +653,15 @@ span.hide-announcement {
|
||||
text-decoration: none;
|
||||
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 {
|
||||
text-decoration: underline line-through;
|
||||
}
|
||||
@ -777,7 +797,7 @@ span.hide-announcement {
|
||||
:root.hide-original-post-form .postingMode,
|
||||
:root.hide-original-post-form #togglePostForm,
|
||||
#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 {
|
||||
display: none;
|
||||
}
|
||||
@ -1187,7 +1207,7 @@ a:only-of-type > .remove {
|
||||
left: 0px;
|
||||
width: 200px;
|
||||
}
|
||||
.export, .import {
|
||||
.export, .import, .reset {
|
||||
cursor: pointer;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
[<a href="./catalog">Catalog</a>]
|
||||
[<time id="index-last-refresh" title="Last index refresh">...</time>]
|
||||
<span class=brackets-wrap id=returnlink><a href=.././>Return</a></span>
|
||||
<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">
|
||||
<a id="index-search-clear" href="javascript:;" title="Clear search">×</a>
|
||||
|
||||
@ -65,7 +65,7 @@
|
||||
</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>
|
||||
<p>
|
||||
One item per line.<br>
|
||||
@ -86,6 +86,7 @@
|
||||
<select name=favicon>
|
||||
<option value=ferongr>ferongr</option>
|
||||
<option value=xat->xat-</option>
|
||||
<option value=4chanJS>4chanJS</option>
|
||||
<option value=Mayhem>Mayhem</option>
|
||||
<option value=Original>Original</option>
|
||||
<option value=Metro>Metro</option>
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
<li><code>%TURL</code>: Thumbnail URL.</li>
|
||||
<li><code>%URL</code>: Full image URL.</li>
|
||||
<li><code>%MD5</code>: MD5 hash.</li>
|
||||
<li><code>%name</code>: Original file name.</li>
|
||||
<li><code>%board</code>: Current board.</li>
|
||||
</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>
|
||||
<p class='imp-exp-result warning'></p>
|
||||
<div class=credits>
|
||||
<a class=export>Export</a> |
|
||||
<a class=import>Import</a> |
|
||||
<input type=file style='display: none;'>
|
||||
<a class=export>Export</a> | 
|
||||
<a class=import>Import</a> | 
|
||||
<a class=reset>Reset Settings</a> | 
|
||||
<input type=file hidden>
|
||||
<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 %>/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>
|
||||
</nav>
|
||||
<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
|
||||
r.setRequestHeader 'If-Modified-Since', lastModified[url] if url of lastModified
|
||||
$.on r, 'load', -> lastModified[url] = r.getResponseHeader 'Last-Modified'
|
||||
if /\.json$/.test url
|
||||
r.responseType = 'json'
|
||||
$.extend r, options
|
||||
$.extend r.upload, upCallbacks
|
||||
r.send form
|
||||
@ -113,11 +115,11 @@ $.X = (path, root) ->
|
||||
# XPathResult.ORDERED_NODE_SNAPSHOT_TYPE === 7
|
||||
d.evaluate path, root, null, 7, null
|
||||
|
||||
$.addClass = (el, className) ->
|
||||
el.classList.add className
|
||||
$.addClass = (el, className...) ->
|
||||
el.classList.add className...
|
||||
|
||||
$.rmClass = (el, className) ->
|
||||
el.classList.remove className
|
||||
$.rmClass = (el, className...) ->
|
||||
el.classList.remove className...
|
||||
|
||||
$.toggleClass = (el, className) ->
|
||||
el.classList.toggle className
|
||||
@ -125,18 +127,12 @@ $.toggleClass = (el, className) ->
|
||||
$.hasClass = (el, className) ->
|
||||
className in el.classList
|
||||
|
||||
$.rm = do ->
|
||||
if 'remove' of Element::
|
||||
(el) -> el.remove()
|
||||
else
|
||||
(el) -> el.parentNode?.removeChild el
|
||||
$.rm = (el) ->
|
||||
el.remove()
|
||||
|
||||
$.rmAll = (root) ->
|
||||
# jsperf.com/emptify-element
|
||||
for node in [root.childNodes...]
|
||||
# HTMLSelectElement.remove !== Element.remove
|
||||
root.removeChild node
|
||||
return
|
||||
# https://gist.github.com/MayhemYDG/8646194
|
||||
root.textContent = null
|
||||
|
||||
$.tn = (s) ->
|
||||
d.createTextNode s
|
||||
@ -268,6 +264,7 @@ $.item = (key, val) ->
|
||||
item
|
||||
|
||||
$.syncing = {}
|
||||
|
||||
<% if (type === 'crx') { %>
|
||||
$.sync = do ->
|
||||
chrome.storage.onChanged.addListener (changes) ->
|
||||
@ -277,6 +274,8 @@ $.sync = do ->
|
||||
return
|
||||
(key, cb) -> $.syncing[key] = cb
|
||||
|
||||
$.desync = (key) -> delete $.syncing[key]
|
||||
|
||||
$.localKeys = [
|
||||
# filters
|
||||
'name',
|
||||
@ -329,29 +328,50 @@ $.get = (key, val, cb) ->
|
||||
chrome.storage.sync.get syncItems, done
|
||||
|
||||
$.set = do ->
|
||||
items = {}
|
||||
localItems = {}
|
||||
items =
|
||||
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
|
||||
if key of items
|
||||
(localItems or= {})[key] = items[key]
|
||||
delete items[key]
|
||||
if key of items.sync
|
||||
items.local[key] = items.sync[key]
|
||||
delete items.sync[key]
|
||||
try
|
||||
chrome.storage.local.set localItems
|
||||
chrome.storage.sync.set items
|
||||
items = {}
|
||||
localItems = {}
|
||||
setArea 'local'
|
||||
setArea 'sync'
|
||||
catch err
|
||||
c.error err.stack
|
||||
|
||||
(key, val) ->
|
||||
if typeof key is 'string'
|
||||
items[key] = val
|
||||
items.sync[key] = val
|
||||
else
|
||||
$.extend items, key
|
||||
set()
|
||||
|
||||
$.extend items.sync, key
|
||||
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 { %>
|
||||
|
||||
# http://wiki.greasespot.net/Main_Page
|
||||
@ -361,6 +381,8 @@ $.sync = do ->
|
||||
cb JSON.parse(newValue), key
|
||||
(key, cb) -> $.syncing[g.NAMESPACE + key] = cb
|
||||
|
||||
$.desync = (key) -> delete $.syncing[g.NAMESPACE + key]
|
||||
|
||||
$.delete = (keys) ->
|
||||
unless keys instanceof Array
|
||||
keys = [keys]
|
||||
@ -397,6 +419,9 @@ $.set = do ->
|
||||
for key, val of keys
|
||||
set key, val
|
||||
return
|
||||
$.clear = (cb) ->
|
||||
$.delete GM_listValues().map (key) -> key.replace g.NAMESPACE, ''
|
||||
cb?()
|
||||
<% } %>
|
||||
|
||||
$$ = (selector, root=d.body) ->
|
||||
|
||||
@ -2,7 +2,7 @@ class Board
|
||||
toString: -> @ID
|
||||
|
||||
constructor: (@ID) ->
|
||||
@threads = {}
|
||||
@posts = {}
|
||||
@threads = new SimpleDict
|
||||
@posts = new SimpleDict
|
||||
|
||||
g.boards[@] = @
|
||||
@ -1,20 +1,23 @@
|
||||
class Callbacks
|
||||
push: ({name, cb}) -> @[name] = cb
|
||||
constructor: (@type) ->
|
||||
@keys = []
|
||||
|
||||
clean: ->
|
||||
@rm name for name of @ when @hasOwnProperty name
|
||||
return
|
||||
push: ({name, cb}) ->
|
||||
@connect name if @[name]
|
||||
@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) ->
|
||||
for name of @ when @hasOwnProperty name
|
||||
for name in @keys
|
||||
try
|
||||
@[name].call node
|
||||
@[name].call node unless @[name].disconnected
|
||||
catch err
|
||||
errors = [] unless errors
|
||||
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
|
||||
|
||||
Main.handleErrors errors if errors
|
||||
|
||||
@ -5,4 +5,5 @@
|
||||
<%= grunt.file.read('src/General/lib/clone.class') %>
|
||||
<%= grunt.file.read('src/General/lib/databoard.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
|
||||
$.on d, '4chanXInitFinished', init
|
||||
|
||||
save: ->
|
||||
$.set @key, @data
|
||||
save: -> $.set @key, @data
|
||||
|
||||
delete: ({boardID, threadID, postID}) ->
|
||||
if postID
|
||||
@ -79,7 +78,7 @@ class DataBoard
|
||||
return
|
||||
board = @data.boards[boardID]
|
||||
threads = {}
|
||||
for page in JSON.parse e.target.response
|
||||
for page in e.target.response
|
||||
for thread in page.threads
|
||||
if thread.no of board
|
||||
threads[thread.no] = board[thread.no]
|
||||
@ -90,3 +89,8 @@ class DataBoard
|
||||
onSync: (data) =>
|
||||
@data = data or boards: {}
|
||||
@sync?()
|
||||
|
||||
disconnect: ->
|
||||
$.desync @key
|
||||
delete @sync
|
||||
delete @data
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
class Post
|
||||
@callbacks = new Callbacks()
|
||||
@callbacks = new Callbacks 'Post'
|
||||
toString: -> @ID
|
||||
|
||||
constructor: (root, @thread, @board, that={}) ->
|
||||
@ -48,18 +48,13 @@ class Post
|
||||
if date = $ '.dateTime', info
|
||||
@nodes.date = date
|
||||
@info.date = new Date date.dataset.utc * 1000
|
||||
if Conf['Quick Reply']
|
||||
@info.yours = QR.db.get
|
||||
boardID: @board
|
||||
threadID: @thread
|
||||
postID: @ID
|
||||
|
||||
@parseComment()
|
||||
@parseQuotes()
|
||||
@parseFile that
|
||||
|
||||
@clones = []
|
||||
g.posts[@fullID] = thread.posts[@] = board.posts[@] = @
|
||||
g.posts.push @fullID, thread.posts.push @, board.posts.push @, @
|
||||
@kill() if that.isArchived
|
||||
|
||||
parseComment: ->
|
||||
@ -185,7 +180,7 @@ class Post
|
||||
# Get quotelinks/backlinks to this post
|
||||
# and paint them (Dead).
|
||||
for quotelink in Get.allQuotelinksLinkingTo @ when not $.hasClass quotelink, 'deadlink'
|
||||
$.add quotelink, $.tn '\u00A0(Dead)'
|
||||
quotelink.textContent = quotelink.textContent + '\u00A0(Dead)'
|
||||
$.addClass quotelink, 'deadlink'
|
||||
return
|
||||
# XXX tmp fix for 4chan's racing condition
|
||||
@ -213,9 +208,9 @@ class Post
|
||||
|
||||
collect: ->
|
||||
@kill()
|
||||
delete g.posts[@fullID]
|
||||
delete @thread.posts[@]
|
||||
delete @board.posts[@]
|
||||
g.posts.rm @fullID
|
||||
@thread.posts.rm @
|
||||
@board.posts.rm @
|
||||
|
||||
addClone: (context) ->
|
||||
new Clone @, context
|
||||
|
||||
@ -1,19 +1,36 @@
|
||||
class RandomAccessList
|
||||
constructor: ->
|
||||
constructor: (items) ->
|
||||
@length = 0
|
||||
@push item for item in items if items
|
||||
|
||||
push: (item) ->
|
||||
{ID} = item
|
||||
push: (data) ->
|
||||
{ID} = data
|
||||
ID or= data.id
|
||||
return if @[ID]
|
||||
{last} = @
|
||||
@[ID] = item =
|
||||
prev: last
|
||||
next: null
|
||||
data: data
|
||||
ID: ID
|
||||
item.prev = last
|
||||
@[ID] = item
|
||||
@last = if last
|
||||
last.next = item
|
||||
else
|
||||
@first = item
|
||||
@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) ->
|
||||
return if item.prev is root
|
||||
|
||||
@ -23,8 +40,8 @@ class RandomAccessList
|
||||
root.next = item
|
||||
item.prev = root
|
||||
item.next = next
|
||||
next.prev = item
|
||||
|
||||
next.prev = item if next
|
||||
|
||||
prepend: (item) ->
|
||||
{first} = @
|
||||
return if item is first or not @[item.ID]
|
||||
@ -36,6 +53,11 @@ class RandomAccessList
|
||||
|
||||
shift: ->
|
||||
@rm @first.ID
|
||||
|
||||
order: ->
|
||||
order = [item = @first]
|
||||
order.push item while item = item.next
|
||||
order
|
||||
|
||||
rm: (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
|
||||
@callbacks = new Callbacks()
|
||||
@callbacks = new Callbacks 'Thread'
|
||||
toString: -> @ID
|
||||
|
||||
constructor: (@ID, @board) ->
|
||||
@fullID = "#{@board}.#{@ID}"
|
||||
@posts = {}
|
||||
@posts = new SimpleDict
|
||||
@isSticky = false
|
||||
@isClosed = false
|
||||
@postLimit = false
|
||||
@fileLimit = false
|
||||
|
||||
g.threads[@fullID] = board.threads[@] = @
|
||||
g.threads.push @fullID, board.threads.push @, @
|
||||
|
||||
setPage: (pageNum) ->
|
||||
icon = $ '.page-num', @OP.nodes.post
|
||||
@ -44,7 +44,6 @@ class Thread
|
||||
@timeOfDeath = Date.now()
|
||||
|
||||
collect: ->
|
||||
for postID, post in @posts
|
||||
post.collect()
|
||||
delete g.threads[@fullID]
|
||||
delete @board.threads[@]
|
||||
@posts.forEach (post) -> post.collect()
|
||||
g.threads.rm @fullID
|
||||
@board.threads.rm @
|
||||
|
||||
@ -14,6 +14,7 @@
|
||||
// @grant GM_getValue
|
||||
// @grant GM_setValue
|
||||
// @grant GM_deleteValue
|
||||
// @grant GM_listValues
|
||||
// @grant GM_openInTab
|
||||
// @grant GM_xmlhttpRequest
|
||||
// @run-at document-start
|
||||
|
||||
@ -201,7 +201,8 @@ Gallery =
|
||||
$.ajax "//api.4chan.org/#{post.board}/res/#{post.thread}.json", onload: ->
|
||||
return if @status isnt 200
|
||||
i = 0
|
||||
while postObj = JSON.parse(@response).posts[i++]
|
||||
{posts} = @response
|
||||
while postObj = posts[i++]
|
||||
break if postObj.no is post.ID
|
||||
unless postObj.no
|
||||
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
|
||||
e.preventDefault()
|
||||
ImageExpand.toggle Get.postFromNode @
|
||||
|
||||
toggleAll: ->
|
||||
$.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'
|
||||
ImageExpand.EAI.className = 'contract-all-shortcut fa fa-compress'
|
||||
ImageExpand.EAI.title = 'Contract All Images'
|
||||
@ -40,16 +50,12 @@ ImageExpand =
|
||||
ImageExpand.EAI.className = 'expand-all-shortcut fa fa-expand'
|
||||
ImageExpand.EAI.title = 'Expand All Images'
|
||||
func = ImageExpand.contract
|
||||
for ID, post of g.posts
|
||||
for post in [post].concat post.clones
|
||||
{file} = post
|
||||
continue 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)
|
||||
continue
|
||||
$.queueTask func, post
|
||||
return
|
||||
|
||||
g.posts.forEach (post) ->
|
||||
toggle post
|
||||
toggle post for post in post.clones
|
||||
return
|
||||
|
||||
setFitness: ->
|
||||
(if @checked then $.addClass else $.rmClass) doc, @name.toLowerCase().replace /\s+/g, '-'
|
||||
|
||||
@ -147,10 +153,19 @@ ImageExpand =
|
||||
return
|
||||
|
||||
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?
|
||||
$.ajax "//a.4cdn.org/#{post.board}/res/#{post.thread}.json", onload: ->
|
||||
return if @status isnt 200
|
||||
for postObj in JSON.parse(@response).posts
|
||||
for postObj in @response.posts
|
||||
break if postObj.no is post.ID
|
||||
if postObj.no isnt post.ID
|
||||
clearTimeout timeoutID
|
||||
@ -158,6 +173,7 @@ ImageExpand =
|
||||
else if postObj.filedeleted
|
||||
clearTimeout timeoutID
|
||||
post.kill true
|
||||
<% } %>
|
||||
|
||||
menu:
|
||||
init: ->
|
||||
|
||||
@ -38,10 +38,19 @@ ImageHover =
|
||||
return
|
||||
|
||||
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?
|
||||
$.ajax "//a.4cdn.org/#{post.board}/res/#{post.thread}.json", onload: ->
|
||||
return if @status isnt 200
|
||||
for postObj in JSON.parse(@response).posts
|
||||
for postObj in @response.posts
|
||||
break if postObj.no is post.ID
|
||||
if postObj.no isnt post.ID
|
||||
clearTimeout timeoutID
|
||||
@ -49,3 +58,4 @@ ImageHover =
|
||||
else if postObj.filedeleted
|
||||
clearTimeout timeoutID
|
||||
post.kill true
|
||||
<% } %>
|
||||
|
||||
@ -7,6 +7,10 @@ ImageLoader =
|
||||
name: 'Image Replace'
|
||||
cb: @node
|
||||
|
||||
Thread.callbacks.push
|
||||
name: 'Image Replace'
|
||||
cb: @thread
|
||||
|
||||
return unless Conf['Image Prefetching'] and g.VIEW is 'thread'
|
||||
|
||||
prefetch = $.el 'label',
|
||||
@ -19,6 +23,9 @@ ImageLoader =
|
||||
type: 'header'
|
||||
el: prefetch
|
||||
order: 104
|
||||
|
||||
thread: ->
|
||||
ImageLoader.thread = @
|
||||
|
||||
node: ->
|
||||
return if @isClone or @isHidden or @thread.isHidden or !@file?.isImage
|
||||
@ -38,5 +45,5 @@ ImageLoader =
|
||||
toggle: ->
|
||||
enabled = Conf['prefetch'] = @checked
|
||||
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
|
||||
@ -15,19 +15,17 @@ Sauce =
|
||||
name: 'Sauce'
|
||||
cb: @node
|
||||
createSauceLink: (link) ->
|
||||
link = link.replace /%(T?URL|MD5|board)/ig, (parameter) ->
|
||||
switch parameter
|
||||
|
||||
when '%TURL'
|
||||
"' + encodeURIComponent(post.file.thumbURL) + '"
|
||||
when '%URL'
|
||||
"' + encodeURIComponent(post.file.URL) + '"
|
||||
when '%MD5'
|
||||
"' + encodeURIComponent(post.file.MD5) + '"
|
||||
when '%board'
|
||||
"' + encodeURIComponent(post.board) + '"
|
||||
else
|
||||
parameter
|
||||
link = link.replace /%(T?URL|MD5|board|name)/g, (parameter) ->
|
||||
return (if type = {
|
||||
'%TURL': 'post.file.thumbURL'
|
||||
'%URL': 'post.file.URL'
|
||||
'%MD5': 'post.file.MD5'
|
||||
'%board': 'post.board'
|
||||
'%name': 'post.file.name'
|
||||
}[parameter]
|
||||
"' + encodeURIComponent(#{type}) + '"
|
||||
else
|
||||
parameter)
|
||||
text = if m = link.match(/;text:(.+)$/) then m[1] else link.match(/(\w+)\.\w+\//)[1]
|
||||
link = link.replace /;text:.+$/, ''
|
||||
Function 'post', 'a', """
|
||||
|
||||
@ -189,8 +189,10 @@ Linkify =
|
||||
embed.dataset.title = title[0]
|
||||
else
|
||||
try
|
||||
$.cache service.api(uid), ->
|
||||
title = Linkify.cb.title @, data
|
||||
$.cache service.api(uid),
|
||||
-> title = Linkify.cb.title @, data
|
||||
,
|
||||
responseType: 'json'
|
||||
catch err
|
||||
if link
|
||||
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
|
||||
switch response.status
|
||||
when 200, 304
|
||||
text = "#{service.text JSON.parse response.responseText}"
|
||||
text = "#{service.text response.response}"
|
||||
if Conf['Embedding']
|
||||
embed.dataset.title = text
|
||||
when 404
|
||||
@ -310,7 +312,7 @@ Linkify =
|
||||
$.cache "https://mediacru.sh/#{a.dataset.uid}.json", ->
|
||||
{status} = @
|
||||
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 file in files
|
||||
if file.type is type
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
PSAHiding =
|
||||
init: ->
|
||||
return if !Conf['Announcement Hiding']
|
||||
|
||||
$.addClass doc, 'hide-announcement'
|
||||
|
||||
$.on d, '4chanXInitFinished', @setup
|
||||
|
||||
setup: ->
|
||||
$.off d, '4chanXInitFinished', PSAHiding.setup
|
||||
|
||||
|
||||
@ -1,21 +1,26 @@
|
||||
ExpandThread =
|
||||
statuses: {}
|
||||
init: ->
|
||||
return if g.VIEW isnt 'index' or !Conf['Thread Expansion']
|
||||
@statuses = {}
|
||||
return if g.VIEW is 'thread' or !Conf['Thread Expansion']
|
||||
$.on d, 'IndexRefresh', @onIndexRefresh
|
||||
|
||||
setButton: (thread) ->
|
||||
return unless a = $.x 'following-sibling::a[contains(@class,"summary")][1]', thread.OP.nodes.root
|
||||
a.textContent = ExpandThread.text '+', a.textContent.match(/\d+/g)...
|
||||
$.on a, 'click', ExpandThread.cbToggle
|
||||
|
||||
onIndexRefresh: ->
|
||||
|
||||
disconnect: (refresh) ->
|
||||
return if g.VIEW is 'thread' or !Conf['Thread Expansion']
|
||||
for threadID, status of ExpandThread.statuses
|
||||
status.req?.abort()
|
||||
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
|
||||
return
|
||||
|
||||
text: (status, posts, files) ->
|
||||
"#{status} #{posts} post#{if posts > 1 then 's' else ''}" +
|
||||
@ -72,13 +77,13 @@ ExpandThread =
|
||||
a.textContent = "Error #{req.statusText} (#{req.status})"
|
||||
return
|
||||
|
||||
data = JSON.parse(req.response).posts
|
||||
Build.spoilerRange[thread.board] = data.shift().custom_spoiler
|
||||
Build.spoilerRange[thread.board] = req.response.posts[0].custom_spoiler
|
||||
|
||||
posts = []
|
||||
postsRoot = []
|
||||
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]
|
||||
filesCount++ if 'file' of post
|
||||
postsRoot.push post.nodes.root
|
||||
|
||||
@ -1,64 +1,82 @@
|
||||
Favicon =
|
||||
init: ->
|
||||
$.ready ->
|
||||
Favicon.el = $ 'link[rel="shortcut icon"]', d.head
|
||||
Favicon.el.type = 'image/x-icon'
|
||||
{href} = Favicon.el
|
||||
Favicon.SFW = /ws\.ico$/.test href
|
||||
Favicon.default = href
|
||||
Favicon.switch()
|
||||
$.asap (-> Favicon.el = $ 'link[rel="shortcut icon"]', d.head), Favicon.initAsap
|
||||
|
||||
initAsap: ->
|
||||
Favicon.el.type = 'image/x-icon'
|
||||
{href} = Favicon.el
|
||||
Favicon.SFW = /ws\.ico$/.test href
|
||||
Favicon.default = href
|
||||
Favicon.switch()
|
||||
|
||||
switch: ->
|
||||
if Favicon.SFW
|
||||
Favicon.default = 'https://s.4cdn.org/image/favicon-ws.ico'
|
||||
items = {
|
||||
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
|
||||
Favicon.default = 'https://s.4cdn.org/image/favicon.ico'
|
||||
switch Conf['favicon']
|
||||
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
|
||||
@unread = @unreadNSFW
|
||||
@unreadY = @unreadNSFWY
|
||||
|
||||
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"}) %>'
|
||||
|
||||
@ -5,5 +5,7 @@ ThreadExcerpt =
|
||||
Thread.callbacks.push
|
||||
name: 'Thread Excerpt'
|
||||
cb: @node
|
||||
node: ->
|
||||
d.title = Get.threadExcerpt @
|
||||
node: -> 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 "")
|
||||
$.ready ->
|
||||
Header.addShortcut sc
|
||||
else
|
||||
else
|
||||
@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>"
|
||||
$.ready =>
|
||||
$.add d.body, sc
|
||||
$.ready =>
|
||||
$.add d.body, sc
|
||||
|
||||
@postCountEl = $ '#post-count', sc
|
||||
@fileCountEl = $ '#file-count', sc
|
||||
@ -26,7 +26,7 @@ ThreadStats =
|
||||
node: ->
|
||||
postCount = 0
|
||||
fileCount = 0
|
||||
for ID, post of @posts
|
||||
@posts.forEach (post) ->
|
||||
postCount++
|
||||
fileCount++ if post.file
|
||||
ThreadStats.thread = @
|
||||
@ -34,6 +34,25 @@ ThreadStats =
|
||||
ThreadStats.update postCount, fileCount
|
||||
$.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) ->
|
||||
return if e.detail[404]
|
||||
{postCount, fileCount} = e.detail
|
||||
@ -48,20 +67,18 @@ ThreadStats =
|
||||
|
||||
fetchPage: ->
|
||||
return if !Conf["Page Count in Stats"]
|
||||
if ThreadStats.thread.isDead
|
||||
if ThreadStats.thread.isDead
|
||||
ThreadStats.pageCountEl.textContent = 'Dead'
|
||||
$.addClass ThreadStats.pageCountEl, 'warning'
|
||||
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,
|
||||
whenModified: true
|
||||
|
||||
onThreadsLoad: ->
|
||||
return unless Conf["Page Count in Stats"] and @status is 200
|
||||
pages = JSON.parse @response
|
||||
for page in pages
|
||||
for thread in page.threads
|
||||
if thread.no is ThreadStats.thread.ID
|
||||
ThreadStats.pageCountEl.textContent = page.page
|
||||
(if page.page is pages.length - 1 then $.addClass else $.rmClass) ThreadStats.pageCountEl, 'warning'
|
||||
return
|
||||
for page in @response
|
||||
for thread in page.threads when thread.no is ThreadStats.thread.ID
|
||||
ThreadStats.pageCountEl.textContent = page.page
|
||||
(if page.page is @response.length - 1 then $.addClass else $.rmClass) ThreadStats.pageCountEl, 'warning'
|
||||
return
|
||||
@ -22,8 +22,8 @@ ThreadUpdater =
|
||||
@status = $ '#update-status', sc
|
||||
@isUpdating = Conf['Auto Update']
|
||||
|
||||
$.on @timer, 'click', ThreadUpdater.update
|
||||
$.on @status, 'click', ThreadUpdater.update
|
||||
$.on @timer, 'click', @update
|
||||
$.on @status, 'click', @update
|
||||
|
||||
subEntries = []
|
||||
for name, conf of Config.updater.checkbox
|
||||
@ -34,20 +34,20 @@ ThreadUpdater =
|
||||
input = el.firstElementChild
|
||||
$.on input, 'change', $.cb.checked
|
||||
if input.name is 'Scroll BG'
|
||||
$.on input, 'change', ThreadUpdater.cb.scrollBG
|
||||
ThreadUpdater.cb.scrollBG()
|
||||
$.on input, 'change', @cb.scrollBG
|
||||
@cb.scrollBG()
|
||||
else if input.name is 'Auto Update'
|
||||
$.on input, 'change', ThreadUpdater.cb.update
|
||||
$.on input, 'change', @cb.update
|
||||
subEntries.push el: el
|
||||
|
||||
settings = $.el 'span',
|
||||
@settings = $.el 'span',
|
||||
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'
|
||||
el: $.el 'span',
|
||||
textContent: 'Updater'
|
||||
@ -57,6 +57,40 @@ ThreadUpdater =
|
||||
Thread.callbacks.push
|
||||
name: 'Thread Updater'
|
||||
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: ->
|
||||
ThreadUpdater.thread = @
|
||||
@ -124,7 +158,7 @@ ThreadUpdater =
|
||||
switch req.status
|
||||
when 200
|
||||
g.DEAD = false
|
||||
ThreadUpdater.parse JSON.parse(req.response).posts
|
||||
ThreadUpdater.parse req.response.posts
|
||||
ThreadUpdater.setInterval()
|
||||
when 404
|
||||
g.DEAD = true
|
||||
@ -256,11 +290,11 @@ ThreadUpdater =
|
||||
deletedFiles = []
|
||||
|
||||
# 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
|
||||
# giving us false-positive dead posts.
|
||||
# continue if post.isDead
|
||||
ID = +ID
|
||||
ID = +post.ID
|
||||
|
||||
unless ID in index
|
||||
post.kill()
|
||||
@ -271,6 +305,7 @@ ThreadUpdater =
|
||||
post.kill true
|
||||
deletedFiles.push post
|
||||
|
||||
# Fetching your own posts after posting
|
||||
if ThreadUpdater.postID and ThreadUpdater.postID is ID
|
||||
ThreadUpdater.foundPost = true
|
||||
|
||||
@ -291,8 +326,7 @@ ThreadUpdater =
|
||||
scroll = Conf['Auto Scroll'] and ThreadUpdater.scrollBG() and
|
||||
ThreadUpdater.root.getBoundingClientRect().bottom - doc.clientHeight < 25
|
||||
|
||||
for key, post of posts
|
||||
continue unless posts.hasOwnProperty key
|
||||
for post in posts
|
||||
root = post.nodes.root
|
||||
if post.cb
|
||||
unless post.cb()
|
||||
|
||||
@ -180,7 +180,9 @@ ThreadWatcher =
|
||||
$.rmAll list
|
||||
$.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
|
||||
watched = ThreadWatcher.db.get {boardID: thread.board.ID, threadID}
|
||||
helper = if watched then ['addClass', 'Unwatch'] else ['rmClass', 'Watch']
|
||||
|
||||
@ -12,6 +12,21 @@ Unread =
|
||||
name: 'Unread'
|
||||
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: ->
|
||||
Unread.thread = @
|
||||
Unread.title = d.title
|
||||
@ -26,9 +41,10 @@ Unread =
|
||||
|
||||
ready: ->
|
||||
$.off d, '4chanXInitFinished', Unread.ready
|
||||
posts = []
|
||||
posts.push post for ID, post of Unread.thread.posts when post.isReply
|
||||
Unread.addPosts posts
|
||||
unless Conf['Quote Threading']
|
||||
posts = []
|
||||
Unread.thread.posts.forEach (post) -> posts.push post if post.isReply
|
||||
Unread.addPosts posts
|
||||
QuoteThreading.force() if Conf['Quote Threading']
|
||||
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
|
||||
if post = Unread.posts.first
|
||||
# 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
|
||||
return unless root
|
||||
down = true
|
||||
else
|
||||
# Scroll to the last read post.
|
||||
posts = Object.keys Unread.thread.posts
|
||||
{root} = Unread.thread.posts[posts[posts.length - 1]].nodes
|
||||
{posts} = Unread.thread
|
||||
{keys} = posts
|
||||
{root} = posts[keys[keys.length - 1]].nodes
|
||||
|
||||
# Scroll to the target unless we scrolled past it.
|
||||
Header.scrollTo root, down if Header.getBottomOf(root) < 0
|
||||
@ -75,16 +92,15 @@ Unread =
|
||||
threadID: post.thread.ID
|
||||
postID: ID
|
||||
}
|
||||
Unread.posts.push post unless post.prev or post.next
|
||||
Unread.posts.push post
|
||||
Unread.addPostQuotingYou post
|
||||
if Conf['Unread Line']
|
||||
# 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.update()
|
||||
|
||||
addPostQuotingYou: (post) ->
|
||||
return unless QR.db
|
||||
for quotelink in post.nodes.quotelinks when QR.db.get Get.postDataFromLink quotelink
|
||||
Unread.postsQuotingYou.push post
|
||||
Unread.openNotification post
|
||||
@ -110,16 +126,20 @@ Unread =
|
||||
onUpdate: (e) ->
|
||||
if e.detail[404]
|
||||
Unread.update()
|
||||
else
|
||||
else if !Conf['Quote Threading']
|
||||
Unread.addPosts e.detail.newPosts
|
||||
else
|
||||
Unread.read()
|
||||
Unread.update()
|
||||
|
||||
readSinglePost: (post) ->
|
||||
{ID} = post
|
||||
return unless Unread.posts[ID]
|
||||
if post is Unread.posts.first
|
||||
{posts} = Unread
|
||||
return unless posts[ID]
|
||||
if post is posts.first
|
||||
Unread.lastReadPost = ID
|
||||
Unread.saveLastReadPost()
|
||||
Unread.posts.rm ID
|
||||
posts.rm ID
|
||||
if (i = Unread.postsQuotingYou.indexOf post) isnt -1
|
||||
Unread.postsQuotingYou.splice i, 1
|
||||
Unread.update()
|
||||
@ -135,12 +155,16 @@ Unread =
|
||||
|
||||
{posts} = Unread
|
||||
while post = posts.first
|
||||
break unless Header.getBottomOf(post.nodes.root) > -1 # post is not completely read
|
||||
{ID} = post
|
||||
break unless Header.getBottomOf(post.data.nodes.root) > -1 # post is not completely read
|
||||
{ID, data} = post
|
||||
posts.rm ID
|
||||
|
||||
if Conf['Mark Quotes of You'] and post.info.yours
|
||||
QuoteYou.lastRead = post.nodes.root
|
||||
if Conf['Mark Quotes of You'] and QR.db.get {
|
||||
boardID: data.board.ID
|
||||
threadID: data.thread.ID
|
||||
postID: ID
|
||||
}
|
||||
QuoteYou.lastRead = data.nodes.root
|
||||
|
||||
return unless ID
|
||||
|
||||
@ -159,8 +183,8 @@ Unread =
|
||||
setLine: (force) ->
|
||||
return unless d.hidden or force is true
|
||||
return $.rm Unread.hr unless post = Unread.posts.first
|
||||
if $.x 'preceding-sibling::div[contains(@class,"replyContainer")]', post.nodes.root # not the first reply
|
||||
$.before post.nodes.root, Unread.hr
|
||||
if $.x 'preceding-sibling::div[contains(@class,"replyContainer")]', post.data.nodes.root # not the first reply
|
||||
$.before post.data.nodes.root, Unread.hr
|
||||
|
||||
update: <% if (type === 'crx') { %>(dontrepeat) <% } %>->
|
||||
count = Unread.posts.length
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
QR =
|
||||
mimeTypes: ['image/jpeg', 'image/png', 'image/gif', 'application/pdf', 'application/x-shockwave-flash', '']
|
||||
|
||||
init: ->
|
||||
return if !Conf['Quick Reply']
|
||||
|
||||
@ -46,6 +47,8 @@ QR =
|
||||
link = $.el 'h1',
|
||||
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"
|
||||
|
||||
QR.link = link.firstElementChild
|
||||
$.on link.firstChild, 'click', ->
|
||||
|
||||
$.event 'CloseMenu'
|
||||
@ -67,15 +70,20 @@ QR =
|
||||
$.on d, 'dragover', QR.dragOver
|
||||
$.on d, 'drop', QR.dropFile
|
||||
$.on d, 'dragstart dragend', QR.drag
|
||||
switch g.VIEW
|
||||
when 'index'
|
||||
{
|
||||
catalog: ->
|
||||
QR.open() if Conf["Persistent QR"]
|
||||
index: ->
|
||||
$.on d, 'IndexRefresh', QR.generatePostableThreadsList
|
||||
when 'thread'
|
||||
$.on d, 'ThreadUpdate', ->
|
||||
if g.DEAD
|
||||
QR.abort()
|
||||
else
|
||||
QR.status()
|
||||
thread: ->
|
||||
$.on d, 'ThreadUpdate', QR.statusCheck
|
||||
}[g.VIEW]()
|
||||
|
||||
statusCheck: ->
|
||||
if g.DEAD
|
||||
QR.abort()
|
||||
else
|
||||
QR.status()
|
||||
|
||||
node: ->
|
||||
$.on $('a[title="Quote this post"]', @nodes.info), 'click', QR.quote
|
||||
@ -385,7 +393,7 @@ QR =
|
||||
return unless QR.nodes
|
||||
list = QR.nodes.thread
|
||||
options = [list.firstChild]
|
||||
for thread of g.BOARD.threads
|
||||
for thread in g.BOARD.threads.keys
|
||||
options.push $.el 'option',
|
||||
value: thread
|
||||
textContent: "Thread No.#{thread}"
|
||||
@ -430,8 +438,7 @@ QR =
|
||||
status: '[type=submit]'
|
||||
fileInput: '[type=file]'
|
||||
}
|
||||
|
||||
# Allow only this board's supported files.
|
||||
|
||||
nodes.fileInput.max = $('input[name=MAX_FILE_SIZE]').value
|
||||
|
||||
QR.spoiler = !!$ 'input[name=spoiler]'
|
||||
@ -454,11 +461,8 @@ QR =
|
||||
"""
|
||||
nodes.flashTag.dataset.default = '4'
|
||||
$.add nodes.form, nodes.flashTag
|
||||
if flagSelector = $ '.flagSelector'
|
||||
nodes.flag = flagSelector.cloneNode true
|
||||
nodes.flag.dataset.name = 'flag'
|
||||
nodes.flag.dataset.default = '0'
|
||||
$.add nodes.form, nodes.flag
|
||||
|
||||
QR.flagsInput()
|
||||
|
||||
$.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.
|
||||
$.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: []
|
||||
|
||||
submit: (e) ->
|
||||
@ -589,13 +641,15 @@ QR =
|
||||
responseType: 'document'
|
||||
withCredentials: true
|
||||
onload: QR.response
|
||||
onerror: ->
|
||||
# Connection error, or
|
||||
# www.4chan.org/banned
|
||||
onerror: (err, url, line) ->
|
||||
# Connection error, or www.4chan.org/banned
|
||||
delete QR.req
|
||||
post.unlock()
|
||||
QR.cooldown.auto = false
|
||||
QR.status()
|
||||
console.log err
|
||||
console.log url
|
||||
console.log line
|
||||
QR.error $.el 'span',
|
||||
innerHTML: """
|
||||
4chan X encountered an error while posting.
|
||||
@ -615,7 +669,7 @@ QR =
|
||||
QR.req.progress = "#{Math.round e.loaded / e.total * 100}%"
|
||||
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.
|
||||
# Provide some feedback that we're starting to submit.
|
||||
QR.req.uploadStartTime = Date.now()
|
||||
@ -667,7 +721,7 @@ QR =
|
||||
# Too many frequent mistyped captchas will auto-ban you!
|
||||
# On connection error, the post most likely didn't go through.
|
||||
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.captcha.captchas.length
|
||||
else
|
||||
@ -700,8 +754,6 @@ QR =
|
||||
|
||||
ThreadUpdater.postID = postID
|
||||
|
||||
|
||||
|
||||
# Post/upload confirmed as successful.
|
||||
$.event 'QRPostSuccessful', {
|
||||
board: g.BOARD
|
||||
|
||||
@ -10,12 +10,12 @@ QuoteBacklink =
|
||||
# Second callback adds relevant containers into posts.
|
||||
# This is is so that fetched posts can get their backlinks,
|
||||
# and that as much backlinks are appended in the background as possible.
|
||||
containers: {}
|
||||
init: ->
|
||||
return if g.VIEW is 'catalog' or !Conf['Quote Backlinks']
|
||||
|
||||
format = Conf['backlink'].replace /%id/g, "' + id + '"
|
||||
@funk = Function 'id', "return '#{format}'"
|
||||
@containers = {}
|
||||
Post.callbacks.push
|
||||
name: 'Quote Backlinking Part 1'
|
||||
cb: @firstNode
|
||||
@ -36,13 +36,13 @@ QuoteBacklink =
|
||||
for clone in post.clones
|
||||
containers.push clone.nodes.backlinkContainer
|
||||
for container in containers
|
||||
frag = [$.tn(' '), link = a.cloneNode true]
|
||||
nodes = [$.tn(' '), link = a.cloneNode true]
|
||||
if Conf['Quote Previewing']
|
||||
$.on link, 'mouseover', QuotePreview.mouseover
|
||||
if Conf['Quote Inlining']
|
||||
$.on link, 'click', QuoteInline.toggle
|
||||
frag.push.apply frag, QuoteInline.qiQuote link, $.hasClass link, 'filtered' if Conf['Quote Hash Navigation']
|
||||
$.add container, frag
|
||||
nodes.push QuoteInline.qiQuote link, $.hasClass link, 'filtered' if Conf['Quote Hash Navigation']
|
||||
$.add container, nodes
|
||||
return
|
||||
secondNode: ->
|
||||
if @isClone and (@origin.isReply or Conf['OP Backlinks'])
|
||||
|
||||
@ -2,18 +2,14 @@ QuoteInline =
|
||||
init: ->
|
||||
return if g.VIEW is 'catalog' or !Conf['Quote Inlining']
|
||||
|
||||
if Conf['Quote Hash Navigation']
|
||||
@node = ->
|
||||
for link in @nodes.quotelinks.concat [@nodes.backlinks...]
|
||||
$.after link, QuoteInline.qiQuote link, $.hasClass link, 'filtered' unless @isClone
|
||||
$.on link, 'click', QuoteInline.toggle
|
||||
return
|
||||
@process = if Conf['Quote Hash Navigation']
|
||||
(link, clone) ->
|
||||
$.after link, QuoteInline.qiQuote link, $.hasClass link, 'filtered' unless clone
|
||||
$.on link, 'click', QuoteInline.toggle
|
||||
|
||||
else
|
||||
@node = ->
|
||||
for link in @nodes.quotelinks.concat [@nodes.backlinks...]
|
||||
$.on link, 'click', QuoteInline.toggle
|
||||
return
|
||||
(link) ->
|
||||
$.on link, 'click', QuoteInline.toggle
|
||||
|
||||
if Conf['Comment Expansion']
|
||||
ExpandComment.callbacks.push @node
|
||||
@ -22,14 +18,18 @@ QuoteInline =
|
||||
name: 'Quote Inlining'
|
||||
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) ->
|
||||
[
|
||||
$.tn(' ')
|
||||
$.el 'a',
|
||||
className: if hidden then 'hashlink filtered' else 'hashlink'
|
||||
textContent: '#'
|
||||
href: link.href
|
||||
]
|
||||
$.el 'a',
|
||||
className: "hashlink#{if hidden then ' filtered' else ''}"
|
||||
textContent: '#'
|
||||
href: link.href
|
||||
|
||||
toggle: (e) ->
|
||||
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
|
||||
$.on input, 'change', @toggle
|
||||
|
||||
$.event 'AddMenuEntry',
|
||||
$.event 'AddMenuEntry', @entry =
|
||||
type: 'header'
|
||||
el: @controls
|
||||
order: 98
|
||||
|
||||
$.on d, '4chanXInitFinished', @setup unless Conf['Unread Count']
|
||||
$.on d, '4chanXInitFinished', @ready unless Conf['Unread Count']
|
||||
|
||||
Post.callbacks.push
|
||||
name: 'Quote Threading'
|
||||
cb: @node
|
||||
|
||||
setup: ->
|
||||
$.off d, '4chanXInitFinished', QuoteThreading.setup
|
||||
disconnect: ->
|
||||
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()
|
||||
|
||||
force: ->
|
||||
post.cb true for ID, post of g.posts when post.cb
|
||||
return
|
||||
g.posts.forEach (post) ->
|
||||
post.cb true if post.cb
|
||||
|
||||
if Conf['Unread Count'] and Unread.thread.OP.nodes.root.parentElement.parentElement
|
||||
Unread.read()
|
||||
Unread.update()
|
||||
|
||||
node: ->
|
||||
{posts} = g
|
||||
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 = []
|
||||
len = g.BOARD.ID.length + 1
|
||||
@ -77,11 +94,11 @@ QuoteThreading =
|
||||
|
||||
return true unless Conf['Unread Count']
|
||||
|
||||
if posts[post.ID]
|
||||
posts.after post, @
|
||||
if post = posts[post.ID]
|
||||
posts.after post, posts[@ID]
|
||||
|
||||
else
|
||||
posts.prepend @
|
||||
posts.prepend posts[@ID]
|
||||
|
||||
return true
|
||||
|
||||
@ -93,8 +110,10 @@ QuoteThreading =
|
||||
thread = $('.thread')
|
||||
posts = []
|
||||
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
|
||||
|
||||
nodes.push post.nodes.root for post in posts
|
||||
@ -103,7 +122,7 @@ QuoteThreading =
|
||||
containers = $$ '.threadContainer', thread
|
||||
$.rm container for container in containers
|
||||
$.rmClass post, 'threadOP' for post in $$ '.threadOP'
|
||||
|
||||
|
||||
return
|
||||
|
||||
kb: ->
|
||||
|
||||
@ -20,7 +20,7 @@ QuoteYou =
|
||||
node: ->
|
||||
return if @isClone
|
||||
|
||||
if @info.yours
|
||||
if QR.db.get {boardID: @board.ID, threadID: @thread.ID, postID: @ID}
|
||||
$.addClass @nodes.root, 'yourPost'
|
||||
|
||||
# Stop there if there's no quotes in that post.
|
||||
|
||||
@ -8,6 +8,7 @@ Quotify =
|
||||
Post.callbacks.push
|
||||
name: 'Resurrect Quotes'
|
||||
cb: @node
|
||||
|
||||
node: ->
|
||||
for deadlink in $$ '.deadlink', @nodes.comment
|
||||
if @isClone
|
||||
|
||||