Merge in new thread watcher and change a few things

This commit is contained in:
seaweedchan 2013-08-13 00:03:29 -07:00
commit b9b4efae5e
29 changed files with 1680 additions and 873 deletions

View File

@ -1,5 +1,5 @@
/* /*
* 4chan X - Version 1.2.27 - 2013-08-12 * 4chan X - Version 1.2.27 - 2013-08-13
* *
* Licensed under the MIT license. * Licensed under the MIT license.
* https://github.com/seaweedchan/4chan-x/blob/master/LICENSE * https://github.com/seaweedchan/4chan-x/blob/master/LICENSE

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -50,8 +50,8 @@ Redirect =
http: true http: true
https: true https: true
software: 'foolfuuka' software: 'foolfuuka'
boards: ['adv', 'asp', 'cm', 'i', 'lgbt', 'n', 'o', 'p', 's4s', 't', 'trv'] boards: ['adv', 'asp', 'cm', 'd', 'e', 'i', 'lgbt', 'n', 'o', 'p', 'pol', 's', 's4s', 't', 'trv', 'y']
files: ['adv', 'asp', 'cm', 'i', 'lgbt', 'n', 'o', 'p', 's4s', 't', 'trv'] files: ['cm', 'd', 'e', 'i', 'n', 'o', 'p', 's', 'trv', 'y']
'Foolz Beta': 'Foolz Beta':
domain: 'beta.foolz.us' domain: 'beta.foolz.us'

View File

@ -27,7 +27,7 @@ Build =
date: data.now date: data.now
dateUTC: data.time dateUTC: data.time
comment: data.com comment: data.com
capReps: data.capcode_replies capcodeReplies: data.capcode_replies
# thread status # thread status
isSticky: !!data.sticky isSticky: !!data.sticky
isClosed: !!data.closed isClosed: !!data.closed
@ -59,7 +59,7 @@ Build =
postID, threadID, boardID postID, threadID, boardID
name, capcode, tripcode, uniqueID, email, subject, flagCode, flagName, date, dateUTC name, capcode, tripcode, uniqueID, email, subject, flagCode, flagName, date, dateUTC
isSticky, isClosed isSticky, isClosed
comment, capReps comment, capcodeReplies
file file
} = o } = o
isOP = postID is threadID isOP = postID is threadID
@ -191,26 +191,6 @@ Build =
else else
'' ''
capcodeReplies = ''
if capReps
generateCapcodeReplies = (capcodeType, array) ->
"<span class=smaller><span class=bold>#{
switch capcodeType
when 'admin'
'Administrator'
when 'mod'
'Moderator'
when 'developer'
'Developer'
} Repl#{if array.length > 1 then 'ies' else 'y'}:</span> #{
array.map (ID) ->
"<a href='/#{boardID}/res/#{threadID}#p#{ID}' class=quotelink>&gt;&gt;#{ID}</a>"
.join ' '
}</span><br>"
for capcodeType, array of capReps
capcodeReplies += generateCapcodeReplies capcodeType, array
capcodeReplies = "<br><br><span class=capcodeReplies>#{capcodeReplies}</span>"
container = $.el 'div', container = $.el 'div',
id: "pc#{postID}" id: "pc#{postID}"
className: "postContainer #{if isOP then 'op' else 'reply'}Container" className: "postContainer #{if isOP then 'op' else 'reply'}Container"
@ -221,4 +201,36 @@ Build =
continue if href[0] is '/' # Cross-board quote, or board link continue if href[0] is '/' # Cross-board quote, or board link
quote.href = "/#{boardID}/res/#{href}" # Fix pathnames quote.href = "/#{boardID}/res/#{href}" # Fix pathnames
Build.capcodeReplies {boardID, threadID, root: container, capcodeReplies}
container container
capcodeReplies: ({boardID, threadID, bq, root, capcodeReplies}) ->
return unless capcodeReplies
generateCapcodeReplies = (capcodeType, array) ->
"<span class=smaller><span class=bold>#{
switch capcodeType
when 'admin'
'Administrator'
when 'mod'
'Moderator'
when 'developer'
'Developer'
} Repl#{if array.length > 1 then 'ies' else 'y'}:</span> #{
array.map (ID) ->
"<a href='/#{boardID}/res/#{threadID}#p#{ID}' class=quotelink>&gt;&gt;#{ID}</a>"
.join ' '
}</span><br>"
html = []
for capcodeType, array of capcodeReplies
html.push generateCapcodeReplies capcodeType, array
bq or= $ 'blockquote', root
$.add bq, [
$.el 'br'
$.el 'br'
$.el 'span',
className: 'capcodeReplies'
innerHTML: html.join ''
]

View File

@ -57,12 +57,6 @@ Config =
true true
'Show dice that were entered into the email field.' 'Show dice that were entered into the email field.'
] ]
<% if (type !== 'crx') { %>
'Check for Updates': [
true
'Check for updated versions of <%= meta.name %>.'
]
<% } %>
'Show Updated Notifications': [ 'Show Updated Notifications': [
true true
'Show notifications when 4chan X is successfully updated.' 'Show notifications when 4chan X is successfully updated.'
@ -255,14 +249,6 @@ Config =
true true
'Adds a shortcut for the thread watcher, hides the watcher by default, and makes it scroll with the page.' 'Adds a shortcut for the thread watcher, hides the watcher by default, and makes it scroll with the page.'
] ]
'Auto Watch': [
true
'Automatically watch threads you start.'
]
'Auto Watch Reply': [
false
'Automatically watch threads you reply to.'
]
'Posting': 'Posting':
'Quick Reply': [ 'Quick Reply': [
@ -399,7 +385,25 @@ Config =
false false
'Advance to next post when contracting an expanded image.' 'Advance to next post when contracting an expanded image.'
] ]
threadWatcher:
'Current Board': [
false
'Only show watched threads from the current board.'
]
'Auto Watch': [
true
'Automatically watch threads you start.'
]
'Auto Watch Reply': [
false
'Automatically watch threads you reply to.'
]
'Auto Prune': [
false
'Automatically prune 404\'d threads.'
]
filter: filter:
name: """ name: """
# Filter any namefags: # Filter any namefags:

View File

@ -109,7 +109,6 @@ Header =
fourchannav = $.id 'boardNavDesktop' fourchannav = $.id 'boardNavDesktop'
if a = $ "a[href*='/#{g.BOARD}/']", fourchannav if a = $ "a[href*='/#{g.BOARD}/']", fourchannav
a.className = 'current' a.className = 'current'
boardList = $.el 'span', boardList = $.el 'span',
id: 'board-list' id: 'board-list'
innerHTML: "<span id=custom-board-list></span><span id=full-board-list hidden><span class='hide-board-list-container fourchanx-link'><a href=javascript:; class='hide-board-list-button'>&nbsp;-&nbsp;</a></span> #{fourchannav.innerHTML}</span>" innerHTML: "<span id=custom-board-list></span><span id=full-board-list hidden><span class='hide-board-list-container fourchanx-link'><a href=javascript:; class='hide-board-list-button'>&nbsp;-&nbsp;</a></span> #{fourchannav.innerHTML}</span>"

View File

@ -126,6 +126,7 @@ Main =
'Thread Stats': ThreadStats 'Thread Stats': ThreadStats
'Thread Updater': ThreadUpdater 'Thread Updater': ThreadUpdater
'Thread Watcher': ThreadWatcher 'Thread Watcher': ThreadWatcher
'Thread Watcher (Menu)': ThreadWatcher.menu
'Index Navigation': Nav 'Index Navigation': Nav
'Keybinds': Keybinds 'Keybinds': Keybinds
'Show Dice Roll': Dice 'Show Dice Roll': Dice
@ -205,9 +206,6 @@ Main =
Main.callbackNodes Thread, threads Main.callbackNodes Thread, threads
Main.callbackNodesDB Post, posts, -> Main.callbackNodesDB Post, posts, ->
$.event '4chanXInitFinished' $.event '4chanXInitFinished'
<% if (type !== 'crx') { %>
Main.checkUpdate()
<% } %>
if styleSelector = $.id 'styleSelector' if styleSelector = $.id 'styleSelector'
passLink = $.el 'a', passLink = $.el 'a',
@ -227,9 +225,6 @@ Main =
new Notification 'warning', 'Cookies need to be enabled on 4chan for <%= meta.name %> to properly function.', 30 new Notification 'warning', 'Cookies need to be enabled on 4chan for <%= meta.name %> to properly function.', 30
$.event '4chanXInitFinished' $.event '4chanXInitFinished'
<% if (type !== 'crx') { %>
Main.checkUpdate()
<% } %>
callbackNodes: (klass, nodes) -> callbackNodes: (klass, nodes) ->
# get the nodes' length only once # get the nodes' length only once
@ -303,27 +298,6 @@ Main =
obj.callback.isAddon = true obj.callback.isAddon = true
Klass::callbacks.push obj.callback Klass::callbacks.push obj.callback
<% if (type !== 'crx') { %>
message: (e) ->
{version} = e.data
if version and version isnt g.VERSION
el = $.el 'span',
innerHTML: "Update: <%= meta.name %> v#{version} is out, get it <a href=<%= meta.page %> target=_blank>here</a>."
new Notification 'info', el, 120
checkUpdate: ->
return unless Conf['Check for Updates'] and Main.isThisPageLegit()
now = Date.now()
$.get 'lastchecked', 0, ({lastchecked}) ->
if (lastchecked > now - $.DAY)
return
$.ready ->
$.on window, 'message', Main.message
$.set 'lastchecked', now
$.add d.head, $.el 'script',
src: '<%= meta.repo %>raw/<%= meta.mainBranch %>/latest.js'
<% } %>
handleErrors: (errors) -> handleErrors: (errors) ->
unless errors instanceof Array unless errors instanceof Array
error = errors error = errors

View File

@ -21,7 +21,8 @@ Settings =
else else
$.on d, '4chanXInitFinished', Settings.open $.on d, '4chanXInitFinished', Settings.open
$.set $.set
lastchecked: Date.now() archives: Conf['archives']
lastarchivecheck: now
previousversion: g.VERSION previousversion: g.VERSION
Settings.addSection 'Main', Settings.main Settings.addSection 'Main', Settings.main
@ -153,7 +154,6 @@ Settings =
data = data =
version: g.VERSION version: g.VERSION
date: now date: now
Conf['WatchedThreads'] = {}
for db in DataBoards for db in DataBoards
Conf[db] = boards: {} Conf[db] = boards: {}
# Make sure to export the most recent data. # Make sure to export the most recent data.
@ -265,13 +265,10 @@ Settings =
for key, val of Config.hotkeys when key of data.Conf for key, val of Config.hotkeys when key of data.Conf
data.Conf[key] = data.Conf[key].replace(/ctrl|alt|meta/g, (s) -> "#{s[0].toUpperCase()}#{s[1..]}").replace /(^|.+\+)[A-Z]$/g, (s) -> data.Conf[key] = data.Conf[key].replace(/ctrl|alt|meta/g, (s) -> "#{s[0].toUpperCase()}#{s[1..]}").replace /(^|.+\+)[A-Z]$/g, (s) ->
"Shift+#{s[0...-1]}#{s[-1..].toLowerCase()}" "Shift+#{s[0...-1]}#{s[-1..].toLowerCase()}"
data.Conf.WatchedThreads = data.WatchedThreads data.Conf['WatchedThreads'] = data.WatchedThreads
else if version[0] is '3' if data.Conf['WatchedThreads']
data = Settings.convertSettings data, data.Conf['watchedThreads'] = boards: ThreadWatcher.convert data.Conf['WatchedThreads']
'Reply Hiding': 'Reply Hiding Buttons' delete data.Conf['WatchedThreads']
'Thread Hiding': 'Thread Hiding Buttons'
'Bottom header': 'Bottom Header'
'Unread Tab Icon': 'Unread Favicon'
$.set data.Conf $.set data.Conf
convertSettings: (data, map) -> convertSettings: (data, map) ->
for prevKey, newKey of map for prevKey, newKey of map

View File

@ -52,7 +52,7 @@
} }
/* Watcher Favicon */ /* Watcher Favicon */
:root.burichan .watch-thread-link :root.burichan .watcher-toggler
{ {
background-image: url("data:image/svg+xml,<svg viewBox='0 0 26 26' preserveAspectRatio='true' xmlns='http://www.w3.org/2000/svg'><path fill='rgb(0,0,0)' d='M24.132,7.971c-2.203-2.205-5.916-2.098-8.25,0.235L15.5,8.588l-0.382-0.382c-2.334-2.333-6.047-2.44-8.25-0.235c-2.204,2.203-2.098,5.916,0.235,8.249l8.396,8.396l8.396-8.396C26.229,13.887,26.336,10.174,24.132,7.971z'/></svg>"); background-image: url("data:image/svg+xml,<svg viewBox='0 0 26 26' preserveAspectRatio='true' xmlns='http://www.w3.org/2000/svg'><path fill='rgb(0,0,0)' d='M24.132,7.971c-2.203-2.205-5.916-2.098-8.25,0.235L15.5,8.588l-0.382-0.382c-2.334-2.333-6.047-2.44-8.25-0.235c-2.204,2.203-2.098,5.916,0.235,8.249l8.396,8.396l8.396-8.396C26.229,13.887,26.336,10.174,24.132,7.971z'/></svg>");
} }

View File

@ -52,7 +52,7 @@
} }
/* Watcher Favicon */ /* Watcher Favicon */
:root.futaba .watch-thread-link :root.futaba .watcher-toggler
{ {
background-image: url("data:image/svg+xml,<svg viewBox='0 0 26 26' preserveAspectRatio='true' xmlns='http://www.w3.org/2000/svg'><path fill='rgb(128,0,0)' d='M24.132,7.971c-2.203-2.205-5.916-2.098-8.25,0.235L15.5,8.588l-0.382-0.382c-2.334-2.333-6.047-2.44-8.25-0.235c-2.204,2.203-2.098,5.916,0.235,8.249l8.396,8.396l8.396-8.396C26.229,13.887,26.336,10.174,24.132,7.971z'/></svg>"); background-image: url("data:image/svg+xml,<svg viewBox='0 0 26 26' preserveAspectRatio='true' xmlns='http://www.w3.org/2000/svg'><path fill='rgb(128,0,0)' d='M24.132,7.971c-2.203-2.205-5.916-2.098-8.25,0.235L15.5,8.588l-0.382-0.382c-2.334-2.333-6.047-2.44-8.25-0.235c-2.204,2.203-2.098,5.916,0.235,8.249l8.396,8.396l8.396-8.396C26.229,13.887,26.336,10.174,24.132,7.971z'/></svg>");
} }

View File

@ -52,7 +52,7 @@
} }
/* Watcher Favicon */ /* Watcher Favicon */
:root.photon .watch-thread-link :root.photon .watcher-toggler
{ {
background-image: url("data:image/svg+xml,<svg viewBox='0 0 26 26' preserveAspectRatio='true' xmlns='http://www.w3.org/2000/svg'><path fill='rgb(51,51,51)' d='M24.132,7.971c-2.203-2.205-5.916-2.098-8.25,0.235L15.5,8.588l-0.382-0.382c-2.334-2.333-6.047-2.44-8.25-0.235c-2.204,2.203-2.098,5.916,0.235,8.249l8.396,8.396l8.396-8.396C26.229,13.887,26.336,10.174,24.132,7.971z'/></svg>"); background-image: url("data:image/svg+xml,<svg viewBox='0 0 26 26' preserveAspectRatio='true' xmlns='http://www.w3.org/2000/svg'><path fill='rgb(51,51,51)' d='M24.132,7.971c-2.203-2.205-5.916-2.098-8.25,0.235L15.5,8.588l-0.382-0.382c-2.334-2.333-6.047-2.44-8.25-0.235c-2.204,2.203-2.098,5.916,0.235,8.249l8.396,8.396l8.396-8.396C26.229,13.887,26.336,10.174,24.132,7.971z'/></svg>");
} }

View File

@ -38,7 +38,8 @@
cursor: move; cursor: move;
overflow: hidden; overflow: hidden;
} }
label, .favicon { label,
.watcher-toggler {
cursor: pointer; cursor: pointer;
} }
a[href="javascript:;"] { a[href="javascript:;"] {
@ -97,10 +98,10 @@ a {
#qr { #qr {
z-index: 30; z-index: 30;
} }
#watcher { #thread-watcher {
z-index: 8; z-index: 8;
} }
:root.fixed-watcher #watcher { :root.fixed-watcher #thread-watcher {
z-index: 20; z-index: 20;
} }
.fixed #header-bar { .fixed #header-bar {
@ -204,6 +205,7 @@ a {
.brackets-wrap::after { .brackets-wrap::after {
content: "]\\00a0"; content: "]\\00a0";
} }
.dead-thread,
.disabled, .disabled,
.expand-all-shortcut { .expand-all-shortcut {
opacity: .45; opacity: .45;
@ -465,44 +467,48 @@ a.hide-announcement {
} }
/* Thread Watcher */ /* Thread Watcher */
#watcher { #thread-watcher {
position: absolute; position: absolute;
} }
#watcher { #thread-watcher {
padding-bottom: 3px; padding-bottom: 3px;
padding-left: 3px;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
min-width: 120px; min-width: 136px;
max-height: 92%; max-height: 92%;
overflow-y: auto; overflow-y: auto;
} }
:root.fixed-watcher #watcher { #thread-watcher .menu-button {
bottom: 1px;
}
:root.fixed-watcher #thread-watcher {
position: fixed; position: fixed;
} }
:root:not(.fixed-watcher) #watcher:not(:hover) { :root:not(.fixed-watcher) #thread-watcher:not(:hover) {
max-height: 210px; max-height: 210px;
overflow-y: hidden; overflow-y: hidden;
} }
#watcher > .move { #thread-watcher > .move {
padding-top: 3px; padding-top: 3px;
} }
#watcher > div { #watched-threads > div {
max-width: 250px; max-width: 250px;
overflow: hidden; overflow: hidden;
padding-left: 3px; padding-left: 3px;
padding-right: 3px; padding-right: 3px;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
#watcher a { #thread-watcher a {
text-decoration: none; text-decoration: none;
} }
#watcher .move>.close { #thread-watcher .move>.close {
position: absolute; position: absolute;
right: 0px; right: 0px;
top: 0px; top: 0px;
padding: 0px 4px; padding: 0px 4px;
} }
.watch-thread-link { .watcher-toggler {
padding-top: 18px; padding-top: 18px;
width: 18px; width: 18px;
height: 0px; height: 0px;
@ -512,10 +518,11 @@ a.hide-announcement {
position: relative; position: relative;
top: 1px; top: 1px;
} }
.watch-thread-link.watched { .watcher-toggler.watched {
opacity: 1; opacity: 1;
} }
/* Thread Stats */ /* Thread Stats */
#thread-stats { #thread-stats {
background: none; background: none;

View File

@ -58,7 +58,7 @@
} }
/* Watcher Favicon */ /* Watcher Favicon */
:root.tomorrow .watch-thread-link :root.tomorrow .watcher-toggler
{ {
background-image: url("data:image/svg+xml,<svg viewBox='0 0 26 26' preserveAspectRatio='true' xmlns='http://www.w3.org/2000/svg'><path fill='rgb(197,200,198)' d='M24.132,7.971c-2.203-2.205-5.916-2.098-8.25,0.235L15.5,8.588l-0.382-0.382c-2.334-2.333-6.047-2.44-8.25-0.235c-2.204,2.203-2.098,5.916,0.235,8.249l8.396,8.396l8.396-8.396C26.229,13.887,26.336,10.174,24.132,7.971z'/></svg>"); background-image: url("data:image/svg+xml,<svg viewBox='0 0 26 26' preserveAspectRatio='true' xmlns='http://www.w3.org/2000/svg'><path fill='rgb(197,200,198)' d='M24.132,7.971c-2.203-2.205-5.916-2.098-8.25,0.235L15.5,8.588l-0.382-0.382c-2.334-2.333-6.047-2.44-8.25-0.235c-2.204,2.203-2.098,5.916,0.235,8.249l8.396,8.396l8.396-8.396C26.229,13.887,26.336,10.174,24.132,7.971z'/></svg>");
} }

View File

@ -52,7 +52,7 @@
} }
/* Watcher Favicon */ /* Watcher Favicon */
:root.yotsuba-b .watch-thread-link :root.yotsuba-b .watcher-toggler
{ {
background-image: url("data:image/svg+xml,<svg viewBox='0 0 26 26' preserveAspectRatio='true' xmlns='http://www.w3.org/2000/svg'><path fill='rgb(0,0,0)' d='M24.132,7.971c-2.203-2.205-5.916-2.098-8.25,0.235L15.5,8.588l-0.382-0.382c-2.334-2.333-6.047-2.44-8.25-0.235c-2.204,2.203-2.098,5.916,0.235,8.249l8.396,8.396l8.396-8.396C26.229,13.887,26.336,10.174,24.132,7.971z'/></svg>"); background-image: url("data:image/svg+xml,<svg viewBox='0 0 26 26' preserveAspectRatio='true' xmlns='http://www.w3.org/2000/svg'><path fill='rgb(0,0,0)' d='M24.132,7.971c-2.203-2.205-5.916-2.098-8.25,0.235L15.5,8.588l-0.382-0.382c-2.334-2.333-6.047-2.44-8.25-0.235c-2.204,2.203-2.098,5.916,0.235,8.249l8.396,8.396l8.396-8.396C26.229,13.887,26.336,10.174,24.132,7.971z'/></svg>");
} }

View File

@ -52,7 +52,7 @@
} }
/* Watcher Favicon */ /* Watcher Favicon */
:root.yotsuba .watch-thread-link :root.yotsuba .watcher-toggler
{ {
background-image: url("data:image/svg+xml,<svg viewBox='0 0 26 26' preserveAspectRatio='true' xmlns='http://www.w3.org/2000/svg'><path fill='rgb(128,0,0)' d='M24.132,7.971c-2.203-2.205-5.916-2.098-8.25,0.235L15.5,8.588l-0.382-0.382c-2.334-2.333-6.047-2.44-8.25-0.235c-2.204,2.203-2.098,5.916,0.235,8.249l8.396,8.396l8.396-8.396C26.229,13.887,26.336,10.174,24.132,7.971z'/></svg>"); background-image: url("data:image/svg+xml,<svg viewBox='0 0 26 26' preserveAspectRatio='true' xmlns='http://www.w3.org/2000/svg'><path fill='rgb(128,0,0)' d='M24.132,7.971c-2.203-2.205-5.916-2.098-8.25,0.235L15.5,8.588l-0.382-0.382c-2.334-2.333-6.047-2.44-8.25-0.235c-2.204,2.203-2.098,5.916,0.235,8.249l8.396,8.396l8.396-8.396C26.229,13.887,26.336,10.174,24.132,7.971z'/></svg>");
} }

View File

@ -54,6 +54,6 @@
#{if isOP then '' else fileHTML} #{if isOP then '' else fileHTML}
<blockquote class=postMessage id=m#{postID}>#{comment or ''}#{capcodeReplies}</blockquote>#{" "} <blockquote class=postMessage id=m#{postID}>#{comment or ''}</blockquote>#{" "}
</div>""" </div>"""

View File

@ -0,0 +1,2 @@
<div class="move">Thread Watcher <span id="watcher-status"></span><a class="menu-button fourchanx-link" href="javascript:;"><i></i></a><a class=close href=javascript:;>×</a></span></div>
<div id="watched-threads"></div>

View File

@ -57,18 +57,23 @@ $.extend = (object, properties) ->
object[key] = val object[key] = val
return return
$.ajax = (url, options, extra={}) -> $.ajax = do ->
{type, headers, upCallbacks, form, sync} = extra # Status Code 304: Not modified
r = new XMLHttpRequest() # With the `If-Modified-Since` header we only receive the HTTP headers and no body for 304 responses.
r.overrideMimeType 'text/html' # This saves a lot of bandwidth and CPU time for both the users and the servers.
type or= form and 'post' or 'get' lastModified = {}
r.open type, url, !sync (url, options, extra={}) ->
for key, val of headers {type, whenModified, upCallbacks, form, sync} = extra
r.setRequestHeader key, val r = new XMLHttpRequest()
$.extend r, options type or= form and 'post' or 'get'
$.extend r.upload, upCallbacks r.open type, url, !sync
r.send form if whenModified
r r.setRequestHeader 'If-Modified-Since', lastModified[url] or '0'
$.on r, 'load', -> lastModified[url] = r.getResponseHeader 'Last-Modified'
$.extend r, options
$.extend r.upload, upCallbacks
r.send form
r
$.cache = do -> $.cache = do ->
reqs = {} reqs = {}

View File

@ -1,10 +1,10 @@
DataBoards = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts'] DataBoards = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads']
class DataBoard class DataBoard
constructor: (@key, sync) -> constructor: (@key, sync, dontClean) ->
@data = Conf[key] @data = Conf[key]
$.sync key, @onSync.bind @ $.sync key, @onSync.bind @
@clean() @clean() unless dontClean
return unless sync return unless sync
# Chrome also fires the onChanged callback on the current tab, # Chrome also fires the onChanged callback on the current tab,
# so we only start syncing when we're ready. # so we only start syncing when we're ready.
@ -13,6 +13,9 @@ class DataBoard
@sync = sync @sync = sync
$.on d, '4chanXInitFinished', init $.on d, '4chanXInitFinished', init
save: ->
$.set @key, @data
delete: ({boardID, threadID, postID}) -> delete: ({boardID, threadID, postID}) ->
if postID if postID
delete @data.boards[boardID][threadID][postID] delete @data.boards[boardID][threadID][postID]
@ -22,7 +25,7 @@ class DataBoard
@deleteIfEmpty {boardID} @deleteIfEmpty {boardID}
else else
delete @data.boards[boardID] delete @data.boards[boardID]
$.set @key, @data @save()
deleteIfEmpty: ({boardID, threadID}) -> deleteIfEmpty: ({boardID, threadID}) ->
if threadID if threadID
@ -39,7 +42,7 @@ class DataBoard
(@data.boards[boardID] or= {})[threadID] = val (@data.boards[boardID] or= {})[threadID] = val
else else
@data.boards[boardID] = val @data.boards[boardID] = val
$.set @key, @data @save()
get: ({boardID, threadID, postID, defaultValue}) -> get: ({boardID, threadID, postID, defaultValue}) ->
if board = @data.boards[boardID] if board = @data.boards[boardID]
@ -67,8 +70,7 @@ class DataBoard
@data.lastChecked = now @data.lastChecked = now
for boardID of @data.boards for boardID of @data.boards
@ajaxClean boardID @ajaxClean boardID
@save()
$.set @key, @data
ajaxClean: (boardID) -> ajaxClean: (boardID) ->
$.cache "//api.4chan.org/#{boardID}/threads.json", (e) => $.cache "//api.4chan.org/#{boardID}/threads.json", (e) =>
@ -84,7 +86,7 @@ class DataBoard
threads[thread.no] = board[thread.no] threads[thread.no] = board[thread.no]
@data.boards[boardID] = threads @data.boards[boardID] = threads
@deleteIfEmpty {boardID} @deleteIfEmpty {boardID}
$.set @key, @data @save()
onSync: (data) -> onSync: (data) ->
@data = data or boards: {} @data = data or boards: {}

View File

@ -1,6 +1,16 @@
Polyfill = Polyfill =
init: -> init: ->
Polyfill.toBlob()
Polyfill.visibility() Polyfill.visibility()
toBlob: ->
HTMLCanvasElement::toBlob or= (cb) ->
data = atob @toDataURL()[22..]
# DataUrl to Binary code from Aeosynth's 4chan X repo
l = data.length
ui8a = new Uint8Array l
for i in [0...l]
ui8a[i] = data.charCodeAt i
cb new Blob [ui8a], type: 'image/png'
visibility: -> visibility: ->
# page visibility API # page visibility API
return unless 'webkitHidden' of document return unless 'webkitHidden' of document

View File

@ -169,8 +169,8 @@ ImageExpand =
{createSubEntry} = ImageExpand.menu {createSubEntry} = ImageExpand.menu
subEntries = [] subEntries = []
for key, conf of Config.imageExpansion for name, conf of Config.imageExpansion
subEntries.push createSubEntry key, conf subEntries.push createSubEntry name, conf[1]
$.event 'AddMenuEntry', $.event 'AddMenuEntry',
type: 'header' type: 'header'
@ -178,17 +178,16 @@ ImageExpand =
order: 105 order: 105
subEntries: subEntries subEntries: subEntries
createSubEntry: (type, config) -> createSubEntry: (name, desc) ->
label = $.el 'label', label = $.el 'label',
innerHTML: "<input type=checkbox name='#{type}'> #{type}" innerHTML: "<input type=checkbox name='#{name}'> #{name}"
title: desc
input = label.firstElementChild input = label.firstElementChild
if type in ['Fit width', 'Fit height'] if name in ['Fit width', 'Fit height']
$.on input, 'change', ImageExpand.cb.setFitness $.on input, 'change', ImageExpand.cb.setFitness
if config input.checked = Conf[name]
label.title = config[1] $.event 'change', null, input
input.checked = Conf[type] $.on input, 'change', $.cb.checked
$.event 'change', null, input
$.on input, 'change', $.cb.checked
el: label el: label
menuToggle: (e) -> menuToggle: (e) ->

View File

@ -4,23 +4,20 @@ Linkify =
@regString = if Conf['Allow False Positives'] @regString = if Conf['Allow False Positives']
///( ///(
\b( [-a-z]+://
[-a-z]+:// |
| [a-z]{3,}\.[-a-z0-9]+\.[a-z]
[a-z]{3,}\.[-a-z0-9]+\.[a-z] |
| [-a-z0-9]+\.[a-z]
[-a-z0-9]+\.[a-z] |
| [\d]+\.[\d]+\.[\d]+\.[\d]+/
[\d]+\.[\d]+\.[\d]+\.[\d]+/ |
| [a-z]{3,}:[a-z0-9?]
[a-z]{3,}:[a-z0-9?] |
| [^\s@]+@[a-z0-9.-]+\.[a-z0-9]
[^\s@]+@[a-z0-9.-]+\.[a-z0-9] )///i
)
[^\s'"]+
)///gi
else else
/(((magnet|mailto)\:|(www\.)|(news|(ht|f)tp(s?))\:\/\/){1}\S+)/gi /(((magnet|mailto)\:|(www\.)|(news|(ht|f)tp(s?))\:\/\/){1})/i
if Conf['Comment Expansion'] if Conf['Comment Expansion']
ExpandComment.callbacks.push @node ExpandComment.callbacks.push @node
@ -43,16 +40,44 @@ Linkify =
return return
test = /[^\s'"]+/g
space = /[\s'"]/
snapshot = $.X './/br|.//text()', @nodes.comment snapshot = $.X './/br|.//text()', @nodes.comment
i = 0 i = 0
while node = snapshot.snapshotItem i++ while node = snapshot.snapshotItem i++
links = []
{data} = node
continue if node.parentElement.nodeName is "A" or not data
continue if node.parentElement.nodeName is "A" while result = test.exec data
links = [] {index} = result
endNode = node
if (length = index + result[0].length) is data.length
if Linkify.regString.test node.data while (saved = snapshot.snapshotItem i++)
Linkify.regString.lastIndex = 0 break if saved.nodeName is 'BR'
Linkify.gatherLinks snapshot, @, node, links, i
endNode = saved
{length} = saved.data
if end = space.exec saved.data
length = end.index
i--
break
if length is endNode.data.length then test.lastIndex = 0
range = Linkify.makeRange node, endNode, index, length
if link = Linkify.regString.exec text = range.toString()
if lIndex = link.index
range.setStart node, lIndex + index
links.push [range, text]
break
else
if link = Linkify.regString.exec result[0]
range = Linkify.makeRange node, node, link.index, link.length
links.push [range, link]
for range in links.reverse() for range in links.reverse()
@nodes.links.push Linkify.makeLink range, @ @nodes.links.push Linkify.makeLink range, @
@ -68,66 +93,29 @@ Linkify =
return return
gatherLinks: (snapshot, post, node, links, i) -> makeRange: (startNode, endNode, startOffset, endOffset) ->
{data} = node range = document.createRange();
len = data.length range.setStart startNode, startOffset
range.setEnd endNode, endOffset
while (match = Linkify.regString.exec data)
{index} = match
link = match[0]
len2 = index + link.length
break if len is len2
range = document.createRange();
range.setStart node, index
range.setEnd node, len2
links.push range
Linkify.regString.lastIndex = 0
if match
links.push Linkify.seek snapshot, post, node, links, match, i
return
seek: (snapshot, post, node, links, match, i) ->
link = match[0]
range = document.createRange()
range.setStart node, match.index
while (next = snapshot.snapshotItem i++) and next.nodeName isnt 'BR'
node = next
data = node.data
if result = /[\s'"]/.exec data
{index} = result
range.setEnd node, index
Linkify.regString.lastIndex = index
Linkify.gatherLinks snapshot, post, node, links, i
return range
if range.collapsed
range.setEndAfter node
range range
makeLink: (range) -> makeLink: ([range, text]) ->
link = range.toString() text
link = text =
if link.contains ':' if text.contains ':'
link text
else ( else (
if link.contains '@' if text.contains '@'
'mailto:' 'mailto:'
else else
'http://' 'http://'
) + link ) + text
a = $.el 'a', a = $.el 'a',
className: 'linkify' className: 'linkify'
rel: 'nofollow noreferrer' rel: 'nofollow noreferrer'
target: '_blank' target: '_blank'
href: link href: text
$.add a, range.extractContents() $.add a, range.extractContents()
range.insertNode a range.insertNode a
a a

View File

@ -16,8 +16,7 @@ ExpandComment =
callbacks: [] callbacks: []
cb: (e) -> cb: (e) ->
e.preventDefault() e.preventDefault()
post = Get.postFromNode @ ExpandComment.expand Get.postFromNode @
ExpandComment.expand post
expand: (post) -> expand: (post) ->
if post.nodes.longComment and !post.nodes.longComment.parentNode if post.nodes.longComment and !post.nodes.longComment.parentNode
$.replace post.nodes.shortComment, post.nodes.longComment $.replace post.nodes.shortComment, post.nodes.longComment
@ -55,6 +54,11 @@ ExpandComment =
href = quote.getAttribute 'href' href = quote.getAttribute 'href'
continue if href[0] is '/' # Cross-board quote, or board link continue if href[0] is '/' # Cross-board quote, or board link
quote.href = "/#{post.board}/res/#{href}" # Fix pathnames quote.href = "/#{post.board}/res/#{href}" # Fix pathnames
Build.capcodeReplies
boardID: post.board.ID
threadID: post.thread.ID
bq: clone
capcodeReplies: postObj.capcode_replies
post.nodes.shortComment = comment post.nodes.shortComment = comment
$.replace comment, clone $.replace comment, clone
post.nodes.comment = post.nodes.longComment = clone post.nodes.comment = post.nodes.longComment = clone

View File

@ -18,7 +18,6 @@ ThreadStats =
@postCountEl = $ '#post-count', sc @postCountEl = $ '#post-count', sc
@fileCountEl = $ '#file-count', sc @fileCountEl = $ '#file-count', sc
@pageCountEl = $ '#page-count', sc @pageCountEl = $ '#page-count', sc
@lastModified = '0'
Thread::callbacks.push Thread::callbacks.push
name: 'Thread Stats' name: 'Thread Stats'
@ -55,12 +54,10 @@ ThreadStats =
return return
setTimeout ThreadStats.fetchPage, 2 * $.MINUTE setTimeout ThreadStats.fetchPage, 2 * $.MINUTE
$.ajax "//api.4chan.org/#{ThreadStats.thread.board}/threads.json", onload: ThreadStats.onThreadsLoad, $.ajax "//api.4chan.org/#{ThreadStats.thread.board}/threads.json", onload: ThreadStats.onThreadsLoad,
headers: 'If-Modified-Since': ThreadStats.lastModified whenModified: true
onThreadsLoad: -> onThreadsLoad: ->
return if !Conf["Page Count in Stats"] return unless Conf["Page Count in Stats"] and @status is 200
ThreadStats.lastModified = @getResponseHeader 'Last-Modified'
return if @status isnt 200
pages = JSON.parse @response pages = JSON.parse @response
for page in pages for page in pages
for thread in page.threads for thread in page.threads

View File

@ -64,7 +64,6 @@ ThreadUpdater =
ThreadUpdater.root = @OP.nodes.root.parentNode ThreadUpdater.root = @OP.nodes.root.parentNode
ThreadUpdater.lastPost = +ThreadUpdater.root.lastElementChild.id.match(/\d+/)[0] ThreadUpdater.lastPost = +ThreadUpdater.root.lastElementChild.id.match(/\d+/)[0]
ThreadUpdater.outdateCount = 0 ThreadUpdater.outdateCount = 0
ThreadUpdater.lastModified = '0'
ThreadUpdater.cb.interval.call $.el 'input', value: Conf['Interval'] ThreadUpdater.cb.interval.call $.el 'input', value: Conf['Interval']
@ -136,9 +135,7 @@ ThreadUpdater =
when 200 when 200
g.DEAD = false g.DEAD = false
ThreadUpdater.parse JSON.parse(req.response).posts ThreadUpdater.parse JSON.parse(req.response).posts
ThreadUpdater.lastModified = req.getResponseHeader 'Last-Modified' ThreadUpdater.set 'timer', ThreadUpdater.getInterval()
if Conf['Auto Update']
ThreadUpdater.set 'timer', ThreadUpdater.getInterval()
when 404 when 404
g.DEAD = true g.DEAD = true
ThreadUpdater.set 'timer', null ThreadUpdater.set 'timer', null
@ -149,14 +146,8 @@ ThreadUpdater =
404: true 404: true
thread: ThreadUpdater.thread thread: ThreadUpdater.thread
else else
if Conf['Auto Update'] ThreadUpdater.outdateCount++
ThreadUpdater.outdateCount++ ThreadUpdater.set 'timer', ThreadUpdater.getInterval()
ThreadUpdater.set 'timer', ThreadUpdater.getInterval()
###
Status Code 304: Not modified
By sending the `If-Modified-Since` header we get a proper status code, and no response.
This saves bandwidth for both the user and the servers and avoid unnecessary computation.
###
[text, klass] = if req.status is 304 [text, klass] = if req.status is 304
[null, null] [null, null]
else else
@ -218,7 +209,7 @@ ThreadUpdater =
ThreadUpdater.req.abort() 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, onloadend: ThreadUpdater.cb.load, ThreadUpdater.req = $.ajax url, onloadend: ThreadUpdater.cb.load,
headers: 'If-Modified-Since': ThreadUpdater.lastModified whenModified: true
updateThreadStatus: (title, OP) -> updateThreadStatus: (title, OP) ->
titleLC = title.toLowerCase() titleLC = title.toLowerCase()

View File

@ -1,116 +1,299 @@
ThreadWatcher = ThreadWatcher =
init: -> init: ->
return unless Conf['Thread Watcher'] return if !Conf['Thread Watcher']
@shortcut = sc = $.el 'a', @shortcut = sc = $.el 'a',
textContent: 'Watcher' textContent: 'Watcher'
id: 'watcher-link' id: 'watcher-link'
href: 'javascript:;' href: 'javascript:;'
className: 'disabled' className: 'disabled'
@dialog = UI.dialog 'watcher', 'top: 50px; left: 0px;', @db = new DataBoard 'watchedThreads', @refresh, true
'<div class=move>Thread Watcher<a class=close href=javascript:;>×</a></div>' @dialog = UI.dialog 'thread-watcher', 'top: 50px; left: 0px;', """
<%= grunt.file.read('src/General/html/Monitoring/ThreadWatcher.html').replace(/>\s+</g, '><').trim() %>
"""
@status = $ '#watcher-status', @dialog
@list = @dialog.lastElementChild
$.on d, 'QRPostSuccessful', @cb.post $.on d, 'QRPostSuccessful', @cb.post
$.sync 'WatchedThreads', @refresh $.on d, 'ThreadUpdate', @cb.threadUpdate if g.VIEW is 'thread'
$.on sc, 'click', @toggleWatcher $.on sc, 'click', @toggleWatcher
$.on $('.move>.close', ThreadWatcher.dialog), 'click', @toggleWatcher $.on $('.move>.close', ThreadWatcher.dialog), 'click', @toggleWatcher
$.on d, '4chanXInitFinished', @ready
if Conf['Toggleable Thread Watcher'] if Conf['Toggleable Thread Watcher']
Header.addShortcut sc Header.addShortcut sc
$.addClass doc, 'fixed-watcher' $.addClass doc, 'fixed-watcher'
$.ready -> now = Date.now()
ThreadWatcher.refresh() if (@db.data.lastChecked or 0) < now - 2 * $.HOUR
$.add d.body, ThreadWatcher.dialog @db.data.lastChecked = now
if Conf['Toggleable Thread Watcher'] ThreadWatcher.fetchAllStatus()
ThreadWatcher.dialog.hidden = true @db.save()
# XXX tmp conversion from old to new format
$.get 'WatchedThreads', null, ({WatchedThreads}) ->
return unless WatchedThreads
for boardID, threads of ThreadWatcher.convert WatchedThreads
for threadID, data of threads
ThreadWatcher.db.set {boardID, threadID, val: data}
$.delete 'WatchedThreads'
Thread::callbacks.push Thread::callbacks.push
name: 'Thread Watcher' name: 'Thread Watcher'
cb: @node cb: @node
node: -> node: ->
favicon = $.el 'a', toggler = $.el 'img',
className: 'watch-thread-link' className: 'watcher-toggler'
href: 'javascript:;' $.on toggler, 'click', ThreadWatcher.cb.toggle
$.on favicon, 'click', ThreadWatcher.cb.toggle $.before $('input', @OP.nodes.post), toggler
$.before $('input', @OP.nodes.post), favicon
return if g.VIEW isnt 'thread' ready: ->
$.get 'AutoWatch', 0, (item) => $.off d, '4chanXInitFinished', ThreadWatcher.ready
return if item['AutoWatch'] isnt @ID return unless Main.isThisPageLegit()
ThreadWatcher.watch @ ThreadWatcher.refresh()
$.add d.body, ThreadWatcher.dialog
if Conf['Toggleable Thread Watcher']
ThreadWatcher.dialog.hidden = true
return unless Conf['Auto Watch']
$.get 'AutoWatch', 0, ({AutoWatch}) ->
return unless thread = g.BOARD.threads[AutoWatch]
ThreadWatcher.add thread
$.delete 'AutoWatch' $.delete 'AutoWatch'
refresh: (watched) ->
unless watched
$.get 'WatchedThreads', {}, (item) ->
ThreadWatcher.refresh item['WatchedThreads']
return
nodes = [$('.move', ThreadWatcher.dialog)]
for board of watched
for id, props of watched[board]
x = $.el 'a',
textContent: '×'
className: 'close'
href: 'javascript:;'
$.on x, 'click', ThreadWatcher.cb.x
link = $.el 'a', props
link.title = link.textContent
div = $.el 'div'
$.add div, [x, $.tn(' '), link]
nodes.push div
$.rmAll ThreadWatcher.dialog
$.add ThreadWatcher.dialog, nodes
watched = watched[g.BOARD] or {}
for ID, thread of g.BOARD.threads
favicon = $ '.watch-thread-link', thread.OP.nodes.post
if ID of watched
$.addClass favicon, 'watched'
else
$.rmClass favicon, 'watched'
return
toggleWatcher: -> toggleWatcher: ->
$.toggleClass ThreadWatcher.shortcut, 'disabled' $.toggleClass ThreadWatcher.shortcut, 'disabled'
ThreadWatcher.dialog.hidden = !ThreadWatcher.dialog.hidden ThreadWatcher.dialog.hidden = !ThreadWatcher.dialog.hidden
cb: cb:
openAll: ->
return if $.hasClass @, 'disabled'
for a in $$ 'a[title]', ThreadWatcher.list
$.open a.href
$.event 'CloseMenu'
checkThreads: ->
return if $.hasClass @, 'disabled'
ThreadWatcher.fetchAllStatus()
pruneDeads: ->
return if $.hasClass @, 'disabled'
for {boardID, threadID, data} in ThreadWatcher.getAll() when data.isDead
delete ThreadWatcher.db.data.boards[boardID][threadID]
ThreadWatcher.db.deleteIfEmpty {boardID}
ThreadWatcher.db.save()
ThreadWatcher.refresh()
$.event 'CloseMenu'
toggle: -> toggle: ->
ThreadWatcher.toggle Get.postFromNode(@).thread ThreadWatcher.toggle Get.postFromNode(@).thread
x: -> rm: ->
thread = @nextElementSibling.pathname.split '/' [boardID, threadID] = @parentNode.dataset.fullID.split '.'
ThreadWatcher.unwatch thread[1], thread[3] ThreadWatcher.rm boardID, +threadID
post: (e) -> post: (e) ->
{board, postID, threadID} = e.detail {board, postID, threadID} = e.detail
if postID is threadID if postID is threadID
if Conf['Auto Watch'] if Conf['Auto Watch']
$.set 'AutoWatch', threadID $.set 'AutoWatch', threadID
else if Conf['Auto Watch Reply'] else if Conf['Auto Watch Reply']
ThreadWatcher.watch board.threads[threadID] ThreadWatcher.add board.threads[threadID]
threadUpdate: (e) ->
{thread} = e.detail
return unless e.detail[404] and ThreadWatcher.db.get {boardID: thread.board.ID, threadID: thread.ID}
# Update 404 status.
ThreadWatcher.add thread
fetchCount:
fetched: 0
fetching: 0
fetchAllStatus: ->
ThreadWatcher.status.textContent = '...'
for thread in ThreadWatcher.getAll()
ThreadWatcher.fetchStatus thread
return
fetchStatus: ({boardID, threadID, data}) ->
return if data.isDead
{fetchCount} = ThreadWatcher
fetchCount.fetching++
$.ajax "//api.4chan.org/#{boardID}/res/#{threadID}.json",
onloadend: ->
fetchCount.fetched++
if fetchCount.fetched is fetchCount.fetching
fetchCount.fetched = 0
fetchCount.fetching = 0
status = ''
else
status = "#{Math.round fetchCount.fetched / fetchCount.fetching * 100}%"
ThreadWatcher.status.textContent = status
return if @status isnt 404
if Conf['Auto Prune']
ThreadWatcher.rm boardID, threadID
else
data.isDead = true
ThreadWatcher.db.set {boardID, threadID, val: data}
ThreadWatcher.refresh()
,
type: 'head'
getAll: ->
all = []
for boardID, threads of ThreadWatcher.db.data.boards
if Conf['Current Board'] and boardID isnt g.BOARD.ID
continue
for threadID, data of threads
all.push {boardID, threadID, data}
all
makeLine: (boardID, threadID, data) ->
x = $.el 'a',
textContent: '×'
href: 'javascript:;'
$.on x, 'click', ThreadWatcher.cb.rm
if data.isDead
href = Redirect.to 'thread', {boardID, threadID}
link = $.el 'a',
href: href or "/#{boardID}/res/#{threadID}"
textContent: data.excerpt
title: data.excerpt
div = $.el 'div'
fullID = "#{boardID}.#{threadID}"
div.dataset.fullID = fullID
$.addClass div, 'current' if g.VIEW is 'thread' and fullID is "#{g.BOARD}.#{g.THREADID}"
$.addClass div, 'dead-thread' if data.isDead
$.add div, [x, $.tn(' '), link]
div
refresh: ->
nodes = []
for {boardID, threadID, data} in ThreadWatcher.getAll()
nodes.push ThreadWatcher.makeLine boardID, threadID, data
{list} = ThreadWatcher
$.rmAll list
$.add list, nodes
for threadID, thread of g.BOARD.threads
toggler = $ '.watcher-toggler', thread.OP.nodes.post
watched = ThreadWatcher.db.get {boardID: thread.board.ID, threadID}
$[if watched then 'addClass' else 'rmClass'] toggler, 'watched'
for refresher in ThreadWatcher.menu.refreshers
refresher()
return
toggle: (thread) -> toggle: (thread) ->
unless $.hasClass $('.watch-thread-link', thread.OP.nodes.post), 'watched' boardID = thread.board.ID
ThreadWatcher.watch thread threadID = thread.ID
if ThreadWatcher.db.get {boardID, threadID}
ThreadWatcher.rm boardID, threadID
else else
ThreadWatcher.unwatch thread.board, thread.ID ThreadWatcher.add thread
add: (thread) ->
data = {}
boardID = thread.board.ID
threadID = thread.ID
if thread.isDead
if Conf['Auto Prune'] and ThreadWatcher.db.get {boardID, threadID}
ThreadWatcher.rm boardID, threadID
return
data.isDead = true
data.excerpt = Get.threadExcerpt thread
ThreadWatcher.db.set {boardID, threadID, val: data}
ThreadWatcher.refresh()
rm: (boardID, threadID) ->
ThreadWatcher.db.delete {boardID, threadID}
ThreadWatcher.refresh()
unwatch: (board, threadID) -> convert: (oldFormat) ->
$.get 'WatchedThreads', {}, (item) -> newFormat = {}
watched = item['WatchedThreads'] for boardID, threads of oldFormat
delete watched[board][threadID] for threadID, data of threads
delete watched[board] unless Object.keys(watched[board]).length (newFormat[boardID] or= {})[threadID] = excerpt: data.textContent
ThreadWatcher.refresh watched newFormat
$.set 'WatchedThreads', watched
watch: (thread) -> menu:
$.get 'WatchedThreads', {}, (item) -> refreshers: []
watched = item['WatchedThreads'] init: ->
watched[thread.board] or= {} return if !Conf['Thread Watcher']
watched[thread.board][thread] = menu = new UI.Menu 'thread watcher'
href: "/#{thread.board}/res/#{thread}" $.on $('.menu-button', ThreadWatcher.dialog), 'click', (e) ->
textContent: Get.threadExcerpt thread menu.toggle e, @, ThreadWatcher
ThreadWatcher.refresh watched @addHeaderMenuEntry()
$.set 'WatchedThreads', watched @addMenuEntries()
addHeaderMenuEntry: ->
return if g.VIEW isnt 'thread'
entryEl = $.el 'a',
href: 'javascript:;'
$.event 'AddMenuEntry',
type: 'header'
el: entryEl
order: 60
$.on entryEl, 'click', -> ThreadWatcher.toggle g.threads["#{g.BOARD}.#{g.THREADID}"]
@refreshers.push ->
[addClass, rmClass, text] = if $ '.current', ThreadWatcher.list
['unwatch-thread', 'watch-thread', 'Unwatch thread']
else
['watch-thread', 'unwatch-thread', 'Watch thread']
$.addClass entryEl, addClass
$.rmClass entryEl, rmClass
entryEl.textContent = text
addMenuEntries: ->
entries = []
# `Open all` entry
entries.push
cb: ThreadWatcher.cb.openAll
entry:
type: 'thread watcher'
el: $.el 'a',
textContent: 'Open all threads'
refresh: -> (if ThreadWatcher.list.firstElementChild then $.rmClass else $.addClass) @el, 'disabled'
# `Check 404'd threads` entry
entries.push
cb: ThreadWatcher.cb.checkThreads
entry:
type: 'thread watcher'
el: $.el 'a',
textContent: 'Check 404\'d threads'
refresh: -> (if $('div:not(.dead-thread)', ThreadWatcher.list) then $.rmClass else $.addClass) @el, 'disabled'
# `Prune 404'd threads` entry
entries.push
cb: ThreadWatcher.cb.pruneDeads
entry:
type: 'thread watcher'
el: $.el 'a',
textContent: 'Prune 404\'d threads'
refresh: -> (if $('.dead-thread', ThreadWatcher.list) then $.rmClass else $.addClass) @el, 'disabled'
# `Settings` entries:
subEntries = []
for name, conf of Config.threadWatcher
subEntries.push @createSubEntry name, conf[1]
entries.push
entry:
type: 'thread watcher'
el: $.el 'span',
textContent: 'Settings'
subEntries: subEntries
for {entry, cb, refresh} in entries
entry.el.href = 'javascript:;' if entry.el.nodeName is 'A'
$.on entry.el, 'click', cb if cb
@refreshers.push refresh.bind entry if refresh
$.event 'AddMenuEntry', entry
createSubEntry: (name, desc) ->
entry =
type: 'thread watcher'
el: $.el 'label',
innerHTML: "<input type=checkbox name='#{name}'> #{name}"
title: desc
input = entry.el.firstElementChild
input.checked = Conf[name]
$.on input, 'change', $.cb.checked
$.on input, 'change', ThreadWatcher.refresh if name is 'Current Board'
entry

View File

@ -660,21 +660,9 @@ QR =
cv.width = img.width = width cv.width = img.width = width
cv.getContext('2d').drawImage img, 0, 0, width, height cv.getContext('2d').drawImage img, 0, 0, width, height
URL.revokeObjectURL fileURL URL.revokeObjectURL fileURL
applyBlob = (blob) => cv.toBlob (blob) =>
@URL = URL.createObjectURL blob @URL = URL.createObjectURL blob
@nodes.el.style.backgroundImage = "url(#{@URL})" @nodes.el.style.backgroundImage = "url(#{@URL})"
if cv.toBlob
cv.toBlob applyBlob
return
data = atob cv.toDataURL().split(',')[1]
# DataUrl to Binary code from Aeosynth's 4chan X repo
l = data.length
ui8a = new Uint8Array l
for i in [0...l]
ui8a[i] = data.charCodeAt i
applyBlob new Blob [ui8a], type: 'image/png'
fileURL = URL.createObjectURL @file fileURL = URL.createObjectURL @file
img.src = fileURL img.src = fileURL

View File

@ -16,7 +16,6 @@ QuoteYou =
cb: @node cb: @node
node: -> node: ->
# Stop there if it's a clone.
return if @isClone return if @isClone
if @info.yours if @info.yours