Merge pull request #1316 from MayhemYDG/index
[WIP] Index navigation improvements
This commit is contained in:
commit
bb83fac363
12
CHANGELOG.md
12
CHANGELOG.md
@ -1,3 +1,15 @@
|
||||
Index navigation improvements:
|
||||
- You can now refresh the index page you are on with the refresh shortcut in the header bar or the same keybind for refreshing threads.
|
||||
- You can now switch between paged and all-threads index modes via the "Index Navigation" header sub-menu:<br>
|
||||

|
||||
- Threads in the index can now be sorted by:
|
||||
- Bump order
|
||||
- Last reply
|
||||
- Creation date
|
||||
- Reply count
|
||||
- File count
|
||||
- Navigating across index pages is now instantaneous.
|
||||
|
||||
Added a keybind to open the catalog search field on index pages.
|
||||
|
||||
### 3.11.5 - *2013-10-03*
|
||||
|
||||
@ -1,11 +1,17 @@
|
||||
module.exports = (grunt) ->
|
||||
|
||||
importHTML = (filename) ->
|
||||
"\"\"\"#{grunt.file.read("html/#{filename}.html").replace(/^\s+|\s+$</gm, '').replace(/\n/g, '')}\"\"\""
|
||||
|
||||
# Project configuration.
|
||||
grunt.initConfig
|
||||
pkg: grunt.file.readJSON 'package.json'
|
||||
concat:
|
||||
options: process: Object.create(null, data:
|
||||
get: -> grunt.config 'pkg'
|
||||
get: ->
|
||||
pkg = grunt.config 'pkg'
|
||||
pkg.importHTML = importHTML
|
||||
pkg
|
||||
enumerable: true
|
||||
)
|
||||
coffee:
|
||||
@ -17,6 +23,7 @@ module.exports = (grunt) ->
|
||||
'src/General/Header.coffee'
|
||||
'src/General/Notice.coffee'
|
||||
'src/General/Settings.coffee'
|
||||
'src/General/Index.coffee'
|
||||
'src/General/Get.coffee'
|
||||
'src/General/Build.coffee'
|
||||
# Features -->
|
||||
@ -78,9 +85,10 @@ module.exports = (grunt) ->
|
||||
stdout: true
|
||||
stderr: true
|
||||
failOnError: true
|
||||
checkout:
|
||||
command: 'git checkout <%= pkg.meta.mainBranch %>'
|
||||
commit:
|
||||
command: """
|
||||
git checkout <%= pkg.meta.mainBranch %>
|
||||
git commit -am "Release <%= pkg.meta.name %> v<%= pkg.version %>."
|
||||
git tag -a <%= pkg.version %> -m "<%= pkg.meta.name %> v<%= pkg.version %>."
|
||||
git tag -af stable-v3 -m "<%= pkg.meta.name %> v<%= pkg.version %>."
|
||||
@ -144,9 +152,9 @@ module.exports = (grunt) ->
|
||||
]
|
||||
|
||||
grunt.registerTask 'release', ['shell:commit', 'shell:push', 'build-crx', 'compress:crx']
|
||||
grunt.registerTask 'patch', ['bump', 'updcl:3', 'release']
|
||||
grunt.registerTask 'minor', ['bump:minor', 'updcl:2', 'release']
|
||||
grunt.registerTask 'major', ['bump:major', 'updcl:1', 'release']
|
||||
grunt.registerTask 'patch', ['shell:checkout', 'bump', 'updcl:3', 'release']
|
||||
grunt.registerTask 'minor', ['shell:checkout', 'bump:minor', 'updcl:2', 'release']
|
||||
grunt.registerTask 'major', ['shell:checkout', 'bump:major', 'updcl:1', 'release']
|
||||
|
||||
grunt.registerTask 'updcl', 'Update the changelog', (headerLevel) ->
|
||||
headerPrefix = new Array(+headerLevel + 1).join '#'
|
||||
|
||||
@ -362,6 +362,15 @@ a[href="javascript:;"] {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Index */
|
||||
:root.index-loading .board,
|
||||
:root.index-loading .pagelist {
|
||||
display: none;
|
||||
}
|
||||
.summary {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Announcement Hiding */
|
||||
:root.hide-announcement #globalMessage,
|
||||
:root.hide-announcement-enabled #toggleMsgBtn {
|
||||
|
||||
11
html/General/Index-pagelist.html
Normal file
11
html/General/Index-pagelist.html
Normal file
@ -0,0 +1,11 @@
|
||||
<div class="prev">
|
||||
<a>
|
||||
<button disabled>Previous</button>
|
||||
</a>
|
||||
</div>
|
||||
<div class="pages"></div>
|
||||
<div class="next">
|
||||
<a>
|
||||
<button disabled>Next</button>
|
||||
</a>
|
||||
</div>
|
||||
@ -23,7 +23,7 @@
|
||||
For example: <code>highlight;</code> or <code>highlight:wallpaper;</code>.
|
||||
</li>
|
||||
<li>
|
||||
Highlighted OPs will have their threads put on top of board pages by default.<br>
|
||||
Highlighted OPs will have their threads put on top of the board index by default.<br>
|
||||
For example: <code>top:yes;</code> or <code>top:no;</code>.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@ -13,5 +13,5 @@
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input value="Update" type="button" name="Update">
|
||||
<input value="Refresh thread" type="button" name="Update">
|
||||
</div>
|
||||
|
||||
BIN
img/changelog/3.12.0/0.png
Normal file
BIN
img/changelog/3.12.0/0.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@ -45,7 +45,7 @@ $.ajax = do ->
|
||||
type or= form and 'post' or 'get'
|
||||
r.open type, url, !sync
|
||||
if whenModified
|
||||
r.setRequestHeader 'If-Modified-Since', lastModified[url] or '0'
|
||||
r.setRequestHeader 'If-Modified-Since', lastModified[url] if url of lastModified
|
||||
$.on r, 'load', -> lastModified[url] = r.getResponseHeader 'Last-Modified'
|
||||
$.extend r, options
|
||||
$.extend r.upload, upCallbacks
|
||||
@ -104,7 +104,7 @@ $.rm = do ->
|
||||
(el) -> el.parentNode?.removeChild el
|
||||
$.rmAll = (root) ->
|
||||
# jsperf.com/emptify-element
|
||||
while node = root.firstChild
|
||||
for node in [root.childNodes...]
|
||||
# HTMLSelectElement.remove !== Element.remove
|
||||
root.removeChild node
|
||||
return
|
||||
|
||||
@ -110,13 +110,8 @@ Filter =
|
||||
|
||||
# Highlight
|
||||
$.addClass @nodes.root, result.class
|
||||
if !@isReply and result.top and g.VIEW is 'index'
|
||||
# Put the highlighted OPs' thread on top of the board page...
|
||||
thisThread = @nodes.root.parentNode
|
||||
# ...before the first non highlighted thread.
|
||||
if firstThread = $ 'div[class="postContainer opContainer"]'
|
||||
unless firstThread is @nodes.root
|
||||
$.before firstThread.parentNode, [thisThread, thisThread.nextElementSibling]
|
||||
if !@isReply and result.top
|
||||
@thread.isOnTop = true
|
||||
|
||||
name: (post) ->
|
||||
if 'name' of post.info
|
||||
|
||||
@ -4,6 +4,7 @@ ThreadHiding =
|
||||
|
||||
@db = new DataBoard 'hiddenThreads'
|
||||
@syncCatalog()
|
||||
$.on d, 'IndexRefresh', @onrefresh
|
||||
Thread.callbacks.push
|
||||
name: 'Thread Hiding'
|
||||
cb: @node
|
||||
@ -14,6 +15,15 @@ ThreadHiding =
|
||||
return unless Conf['Thread Hiding']
|
||||
$.prepend @OP.nodes.root, ThreadHiding.makeButton @, 'hide'
|
||||
|
||||
onrefresh: ->
|
||||
for threadID, thread of g.BOARD.threads when thread.isHidden
|
||||
root = thread.OP.nodes.root.parentNode
|
||||
if thread.stub
|
||||
$.prepend root, thread.stub
|
||||
else
|
||||
threadRoot.nextElementSibling.hidden = true
|
||||
return
|
||||
|
||||
syncCatalog: ->
|
||||
# Sync hidden threads from the catalog into the index.
|
||||
hiddenThreads = ThreadHiding.db.get
|
||||
|
||||
@ -60,6 +60,10 @@ Build =
|
||||
isOP = postID is threadID
|
||||
|
||||
staticPath = '//static.4chan.org/image/'
|
||||
gifIcon = if window.devicePixelRatio >= 2
|
||||
'@2x.gif'
|
||||
else
|
||||
'.gif'
|
||||
|
||||
if email
|
||||
emailStart = '<a href="mailto:' + email + '" class="useremail">'
|
||||
@ -82,21 +86,21 @@ Build =
|
||||
capcodeClass = " capcodeAdmin"
|
||||
capcodeStart = " <strong class='capcode hand id_admin'" +
|
||||
"title='Highlight posts by the Administrator'>## Admin</strong>"
|
||||
capcode = " <img src='#{staticPath}adminicon.gif' " +
|
||||
capcode = " <img src='#{staticPath}adminicon#{gifIcon}' " +
|
||||
"alt='This user is the 4chan Administrator.' " +
|
||||
"title='This user is the 4chan Administrator.' class=identityIcon>"
|
||||
when 'mod'
|
||||
capcodeClass = " capcodeMod"
|
||||
capcodeStart = " <strong class='capcode hand id_mod' " +
|
||||
"title='Highlight posts by Moderators'>## Mod</strong>"
|
||||
capcode = " <img src='#{staticPath}modicon.gif' " +
|
||||
capcode = " <img src='#{staticPath}modicon#{gifIcon}' " +
|
||||
"alt='This user is a 4chan Moderator.' " +
|
||||
"title='This user is a 4chan Moderator.' class=identityIcon>"
|
||||
when 'developer'
|
||||
capcodeClass = " capcodeDeveloper"
|
||||
capcodeStart = " <strong class='capcode hand id_developer' " +
|
||||
"title='Highlight posts by Developers'>## Developer</strong>"
|
||||
capcode = " <img src='#{staticPath}developericon.gif' " +
|
||||
capcode = " <img src='#{staticPath}developericon#{gifIcon}' " +
|
||||
"alt='This user is a 4chan Developer.' " +
|
||||
"title='This user is a 4chan Developer.' class=identityIcon>"
|
||||
else
|
||||
@ -114,11 +118,11 @@ Build =
|
||||
if file?.isDeleted
|
||||
fileHTML = if isOP
|
||||
"<div class=file id=f#{postID}><div class=fileInfo></div><span class=fileThumb>" +
|
||||
"<img src='#{staticPath}filedeleted.gif' alt='File deleted.' class=fileDeletedRes>" +
|
||||
"<img src='#{staticPath}filedeleted#{gifIcon}' alt='File deleted.' class=fileDeletedRes>" +
|
||||
"</span></div>"
|
||||
else
|
||||
"<div class=file id=f#{postID}><span class=fileThumb>" +
|
||||
"<img src='#{staticPath}filedeleted-res.gif' alt='File deleted.' class=fileDeletedRes>" +
|
||||
"<img src='#{staticPath}filedeleted-res#{gifIcon}' alt='File deleted.' class=fileDeletedRes>" +
|
||||
"</span></div>"
|
||||
else if file
|
||||
ext = file.name[-3..]
|
||||
@ -178,11 +182,16 @@ Build =
|
||||
''
|
||||
|
||||
sticky = if isSticky
|
||||
" <img src=#{staticPath}sticky.gif alt=Sticky title=Sticky class=stickyIcon>"
|
||||
" <img src=#{staticPath}sticky#{gifIcon} alt=Sticky title=Sticky class=stickyIcon>"
|
||||
else
|
||||
''
|
||||
closed = if isClosed
|
||||
" <img src=#{staticPath}closed.gif alt=Closed title=Closed class=closedIcon>"
|
||||
" <img src=#{staticPath}closed#{gifIcon} alt=Closed title=Closed class=closedIcon>"
|
||||
else
|
||||
''
|
||||
|
||||
replyLink = if isOP and g.VIEW is 'index'
|
||||
" <span>[<a href='/#{boardID}/res/#{threadID}' class=replylink>Reply</a>]</span>"
|
||||
else
|
||||
''
|
||||
|
||||
@ -222,7 +231,7 @@ Build =
|
||||
"<span class='nameBlock#{capcodeClass}'>" +
|
||||
emailStart +
|
||||
"<span class=name>#{name or ''}</span>" + tripcode +
|
||||
capcodeStart + emailEnd + capcode + userID + flag + sticky + closed +
|
||||
capcodeStart + emailEnd + capcode + userID + flag +
|
||||
' </span> ' +
|
||||
"<span class=dateTime data-utc=#{dateUTC}>#{date}</span> " +
|
||||
"<span class='postNum desktop'>" +
|
||||
@ -233,6 +242,7 @@ Build =
|
||||
else
|
||||
"/#{boardID}/res/#{threadID}#q#{postID}"
|
||||
}' title='Quote this post'>#{postID}</a>" +
|
||||
sticky + closed + replyLink +
|
||||
'</span>' +
|
||||
'</div>' +
|
||||
|
||||
@ -248,3 +258,29 @@ Build =
|
||||
quote.href = "/#{boardID}/res/#{href}" # Fix pathnames
|
||||
|
||||
container
|
||||
|
||||
summary: (boardID, threadID, posts, files) ->
|
||||
text = []
|
||||
text.push "#{posts} post#{if posts > 1 then 's' else ''}"
|
||||
text.push "and #{files} image repl#{if files > 1 then 'ies' else 'y'}" if files
|
||||
text.push 'omitted.'
|
||||
$.el 'a',
|
||||
className: 'summary'
|
||||
textContent: text.join ' '
|
||||
href: "/#{boardID}/res/#{threadID}"
|
||||
thread: (board, data) ->
|
||||
Build.spoilerRange[board] = data.custom_spoiler
|
||||
|
||||
if (OP = board.posts[data.no]) and root = OP.nodes.root.parentNode
|
||||
$.rmAll root
|
||||
else
|
||||
root = $.el 'div',
|
||||
className: 'thread'
|
||||
id: "t#{data.no}"
|
||||
|
||||
nodes = [if OP then OP.nodes.root else Build.postFromObject data, board.ID]
|
||||
if data.omitted_posts
|
||||
nodes.push Build.summary board.ID, data.no, data.omitted_posts, data.omitted_images
|
||||
|
||||
$.add root, nodes
|
||||
root
|
||||
|
||||
@ -9,7 +9,6 @@ Config =
|
||||
'Time Formatting': [true, 'Localize and format timestamps.']
|
||||
'Relative Post Dates': [false, 'Display dates like "3 minutes ago". Tooltip shows the timestamp.']
|
||||
'File Info Formatting': [true, 'Reformat the file information.']
|
||||
'Comment Expansion': [true, 'Add buttons to expand too long comments.']
|
||||
'Thread Expansion': [true, 'Add buttons to expand threads.']
|
||||
'Index Navigation': [false, 'Add buttons to navigate between threads.']
|
||||
'Reply Navigation': [false, 'Add buttons to navigate to top / bottom of thread.']
|
||||
@ -140,6 +139,9 @@ Config =
|
||||
#//archive.installgentoo.net/%board/image/%MD5;text:View same on installgentoo /%board/
|
||||
"""
|
||||
'Custom CSS': false
|
||||
Index:
|
||||
'Index Mode': 'paged'
|
||||
'Index Sort': 'bump'
|
||||
Header:
|
||||
'Header auto-hide': false
|
||||
'Bottom header': false
|
||||
@ -170,9 +172,9 @@ Config =
|
||||
'Eqn tags': ['Alt+e', 'Insert eqn tags.']
|
||||
'Math tags': ['Alt+m', 'Insert math tags.']
|
||||
'Submit QR': ['Alt+s', 'Submit post.']
|
||||
# Thread related
|
||||
# Index/Thread related
|
||||
'Update': ['r', 'Refresh the index/thread.']
|
||||
'Watch': ['w', 'Watch thread.']
|
||||
'Update': ['r', 'Update the thread.']
|
||||
# Images
|
||||
'Expand image': ['Shift+e', 'Expand selected image.']
|
||||
'Expand images': ['e', 'Expand all images.']
|
||||
|
||||
@ -2,12 +2,11 @@ Header =
|
||||
init: ->
|
||||
headerEl = $.el 'div',
|
||||
id: 'header'
|
||||
innerHTML: """
|
||||
<%= grunt.file.read('html/General/Header.html').replace(/>\s+</g, '><').trim() %>
|
||||
"""
|
||||
innerHTML: <%= importHTML('General/Header') %>
|
||||
|
||||
@bar = $ '#header-bar', headerEl
|
||||
@toggle = $ '#toggle-header-bar', @bar
|
||||
@noticesRoot = $ '#notifications', headerEl
|
||||
|
||||
@menu = new UI.Menu 'header'
|
||||
menuButton = $.el 'a',
|
||||
@ -244,32 +243,48 @@ Header =
|
||||
$('input[name=boardnav]', settings).focus()
|
||||
|
||||
hashScroll: ->
|
||||
return unless (hash = @location.hash[1..]) and post = $.id hash
|
||||
hash = @location.hash[1..]
|
||||
return unless /^p\d+$/.test(hash) and post = $.id hash
|
||||
return if (Get.postFromRoot post).isHidden
|
||||
Header.scrollToPost post
|
||||
scrollToPost: (post) ->
|
||||
{top} = post.getBoundingClientRect()
|
||||
Header.scrollTo post
|
||||
scrollTo: (root, down, needed) ->
|
||||
if down
|
||||
x = Header.getBottomOf root
|
||||
window.scrollBy 0, -x unless needed and x >= 0
|
||||
else
|
||||
x = Header.getTopOf root
|
||||
window.scrollBy 0, x unless needed and x >= 0
|
||||
scrollToIfNeeded: (root, down) ->
|
||||
Header.scrollTo root, down, true
|
||||
getTopOf: (root) ->
|
||||
{top} = root.getBoundingClientRect()
|
||||
unless Conf['Bottom header']
|
||||
headRect = Header.toggle.getBoundingClientRect()
|
||||
top -= headRect.top + headRect.height
|
||||
window.scrollBy 0, top
|
||||
top -= headRect.top + headRect.height
|
||||
top
|
||||
getBottomOf: (root) ->
|
||||
{clientHeight} = doc
|
||||
bottom = clientHeight - root.getBoundingClientRect().bottom
|
||||
if Conf['Bottom header']
|
||||
headRect = Header.toggle.getBoundingClientRect()
|
||||
bottom -= clientHeight - headRect.bottom + headRect.height
|
||||
bottom
|
||||
|
||||
addShortcut: (el, index) ->
|
||||
shortcut = $.el 'span',
|
||||
className: 'shortcut'
|
||||
shortcut.dataset.index = index
|
||||
$.add shortcut, el
|
||||
shortcuts = $ '#shortcuts', Header.bar
|
||||
nodes = [shortcuts.childNodes...]
|
||||
nodes.splice index, 0, shortcut
|
||||
$.add shortcuts, nodes
|
||||
$.add shortcuts, [shortcuts.childNodes...].concat(shortcut).sort (a, b) -> a.dataset.index - b.dataset.index
|
||||
|
||||
menuToggle: (e) ->
|
||||
Header.menu.toggle e, @, g
|
||||
|
||||
createNotification: (e) ->
|
||||
{type, content, lifetime, cb} = e.detail
|
||||
notif = new Notice type, content, lifetime
|
||||
cb notif if cb
|
||||
notice = new Notice type, content, lifetime
|
||||
cb notice if cb
|
||||
|
||||
areNotificationsEnabled: false
|
||||
enableDesktopNotifications: ->
|
||||
|
||||
281
src/General/Index.coffee
Normal file
281
src/General/Index.coffee
Normal file
@ -0,0 +1,281 @@
|
||||
Index =
|
||||
init: ->
|
||||
return if g.VIEW isnt 'index' or g.BOARD.ID is 'f'
|
||||
|
||||
@button = $.el 'a',
|
||||
className: 'index-refresh-shortcut fa fa-refresh'
|
||||
title: 'Refresh Index'
|
||||
href: 'javascript:;'
|
||||
$.on @button, 'click', @update
|
||||
Header.addShortcut @button, 1
|
||||
|
||||
modeEntry =
|
||||
el: $.el 'span', textContent: 'Index mode'
|
||||
subEntries: [
|
||||
{ el: $.el 'label', innerHTML: '<input type=radio name="Index Mode" value="paged"> Paged' }
|
||||
{ el: $.el 'label', innerHTML: '<input type=radio name="Index Mode" value="all pages"> All threads' }
|
||||
]
|
||||
for label in modeEntry.subEntries
|
||||
input = label.el.firstChild
|
||||
input.checked = Conf['Index Mode'] is input.value
|
||||
$.on input, 'change', $.cb.value
|
||||
$.on input, 'change', @cb.mode
|
||||
|
||||
sortEntry =
|
||||
el: $.el 'span', textContent: 'Sort by'
|
||||
subEntries: [
|
||||
{ el: $.el 'label', innerHTML: '<input type=radio name="Index Sort" value="bump"> Bump order' }
|
||||
{ el: $.el 'label', innerHTML: '<input type=radio name="Index Sort" value="lastreply"> Last reply' }
|
||||
{ el: $.el 'label', innerHTML: '<input type=radio name="Index Sort" value="birth"> Creation date' }
|
||||
{ el: $.el 'label', innerHTML: '<input type=radio name="Index Sort" value="replycount"> Reply count' }
|
||||
{ el: $.el 'label', innerHTML: '<input type=radio name="Index Sort" value="filecount"> File count' }
|
||||
]
|
||||
for label in sortEntry.subEntries
|
||||
input = label.el.firstChild
|
||||
input.checked = Conf['Index Sort'] is input.value
|
||||
$.on input, 'change', $.cb.value
|
||||
$.on input, 'change', @cb.sort
|
||||
|
||||
$.event 'AddMenuEntry',
|
||||
type: 'header'
|
||||
el: $.el 'span',
|
||||
textContent: 'Index Navigation'
|
||||
order: 90
|
||||
subEntries: [modeEntry, sortEntry]
|
||||
|
||||
$.addClass doc, 'index-loading'
|
||||
@update()
|
||||
@root = $.el 'div', className: 'board'
|
||||
@pagelist = $.el 'div',
|
||||
className: 'pagelist'
|
||||
hidden: true
|
||||
innerHTML: <%= importHTML('General/Index-pagelist') %>
|
||||
@currentPage = @getCurrentPage()
|
||||
$.on window, 'popstate', @cb.popstate
|
||||
$.on @pagelist, 'click', @cb.pageNav
|
||||
$.asap (-> $('.pagelist', doc) or d.readyState isnt 'loading'), ->
|
||||
$.replace $('.board'), Index.root
|
||||
$.replace $('.pagelist'), Index.pagelist
|
||||
$.rmClass doc, 'index-loading'
|
||||
|
||||
cb:
|
||||
mode: ->
|
||||
Index.togglePagelist()
|
||||
Index.buildIndex()
|
||||
sort: ->
|
||||
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
|
||||
when 'BUTTON'
|
||||
a = e.target.parentNode
|
||||
when 'A'
|
||||
a = e.target
|
||||
else
|
||||
return
|
||||
e.preventDefault()
|
||||
Index.pageNav +a.pathname.split('/')[2]
|
||||
|
||||
scrollToIndex: ->
|
||||
Header.scrollToIfNeeded Index.root
|
||||
|
||||
getCurrentPage: ->
|
||||
+window.location.pathname.split('/')[2]
|
||||
pageNav: (pageNum) ->
|
||||
return if Index.currentPage is pageNum
|
||||
history.pushState null, '', if pageNum is 0 then './' else pageNum
|
||||
Index.pageLoad pageNum
|
||||
pageLoad: (pageNum) ->
|
||||
Index.currentPage = pageNum
|
||||
return if Conf['Index Mode'] isnt 'paged'
|
||||
Index.buildIndex()
|
||||
Index.setPage()
|
||||
Index.scrollToIndex()
|
||||
|
||||
togglePagelist: ->
|
||||
Index.pagelist.hidden = Conf['Index Mode'] isnt 'paged'
|
||||
buildPagelist: ->
|
||||
pagesRoot = $ '.pages', Index.pagelist
|
||||
if pagesRoot.childElementCount isnt Index.pagesNum
|
||||
nodes = []
|
||||
for i in [0..Index.pagesNum - 1]
|
||||
a = $.el 'a',
|
||||
textContent: i
|
||||
href: if i then i else './'
|
||||
nodes.push $.tn('['), a, $.tn '] '
|
||||
$.rmAll pagesRoot
|
||||
$.add pagesRoot, nodes
|
||||
Index.setPage()
|
||||
Index.togglePagelist()
|
||||
setPage: ->
|
||||
pageNum = Index.getCurrentPage()
|
||||
pagesRoot = $ '.pages', Index.pagelist
|
||||
# Previous/Next buttons
|
||||
prev = pagesRoot.previousSibling.firstChild
|
||||
next = pagesRoot.nextSibling.firstChild
|
||||
href = Math.max pageNum - 1, 0
|
||||
prev.href = if href is 0 then './' else href
|
||||
prev.firstChild.disabled = href is pageNum
|
||||
href = Math.min pageNum + 1, Index.pagesNum - 1
|
||||
next.href = if href is 0 then './' else href
|
||||
next.firstChild.disabled = href is pageNum
|
||||
# <strong> current page
|
||||
if strong = $ 'strong', pagesRoot
|
||||
return if +strong.textContent is pageNum
|
||||
$.replace strong, strong.firstChild
|
||||
else
|
||||
strong = $.el 'strong'
|
||||
a = pagesRoot.children[pageNum]
|
||||
$.before a, strong
|
||||
$.add strong, a
|
||||
|
||||
update: ->
|
||||
return unless navigator.onLine
|
||||
Index.req?.abort()
|
||||
Index.notice?.close()
|
||||
Index.notice = new Notice 'info', 'Refreshing index...'
|
||||
Index.req = $.ajax "//api.4chan.org/#{g.BOARD}/catalog.json",
|
||||
onabort: Index.load
|
||||
onloadend: Index.load
|
||||
,
|
||||
whenModified: true
|
||||
$.addClass Index.button, 'fa-spin'
|
||||
load: (e) ->
|
||||
$.rmClass Index.button, 'fa-spin'
|
||||
{req, notice} = Index
|
||||
delete Index.req
|
||||
delete Index.notice
|
||||
|
||||
if e.type is 'abort'
|
||||
req.onloadend = null
|
||||
notice.close()
|
||||
return
|
||||
|
||||
try
|
||||
Index.parse JSON.parse req.response if req.status is 200
|
||||
catch err
|
||||
c.error 'Index failure:', err.stack
|
||||
# network error or non-JSON content for example.
|
||||
notice.setType 'error'
|
||||
notice.el.lastElementChild.textContent = 'Index refresh failed.'
|
||||
setTimeout notice.close, 2 * $.SECOND
|
||||
return
|
||||
|
||||
notice.setType 'success'
|
||||
notice.el.lastElementChild.textContent = 'Index refreshed!'
|
||||
setTimeout notice.close, $.SECOND
|
||||
|
||||
Index.scrollToIndex()
|
||||
parse: (pages) ->
|
||||
Index.parseThreadList pages
|
||||
Index.buildThreads()
|
||||
Index.sort()
|
||||
Index.buildIndex()
|
||||
Index.buildPagelist()
|
||||
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()
|
||||
return
|
||||
buildThreads: ->
|
||||
Index.nodes = []
|
||||
threads = []
|
||||
posts = []
|
||||
for threadData in Index.liveThreadData
|
||||
threadRoot = Build.thread g.BOARD, threadData
|
||||
Index.nodes.push threadRoot, $.el 'hr'
|
||||
if thread = g.BOARD.threads[threadData.no]
|
||||
thread.setStatus 'Sticky', !!threadData.sticky
|
||||
thread.setStatus 'Closed', !!threadData.closed
|
||||
else
|
||||
thread = new Thread threadData.no, g.BOARD
|
||||
threads.push thread
|
||||
# postRoots = $$ '.thread > .postContainer', threadRoot
|
||||
# for postRoot in postRoots when postRoot.id.match(/\d+/)[0] not of thread.posts
|
||||
OPRoot = $ '.opContainer', threadRoot
|
||||
continue if OPRoot.id.match(/\d+/)[0] of thread.posts
|
||||
try
|
||||
posts.push new Post OPRoot, thread, g.BOARD
|
||||
catch err
|
||||
# Skip posts that we failed to parse.
|
||||
Main.handleErrors
|
||||
message: "Parsing of Post No.#{postRoot.id.match /\d+/} failed. Post will be skipped."
|
||||
error: err
|
||||
|
||||
# Add the threads and <hr>s in a container to make sure all features work.
|
||||
$.nodes Index.nodes
|
||||
Main.callbackNodes Thread, threads
|
||||
Main.callbackNodes Post, posts
|
||||
buildReplies: (threadRoots) ->
|
||||
posts = []
|
||||
for threadRoot in threadRoots by 2
|
||||
thread = Get.threadFromRoot threadRoot
|
||||
i = Index.liveThreadIDs.indexOf thread.ID
|
||||
continue unless lastReplies = Index.liveThreadData[i].last_replies
|
||||
nodes = []
|
||||
for data in lastReplies
|
||||
if post = thread.posts[data.no]
|
||||
nodes.push post.nodes.root
|
||||
continue
|
||||
nodes.push node = Build.postFromObject data, thread.board.ID
|
||||
try
|
||||
posts.push new Post node, thread, thread.board
|
||||
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
|
||||
$.add threadRoot, nodes
|
||||
|
||||
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
|
||||
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 = []
|
||||
for threadID in sortedThreadIDs
|
||||
i = Index.liveThreadIDs.indexOf(threadID) * 2
|
||||
Index.sortedNodes.push Index.nodes[i], Index.nodes[i + 1]
|
||||
# Put the sticky threads on top of the index.
|
||||
offset = 0
|
||||
for threadRoot, i in Index.sortedNodes by 2 when Get.threadFromRoot(threadRoot).isSticky
|
||||
Index.sortedNodes.splice offset++ * 2, 0, Index.sortedNodes.splice(i, 2)...
|
||||
return unless Conf['Filter']
|
||||
# Put the highlighted thread & <hr> on top of the index
|
||||
# while keeping the original order they appear in.
|
||||
offset = 0
|
||||
for threadRoot, i in Index.sortedNodes by 2 when Get.threadFromRoot(threadRoot).isOnTop
|
||||
Index.sortedNodes.splice offset++ * 2, 0, Index.sortedNodes.splice(i, 2)...
|
||||
return
|
||||
buildIndex: ->
|
||||
if Conf['Index Mode'] is 'paged'
|
||||
pageNum = Index.getCurrentPage()
|
||||
nodesPerPage = Index.threadsNumPerPage * 2
|
||||
nodes = Index.sortedNodes[nodesPerPage * pageNum ... nodesPerPage * (pageNum + 1)]
|
||||
else
|
||||
nodes = Index.sortedNodes
|
||||
$.rmAll Index.root
|
||||
Index.buildReplies nodes
|
||||
$.event 'IndexRefresh'
|
||||
$.add Index.root, nodes
|
||||
@ -69,6 +69,7 @@ Main =
|
||||
initFeature 'Polyfill', Polyfill
|
||||
initFeature 'Header', Header
|
||||
initFeature 'Settings', Settings
|
||||
initFeature 'Index Generator', Index
|
||||
initFeature 'Announcement Hiding', PSAHiding
|
||||
initFeature 'Fourchan thingies', Fourchan
|
||||
initFeature 'Custom CSS', CustomCSS
|
||||
@ -105,7 +106,6 @@ Main =
|
||||
initFeature 'Reveal Spoilers', RevealSpoilers
|
||||
initFeature 'Auto-GIF', AutoGIF
|
||||
initFeature 'Image Hover', ImageHover
|
||||
initFeature 'Comment Expansion', ExpandComment
|
||||
initFeature 'Thread Expansion', ExpandThread
|
||||
initFeature 'Thread Excerpt', ThreadExcerpt
|
||||
initFeature 'Favicon', Favicon
|
||||
@ -169,26 +169,21 @@ Main =
|
||||
# Something might have gone wrong!
|
||||
Main.initStyle()
|
||||
|
||||
if board = $ '.board'
|
||||
threads = []
|
||||
posts = []
|
||||
|
||||
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
|
||||
if g.VIEW is 'thread' and 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
|
||||
catch err
|
||||
# Skip posts that we failed to parse.
|
||||
errors = [] unless errors
|
||||
errors.push
|
||||
message: "Parsing of Post No.#{postRoot.id.match /\d+/} failed. Post will be skipped."
|
||||
error: err
|
||||
Main.handleErrors errors if errors
|
||||
|
||||
Main.callbackNodes Thread, threads
|
||||
Main.callbackNodes Thread, [thread]
|
||||
Main.callbackNodes Post, posts
|
||||
|
||||
if $.hasClass d.body, 'fourchan_x'
|
||||
@ -207,7 +202,7 @@ Main =
|
||||
try
|
||||
localStorage.getItem '4chan-settings'
|
||||
catch err
|
||||
new Notice 'warning', 'Cookies need to be enabled on 4chan for <%= meta.name %> to properly function.', 30
|
||||
new Notice 'warning', 'Cookies need to be enabled on 4chan for <%= meta.name %> to operate properly.', 30
|
||||
Main.disableReports = true
|
||||
|
||||
$.event '4chanXInitFinished'
|
||||
|
||||
@ -19,7 +19,7 @@ class Notice
|
||||
$.on d, 'visibilitychange', @add
|
||||
return
|
||||
$.off d, 'visibilitychange', @add
|
||||
$.add $.id('notifications'), @el
|
||||
$.add Header.noticesRoot, @el
|
||||
@el.clientHeight # force reflow
|
||||
@el.style.opacity = 1
|
||||
setTimeout @close, @timeout * $.SECOND if @timeout
|
||||
|
||||
@ -193,6 +193,13 @@ class Post
|
||||
quotelink.textContent = quotelink.textContent.replace '\u00A0(Dead)', ''
|
||||
$.rmClass quotelink, 'deadlink'
|
||||
return
|
||||
|
||||
collect: ->
|
||||
@kill()
|
||||
delete g.posts[@fullID]
|
||||
delete @thread.posts[@]
|
||||
delete @board.posts[@]
|
||||
|
||||
addClone: (context) ->
|
||||
new Clone @, context
|
||||
rmClone: (index) ->
|
||||
|
||||
@ -61,9 +61,7 @@ Settings =
|
||||
return if Settings.dialog
|
||||
$.event 'CloseMenu'
|
||||
|
||||
html = """
|
||||
<%= grunt.file.read('html/General/Settings.html').replace(/>\s+</g, '><').trim() %>
|
||||
"""
|
||||
html = <%= importHTML('General/Settings') %>
|
||||
|
||||
Settings.dialog = overlay = $.el 'div',
|
||||
id: 'overlay'
|
||||
@ -113,9 +111,7 @@ Settings =
|
||||
section.scrollTop = 0
|
||||
|
||||
main: (section) ->
|
||||
section.innerHTML = """
|
||||
<%= grunt.file.read('html/General/Settings-section-Main.html').replace(/>\s+</g, '><').trim() %>
|
||||
"""
|
||||
section.innerHTML = <%= importHTML('General/Settings-section-Main') %>
|
||||
$.on $('.export', section), 'click', Settings.export
|
||||
$.on $('.import', section), 'click', Settings.import
|
||||
$.on $('input', section), 'change', Settings.onImport
|
||||
@ -142,7 +138,7 @@ Settings =
|
||||
return
|
||||
|
||||
div = $.el 'div',
|
||||
innerHTML: "<button></button><span class=description>: Clear manually-hidden threads and posts on all boards. Refresh the page to apply."
|
||||
innerHTML: "<button></button><span class=description>: Clear manually-hidden threads and posts on all boards. Reload the page to apply."
|
||||
button = $ 'button', div
|
||||
hiddenNum = 0
|
||||
$.get 'hiddenThreads', boards: {}, (item) ->
|
||||
@ -205,7 +201,7 @@ Settings =
|
||||
try
|
||||
data = JSON.parse e.target.result
|
||||
Settings.loadSettings data
|
||||
if confirm 'Import successful. Refresh now?'
|
||||
if confirm 'Import successful. Reload now?'
|
||||
window.location.reload()
|
||||
catch err
|
||||
output.textContent = 'Import failed due to an error.'
|
||||
@ -290,9 +286,7 @@ Settings =
|
||||
data
|
||||
|
||||
filter: (section) ->
|
||||
section.innerHTML = """
|
||||
<%= grunt.file.read('html/General/Settings-section-Filter.html').replace(/>\s+</g, '><').trim() %>
|
||||
"""
|
||||
section.innerHTML = <%= importHTML('General/Settings-section-Filter') %>
|
||||
select = $ 'select', section
|
||||
$.on select, 'change', Settings.selectFilter
|
||||
Settings.selectFilter.call select
|
||||
@ -309,32 +303,24 @@ Settings =
|
||||
$.on ta, 'change', $.cb.value
|
||||
$.add div, ta
|
||||
return
|
||||
div.innerHTML = """
|
||||
<%= grunt.file.read('html/General/Settings-section-Filter-guide.html').replace(/>\s+</g, '><').trim() %>
|
||||
"""
|
||||
div.innerHTML = <%= importHTML('General/Settings-section-Filter-guide') %>
|
||||
|
||||
qr: (section) ->
|
||||
section.innerHTML = """
|
||||
<%= grunt.file.read('html/General/Settings-section-QR.html').replace(/>\s+</g, '><').trim() %>
|
||||
"""
|
||||
section.innerHTML = <%= importHTML('General/Settings-section-QR') %>
|
||||
ta = $ 'textarea', section
|
||||
$.get 'QR.personas', Conf['QR.personas'], (item) ->
|
||||
ta.value = item['QR.personas']
|
||||
$.on ta, 'change', $.cb.value
|
||||
|
||||
sauce: (section) ->
|
||||
section.innerHTML = """
|
||||
<%= grunt.file.read('html/General/Settings-section-Sauce.html').replace(/>\s+</g, '><').trim() %>
|
||||
"""
|
||||
section.innerHTML = <%= importHTML('General/Settings-section-Sauce') %>
|
||||
ta = $ 'textarea', section
|
||||
$.get 'sauces', Conf['sauces'], (item) ->
|
||||
ta.value = item['sauces']
|
||||
$.on ta, 'change', $.cb.value
|
||||
|
||||
rice: (section) ->
|
||||
section.innerHTML = """
|
||||
<%= grunt.file.read('html/General/Settings-section-Rice.html').replace(/>\s+</g, '><').trim() %>
|
||||
"""
|
||||
section.innerHTML = <%= importHTML('General/Settings-section-Rice') %>
|
||||
items = {}
|
||||
inputs = {}
|
||||
for name in ['boardnav', 'time', 'backlink', 'fileInfo', 'favicon', 'usercss']
|
||||
@ -395,9 +381,7 @@ Settings =
|
||||
CustomCSS.update()
|
||||
|
||||
archives: (section) ->
|
||||
section.innerHTML = """
|
||||
<%= grunt.file.read('html/General/Settings-section-Archives.html').replace(/>\s+</g, '><').trim() %>
|
||||
"""
|
||||
section.innerHTML = <%= importHTML('General/Settings-section-Archives') %>
|
||||
|
||||
showLastUpdateTime = (time) ->
|
||||
$('time', section).textContent = new Date(time).toLocaleString()
|
||||
@ -470,9 +454,7 @@ Settings =
|
||||
$.set 'selectedArchives', selectedArchives
|
||||
|
||||
keybinds: (section) ->
|
||||
section.innerHTML = """
|
||||
<%= grunt.file.read('html/General/Settings-section-Keybinds.html').replace(/>\s+</g, '><').trim() %>
|
||||
"""
|
||||
section.innerHTML = <%= importHTML('General/Settings-section-Keybinds') %>
|
||||
tbody = $ 'tbody', section
|
||||
items = {}
|
||||
inputs = {}
|
||||
|
||||
@ -3,11 +3,41 @@ class Thread
|
||||
toString: -> @ID
|
||||
|
||||
constructor: (@ID, @board) ->
|
||||
@fullID = "#{@board}.#{@ID}"
|
||||
@posts = {}
|
||||
@fullID = "#{@board}.#{@ID}"
|
||||
@posts = {}
|
||||
@isSticky = false
|
||||
@isClosed = false
|
||||
@postLimit = false
|
||||
@fileLimit = false
|
||||
|
||||
g.threads[@fullID] = board.threads[@] = @
|
||||
|
||||
setStatus: (type, status) ->
|
||||
name = "is#{type}"
|
||||
return if @[name] is status
|
||||
@[name] = status
|
||||
return unless @OP
|
||||
typeLC = type.toLowerCase()
|
||||
unless status
|
||||
$.rm $ ".#{typeLC}Icon", @OP.nodes.info
|
||||
return
|
||||
icon = $.el 'img',
|
||||
src: "//static.4chan.org/image/#{typeLC}#{if window.devicePixelRatio >= 2 then '@2x' else ''}.gif"
|
||||
alt: type
|
||||
title: type
|
||||
className: "#{typeLC}Icon"
|
||||
root = if type is 'Closed' and @isSticky
|
||||
$ '.stickyIcon', @OP.nodes.info
|
||||
else
|
||||
$ '[title="Quote this post"]', @OP.nodes.info
|
||||
$.after root, [$.tn(' '), icon]
|
||||
|
||||
kill: ->
|
||||
@isDead = true
|
||||
@timeOfDeath = Date.now()
|
||||
|
||||
collect: ->
|
||||
for postID, post in @posts
|
||||
post.collect()
|
||||
delete g.threads[@fullID]
|
||||
delete @board.threads[@]
|
||||
|
||||
@ -7,7 +7,7 @@ ImageExpand =
|
||||
title: 'Expand All Images'
|
||||
href: 'javascript:;'
|
||||
$.on @EAI, 'click', ImageExpand.cb.toggleAll
|
||||
Header.addShortcut @EAI, 2
|
||||
Header.addShortcut @EAI, 3
|
||||
|
||||
Post.callbacks.push
|
||||
name: 'Image Expansion'
|
||||
@ -45,7 +45,7 @@ ImageExpand =
|
||||
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 file.thumb.getBoundingClientRect().top < 0)
|
||||
Conf['Expand from here'] and Header.getTopOf(file.thumb) < 0)
|
||||
continue
|
||||
$.queueTask func, post
|
||||
return
|
||||
@ -60,13 +60,10 @@ ImageExpand =
|
||||
|
||||
# Scroll back to the thumbnail when contracting the image
|
||||
# to avoid being left miles away from the relevant post.
|
||||
rect = post.nodes.root.getBoundingClientRect()
|
||||
if rect.top < 0
|
||||
y = rect.top
|
||||
unless Conf['Bottom header']
|
||||
headRect = Header.toggle.getBoundingClientRect()
|
||||
y -= headRect.top + headRect.height
|
||||
if rect.left < 0
|
||||
top = Header.getTopOf post.nodes.root
|
||||
if top < 0
|
||||
y = top
|
||||
if post.nodes.root.getBoundingClientRect().left < 0
|
||||
x = -window.scrollX
|
||||
window.scrollBy x, y if x or y
|
||||
ImageExpand.contract post
|
||||
@ -104,13 +101,12 @@ ImageExpand =
|
||||
$.addClass post.nodes.root, 'expanded-image'
|
||||
$.rmClass post.file.thumb, 'expanding'
|
||||
return
|
||||
prev = post.nodes.root.getBoundingClientRect()
|
||||
{bottom} = post.nodes.root.getBoundingClientRect()
|
||||
$.queueTask ->
|
||||
$.addClass post.nodes.root, 'expanded-image'
|
||||
$.rmClass post.file.thumb, 'expanding'
|
||||
return unless prev.top + prev.height <= 0
|
||||
curr = post.nodes.root.getBoundingClientRect()
|
||||
window.scrollBy 0, curr.height - prev.height + curr.top - prev.top
|
||||
return unless bottom <= 0
|
||||
window.scrollBy 0, post.nodes.root.getBoundingClientRect().bottom - bottom
|
||||
|
||||
error: ->
|
||||
post = Get.postFromNode @
|
||||
|
||||
@ -1,71 +0,0 @@
|
||||
ExpandComment =
|
||||
init: ->
|
||||
return if g.VIEW isnt 'index' or !Conf['Comment Expansion']
|
||||
|
||||
Post.callbacks.push
|
||||
name: 'Comment Expansion'
|
||||
cb: @node
|
||||
node: ->
|
||||
if a = $ '.abbr > a:not([onclick])', @nodes.comment
|
||||
$.on a, 'click', ExpandComment.cb
|
||||
cb: (e) ->
|
||||
e.preventDefault()
|
||||
ExpandComment.expand Get.postFromNode @
|
||||
expand: (post) ->
|
||||
if post.nodes.longComment and !post.nodes.longComment.parentNode
|
||||
$.replace post.nodes.shortComment, post.nodes.longComment
|
||||
post.nodes.comment = post.nodes.longComment
|
||||
return
|
||||
return unless a = $ '.abbr > a', post.nodes.comment
|
||||
a.textContent = "Post No.#{post} Loading..."
|
||||
$.cache "//api.4chan.org#{a.pathname}.json", -> ExpandComment.parse @, a, post
|
||||
contract: (post) ->
|
||||
return unless post.nodes.shortComment
|
||||
a = $ '.abbr > a', post.nodes.shortComment
|
||||
a.textContent = 'here'
|
||||
$.replace post.nodes.longComment, post.nodes.shortComment
|
||||
post.nodes.comment = post.nodes.shortComment
|
||||
parse: (req, a, post) ->
|
||||
{status} = req
|
||||
if status not in [200, 304]
|
||||
a.textContent = "Error #{req.statusText} (#{status})"
|
||||
return
|
||||
|
||||
posts = JSON.parse(req.response).posts
|
||||
if spoilerRange = posts[0].custom_spoiler
|
||||
Build.spoilerRange[g.BOARD] = spoilerRange
|
||||
|
||||
for postObj in posts
|
||||
break if postObj.no is post.ID
|
||||
if postObj.no isnt post.ID
|
||||
a.textContent = "Post No.#{post} not found."
|
||||
return
|
||||
|
||||
{comment} = post.nodes
|
||||
clone = comment.cloneNode false
|
||||
clone.innerHTML = postObj.com
|
||||
for quote in $$ '.quotelink', clone
|
||||
href = quote.getAttribute 'href'
|
||||
continue if href[0] is '/' # Cross-board quote, or board link
|
||||
quote.href = "/#{post.board}/res/#{href}" # Fix pathnames
|
||||
post.nodes.shortComment = comment
|
||||
$.replace comment, clone
|
||||
post.nodes.comment = post.nodes.longComment = clone
|
||||
post.parseComment()
|
||||
post.parseQuotes()
|
||||
if Conf['Resurrect Quotes']
|
||||
Quotify.node.call post
|
||||
if Conf['Quote Previewing']
|
||||
QuotePreview.node.call post
|
||||
if Conf['Quote Inlining']
|
||||
QuoteInline.node.call post
|
||||
if Conf['Mark OP Quotes']
|
||||
QuoteOP.node.call post
|
||||
if Conf['Mark Cross-thread Quotes']
|
||||
QuoteCT.node.call post
|
||||
if g.BOARD.ID is 'g'
|
||||
Fourchan.code.call post
|
||||
if g.BOARD.ID is 'sci'
|
||||
Fourchan.math.call post
|
||||
if Conf['Linkify']
|
||||
Linkify.node.call post
|
||||
@ -1,19 +1,26 @@
|
||||
ExpandThread =
|
||||
init: ->
|
||||
return if g.VIEW isnt 'index' or !Conf['Thread Expansion']
|
||||
|
||||
@statuses = {}
|
||||
$.on d, 'IndexRefresh', @onIndexRefresh
|
||||
Thread.callbacks.push
|
||||
name: 'Thread Expansion'
|
||||
cb: @node
|
||||
node: ->
|
||||
return unless span = $.x 'following-sibling::span[contains(@class,"summary")][1]', @OP.nodes.root
|
||||
[posts, files] = span.textContent.match /\d+/g
|
||||
a = $.el 'a',
|
||||
textContent: ExpandThread.text '+', posts, files
|
||||
className: 'summary'
|
||||
href: 'javascript:;'
|
||||
ExpandThread.setButton @
|
||||
|
||||
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
|
||||
$.replace span, a
|
||||
|
||||
onIndexRefresh: ->
|
||||
for threadID, status of ExpandThread.statuses
|
||||
status.req?.abort()
|
||||
delete ExpandThread.statuses[threadID]
|
||||
for threadID, thread of g.BOARD.threads
|
||||
ExpandThread.setButton thread
|
||||
return
|
||||
|
||||
text: (status, posts, files) ->
|
||||
text = [status]
|
||||
@ -22,95 +29,77 @@ ExpandThread =
|
||||
text.push if status is '-' then 'shown' else 'omitted'
|
||||
text.join(' ') + '.'
|
||||
|
||||
cbToggle: ->
|
||||
cbToggle: (e) ->
|
||||
return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or e.button isnt 0
|
||||
e.preventDefault()
|
||||
ExpandThread.toggle Get.threadFromNode @
|
||||
|
||||
toggle: (thread) ->
|
||||
threadRoot = thread.OP.nodes.root.parentNode
|
||||
a = $ '.summary', threadRoot
|
||||
|
||||
switch thread.isExpanded
|
||||
when false, undefined
|
||||
for post in $$ '.thread > .postContainer', threadRoot
|
||||
ExpandComment.expand Get.postFromRoot post
|
||||
unless a
|
||||
thread.isExpanded = true
|
||||
return
|
||||
thread.isExpanded = 'loading'
|
||||
[posts, files] = a.textContent.match /\d+/g
|
||||
a.textContent = ExpandThread.text '...', posts, files
|
||||
$.cache "//api.4chan.org/#{thread.board}/res/#{thread}.json", ->
|
||||
ExpandThread.parse @, thread, a
|
||||
|
||||
when 'loading'
|
||||
thread.isExpanded = false
|
||||
return unless a
|
||||
[posts, files] = a.textContent.match /\d+/g
|
||||
a.textContent = ExpandThread.text '+', posts, files
|
||||
|
||||
when true
|
||||
thread.isExpanded = false
|
||||
#goddamit moot
|
||||
num = if thread.isSticky
|
||||
1
|
||||
else switch g.BOARD.ID
|
||||
# XXX boards config
|
||||
when 'b', 'vg' then 3
|
||||
when 't' then 1
|
||||
else 5
|
||||
posts = $$ ".thread > .replyContainer", threadRoot
|
||||
for post in [thread.OP.nodes.root].concat posts[-num..]
|
||||
ExpandComment.contract Get.postFromRoot post
|
||||
return unless a
|
||||
postsCount = 0
|
||||
filesCount = 0
|
||||
for reply in posts[...-num]
|
||||
if Conf['Quote Inlining']
|
||||
# rm clones
|
||||
inlined.click() while inlined = $ '.inlined', reply
|
||||
postsCount++
|
||||
filesCount++ if 'file' of Get.postFromRoot reply
|
||||
$.rm reply
|
||||
a.textContent = ExpandThread.text '+', postsCount, filesCount
|
||||
return
|
||||
|
||||
parse: (req, thread, a) ->
|
||||
return if a.textContent[0] is '+'
|
||||
if req.status not in [200, 304]
|
||||
a.textContent = "Error #{req.statusText} (#{req.status})"
|
||||
$.off a, 'click', ExpandThread.cbToggle
|
||||
return unless a = $ '.summary', threadRoot
|
||||
if thread.ID of ExpandThread.statuses
|
||||
ExpandThread.contract thread, a, threadRoot
|
||||
else
|
||||
ExpandThread.expand thread, a, threadRoot
|
||||
expand: (thread, a, threadRoot) ->
|
||||
ExpandThread.statuses[thread] = status = {}
|
||||
a.textContent = ExpandThread.text '...', a.textContent.match(/\d+/g)...
|
||||
status.req = $.cache "//api.4chan.org/#{thread.board}/res/#{thread}.json", ->
|
||||
delete status.req
|
||||
ExpandThread.parse @, thread, a
|
||||
contract: (thread, a, threadRoot) ->
|
||||
status = ExpandThread.statuses[thread]
|
||||
delete ExpandThread.statuses[thread]
|
||||
if status.req
|
||||
status.req.abort()
|
||||
a.textContent = ExpandThread.text '+', a.textContent.match(/\d+/g)... if a
|
||||
return
|
||||
|
||||
thread.isExpanded = true
|
||||
num = if thread.isSticky
|
||||
1
|
||||
else switch g.BOARD.ID
|
||||
# XXX boards config
|
||||
when 'b', 'vg' then 3
|
||||
when 't' then 1
|
||||
else 5
|
||||
postsCount = 0
|
||||
filesCount = 0
|
||||
for reply in $$('.thread > .replyContainer', threadRoot)[...-num]
|
||||
# rm clones
|
||||
inlined.click() while inlined = $ '.inlined', reply if Conf['Quote Inlining']
|
||||
postsCount++
|
||||
filesCount++ if 'file' of Get.postFromRoot reply
|
||||
$.rm reply
|
||||
a.textContent = ExpandThread.text '+', postsCount, filesCount
|
||||
parse: (req, thread, a) ->
|
||||
if req.status not in [200, 304]
|
||||
a.textContent = "Error #{req.statusText} (#{req.status})"
|
||||
return
|
||||
|
||||
{posts} = JSON.parse req.response
|
||||
if spoilerRange = posts.shift().custom_spoiler
|
||||
Build.spoilerRange[thread.board] = spoilerRange
|
||||
data = JSON.parse(req.response).posts
|
||||
Build.spoilerRange[thread.board] = data.shift().custom_spoiler
|
||||
|
||||
postsObj = []
|
||||
posts = []
|
||||
postsRoot = []
|
||||
filesCount = 0
|
||||
for reply in posts
|
||||
if post = thread.posts[reply.no]
|
||||
for postData in data
|
||||
if post = thread.posts[postData.no]
|
||||
filesCount++ if 'file' of post
|
||||
postsRoot.push post.nodes.root
|
||||
continue
|
||||
root = Build.postFromObject reply, thread.board.ID
|
||||
root = Build.postFromObject postData, thread.board.ID
|
||||
post = new Post root, thread, thread.board
|
||||
link = $ 'a[title="Highlight this post"]', root
|
||||
link.href = "res/#{thread}#p#{post}"
|
||||
link.nextSibling.href = "res/#{thread}#q#{post}"
|
||||
filesCount++ if 'file' of post
|
||||
postsObj.push post
|
||||
posts.push post
|
||||
postsRoot.push root
|
||||
Main.callbackNodes Post, postsObj
|
||||
Main.callbackNodes Post, posts
|
||||
$.after a, postsRoot
|
||||
|
||||
postsCount = postsRoot.length
|
||||
postsCount = postsRoot.length
|
||||
a.textContent = ExpandThread.text '-', postsCount, filesCount
|
||||
|
||||
# Enable 4chan features.
|
||||
if Conf['Enable 4chan\'s Extension']
|
||||
$.globalEval "Parser.parseThread(#{thread.ID}, 1, #{postsCount})"
|
||||
$.globalEval "Parser.parseThread(#{thread}, 1, #{postsCount})"
|
||||
else
|
||||
Fourchan.parseThread thread.ID, 1, postsCount
|
||||
|
||||
@ -6,8 +6,9 @@ Fourchan =
|
||||
if board is 'g'
|
||||
$.globalEval """
|
||||
window.addEventListener('prettyprint', function(e) {
|
||||
var pre = e.detail;
|
||||
pre.innerHTML = prettyPrintOne(pre.innerHTML);
|
||||
window.dispatchEvent(new CustomEvent('prettyprint:cb', {
|
||||
detail: prettyPrintOne(e.detail)
|
||||
}));
|
||||
}, false);
|
||||
"""
|
||||
Post.callbacks.push
|
||||
@ -32,9 +33,11 @@ Fourchan =
|
||||
cb: @math
|
||||
code: ->
|
||||
return if @isClone
|
||||
apply = (e) -> pre.innerHTML = e.detail
|
||||
$.on window, 'prettyprint:cb', apply
|
||||
for pre in $$ '.prettyprint:not(.prettyprinted)', @nodes.comment
|
||||
$.event 'prettyprint', pre, window
|
||||
$.addClass pre, 'prettyprinted'
|
||||
$.event 'prettyprint', pre.innerHTML, window
|
||||
$.off window, 'prettyprint:cb', apply
|
||||
return
|
||||
math: ->
|
||||
return if @isClone or !$ '.math', @nodes.comment
|
||||
|
||||
@ -7,7 +7,7 @@ Keybinds =
|
||||
|
||||
init = ->
|
||||
$.off d, '4chanXInitFinished', init
|
||||
$.on d, 'keydown', Keybinds.keydown
|
||||
$.on d, 'keydown', Keybinds.keydown
|
||||
for node in $$ '[accesskey]'
|
||||
node.removeAttribute 'accesskey'
|
||||
return
|
||||
@ -58,11 +58,15 @@ Keybinds =
|
||||
Keybinds.tags 'math', target
|
||||
when Conf['Submit QR']
|
||||
QR.submit() if QR.nodes and !QR.status()
|
||||
# Thread related
|
||||
# Index/Thread related
|
||||
when Conf['Update']
|
||||
switch g.VIEW
|
||||
when 'thread'
|
||||
ThreadUpdater.update()
|
||||
when 'index'
|
||||
Index.update()
|
||||
when Conf['Watch']
|
||||
ThreadWatcher.toggle thread
|
||||
when Conf['Update']
|
||||
ThreadUpdater.update()
|
||||
# Images
|
||||
when Conf['Expand image']
|
||||
Keybinds.img threadRoot
|
||||
@ -70,15 +74,18 @@ Keybinds =
|
||||
Keybinds.img threadRoot, true
|
||||
# Board Navigation
|
||||
when Conf['Front page']
|
||||
window.location = "/#{g.BOARD}/0#delform"
|
||||
if g.VIEW is 'index'
|
||||
Index.pageNav 0
|
||||
else
|
||||
window.location = "/#{g.BOARD}/"
|
||||
when Conf['Open front page']
|
||||
$.open "/#{g.BOARD}/#delform"
|
||||
$.open "/#{g.BOARD}/"
|
||||
when Conf['Next page']
|
||||
if form = $ '.next form'
|
||||
window.location = form.action
|
||||
return unless g.VIEW is 'index' and Conf['Index Mode'] is 'paged'
|
||||
$('.next button', Index.pagelist).click()
|
||||
when Conf['Previous page']
|
||||
if form = $ '.prev form'
|
||||
window.location = form.action
|
||||
return unless g.VIEW is 'index' and Conf['Index Mode'] is 'paged'
|
||||
$('.prev button', Index.pagelist).click()
|
||||
when Conf['Search form']
|
||||
$.id('search-btn').click()
|
||||
# Thread Navigation
|
||||
@ -176,43 +183,31 @@ Keybinds =
|
||||
location.href = url
|
||||
|
||||
hl: (delta, thread) ->
|
||||
postEl = $ '.reply.highlight', thread
|
||||
|
||||
unless delta
|
||||
if postEl = $ '.reply.highlight', thread
|
||||
$.rmClass postEl, 'highlight'
|
||||
$.rmClass postEl, 'highlight' if postEl
|
||||
return
|
||||
if Conf['Bottom header']
|
||||
topMargin = 0
|
||||
else
|
||||
headRect = Header.toggle.getBoundingClientRect()
|
||||
topMargin = headRect.top + headRect.height
|
||||
if postEl = $ '.reply.highlight', thread
|
||||
$.rmClass postEl, 'highlight'
|
||||
rect = postEl.getBoundingClientRect()
|
||||
if rect.bottom >= topMargin and rect.top <= doc.clientHeight # We're at least partially visible
|
||||
|
||||
if postEl
|
||||
{height} = postEl.getBoundingClientRect()
|
||||
if Header.getTopOf(postEl) >= -height and Header.getBottomOf(postEl) >= -height # We're at least partially visible
|
||||
root = postEl.parentNode
|
||||
axe = if delta is +1
|
||||
'following'
|
||||
else
|
||||
'preceding'
|
||||
next = $.x "#{axe}-sibling::div[contains(@class,'replyContainer')][1]/child::div[contains(@class,'reply')]", root
|
||||
unless next
|
||||
@focus postEl
|
||||
return
|
||||
return unless g.VIEW is 'thread' or $.x('ancestor::div[parent::div[@class="board"]]', next) is thread
|
||||
rect = next.getBoundingClientRect()
|
||||
if rect.top < 0 or rect.bottom > doc.clientHeight
|
||||
if delta is -1
|
||||
window.scrollBy 0, rect.top - topMargin
|
||||
else
|
||||
next.scrollIntoView false
|
||||
return unless next = $.x "#{axe}-sibling::div[contains(@class,'replyContainer')][1]/child::div[contains(@class,'reply')]", root
|
||||
Header.scrollToIfNeeded next, delta is +1
|
||||
@focus next
|
||||
$.rmClass postEl, 'highlight'
|
||||
return
|
||||
$.rmClass postEl, 'highlight'
|
||||
|
||||
replies = $$ '.reply', thread
|
||||
replies.reverse() if delta is -1
|
||||
for reply in replies
|
||||
rect = reply.getBoundingClientRect()
|
||||
if delta is +1 and rect.top >= topMargin or delta is -1 and rect.bottom <= doc.clientHeight
|
||||
if delta is +1 and Header.getTopOf(reply) > 0 or delta is -1 and Header.getBottomOf(reply) > 0
|
||||
@focus reply
|
||||
return
|
||||
|
||||
|
||||
@ -38,29 +38,24 @@ Nav =
|
||||
else
|
||||
Nav.scroll +1
|
||||
|
||||
getThread: (full) ->
|
||||
if Conf['Bottom header']
|
||||
topMargin = 0
|
||||
else
|
||||
headRect = Header.toggle.getBoundingClientRect()
|
||||
topMargin = headRect.top + headRect.height
|
||||
threads = $$('.thread').filter (thread) ->
|
||||
thread = Get.threadFromRoot thread
|
||||
!(thread.isHidden and !thread.stub)
|
||||
for thread, i in threads
|
||||
rect = thread.getBoundingClientRect()
|
||||
if rect.bottom > topMargin # not scrolled past
|
||||
return if full then [threads, thread, i, rect, topMargin] else thread
|
||||
getThread: ->
|
||||
for threadRoot in $$ '.thread'
|
||||
thread = Get.threadFromRoot threadRoot
|
||||
continue if thread.isHidden and !thread.stub
|
||||
if Header.getTopOf(threadRoot) >= -threadRoot.getBoundingClientRect().height # not scrolled past
|
||||
return threadRoot
|
||||
return $ '.board'
|
||||
|
||||
scroll: (delta) ->
|
||||
[threads, thread, i, rect, topMargin] = Nav.getThread true
|
||||
top = rect.top - topMargin
|
||||
|
||||
# unless we're not at the beginning of the current thread
|
||||
# (and thus wanting to move to beginning)
|
||||
# or we're above the first thread and don't want to skip it
|
||||
if (delta is -1 and top > -5) or (delta is +1 and top < 5)
|
||||
top = threads[i + delta]?.getBoundingClientRect().top - topMargin
|
||||
|
||||
window.scrollBy 0, top
|
||||
thread = Nav.getThread()
|
||||
axe = if delta is +1
|
||||
'following'
|
||||
else
|
||||
'preceding'
|
||||
if next = $.x "#{axe}-sibling::div[contains(@class,'thread')][1]", thread
|
||||
# Unless we're not at the beginning of the current thread,
|
||||
# and thus wanting to move to beginning,
|
||||
# or we're above the first thread and don't want to skip it.
|
||||
top = Header.getTopOf thread
|
||||
thread = next if delta is +1 and top < 5 or delta is -1 and top > -5
|
||||
Header.scrollTo thread
|
||||
|
||||
@ -45,5 +45,5 @@ Favicon =
|
||||
Favicon.unread = Favicon.unreadNSFW
|
||||
Favicon.unreadY = Favicon.unreadNSFWY
|
||||
|
||||
dead: 'data:image/gif;base64,<%= grunt.file.read("img/favicons/dead.gif", {encoding: "base64"}) %>'
|
||||
logo: 'data:image/png;base64,<%= grunt.file.read("img/icon128.png", {encoding: "base64"}) %>'
|
||||
dead: 'data:image/gif;base64,<%= grunt.file.read("img/favicons/dead.gif", {encoding: "base64"}) %>'
|
||||
logo: 'data:image/png;base64,<%= grunt.file.read("img/icon128.png", {encoding: "base64"}) %>'
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
ThreadStats =
|
||||
init: ->
|
||||
return if g.VIEW isnt 'thread' or !Conf['Thread Stats']
|
||||
@dialog = UI.dialog 'thread-stats', 'bottom: 0; left: 0;', """
|
||||
<%= grunt.file.read('html/Monitoring/ThreadStats.html').replace(/>\s+</g, '><').trim() %>
|
||||
"""
|
||||
@dialog = UI.dialog 'thread-stats', 'bottom: 0; left: 0;', <%= importHTML('Monitoring/ThreadStats') %>
|
||||
|
||||
@postCountEl = $ '#post-count', @dialog
|
||||
@fileCountEl = $ '#file-count', @dialog
|
||||
|
||||
@ -2,14 +2,19 @@ ThreadUpdater =
|
||||
init: ->
|
||||
return if g.VIEW isnt 'thread' or !Conf['Thread Updater']
|
||||
|
||||
@button = $.el 'a',
|
||||
className: 'thread-refresh-shortcut fa fa-refresh'
|
||||
title: 'Refresh Thread'
|
||||
href: 'javascript:;'
|
||||
$.on @button, 'click', @update
|
||||
Header.addShortcut @button, 1
|
||||
|
||||
html = ''
|
||||
for name, conf of Config.updater.checkbox
|
||||
checked = if Conf[name] then 'checked' else ''
|
||||
html += "<div><label title='#{conf[1]}'><input name='#{name}' type=checkbox #{checked}> #{name}</label></div>"
|
||||
|
||||
html = """
|
||||
<%= grunt.file.read('html/Monitoring/ThreadUpdater.html').replace(/>\s+</g, '><').trim() %>
|
||||
"""
|
||||
html = <%= importHTML('Monitoring/ThreadUpdater') %>
|
||||
|
||||
@dialog = UI.dialog 'updater', 'bottom: 0; right: 0;', html
|
||||
@timer = $ '#update-timer', @dialog
|
||||
@ -81,6 +86,7 @@ ThreadUpdater =
|
||||
ThreadUpdater.interval = @value = val
|
||||
$.cb.value.call @ if e
|
||||
load: (e) ->
|
||||
$.rmClass ThreadUpdater.button, 'fa-spin'
|
||||
{req} = ThreadUpdater
|
||||
delete ThreadUpdater.req
|
||||
if e.type isnt 'loadend' # timeout or abort
|
||||
@ -143,9 +149,10 @@ ThreadUpdater =
|
||||
|
||||
update: ->
|
||||
return unless navigator.onLine
|
||||
$.addClass ThreadUpdater.button, 'fa-spin'
|
||||
ThreadUpdater.count()
|
||||
ThreadUpdater.set 'timer', '...'
|
||||
ThreadUpdater.req.abort() if ThreadUpdater.req
|
||||
ThreadUpdater.req?.abort()
|
||||
url = "//api.4chan.org/#{ThreadUpdater.thread.board}/res/#{ThreadUpdater.thread}.json"
|
||||
ThreadUpdater.req = $.ajax url,
|
||||
onabort: ThreadUpdater.cb.load
|
||||
@ -155,38 +162,27 @@ ThreadUpdater =
|
||||
,
|
||||
whenModified: true
|
||||
|
||||
updateThreadStatus: (title, OP) ->
|
||||
titleLC = title.toLowerCase()
|
||||
return if ThreadUpdater.thread["is#{title}"] is !!OP[titleLC]
|
||||
unless ThreadUpdater.thread["is#{title}"] = !!OP[titleLC]
|
||||
message = if title is 'Sticky'
|
||||
'The thread is not a sticky anymore.'
|
||||
updateThreadStatus: (type, status) ->
|
||||
return unless hasChanged = ThreadUpdater.thread["is#{type}"] isnt status
|
||||
ThreadUpdater.thread.setStatus type, status
|
||||
change = if type is 'Sticky'
|
||||
if status
|
||||
'now a sticky'
|
||||
else
|
||||
'The thread is not closed anymore.'
|
||||
new Notice 'info', message, 30
|
||||
$.rm $ ".#{titleLC}Icon", ThreadUpdater.thread.OP.nodes.info
|
||||
return
|
||||
message = if title is 'Sticky'
|
||||
'The thread is now a sticky.'
|
||||
'not a sticky anymore'
|
||||
else
|
||||
'The thread is now closed.'
|
||||
new Notice 'info', message, 30
|
||||
icon = $.el 'img',
|
||||
src: "//static.4chan.org/image/#{titleLC}.gif"
|
||||
alt: title
|
||||
title: title
|
||||
className: "#{titleLC}Icon"
|
||||
root = $ '[title="Quote this post"]', ThreadUpdater.thread.OP.nodes.info
|
||||
if title is 'Closed'
|
||||
root = $('.stickyIcon', ThreadUpdater.thread.OP.nodes.info) or root
|
||||
$.after root, [$.tn(' '), icon]
|
||||
if status
|
||||
'now closed'
|
||||
else
|
||||
'not closed anymore'
|
||||
new Notice 'info', "The thread is #{change}.", 30
|
||||
|
||||
parse: (postObjects) ->
|
||||
OP = postObjects[0]
|
||||
Build.spoilerRange[ThreadUpdater.thread.board] = OP.custom_spoiler
|
||||
|
||||
ThreadUpdater.updateThreadStatus 'Sticky', OP
|
||||
ThreadUpdater.updateThreadStatus 'Closed', OP
|
||||
ThreadUpdater.updateThreadStatus 'Sticky', !!OP.sticky
|
||||
ThreadUpdater.updateThreadStatus 'Closed', !!OP.closed
|
||||
ThreadUpdater.thread.postLimit = !!OP.bumplimit
|
||||
ThreadUpdater.thread.fileLimit = !!OP.imagelimit
|
||||
|
||||
@ -250,15 +246,14 @@ ThreadUpdater =
|
||||
ThreadUpdater.lastPost = posts[count - 1].ID
|
||||
Main.callbackNodes Post, posts
|
||||
|
||||
scroll = Conf['Auto Scroll'] and ThreadUpdater.scrollBG() and
|
||||
ThreadUpdater.root.getBoundingClientRect().bottom - doc.clientHeight < 25
|
||||
scroll = Conf['Auto Scroll'] and ThreadUpdater.scrollBG() and Header.getBottomOf(ThreadUpdater.root) > -25
|
||||
$.add ThreadUpdater.root, nodes
|
||||
sendEvent()
|
||||
if scroll
|
||||
if Conf['Bottom Scroll']
|
||||
window.scrollTo 0, d.body.clientHeight
|
||||
else
|
||||
Header.scrollToPost nodes[0]
|
||||
Header.scrollTo nodes[0]
|
||||
|
||||
# Enable 4chan features.
|
||||
threadID = ThreadUpdater.thread.ID
|
||||
|
||||
@ -3,15 +3,17 @@ ThreadWatcher =
|
||||
return if !Conf['Thread Watcher']
|
||||
|
||||
@db = new DataBoard 'watchedThreads', @refresh, true
|
||||
@dialog = UI.dialog 'thread-watcher', 'top: 50px; left: 0px;', """
|
||||
<%= grunt.file.read('html/Monitoring/ThreadWatcher.html').replace(/>\s+</g, '><').trim() %>
|
||||
"""
|
||||
@dialog = UI.dialog 'thread-watcher', 'top: 50px; left: 0px;', <%= importHTML('Monitoring/ThreadWatcher') %>
|
||||
@status = $ '#watcher-status', @dialog
|
||||
@list = @dialog.lastElementChild
|
||||
|
||||
$.on d, 'QRPostSuccessful', @cb.post
|
||||
$.on d, 'ThreadUpdate', @cb.threadUpdate if g.VIEW is 'thread'
|
||||
$.on d, '4chanXInitFinished', @ready
|
||||
switch g.VIEW
|
||||
when 'index'
|
||||
$.on d, 'IndexRefresh', @cb.onIndexRefresh
|
||||
when 'thread'
|
||||
$.on d, 'ThreadUpdate', @cb.onThreadRefresh
|
||||
|
||||
now = Date.now()
|
||||
if (@db.data.lastChecked or 0) < now - 2 * $.HOUR
|
||||
@ -69,7 +71,17 @@ ThreadWatcher =
|
||||
$.set 'AutoWatch', threadID
|
||||
else if Conf['Auto Watch Reply']
|
||||
ThreadWatcher.add board.threads[threadID]
|
||||
threadUpdate: (e) ->
|
||||
onIndexRefresh: ->
|
||||
{db} = ThreadWatcher
|
||||
boardID = g.BOARD.ID
|
||||
for threadID, data of db.data.boards[boardID] when not data.isDead and threadID not of g.BOARD.threads
|
||||
if Conf['Auto Prune']
|
||||
ThreadWatcher.db.delete {boardID, threadID}
|
||||
else
|
||||
data.isDead = true
|
||||
ThreadWatcher.db.set {boardID, threadID, val: data}
|
||||
ThreadWatcher.refresh()
|
||||
onThreadRefresh: (e) ->
|
||||
{thread} = e.detail
|
||||
return unless e.detail[404] and ThreadWatcher.db.get {boardID: thread.board.ID, threadID: thread.ID}
|
||||
# Update 404 status.
|
||||
@ -100,7 +112,7 @@ ThreadWatcher =
|
||||
ThreadWatcher.status.textContent = status
|
||||
return if @status isnt 404
|
||||
if Conf['Auto Prune']
|
||||
ThreadWatcher.rm boardID, threadID
|
||||
ThreadWatcher.db.delete {boardID, threadID}
|
||||
else
|
||||
data.isDead = true
|
||||
ThreadWatcher.db.set {boardID, threadID, val: data}
|
||||
|
||||
@ -42,18 +42,16 @@ Unread =
|
||||
while root = $.x 'preceding-sibling::div[contains(@class,"replyContainer")][1]', post.nodes.root
|
||||
break unless (post = Get.postFromRoot root).isHidden
|
||||
return unless root
|
||||
onload = -> root.scrollIntoView false if checkPosition 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
|
||||
onload = -> Header.scrollToPost root if checkPosition root
|
||||
checkPosition = (target) ->
|
||||
# Scroll to the target unless we scrolled past it.
|
||||
target.getBoundingClientRect().bottom > doc.clientHeight
|
||||
# Prevent the browser to scroll back to
|
||||
# the previous scroll location on page load.
|
||||
$.on window, 'load', onload
|
||||
$.on window, 'load', ->
|
||||
# Scroll to the target unless we scrolled past it.
|
||||
Header.scrollTo root, down if Header.getBottomOf(root) < 0
|
||||
|
||||
sync: ->
|
||||
lastReadPost = Unread.db.get
|
||||
@ -102,7 +100,7 @@ Unread =
|
||||
body: post.info.comment
|
||||
icon: Favicon.logo
|
||||
notif.onclick = ->
|
||||
Header.scrollToPost post.nodes.root
|
||||
Header.scrollToIfNeeded post.nodes.root, true
|
||||
window.focus()
|
||||
notif.onshow = ->
|
||||
setTimeout ->
|
||||
@ -132,9 +130,8 @@ Unread =
|
||||
|
||||
read: (e) ->
|
||||
return if d.hidden or !Unread.posts.length
|
||||
height = doc.clientHeight
|
||||
for post, i in Unread.posts
|
||||
break if post.nodes.root.getBoundingClientRect().bottom > height # post is not completely read
|
||||
break if Header.getBottomOf(post.nodes.root) < -1 # post is not completely read
|
||||
return unless i
|
||||
|
||||
Unread.lastReadPost = Unread.posts.splice(0, i)[i - 1].ID
|
||||
|
||||
@ -26,7 +26,7 @@ QR =
|
||||
$.event 'CloseMenu'
|
||||
QR.open()
|
||||
QR.nodes.com.focus()
|
||||
Header.addShortcut sc, 1
|
||||
Header.addShortcut sc, 2
|
||||
|
||||
$.on d, 'QRGetSelectedPost', ({detail: cb}) ->
|
||||
cb QR.selected
|
||||
@ -39,11 +39,15 @@ QR =
|
||||
$.on d, 'dragover', QR.dragOver
|
||||
$.on d, 'drop', QR.dropFile
|
||||
$.on d, 'dragstart dragend', QR.drag
|
||||
$.on d, 'ThreadUpdate', ->
|
||||
if g.DEAD
|
||||
QR.abort()
|
||||
else
|
||||
QR.status()
|
||||
switch g.VIEW
|
||||
when 'index'
|
||||
$.on d, 'IndexRefresh', QR.generatePostableThreadsList
|
||||
when 'thread'
|
||||
$.on d, 'ThreadUpdate', ->
|
||||
if g.DEAD
|
||||
QR.abort()
|
||||
else
|
||||
QR.status()
|
||||
|
||||
QR.persist() if Conf['Persistent QR']
|
||||
|
||||
@ -697,7 +701,7 @@ QR =
|
||||
|
||||
imgContainer = $.el 'div',
|
||||
className: 'captcha-img'
|
||||
title: 'Reload'
|
||||
title: 'Reload reCAPTCHA'
|
||||
innerHTML: '<img>'
|
||||
input = $.el 'input',
|
||||
className: 'captcha-input field'
|
||||
@ -796,10 +800,27 @@ QR =
|
||||
return
|
||||
e.preventDefault()
|
||||
|
||||
generatePostableThreadsList: ->
|
||||
return unless QR.nodes
|
||||
list = QR.nodes.thread
|
||||
options = [list.firstChild]
|
||||
for thread of g.BOARD.threads
|
||||
options.push $.el 'option',
|
||||
value: thread
|
||||
textContent: "Thread No.#{thread}"
|
||||
val = list.value
|
||||
$.rmAll list
|
||||
$.add list, options
|
||||
list.value = val
|
||||
return unless list.value
|
||||
# Fix the value if the option disappeared.
|
||||
list.value = if g.VIEW is 'thread'
|
||||
g.THREADID
|
||||
else
|
||||
'new'
|
||||
|
||||
dialog: ->
|
||||
dialog = UI.dialog 'qr', 'top:0;right:0;', """
|
||||
<%= grunt.file.read('html/Posting/QR.html').replace(/>\s+</g, '><').trim() %>
|
||||
"""
|
||||
dialog = UI.dialog 'qr', 'top:0;right:0;', <%= importHTML('Posting/QR') %>
|
||||
|
||||
QR.nodes = nodes =
|
||||
el: dialog
|
||||
@ -867,12 +888,6 @@ QR =
|
||||
nodes.flag.dataset.default = '0'
|
||||
$.add nodes.form, nodes.flag
|
||||
|
||||
# Make a list of threads.
|
||||
for thread of g.BOARD.threads
|
||||
$.add nodes.thread, $.el 'option',
|
||||
value: thread
|
||||
textContent: "Thread No.#{thread}"
|
||||
|
||||
<% if (type === 'userscript') { %>
|
||||
# XXX Firefox lacks focusin/focusout support.
|
||||
for elm in $$ '*', QR.nodes.el
|
||||
@ -906,6 +921,7 @@ QR =
|
||||
$.set 'QR Size', @style.cssText
|
||||
<% } %>
|
||||
|
||||
QR.generatePostableThreadsList()
|
||||
QR.persona.init()
|
||||
new QR.post true
|
||||
QR.status()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user