Merge pull request #1316 from MayhemYDG/index

[WIP] Index navigation improvements
This commit is contained in:
Mayhem 2013-11-03 12:13:44 -08:00
commit bb83fac363
31 changed files with 700 additions and 382 deletions

View File

@ -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>
![index navigation](img/changelog/3.12.0/0.png)
- 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. Added a keybind to open the catalog search field on index pages.
### 3.11.5 - *2013-10-03* ### 3.11.5 - *2013-10-03*

View File

@ -1,11 +1,17 @@
module.exports = (grunt) -> module.exports = (grunt) ->
importHTML = (filename) ->
"\"\"\"#{grunt.file.read("html/#{filename}.html").replace(/^\s+|\s+$</gm, '').replace(/\n/g, '')}\"\"\""
# Project configuration. # Project configuration.
grunt.initConfig grunt.initConfig
pkg: grunt.file.readJSON 'package.json' pkg: grunt.file.readJSON 'package.json'
concat: concat:
options: process: Object.create(null, data: options: process: Object.create(null, data:
get: -> grunt.config 'pkg' get: ->
pkg = grunt.config 'pkg'
pkg.importHTML = importHTML
pkg
enumerable: true enumerable: true
) )
coffee: coffee:
@ -17,6 +23,7 @@ module.exports = (grunt) ->
'src/General/Header.coffee' 'src/General/Header.coffee'
'src/General/Notice.coffee' 'src/General/Notice.coffee'
'src/General/Settings.coffee' 'src/General/Settings.coffee'
'src/General/Index.coffee'
'src/General/Get.coffee' 'src/General/Get.coffee'
'src/General/Build.coffee' 'src/General/Build.coffee'
# Features --> # Features -->
@ -78,9 +85,10 @@ module.exports = (grunt) ->
stdout: true stdout: true
stderr: true stderr: true
failOnError: true failOnError: true
checkout:
command: 'git checkout <%= pkg.meta.mainBranch %>'
commit: commit:
command: """ command: """
git checkout <%= pkg.meta.mainBranch %>
git commit -am "Release <%= pkg.meta.name %> v<%= pkg.version %>." git commit -am "Release <%= pkg.meta.name %> v<%= pkg.version %>."
git tag -a <%= pkg.version %> -m "<%= 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 %>." 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 'release', ['shell:commit', 'shell:push', 'build-crx', 'compress:crx']
grunt.registerTask 'patch', ['bump', 'updcl:3', 'release'] grunt.registerTask 'patch', ['shell:checkout', 'bump', 'updcl:3', 'release']
grunt.registerTask 'minor', ['bump:minor', 'updcl:2', 'release'] grunt.registerTask 'minor', ['shell:checkout', 'bump:minor', 'updcl:2', 'release']
grunt.registerTask 'major', ['bump:major', 'updcl:1', 'release'] grunt.registerTask 'major', ['shell:checkout', 'bump:major', 'updcl:1', 'release']
grunt.registerTask 'updcl', 'Update the changelog', (headerLevel) -> grunt.registerTask 'updcl', 'Update the changelog', (headerLevel) ->
headerPrefix = new Array(+headerLevel + 1).join '#' headerPrefix = new Array(+headerLevel + 1).join '#'

View File

@ -362,6 +362,15 @@ a[href="javascript:;"] {
overflow: hidden; overflow: hidden;
} }
/* Index */
:root.index-loading .board,
:root.index-loading .pagelist {
display: none;
}
.summary {
text-decoration: none;
}
/* Announcement Hiding */ /* Announcement Hiding */
:root.hide-announcement #globalMessage, :root.hide-announcement #globalMessage,
:root.hide-announcement-enabled #toggleMsgBtn { :root.hide-announcement-enabled #toggleMsgBtn {

View 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>

View File

@ -23,7 +23,7 @@
For example: <code>highlight;</code> or <code>highlight:wallpaper;</code>. For example: <code>highlight;</code> or <code>highlight:wallpaper;</code>.
</li> </li>
<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>. For example: <code>top:yes;</code> or <code>top:no;</code>.
</li> </li>
</ul> </ul>

View File

@ -13,5 +13,5 @@
</label> </label>
</div> </div>
<div> <div>
<input value="Update" type="button" name="Update"> <input value="Refresh thread" type="button" name="Update">
</div> </div>

BIN
img/changelog/3.12.0/0.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -45,7 +45,7 @@ $.ajax = do ->
type or= form and 'post' or 'get' type or= form and 'post' or 'get'
r.open type, url, !sync r.open type, url, !sync
if whenModified 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' $.on r, 'load', -> lastModified[url] = r.getResponseHeader 'Last-Modified'
$.extend r, options $.extend r, options
$.extend r.upload, upCallbacks $.extend r.upload, upCallbacks
@ -104,7 +104,7 @@ $.rm = do ->
(el) -> el.parentNode?.removeChild el (el) -> el.parentNode?.removeChild el
$.rmAll = (root) -> $.rmAll = (root) ->
# jsperf.com/emptify-element # jsperf.com/emptify-element
while node = root.firstChild for node in [root.childNodes...]
# HTMLSelectElement.remove !== Element.remove # HTMLSelectElement.remove !== Element.remove
root.removeChild node root.removeChild node
return return

View File

@ -110,13 +110,8 @@ Filter =
# Highlight # Highlight
$.addClass @nodes.root, result.class $.addClass @nodes.root, result.class
if !@isReply and result.top and g.VIEW is 'index' if !@isReply and result.top
# Put the highlighted OPs' thread on top of the board page... @thread.isOnTop = true
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]
name: (post) -> name: (post) ->
if 'name' of post.info if 'name' of post.info

View File

@ -4,6 +4,7 @@ ThreadHiding =
@db = new DataBoard 'hiddenThreads' @db = new DataBoard 'hiddenThreads'
@syncCatalog() @syncCatalog()
$.on d, 'IndexRefresh', @onrefresh
Thread.callbacks.push Thread.callbacks.push
name: 'Thread Hiding' name: 'Thread Hiding'
cb: @node cb: @node
@ -14,6 +15,15 @@ ThreadHiding =
return unless Conf['Thread Hiding'] return unless Conf['Thread Hiding']
$.prepend @OP.nodes.root, ThreadHiding.makeButton @, 'hide' $.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: -> syncCatalog: ->
# Sync hidden threads from the catalog into the index. # Sync hidden threads from the catalog into the index.
hiddenThreads = ThreadHiding.db.get hiddenThreads = ThreadHiding.db.get

View File

@ -60,6 +60,10 @@ Build =
isOP = postID is threadID isOP = postID is threadID
staticPath = '//static.4chan.org/image/' staticPath = '//static.4chan.org/image/'
gifIcon = if window.devicePixelRatio >= 2
'@2x.gif'
else
'.gif'
if email if email
emailStart = '<a href="mailto:' + email + '" class="useremail">' emailStart = '<a href="mailto:' + email + '" class="useremail">'
@ -82,21 +86,21 @@ Build =
capcodeClass = " capcodeAdmin" capcodeClass = " capcodeAdmin"
capcodeStart = " <strong class='capcode hand id_admin'" + capcodeStart = " <strong class='capcode hand id_admin'" +
"title='Highlight posts by the Administrator'>## Admin</strong>" "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.' " + "alt='This user is the 4chan Administrator.' " +
"title='This user is the 4chan Administrator.' class=identityIcon>" "title='This user is the 4chan Administrator.' class=identityIcon>"
when 'mod' when 'mod'
capcodeClass = " capcodeMod" capcodeClass = " capcodeMod"
capcodeStart = " <strong class='capcode hand id_mod' " + capcodeStart = " <strong class='capcode hand id_mod' " +
"title='Highlight posts by Moderators'>## Mod</strong>" "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.' " + "alt='This user is a 4chan Moderator.' " +
"title='This user is a 4chan Moderator.' class=identityIcon>" "title='This user is a 4chan Moderator.' class=identityIcon>"
when 'developer' when 'developer'
capcodeClass = " capcodeDeveloper" capcodeClass = " capcodeDeveloper"
capcodeStart = " <strong class='capcode hand id_developer' " + capcodeStart = " <strong class='capcode hand id_developer' " +
"title='Highlight posts by Developers'>## Developer</strong>" "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.' " + "alt='This user is a 4chan Developer.' " +
"title='This user is a 4chan Developer.' class=identityIcon>" "title='This user is a 4chan Developer.' class=identityIcon>"
else else
@ -114,11 +118,11 @@ Build =
if file?.isDeleted if file?.isDeleted
fileHTML = if isOP fileHTML = if isOP
"<div class=file id=f#{postID}><div class=fileInfo></div><span class=fileThumb>" + "<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>" "</span></div>"
else else
"<div class=file id=f#{postID}><span class=fileThumb>" + "<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>" "</span></div>"
else if file else if file
ext = file.name[-3..] ext = file.name[-3..]
@ -178,11 +182,16 @@ Build =
'' ''
sticky = if isSticky 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 else
'' ''
closed = if isClosed 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'
" &nbsp; <span>[<a href='/#{boardID}/res/#{threadID}' class=replylink>Reply</a>]</span>"
else else
'' ''
@ -222,7 +231,7 @@ Build =
"<span class='nameBlock#{capcodeClass}'>" + "<span class='nameBlock#{capcodeClass}'>" +
emailStart + emailStart +
"<span class=name>#{name or ''}</span>" + tripcode + "<span class=name>#{name or ''}</span>" + tripcode +
capcodeStart + emailEnd + capcode + userID + flag + sticky + closed + capcodeStart + emailEnd + capcode + userID + flag +
' </span> ' + ' </span> ' +
"<span class=dateTime data-utc=#{dateUTC}>#{date}</span> " + "<span class=dateTime data-utc=#{dateUTC}>#{date}</span> " +
"<span class='postNum desktop'>" + "<span class='postNum desktop'>" +
@ -233,6 +242,7 @@ Build =
else else
"/#{boardID}/res/#{threadID}#q#{postID}" "/#{boardID}/res/#{threadID}#q#{postID}"
}' title='Quote this post'>#{postID}</a>" + }' title='Quote this post'>#{postID}</a>" +
sticky + closed + replyLink +
'</span>' + '</span>' +
'</div>' + '</div>' +
@ -248,3 +258,29 @@ Build =
quote.href = "/#{boardID}/res/#{href}" # Fix pathnames quote.href = "/#{boardID}/res/#{href}" # Fix pathnames
container 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

View File

@ -9,7 +9,6 @@ Config =
'Time Formatting': [true, 'Localize and format timestamps.'] 'Time Formatting': [true, 'Localize and format timestamps.']
'Relative Post Dates': [false, 'Display dates like "3 minutes ago". Tooltip shows the timestamp.'] 'Relative Post Dates': [false, 'Display dates like "3 minutes ago". Tooltip shows the timestamp.']
'File Info Formatting': [true, 'Reformat the file information.'] '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.'] 'Thread Expansion': [true, 'Add buttons to expand threads.']
'Index Navigation': [false, 'Add buttons to navigate between threads.'] 'Index Navigation': [false, 'Add buttons to navigate between threads.']
'Reply Navigation': [false, 'Add buttons to navigate to top / bottom of thread.'] '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/ #//archive.installgentoo.net/%board/image/%MD5;text:View same on installgentoo /%board/
""" """
'Custom CSS': false 'Custom CSS': false
Index:
'Index Mode': 'paged'
'Index Sort': 'bump'
Header: Header:
'Header auto-hide': false 'Header auto-hide': false
'Bottom header': false 'Bottom header': false
@ -170,9 +172,9 @@ Config =
'Eqn tags': ['Alt+e', 'Insert eqn tags.'] 'Eqn tags': ['Alt+e', 'Insert eqn tags.']
'Math tags': ['Alt+m', 'Insert math tags.'] 'Math tags': ['Alt+m', 'Insert math tags.']
'Submit QR': ['Alt+s', 'Submit post.'] 'Submit QR': ['Alt+s', 'Submit post.']
# Thread related # Index/Thread related
'Update': ['r', 'Refresh the index/thread.']
'Watch': ['w', 'Watch thread.'] 'Watch': ['w', 'Watch thread.']
'Update': ['r', 'Update the thread.']
# Images # Images
'Expand image': ['Shift+e', 'Expand selected image.'] 'Expand image': ['Shift+e', 'Expand selected image.']
'Expand images': ['e', 'Expand all images.'] 'Expand images': ['e', 'Expand all images.']

View File

@ -2,12 +2,11 @@ Header =
init: -> init: ->
headerEl = $.el 'div', headerEl = $.el 'div',
id: 'header' id: 'header'
innerHTML: """ innerHTML: <%= importHTML('General/Header') %>
<%= grunt.file.read('html/General/Header.html').replace(/>\s+</g, '><').trim() %>
"""
@bar = $ '#header-bar', headerEl @bar = $ '#header-bar', headerEl
@toggle = $ '#toggle-header-bar', @bar @toggle = $ '#toggle-header-bar', @bar
@noticesRoot = $ '#notifications', headerEl
@menu = new UI.Menu 'header' @menu = new UI.Menu 'header'
menuButton = $.el 'a', menuButton = $.el 'a',
@ -244,32 +243,48 @@ Header =
$('input[name=boardnav]', settings).focus() $('input[name=boardnav]', settings).focus()
hashScroll: -> 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 return if (Get.postFromRoot post).isHidden
Header.scrollToPost post Header.scrollTo post
scrollToPost: (post) -> scrollTo: (root, down, needed) ->
{top} = post.getBoundingClientRect() 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'] unless Conf['Bottom header']
headRect = Header.toggle.getBoundingClientRect() headRect = Header.toggle.getBoundingClientRect()
top -= headRect.top + headRect.height top -= headRect.top + headRect.height
window.scrollBy 0, top 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) -> addShortcut: (el, index) ->
shortcut = $.el 'span', shortcut = $.el 'span',
className: 'shortcut' className: 'shortcut'
shortcut.dataset.index = index
$.add shortcut, el $.add shortcut, el
shortcuts = $ '#shortcuts', Header.bar shortcuts = $ '#shortcuts', Header.bar
nodes = [shortcuts.childNodes...] $.add shortcuts, [shortcuts.childNodes...].concat(shortcut).sort (a, b) -> a.dataset.index - b.dataset.index
nodes.splice index, 0, shortcut
$.add shortcuts, nodes
menuToggle: (e) -> menuToggle: (e) ->
Header.menu.toggle e, @, g Header.menu.toggle e, @, g
createNotification: (e) -> createNotification: (e) ->
{type, content, lifetime, cb} = e.detail {type, content, lifetime, cb} = e.detail
notif = new Notice type, content, lifetime notice = new Notice type, content, lifetime
cb notif if cb cb notice if cb
areNotificationsEnabled: false areNotificationsEnabled: false
enableDesktopNotifications: -> enableDesktopNotifications: ->

281
src/General/Index.coffee Normal file
View 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

View File

@ -69,6 +69,7 @@ Main =
initFeature 'Polyfill', Polyfill initFeature 'Polyfill', Polyfill
initFeature 'Header', Header initFeature 'Header', Header
initFeature 'Settings', Settings initFeature 'Settings', Settings
initFeature 'Index Generator', Index
initFeature 'Announcement Hiding', PSAHiding initFeature 'Announcement Hiding', PSAHiding
initFeature 'Fourchan thingies', Fourchan initFeature 'Fourchan thingies', Fourchan
initFeature 'Custom CSS', CustomCSS initFeature 'Custom CSS', CustomCSS
@ -105,7 +106,6 @@ Main =
initFeature 'Reveal Spoilers', RevealSpoilers initFeature 'Reveal Spoilers', RevealSpoilers
initFeature 'Auto-GIF', AutoGIF initFeature 'Auto-GIF', AutoGIF
initFeature 'Image Hover', ImageHover initFeature 'Image Hover', ImageHover
initFeature 'Comment Expansion', ExpandComment
initFeature 'Thread Expansion', ExpandThread initFeature 'Thread Expansion', ExpandThread
initFeature 'Thread Excerpt', ThreadExcerpt initFeature 'Thread Excerpt', ThreadExcerpt
initFeature 'Favicon', Favicon initFeature 'Favicon', Favicon
@ -169,26 +169,21 @@ Main =
# Something might have gone wrong! # Something might have gone wrong!
Main.initStyle() Main.initStyle()
if board = $ '.board' if g.VIEW is 'thread' and threadRoot = $ '.thread'
threads = [] thread = new Thread +threadRoot.id[1..], g.BOARD
posts = [] posts = []
for postRoot in $$ '.thread > .postContainer', threadRoot
for threadRoot in $$ '.board > .thread', board try
thread = new Thread +threadRoot.id[1..], g.BOARD posts.push new Post postRoot, thread, g.BOARD
threads.push thread catch err
for postRoot in $$ '.thread > .postContainer', threadRoot # Skip posts that we failed to parse.
try errors = [] unless errors
posts.push new Post postRoot, thread, g.BOARD errors.push
catch err message: "Parsing of Post No.#{postRoot.id.match /\d+/} failed. Post will be skipped."
# Skip posts that we failed to parse. error: err
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.handleErrors errors if errors
Main.callbackNodes Thread, threads Main.callbackNodes Thread, [thread]
Main.callbackNodes Post, posts Main.callbackNodes Post, posts
if $.hasClass d.body, 'fourchan_x' if $.hasClass d.body, 'fourchan_x'
@ -207,7 +202,7 @@ Main =
try try
localStorage.getItem '4chan-settings' localStorage.getItem '4chan-settings'
catch err 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 Main.disableReports = true
$.event '4chanXInitFinished' $.event '4chanXInitFinished'

View File

@ -19,7 +19,7 @@ class Notice
$.on d, 'visibilitychange', @add $.on d, 'visibilitychange', @add
return return
$.off d, 'visibilitychange', @add $.off d, 'visibilitychange', @add
$.add $.id('notifications'), @el $.add Header.noticesRoot, @el
@el.clientHeight # force reflow @el.clientHeight # force reflow
@el.style.opacity = 1 @el.style.opacity = 1
setTimeout @close, @timeout * $.SECOND if @timeout setTimeout @close, @timeout * $.SECOND if @timeout

View File

@ -193,6 +193,13 @@ class Post
quotelink.textContent = quotelink.textContent.replace '\u00A0(Dead)', '' quotelink.textContent = quotelink.textContent.replace '\u00A0(Dead)', ''
$.rmClass quotelink, 'deadlink' $.rmClass quotelink, 'deadlink'
return return
collect: ->
@kill()
delete g.posts[@fullID]
delete @thread.posts[@]
delete @board.posts[@]
addClone: (context) -> addClone: (context) ->
new Clone @, context new Clone @, context
rmClone: (index) -> rmClone: (index) ->

View File

@ -61,9 +61,7 @@ Settings =
return if Settings.dialog return if Settings.dialog
$.event 'CloseMenu' $.event 'CloseMenu'
html = """ html = <%= importHTML('General/Settings') %>
<%= grunt.file.read('html/General/Settings.html').replace(/>\s+</g, '><').trim() %>
"""
Settings.dialog = overlay = $.el 'div', Settings.dialog = overlay = $.el 'div',
id: 'overlay' id: 'overlay'
@ -113,9 +111,7 @@ Settings =
section.scrollTop = 0 section.scrollTop = 0
main: (section) -> main: (section) ->
section.innerHTML = """ section.innerHTML = <%= importHTML('General/Settings-section-Main') %>
<%= grunt.file.read('html/General/Settings-section-Main.html').replace(/>\s+</g, '><').trim() %>
"""
$.on $('.export', section), 'click', Settings.export $.on $('.export', section), 'click', Settings.export
$.on $('.import', section), 'click', Settings.import $.on $('.import', section), 'click', Settings.import
$.on $('input', section), 'change', Settings.onImport $.on $('input', section), 'change', Settings.onImport
@ -142,7 +138,7 @@ Settings =
return return
div = $.el 'div', 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 button = $ 'button', div
hiddenNum = 0 hiddenNum = 0
$.get 'hiddenThreads', boards: {}, (item) -> $.get 'hiddenThreads', boards: {}, (item) ->
@ -205,7 +201,7 @@ Settings =
try try
data = JSON.parse e.target.result data = JSON.parse e.target.result
Settings.loadSettings data Settings.loadSettings data
if confirm 'Import successful. Refresh now?' if confirm 'Import successful. Reload now?'
window.location.reload() window.location.reload()
catch err catch err
output.textContent = 'Import failed due to an error.' output.textContent = 'Import failed due to an error.'
@ -290,9 +286,7 @@ Settings =
data data
filter: (section) -> filter: (section) ->
section.innerHTML = """ section.innerHTML = <%= importHTML('General/Settings-section-Filter') %>
<%= grunt.file.read('html/General/Settings-section-Filter.html').replace(/>\s+</g, '><').trim() %>
"""
select = $ 'select', section select = $ 'select', section
$.on select, 'change', Settings.selectFilter $.on select, 'change', Settings.selectFilter
Settings.selectFilter.call select Settings.selectFilter.call select
@ -309,32 +303,24 @@ Settings =
$.on ta, 'change', $.cb.value $.on ta, 'change', $.cb.value
$.add div, ta $.add div, ta
return return
div.innerHTML = """ div.innerHTML = <%= importHTML('General/Settings-section-Filter-guide') %>
<%= grunt.file.read('html/General/Settings-section-Filter-guide.html').replace(/>\s+</g, '><').trim() %>
"""
qr: (section) -> qr: (section) ->
section.innerHTML = """ section.innerHTML = <%= importHTML('General/Settings-section-QR') %>
<%= grunt.file.read('html/General/Settings-section-QR.html').replace(/>\s+</g, '><').trim() %>
"""
ta = $ 'textarea', section ta = $ 'textarea', section
$.get 'QR.personas', Conf['QR.personas'], (item) -> $.get 'QR.personas', Conf['QR.personas'], (item) ->
ta.value = item['QR.personas'] ta.value = item['QR.personas']
$.on ta, 'change', $.cb.value $.on ta, 'change', $.cb.value
sauce: (section) -> sauce: (section) ->
section.innerHTML = """ section.innerHTML = <%= importHTML('General/Settings-section-Sauce') %>
<%= grunt.file.read('html/General/Settings-section-Sauce.html').replace(/>\s+</g, '><').trim() %>
"""
ta = $ 'textarea', section ta = $ 'textarea', section
$.get 'sauces', Conf['sauces'], (item) -> $.get 'sauces', Conf['sauces'], (item) ->
ta.value = item['sauces'] ta.value = item['sauces']
$.on ta, 'change', $.cb.value $.on ta, 'change', $.cb.value
rice: (section) -> rice: (section) ->
section.innerHTML = """ section.innerHTML = <%= importHTML('General/Settings-section-Rice') %>
<%= grunt.file.read('html/General/Settings-section-Rice.html').replace(/>\s+</g, '><').trim() %>
"""
items = {} items = {}
inputs = {} inputs = {}
for name in ['boardnav', 'time', 'backlink', 'fileInfo', 'favicon', 'usercss'] for name in ['boardnav', 'time', 'backlink', 'fileInfo', 'favicon', 'usercss']
@ -395,9 +381,7 @@ Settings =
CustomCSS.update() CustomCSS.update()
archives: (section) -> archives: (section) ->
section.innerHTML = """ section.innerHTML = <%= importHTML('General/Settings-section-Archives') %>
<%= grunt.file.read('html/General/Settings-section-Archives.html').replace(/>\s+</g, '><').trim() %>
"""
showLastUpdateTime = (time) -> showLastUpdateTime = (time) ->
$('time', section).textContent = new Date(time).toLocaleString() $('time', section).textContent = new Date(time).toLocaleString()
@ -470,9 +454,7 @@ Settings =
$.set 'selectedArchives', selectedArchives $.set 'selectedArchives', selectedArchives
keybinds: (section) -> keybinds: (section) ->
section.innerHTML = """ section.innerHTML = <%= importHTML('General/Settings-section-Keybinds') %>
<%= grunt.file.read('html/General/Settings-section-Keybinds.html').replace(/>\s+</g, '><').trim() %>
"""
tbody = $ 'tbody', section tbody = $ 'tbody', section
items = {} items = {}
inputs = {} inputs = {}

View File

@ -3,11 +3,41 @@ class Thread
toString: -> @ID toString: -> @ID
constructor: (@ID, @board) -> constructor: (@ID, @board) ->
@fullID = "#{@board}.#{@ID}" @fullID = "#{@board}.#{@ID}"
@posts = {} @posts = {}
@isSticky = false
@isClosed = false
@postLimit = false
@fileLimit = false
g.threads[@fullID] = board.threads[@] = @ 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: -> kill: ->
@isDead = true @isDead = true
@timeOfDeath = Date.now() @timeOfDeath = Date.now()
collect: ->
for postID, post in @posts
post.collect()
delete g.threads[@fullID]
delete @board.threads[@]

View File

@ -7,7 +7,7 @@ ImageExpand =
title: 'Expand All Images' title: 'Expand All Images'
href: 'javascript:;' href: 'javascript:;'
$.on @EAI, 'click', ImageExpand.cb.toggleAll $.on @EAI, 'click', ImageExpand.cb.toggleAll
Header.addShortcut @EAI, 2 Header.addShortcut @EAI, 3
Post.callbacks.push Post.callbacks.push
name: 'Image Expansion' name: 'Image Expansion'
@ -45,7 +45,7 @@ ImageExpand =
continue unless file and file.isImage and doc.contains post.nodes.root continue unless file and file.isImage and doc.contains post.nodes.root
if ImageExpand.on and if ImageExpand.on and
(!Conf['Expand spoilers'] and file.isSpoiler or (!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 continue
$.queueTask func, post $.queueTask func, post
return return
@ -60,13 +60,10 @@ ImageExpand =
# Scroll back to the thumbnail when contracting the image # Scroll back to the thumbnail when contracting the image
# to avoid being left miles away from the relevant post. # to avoid being left miles away from the relevant post.
rect = post.nodes.root.getBoundingClientRect() top = Header.getTopOf post.nodes.root
if rect.top < 0 if top < 0
y = rect.top y = top
unless Conf['Bottom header'] if post.nodes.root.getBoundingClientRect().left < 0
headRect = Header.toggle.getBoundingClientRect()
y -= headRect.top + headRect.height
if rect.left < 0
x = -window.scrollX x = -window.scrollX
window.scrollBy x, y if x or y window.scrollBy x, y if x or y
ImageExpand.contract post ImageExpand.contract post
@ -104,13 +101,12 @@ ImageExpand =
$.addClass post.nodes.root, 'expanded-image' $.addClass post.nodes.root, 'expanded-image'
$.rmClass post.file.thumb, 'expanding' $.rmClass post.file.thumb, 'expanding'
return return
prev = post.nodes.root.getBoundingClientRect() {bottom} = post.nodes.root.getBoundingClientRect()
$.queueTask -> $.queueTask ->
$.addClass post.nodes.root, 'expanded-image' $.addClass post.nodes.root, 'expanded-image'
$.rmClass post.file.thumb, 'expanding' $.rmClass post.file.thumb, 'expanding'
return unless prev.top + prev.height <= 0 return unless bottom <= 0
curr = post.nodes.root.getBoundingClientRect() window.scrollBy 0, post.nodes.root.getBoundingClientRect().bottom - bottom
window.scrollBy 0, curr.height - prev.height + curr.top - prev.top
error: -> error: ->
post = Get.postFromNode @ post = Get.postFromNode @

View File

@ -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

View File

@ -1,19 +1,26 @@
ExpandThread = ExpandThread =
init: -> init: ->
return if g.VIEW isnt 'index' or !Conf['Thread Expansion'] return if g.VIEW isnt 'index' or !Conf['Thread Expansion']
@statuses = {}
$.on d, 'IndexRefresh', @onIndexRefresh
Thread.callbacks.push Thread.callbacks.push
name: 'Thread Expansion' name: 'Thread Expansion'
cb: @node cb: @node
node: -> node: ->
return unless span = $.x 'following-sibling::span[contains(@class,"summary")][1]', @OP.nodes.root ExpandThread.setButton @
[posts, files] = span.textContent.match /\d+/g
a = $.el 'a', setButton: (thread) ->
textContent: ExpandThread.text '+', posts, files return unless a = $.x 'following-sibling::a[contains(@class,"summary")][1]', thread.OP.nodes.root
className: 'summary' a.textContent = ExpandThread.text '+', a.textContent.match(/\d+/g)...
href: 'javascript:;'
$.on a, 'click', ExpandThread.cbToggle $.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, posts, files) ->
text = [status] text = [status]
@ -22,95 +29,77 @@ ExpandThread =
text.push if status is '-' then 'shown' else 'omitted' text.push if status is '-' then 'shown' else 'omitted'
text.join(' ') + '.' 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 @ ExpandThread.toggle Get.threadFromNode @
toggle: (thread) -> toggle: (thread) ->
threadRoot = thread.OP.nodes.root.parentNode threadRoot = thread.OP.nodes.root.parentNode
a = $ '.summary', threadRoot return unless a = $ '.summary', threadRoot
if thread.ID of ExpandThread.statuses
switch thread.isExpanded ExpandThread.contract thread, a, threadRoot
when false, undefined else
for post in $$ '.thread > .postContainer', threadRoot ExpandThread.expand thread, a, threadRoot
ExpandComment.expand Get.postFromRoot post expand: (thread, a, threadRoot) ->
unless a ExpandThread.statuses[thread] = status = {}
thread.isExpanded = true a.textContent = ExpandThread.text '...', a.textContent.match(/\d+/g)...
return status.req = $.cache "//api.4chan.org/#{thread.board}/res/#{thread}.json", ->
thread.isExpanded = 'loading' delete status.req
[posts, files] = a.textContent.match /\d+/g ExpandThread.parse @, thread, a
a.textContent = ExpandThread.text '...', posts, files contract: (thread, a, threadRoot) ->
$.cache "//api.4chan.org/#{thread.board}/res/#{thread}.json", -> status = ExpandThread.statuses[thread]
ExpandThread.parse @, thread, a delete ExpandThread.statuses[thread]
if status.req
when 'loading' status.req.abort()
thread.isExpanded = false a.textContent = ExpandThread.text '+', a.textContent.match(/\d+/g)... if a
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 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 data = JSON.parse(req.response).posts
if spoilerRange = posts.shift().custom_spoiler Build.spoilerRange[thread.board] = data.shift().custom_spoiler
Build.spoilerRange[thread.board] = spoilerRange
postsObj = [] posts = []
postsRoot = [] postsRoot = []
filesCount = 0 filesCount = 0
for reply in posts for postData in data
if post = thread.posts[reply.no] if post = thread.posts[postData.no]
filesCount++ if 'file' of post filesCount++ if 'file' of post
postsRoot.push post.nodes.root postsRoot.push post.nodes.root
continue continue
root = Build.postFromObject reply, thread.board.ID root = Build.postFromObject postData, thread.board.ID
post = new Post root, thread, thread.board 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 filesCount++ if 'file' of post
postsObj.push post posts.push post
postsRoot.push root postsRoot.push root
Main.callbackNodes Post, postsObj Main.callbackNodes Post, posts
$.after a, postsRoot $.after a, postsRoot
postsCount = postsRoot.length postsCount = postsRoot.length
a.textContent = ExpandThread.text '-', postsCount, filesCount a.textContent = ExpandThread.text '-', postsCount, filesCount
# Enable 4chan features. # Enable 4chan features.
if Conf['Enable 4chan\'s Extension'] if Conf['Enable 4chan\'s Extension']
$.globalEval "Parser.parseThread(#{thread.ID}, 1, #{postsCount})" $.globalEval "Parser.parseThread(#{thread}, 1, #{postsCount})"
else else
Fourchan.parseThread thread.ID, 1, postsCount Fourchan.parseThread thread.ID, 1, postsCount

View File

@ -6,8 +6,9 @@ Fourchan =
if board is 'g' if board is 'g'
$.globalEval """ $.globalEval """
window.addEventListener('prettyprint', function(e) { window.addEventListener('prettyprint', function(e) {
var pre = e.detail; window.dispatchEvent(new CustomEvent('prettyprint:cb', {
pre.innerHTML = prettyPrintOne(pre.innerHTML); detail: prettyPrintOne(e.detail)
}));
}, false); }, false);
""" """
Post.callbacks.push Post.callbacks.push
@ -32,9 +33,11 @@ Fourchan =
cb: @math cb: @math
code: -> code: ->
return if @isClone return if @isClone
apply = (e) -> pre.innerHTML = e.detail
$.on window, 'prettyprint:cb', apply
for pre in $$ '.prettyprint:not(.prettyprinted)', @nodes.comment for pre in $$ '.prettyprint:not(.prettyprinted)', @nodes.comment
$.event 'prettyprint', pre, window $.event 'prettyprint', pre.innerHTML, window
$.addClass pre, 'prettyprinted' $.off window, 'prettyprint:cb', apply
return return
math: -> math: ->
return if @isClone or !$ '.math', @nodes.comment return if @isClone or !$ '.math', @nodes.comment

View File

@ -7,7 +7,7 @@ Keybinds =
init = -> init = ->
$.off d, '4chanXInitFinished', init $.off d, '4chanXInitFinished', init
$.on d, 'keydown', Keybinds.keydown $.on d, 'keydown', Keybinds.keydown
for node in $$ '[accesskey]' for node in $$ '[accesskey]'
node.removeAttribute 'accesskey' node.removeAttribute 'accesskey'
return return
@ -58,11 +58,15 @@ Keybinds =
Keybinds.tags 'math', target Keybinds.tags 'math', target
when Conf['Submit QR'] when Conf['Submit QR']
QR.submit() if QR.nodes and !QR.status() 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'] when Conf['Watch']
ThreadWatcher.toggle thread ThreadWatcher.toggle thread
when Conf['Update']
ThreadUpdater.update()
# Images # Images
when Conf['Expand image'] when Conf['Expand image']
Keybinds.img threadRoot Keybinds.img threadRoot
@ -70,15 +74,18 @@ Keybinds =
Keybinds.img threadRoot, true Keybinds.img threadRoot, true
# Board Navigation # Board Navigation
when Conf['Front page'] 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'] when Conf['Open front page']
$.open "/#{g.BOARD}/#delform" $.open "/#{g.BOARD}/"
when Conf['Next page'] when Conf['Next page']
if form = $ '.next form' return unless g.VIEW is 'index' and Conf['Index Mode'] is 'paged'
window.location = form.action $('.next button', Index.pagelist).click()
when Conf['Previous page'] when Conf['Previous page']
if form = $ '.prev form' return unless g.VIEW is 'index' and Conf['Index Mode'] is 'paged'
window.location = form.action $('.prev button', Index.pagelist).click()
when Conf['Search form'] when Conf['Search form']
$.id('search-btn').click() $.id('search-btn').click()
# Thread Navigation # Thread Navigation
@ -176,43 +183,31 @@ Keybinds =
location.href = url location.href = url
hl: (delta, thread) -> hl: (delta, thread) ->
postEl = $ '.reply.highlight', thread
unless delta unless delta
if postEl = $ '.reply.highlight', thread $.rmClass postEl, 'highlight' if postEl
$.rmClass postEl, 'highlight'
return return
if Conf['Bottom header']
topMargin = 0 if postEl
else {height} = postEl.getBoundingClientRect()
headRect = Header.toggle.getBoundingClientRect() if Header.getTopOf(postEl) >= -height and Header.getBottomOf(postEl) >= -height # We're at least partially visible
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
root = postEl.parentNode root = postEl.parentNode
axe = if delta is +1 axe = if delta is +1
'following' 'following'
else else
'preceding' 'preceding'
next = $.x "#{axe}-sibling::div[contains(@class,'replyContainer')][1]/child::div[contains(@class,'reply')]", root return unless next = $.x "#{axe}-sibling::div[contains(@class,'replyContainer')][1]/child::div[contains(@class,'reply')]", root
unless next Header.scrollToIfNeeded next, delta is +1
@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
@focus next @focus next
$.rmClass postEl, 'highlight'
return return
$.rmClass postEl, 'highlight'
replies = $$ '.reply', thread replies = $$ '.reply', thread
replies.reverse() if delta is -1 replies.reverse() if delta is -1
for reply in replies for reply in replies
rect = reply.getBoundingClientRect() if delta is +1 and Header.getTopOf(reply) > 0 or delta is -1 and Header.getBottomOf(reply) > 0
if delta is +1 and rect.top >= topMargin or delta is -1 and rect.bottom <= doc.clientHeight
@focus reply @focus reply
return return

View File

@ -38,29 +38,24 @@ Nav =
else else
Nav.scroll +1 Nav.scroll +1
getThread: (full) -> getThread: ->
if Conf['Bottom header'] for threadRoot in $$ '.thread'
topMargin = 0 thread = Get.threadFromRoot threadRoot
else continue if thread.isHidden and !thread.stub
headRect = Header.toggle.getBoundingClientRect() if Header.getTopOf(threadRoot) >= -threadRoot.getBoundingClientRect().height # not scrolled past
topMargin = headRect.top + headRect.height return threadRoot
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
return $ '.board' return $ '.board'
scroll: (delta) -> scroll: (delta) ->
[threads, thread, i, rect, topMargin] = Nav.getThread true thread = Nav.getThread()
top = rect.top - topMargin axe = if delta is +1
'following'
# unless we're not at the beginning of the current thread else
# (and thus wanting to move to beginning) 'preceding'
# or we're above the first thread and don't want to skip it if next = $.x "#{axe}-sibling::div[contains(@class,'thread')][1]", thread
if (delta is -1 and top > -5) or (delta is +1 and top < 5) # Unless we're not at the beginning of the current thread,
top = threads[i + delta]?.getBoundingClientRect().top - topMargin # and thus wanting to move to beginning,
# or we're above the first thread and don't want to skip it.
window.scrollBy 0, top top = Header.getTopOf thread
thread = next if delta is +1 and top < 5 or delta is -1 and top > -5
Header.scrollTo thread

View File

@ -45,5 +45,5 @@ Favicon =
Favicon.unread = Favicon.unreadNSFW Favicon.unread = Favicon.unreadNSFW
Favicon.unreadY = Favicon.unreadNSFWY Favicon.unreadY = Favicon.unreadNSFWY
dead: 'data:image/gif;base64,<%= grunt.file.read("img/favicons/dead.gif", {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"}) %>' logo: 'data:image/png;base64,<%= grunt.file.read("img/icon128.png", {encoding: "base64"}) %>'

View File

@ -1,9 +1,7 @@
ThreadStats = ThreadStats =
init: -> init: ->
return if g.VIEW isnt 'thread' or !Conf['Thread Stats'] return if g.VIEW isnt 'thread' or !Conf['Thread Stats']
@dialog = UI.dialog 'thread-stats', 'bottom: 0; left: 0;', """ @dialog = UI.dialog 'thread-stats', 'bottom: 0; left: 0;', <%= importHTML('Monitoring/ThreadStats') %>
<%= grunt.file.read('html/Monitoring/ThreadStats.html').replace(/>\s+</g, '><').trim() %>
"""
@postCountEl = $ '#post-count', @dialog @postCountEl = $ '#post-count', @dialog
@fileCountEl = $ '#file-count', @dialog @fileCountEl = $ '#file-count', @dialog

View File

@ -2,14 +2,19 @@ ThreadUpdater =
init: -> init: ->
return if g.VIEW isnt 'thread' or !Conf['Thread Updater'] 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 = '' html = ''
for name, conf of Config.updater.checkbox for name, conf of Config.updater.checkbox
checked = if Conf[name] then 'checked' else '' checked = if Conf[name] then 'checked' else ''
html += "<div><label title='#{conf[1]}'><input name='#{name}' type=checkbox #{checked}> #{name}</label></div>" html += "<div><label title='#{conf[1]}'><input name='#{name}' type=checkbox #{checked}> #{name}</label></div>"
html = """ html = <%= importHTML('Monitoring/ThreadUpdater') %>
<%= grunt.file.read('html/Monitoring/ThreadUpdater.html').replace(/>\s+</g, '><').trim() %>
"""
@dialog = UI.dialog 'updater', 'bottom: 0; right: 0;', html @dialog = UI.dialog 'updater', 'bottom: 0; right: 0;', html
@timer = $ '#update-timer', @dialog @timer = $ '#update-timer', @dialog
@ -81,6 +86,7 @@ ThreadUpdater =
ThreadUpdater.interval = @value = val ThreadUpdater.interval = @value = val
$.cb.value.call @ if e $.cb.value.call @ if e
load: (e) -> load: (e) ->
$.rmClass ThreadUpdater.button, 'fa-spin'
{req} = ThreadUpdater {req} = ThreadUpdater
delete ThreadUpdater.req delete ThreadUpdater.req
if e.type isnt 'loadend' # timeout or abort if e.type isnt 'loadend' # timeout or abort
@ -143,9 +149,10 @@ ThreadUpdater =
update: -> update: ->
return unless navigator.onLine return unless navigator.onLine
$.addClass ThreadUpdater.button, 'fa-spin'
ThreadUpdater.count() ThreadUpdater.count()
ThreadUpdater.set 'timer', '...' ThreadUpdater.set 'timer', '...'
ThreadUpdater.req.abort() if ThreadUpdater.req ThreadUpdater.req?.abort()
url = "//api.4chan.org/#{ThreadUpdater.thread.board}/res/#{ThreadUpdater.thread}.json" url = "//api.4chan.org/#{ThreadUpdater.thread.board}/res/#{ThreadUpdater.thread}.json"
ThreadUpdater.req = $.ajax url, ThreadUpdater.req = $.ajax url,
onabort: ThreadUpdater.cb.load onabort: ThreadUpdater.cb.load
@ -155,38 +162,27 @@ ThreadUpdater =
, ,
whenModified: true whenModified: true
updateThreadStatus: (title, OP) -> updateThreadStatus: (type, status) ->
titleLC = title.toLowerCase() return unless hasChanged = ThreadUpdater.thread["is#{type}"] isnt status
return if ThreadUpdater.thread["is#{title}"] is !!OP[titleLC] ThreadUpdater.thread.setStatus type, status
unless ThreadUpdater.thread["is#{title}"] = !!OP[titleLC] change = if type is 'Sticky'
message = if title is 'Sticky' if status
'The thread is not a sticky anymore.' 'now a sticky'
else else
'The thread is not closed anymore.' 'not a sticky 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.'
else else
'The thread is now closed.' if status
new Notice 'info', message, 30 'now closed'
icon = $.el 'img', else
src: "//static.4chan.org/image/#{titleLC}.gif" 'not closed anymore'
alt: title new Notice 'info', "The thread is #{change}.", 30
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]
parse: (postObjects) -> parse: (postObjects) ->
OP = postObjects[0] OP = postObjects[0]
Build.spoilerRange[ThreadUpdater.thread.board] = OP.custom_spoiler Build.spoilerRange[ThreadUpdater.thread.board] = OP.custom_spoiler
ThreadUpdater.updateThreadStatus 'Sticky', OP ThreadUpdater.updateThreadStatus 'Sticky', !!OP.sticky
ThreadUpdater.updateThreadStatus 'Closed', OP ThreadUpdater.updateThreadStatus 'Closed', !!OP.closed
ThreadUpdater.thread.postLimit = !!OP.bumplimit ThreadUpdater.thread.postLimit = !!OP.bumplimit
ThreadUpdater.thread.fileLimit = !!OP.imagelimit ThreadUpdater.thread.fileLimit = !!OP.imagelimit
@ -250,15 +246,14 @@ ThreadUpdater =
ThreadUpdater.lastPost = posts[count - 1].ID ThreadUpdater.lastPost = posts[count - 1].ID
Main.callbackNodes Post, posts Main.callbackNodes Post, posts
scroll = Conf['Auto Scroll'] and ThreadUpdater.scrollBG() and scroll = Conf['Auto Scroll'] and ThreadUpdater.scrollBG() and Header.getBottomOf(ThreadUpdater.root) > -25
ThreadUpdater.root.getBoundingClientRect().bottom - doc.clientHeight < 25
$.add ThreadUpdater.root, nodes $.add ThreadUpdater.root, nodes
sendEvent() sendEvent()
if scroll if scroll
if Conf['Bottom Scroll'] if Conf['Bottom Scroll']
window.scrollTo 0, d.body.clientHeight window.scrollTo 0, d.body.clientHeight
else else
Header.scrollToPost nodes[0] Header.scrollTo nodes[0]
# Enable 4chan features. # Enable 4chan features.
threadID = ThreadUpdater.thread.ID threadID = ThreadUpdater.thread.ID

View File

@ -3,15 +3,17 @@ ThreadWatcher =
return if !Conf['Thread Watcher'] return if !Conf['Thread Watcher']
@db = new DataBoard 'watchedThreads', @refresh, true @db = new DataBoard 'watchedThreads', @refresh, true
@dialog = UI.dialog 'thread-watcher', 'top: 50px; left: 0px;', """ @dialog = UI.dialog 'thread-watcher', 'top: 50px; left: 0px;', <%= importHTML('Monitoring/ThreadWatcher') %>
<%= grunt.file.read('html/Monitoring/ThreadWatcher.html').replace(/>\s+</g, '><').trim() %>
"""
@status = $ '#watcher-status', @dialog @status = $ '#watcher-status', @dialog
@list = @dialog.lastElementChild @list = @dialog.lastElementChild
$.on d, 'QRPostSuccessful', @cb.post $.on d, 'QRPostSuccessful', @cb.post
$.on d, 'ThreadUpdate', @cb.threadUpdate if g.VIEW is 'thread'
$.on d, '4chanXInitFinished', @ready $.on d, '4chanXInitFinished', @ready
switch g.VIEW
when 'index'
$.on d, 'IndexRefresh', @cb.onIndexRefresh
when 'thread'
$.on d, 'ThreadUpdate', @cb.onThreadRefresh
now = Date.now() now = Date.now()
if (@db.data.lastChecked or 0) < now - 2 * $.HOUR if (@db.data.lastChecked or 0) < now - 2 * $.HOUR
@ -69,7 +71,17 @@ ThreadWatcher =
$.set 'AutoWatch', threadID $.set 'AutoWatch', threadID
else if Conf['Auto Watch Reply'] else if Conf['Auto Watch Reply']
ThreadWatcher.add board.threads[threadID] 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 {thread} = e.detail
return unless e.detail[404] and ThreadWatcher.db.get {boardID: thread.board.ID, threadID: thread.ID} return unless e.detail[404] and ThreadWatcher.db.get {boardID: thread.board.ID, threadID: thread.ID}
# Update 404 status. # Update 404 status.
@ -100,7 +112,7 @@ ThreadWatcher =
ThreadWatcher.status.textContent = status ThreadWatcher.status.textContent = status
return if @status isnt 404 return if @status isnt 404
if Conf['Auto Prune'] if Conf['Auto Prune']
ThreadWatcher.rm boardID, threadID ThreadWatcher.db.delete {boardID, threadID}
else else
data.isDead = true data.isDead = true
ThreadWatcher.db.set {boardID, threadID, val: data} ThreadWatcher.db.set {boardID, threadID, val: data}

View File

@ -42,18 +42,16 @@ Unread =
while root = $.x 'preceding-sibling::div[contains(@class,"replyContainer")][1]', post.nodes.root while root = $.x 'preceding-sibling::div[contains(@class,"replyContainer")][1]', post.nodes.root
break unless (post = Get.postFromRoot root).isHidden break unless (post = Get.postFromRoot root).isHidden
return unless root return unless root
onload = -> root.scrollIntoView false if checkPosition root down = true
else else
# Scroll to the last read post. # Scroll to the last read post.
posts = Object.keys Unread.thread.posts posts = Object.keys Unread.thread.posts
{root} = Unread.thread.posts[posts[posts.length - 1]].nodes {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 # Prevent the browser to scroll back to
# the previous scroll location on page load. # 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: -> sync: ->
lastReadPost = Unread.db.get lastReadPost = Unread.db.get
@ -102,7 +100,7 @@ Unread =
body: post.info.comment body: post.info.comment
icon: Favicon.logo icon: Favicon.logo
notif.onclick = -> notif.onclick = ->
Header.scrollToPost post.nodes.root Header.scrollToIfNeeded post.nodes.root, true
window.focus() window.focus()
notif.onshow = -> notif.onshow = ->
setTimeout -> setTimeout ->
@ -132,9 +130,8 @@ Unread =
read: (e) -> read: (e) ->
return if d.hidden or !Unread.posts.length return if d.hidden or !Unread.posts.length
height = doc.clientHeight
for post, i in Unread.posts 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 return unless i
Unread.lastReadPost = Unread.posts.splice(0, i)[i - 1].ID Unread.lastReadPost = Unread.posts.splice(0, i)[i - 1].ID

View File

@ -26,7 +26,7 @@ QR =
$.event 'CloseMenu' $.event 'CloseMenu'
QR.open() QR.open()
QR.nodes.com.focus() QR.nodes.com.focus()
Header.addShortcut sc, 1 Header.addShortcut sc, 2
$.on d, 'QRGetSelectedPost', ({detail: cb}) -> $.on d, 'QRGetSelectedPost', ({detail: cb}) ->
cb QR.selected cb QR.selected
@ -39,11 +39,15 @@ QR =
$.on d, 'dragover', QR.dragOver $.on d, 'dragover', QR.dragOver
$.on d, 'drop', QR.dropFile $.on d, 'drop', QR.dropFile
$.on d, 'dragstart dragend', QR.drag $.on d, 'dragstart dragend', QR.drag
$.on d, 'ThreadUpdate', -> switch g.VIEW
if g.DEAD when 'index'
QR.abort() $.on d, 'IndexRefresh', QR.generatePostableThreadsList
else when 'thread'
QR.status() $.on d, 'ThreadUpdate', ->
if g.DEAD
QR.abort()
else
QR.status()
QR.persist() if Conf['Persistent QR'] QR.persist() if Conf['Persistent QR']
@ -697,7 +701,7 @@ QR =
imgContainer = $.el 'div', imgContainer = $.el 'div',
className: 'captcha-img' className: 'captcha-img'
title: 'Reload' title: 'Reload reCAPTCHA'
innerHTML: '<img>' innerHTML: '<img>'
input = $.el 'input', input = $.el 'input',
className: 'captcha-input field' className: 'captcha-input field'
@ -796,10 +800,27 @@ QR =
return return
e.preventDefault() 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: ->
dialog = UI.dialog 'qr', 'top:0;right:0;', """ dialog = UI.dialog 'qr', 'top:0;right:0;', <%= importHTML('Posting/QR') %>
<%= grunt.file.read('html/Posting/QR.html').replace(/>\s+</g, '><').trim() %>
"""
QR.nodes = nodes = QR.nodes = nodes =
el: dialog el: dialog
@ -867,12 +888,6 @@ QR =
nodes.flag.dataset.default = '0' nodes.flag.dataset.default = '0'
$.add nodes.form, nodes.flag $.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') { %> <% if (type === 'userscript') { %>
# XXX Firefox lacks focusin/focusout support. # XXX Firefox lacks focusin/focusout support.
for elm in $$ '*', QR.nodes.el for elm in $$ '*', QR.nodes.el
@ -906,6 +921,7 @@ QR =
$.set 'QR Size', @style.cssText $.set 'QR Size', @style.cssText
<% } %> <% } %>
QR.generatePostableThreadsList()
QR.persona.init() QR.persona.init()
new QR.post true new QR.post true
QR.status() QR.status()