Merge branch 'v3'

Conflicts:
	CHANGELOG.md
	LICENSE
	builds/4chan-X.js
	builds/4chan-X.meta.js
	builds/4chan-X.user.js
	builds/crx/manifest.json
	builds/crx/script.js
	latest.js
	package.json
	src/General/Header.coffee
	src/General/Main.coffee
	src/General/Settings.coffee
	src/General/css/burichan.css
	src/General/css/futaba.css
	src/General/css/photon.css
	src/General/css/style.css
	src/General/css/tomorrow.css
	src/General/css/yotsuba-b.css
	src/General/css/yotsuba.css
	src/General/html/Settings/Advanced.html
	src/Monitoring/Favicon.coffee
	src/Monitoring/ThreadWatcher.coffee
This commit is contained in:
Zixaphir 2013-05-14 11:40:38 -07:00
commit 1debf61f2e
30 changed files with 1681 additions and 890 deletions

View File

@ -1,5 +1,19 @@
**MayhemYDG**:
- Add new archive selection
**seaweedchan**:
- Change watcher favicon to a heart. Change class name from `.favicon` to `.watch-thread-link`. Add `.watched` if thread is watched.
- Remove new archive selection back into Advanced
- Some styling fixes
**zixaphir**:
- Make new archive selection not depend on a JSON file
- Remove some code that sends user errors back to us (we didn't have a working link anyway)
### v2.0.3 ### v2.0.3
*2013-05-10* *2013-05-10*
**seaweedchan**:
- bug fixes
**zixaphir**: **zixaphir**:
- Change Custom Board Navigation input into textarea, new lines will convert to spaces - Change Custom Board Navigation input into textarea, new lines will convert to spaces

View File

@ -1,5 +1,5 @@
/* /*
* appchan x - Version 2.0.3 - 2013-05-13 * appchan x - Version 2.0.3 - 2013-05-14
* *
* Licensed under the MIT license. * Licensed under the MIT license.
* https://github.com/zixaphir/appchan-x/blob/master/LICENSE * https://github.com/zixaphir/appchan-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

File diff suppressed because one or more lines are too long

View File

@ -1,159 +1,170 @@
Redirect = Redirect =
thread: {}
post: {}
file: {}
init: -> init: ->
$.sync 'archivers', @updateArchives for boardID, data of Conf['selectedArchives']
for type, id of data
for name, archive of Redirect.archives
continue if name isnt id or type is 'post' and archive.software isnt 'foolfuuka'
arr = if type is 'file'
archive.files
else
archive.boards
Redirect[type][boardID] = archive if arr.contains boardID
for name, archive of Redirect.archives
for boardID in archive.boards
unless boardID of Redirect.thread
Redirect.thread[boardID] = archive
unless boardID of Redirect.post or archive.software isnt 'foolfuuka'
Redirect.post[boardID] = archive
unless boardID of Redirect.file or !archive.files.contains boardID
Redirect.file[boardID] = archive
return
updateArchives: -> archives:
$.get 'archivers', {}, ({archivers}) ->
Conf['archivers'] = archivers
imageArchives: do ->
o =
a: "//archive.foolz.us/"
ck: "//fuuka.warosu.org/"
an: "http://archive.heinessen.com/"
cgl: "//rbt.asia/"
c: "//archive.nyafuu.org/"
d: "//loveisover.me/"
e: "http://archive.foolzashit.com"
hr: "http://archive.4plebs.org/"
u: "//nsfw.foolz.us/"
po: "//archive.thedarkcave.org/"
vg: "http://nth.pensivenonsen.se/"
c: "//archive.nyafuu.org/"
o.adv = o.asp = o.cm = o.i = o.n = o.o = o.p = o.s = o.t = o.trv = o.y = o.lgbt = o.s4s = o.e
o.gd = o.jp = o.m = o.q = o.tg = o.vp = o.vr = o.wsg = o.a
o.fa = o.lit = o.ck
o.k = o.toy = o.x = o.an
o.g = o.mu = o.cgl
o.w = o.wg = o.c
o.h = o.v = o.d
o.tv = o.hr
return o
image: (boardID, filename) ->
# Do not use g.BOARD, the image url can originate from a cross-quote.
# Fuck. Your. Shit.
"#{Redirect.imageArchives[boardID]}#{boardID}/full_image/#{filename}"
post: (boardID, postID) ->
unless Redirect.post[boardID]?
for name, archive of @archiver
if archive.type is 'foolfuuka' and archive.boards.contains boardID
Redirect.post[boardID] = archive.base
break
Redirect.post[boardID] or= false
return if Redirect.post[boardID]
"#{Redirect.post[boardID]}/_/api/chan/post/?board=#{boardID}&num=#{postID}"
else
null
select: (board) ->
for name, archive of @archiver
continue unless archive.boards.contains board
name
to: (data) ->
{boardID} = data
unless (arch = Conf.archivers[boardID])?
Conf.archivers[boardID] = arch = @select(boardID)[0]
$.set 'archivers', Conf.archivers
return (if arch and archive = @archiver[arch]
Redirect.path archive.base, archive.type, data
else if data.threadID
"//boards.4chan.org/#{boardID}/"
else
null)
unless archive.boards.contains g.BOARD.ID
Conf['archivers'] = archive
archiver:
'Foolz': 'Foolz':
base: 'https://archive.foolz.us' 'domain': 'archive.foolz.us'
boards: ['a', 'co', 'gd', 'jp', 'm', 'q', 'sp', 'tg', 'tv', 'vp', 'vr', 'wsg'] 'http': true
type: 'foolfuuka' 'https': true
'NSFWFoolz': 'software': 'foolfuuka'
base: 'https://nsfw.foolz.us' 'boards': ['a', 'co', 'gd', 'jp', 'm', 'q', 'sp', 'tg', 'tv', 'vp', 'vr', 'wsg']
boards: ['u'] 'files': ['a', 'gd', 'jp', 'm', 'q', 'tg', 'vp', 'vr', 'wsg']
type: 'foolfuuka'
'TheDarkCave': 'NSFW Foolz':
base: 'http://archive.thedarkcave.org' 'domain': 'nsfw.foolz.us'
boards: ['c', 'int', 'out', 'po'] 'http': true
type: 'foolfuuka' 'https': true
'software': 'foolfuuka'
'boards': ['u']
'files': ['u']
'The Dark Cave':
'domain': 'archive.thedarkcave.org'
'http': true
'https': true
'software': 'foolfuuka'
'boards': ['c', 'int', 'out', 'po']
'files': ['c', 'po']
'4plebs': '4plebs':
base: 'http://archive.4plebs.org' 'domain': 'archive.4plebs.org'
boards: ['hr', 'tg', 'tv', 'x'] 'http': true
base: 'foolfuuka' 'software': 'foolfuuka'
'NyaFuu': 'boards': ['hr', 'tg', 'tv', 'x']
base: '//archive.nyafuu.org' 'files': ['hr', 'tg', 'tv', 'x']
boards: ['c', 'w', 'wg']
type: 'foolfuuka' 'Nyafuu':
'LoveIsOver': 'http': true
base: '//loveisover.me' 'https': true
boards: ['d', 'h', 'v'] 'software': 'foolfuuka'
type: 'foolfuuka' 'boards': ['c', 'w', 'wg']
'PensiveNonsen': 'files': ['c', 'w', 'wg']
base: 'http://nth.pensivenonsen.se'
boards: ['vg'] 'Love is Over':
type: 'foolfuuka' 'domain': 'loveisover.me'
'FoolzaShit': 'http': true
base: 'http://archive.foolzashit.com' 'https': true
boards: ["adv", "asp", "cm", "e", "i", "lgbt", "n", "o", "p", "s", "s4s", "t", "trv", "y"] 'software': 'foolfuuka'
type: 'foolfuuka' 'boards': ['d', 'h', 'v']
'Warosu': 'files': ['d', 'h', 'v']
base: '//fuuka.warosu.org'
boards: ['cgl', 'ck', 'fa', 'jp', 'lit', 's4s', 'q', 'tg', 'vr'] 'nth-chan':
type: 'fuuka' 'domain': 'nth.pensivenonsen.se'
'InstallGentoo': 'http': true
base: '//archive.installgentoo.net' 'software': 'foolfuuka'
boards: ['diy', 'g', 'sci'] 'boards': ['vg']
type: 'fuuka' 'files': ['vg']
'RebeccaBlackTech':
base: '//rbt.asia' 'Foolz a Shit':
boards: ['cgl', 'g', 'mu', 'w'] 'domain': 'archive.foolzashit.com'
type: 'fuuka_mail' 'http': true
'https': true
'software': 'foolfuuka'
'boards': ['adv', 'asp', 'cm', 'e', 'i', 'lgbt', 'n', 'o', 'p', 's', 's4s', 't', 'trv', 'y']
'files': ['adv', 'asp', 'cm', 'e', 'i', 'lgbt', 'n', 'o', 'p', 's', 's4s', 't', 'trv', 'y']
'Install Gentoo':
'domain': 'archive.installgentoo.net'
'http': true
'https': true
'software': 'fuuka'
'boards': ['diy', 'g', 'sci']
'files': []
'Rebecca Black Tech':
'domain': 'rbt.asia'
'http': true
'https': true
'software': 'fuuka'
'boards': ['cgl', 'g', 'mu', 'w']
'files': ['cgl', 'g', 'mu', 'w']
'Heinessen': 'Heinessen':
base: 'http://archive.heinessen.com' 'domain': 'archive.heinessen.com'
boards: ['an', 'fit', 'k', 'mlp', 'r9k', 'toy', 'x'] 'http': true
type: 'fuuka' 'software': 'fuuka'
'Cliche': 'boards': ['an', 'fit', 'k', 'mlp', 'r9k', 'toy', 'x']
base: '//www.cliché.net/4chan/cgi-board.pl' 'files': ['an', 'k', 'toy', 'x']
boards: ['e']
type: 'fuuka'
path: (base, archiver, data) -> 'warosu':
if data.isSearch 'domain': 'fuuka.warosu.org'
{boardID, type, value} = data 'http': true
type = if type is 'name' 'https': true
'username' 'software': 'fuuka'
else if type is 'MD5' 'boards': ['3', 'cgl', 'ck', 'fa', 'ic', 'jp', 'lit', 'q', 's4s', 'tg', 'vr']
'image' 'files': ['3', 'cgl', 'ck', 'fa', 'ic', 'jp', 'lit', 'q', 's4s', 'vr']
else
type
value = encodeURIComponent value
return if archiver is 'foolfuuka'
"#{base}/#{boardID}/search/#{type}/#{value}"
else if type is 'image'
"#{base}/#{boardID}/?task=search2&search_media_hash=#{value}"
else
"#{base}/#{boardID}/?task=search2&search_#{type}=#{value}"
{boardID, threadID, postID} = data to: (dest, data) ->
# keep the number only if the location.hash was sent f.e. archive = (if dest is 'search' then Redirect.thread else Redirect[dest])[data.boardID]
return '' unless archive
Redirect[dest] archive, data
protocol: (archive) ->
protocol = location.protocol
unless archive[protocol[0...-1]]
protocol = if protocol is 'https:' then 'http:' else 'https:'
"#{protocol}//"
thread: (archive, {boardID, threadID, postID}) ->
# Keep the post number only if the location.hash was sent f.e.
path = if threadID path = if threadID
"#{boardID}/thread/#{threadID}" "#{boardID}/thread/#{threadID}"
else else
"#{boardID}/post/#{postID}" "#{boardID}/post/#{postID}"
if archiver is 'foolfuuka' if archive.software is 'foolfuuka'
path += '/' path += '/'
if threadID and postID if threadID and postID
path += if archiver is 'foolfuuka' path += if archive.software is 'foolfuuka'
"##{postID}" "##{postID}"
else else
"#p#{postID}" "#p#{postID}"
"#{base}/#{path}" "#{Redirect.protocol archive}#{archive.domain}/#{path}"
post: (archive, {boardID, postID}) ->
# For fuuka-based archives:
# https://github.com/eksopl/fuuka/issues/27
protocol = Redirect.protocol archive
# XXX foolz had HSTS set for 120 days, which broke XHR+CORS+Redirection when on HTTP.
# Remove necessary HTTPS procotol in September 2013.
if ['Foolz', 'NSFW Foolz'].contains archive.name
protocol = 'https://'
"#{protocol}#{archive.domain}/_/api/chan/post/?board=#{boardID}&num=#{postID}"
file: (archive, {boardID, filename}) ->
"#{Redirect.protocol archive}#{archive.domain}/#{boardID}/full_image/#{filename}"
search: (archive, {boardID, type, value}) ->
type = if type is 'name'
'username'
else if type is 'MD5'
'image'
else
type
value = encodeURIComponent value
path = if archive.software is 'foolfuuka'
"#{boardID}/search/#{type}/#{value}"
else
"#{boardID}/?task=search2&search_#{if type is 'image' then 'media_hash' else type}=#{value}"
"#{Redirect.protocol archive}#{archive.domain}/#{path}"

View File

@ -229,9 +229,9 @@ Config =
true true
'Bookmark threads.' 'Bookmark threads.'
] ]
'Persistent Thread Watcher': [ 'Toggleable Thread Watcher': [
false false
'Opens the thread watcher by default.' 'Adds a shortcut for the thread watcher, hides the watcher by default, and makes it scroll with the page.'
] ]
'Auto Watch': [ 'Auto Watch': [
true true

View File

@ -71,7 +71,7 @@ Get =
if threadID if threadID
$.cache "//api.4chan.org/#{boardID}/res/#{threadID}.json", -> $.cache "//api.4chan.org/#{boardID}/res/#{threadID}.json", ->
Get.fetchedPost @, boardID, threadID, postID, root, context Get.fetchedPost @, boardID, threadID, postID, root, context
else if url = Redirect.post boardID, postID else if url = Redirect.to 'post', {boardID, postID}
$.cache url, -> $.cache url, ->
Get.archivedPost @, boardID, postID, root, context Get.archivedPost @, boardID, postID, root, context
insert: (post, root, context) -> insert: (post, root, context) ->
@ -97,7 +97,7 @@ Get =
{status} = req {status} = req
unless [200, 304].contains status unless [200, 304].contains status
# The thread can die by the time we check a quote. # The thread can die by the time we check a quote.
if url = Redirect.post boardID, postID if url = Redirect.to 'post', {boardID, postID}
$.cache url, -> $.cache url, ->
Get.archivedPost @, boardID, postID, root, context Get.archivedPost @, boardID, postID, root, context
else else
@ -115,7 +115,7 @@ Get =
break if post.no is postID # we found it! break if post.no is postID # we found it!
if post.no > postID if post.no > postID
# The post can be deleted by the time we check a quote. # The post can be deleted by the time we check a quote.
if url = Redirect.post boardID, postID if url = Redirect.to 'post', {boardID, postID}
$.cache url, -> $.cache url, ->
Get.archivedPost @, boardID, postID, root, context Get.archivedPost @, boardID, postID, root, context
else else

View File

@ -82,7 +82,7 @@ Header =
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><a href=javascript:; class='hide-board-list-button brackets-wrap'>&nbsp;&nbsp;-&nbsp;&nbsp;</a> #{fourchannav.innerHTML}</span>" innerHTML: "<span id=custom-board-list></span><span id=full-board-list hidden><span class='hide-board-list-container brackets-wrap'><a href=javascript:; class='hide-board-list-button'>&nbsp;-&nbsp;</a></span> #{fourchannav.innerHTML}</span>"
fullBoardList = $ '#full-board-list', boardList fullBoardList = $ '#full-board-list', boardList
btn = $ '.hide-board-list-button', fullBoardList btn = $ '.hide-board-list-button', fullBoardList
$.on btn, 'click', Header.toggleBoardList $.on btn, 'click', Header.toggleBoardList

View File

@ -23,7 +23,7 @@ Main =
'Enabled Mascots nsfw': [] 'Enabled Mascots nsfw': []
'Deleted Mascots': [] 'Deleted Mascots': []
'Hidden Categories': ["Questionable"] 'Hidden Categories': ["Questionable"]
'archivers': {} selectedArchives: {}
$.get Conf, Main.initFeatures $.get Conf, Main.initFeatures
@ -69,7 +69,10 @@ Main =
when 'images.4chan.org' when 'images.4chan.org'
$.ready -> $.ready ->
if Conf['404 Redirect'] and d.title is '4chan - 404 Not Found' if Conf['404 Redirect'] and d.title is '4chan - 404 Not Found'
url = Redirect.image pathname[1], pathname[3] Redirect.init()
url = Redirect.to 'file',
boardID: pathname[1]
filename: pathname[3]
location.href = url if url location.href = url if url
return return
@ -159,7 +162,7 @@ Main =
initReady: -> initReady: ->
if d.title is '4chan - 404 Not Found' if d.title is '4chan - 404 Not Found'
if Conf['404 Redirect'] and g.VIEW is 'thread' if Conf['404 Redirect'] and g.VIEW is 'thread'
href = Redirect.to href = Redirect.to 'thread',
boardID: g.BOARD.ID boardID: g.BOARD.ID
threadID: g.THREADID threadID: g.THREADID
postID: +location.hash.match /\d+/ # post number or 0 postID: +location.hash.match /\d+/ # post number or 0
@ -333,22 +336,9 @@ Main =
errors: [] errors: []
logError: (data) -> logError: (data) ->
unless Main.errors.length
$.on window, 'unload', Main.postErrors
c.error data.message, data.error.stack c.error data.message, data.error.stack
Main.errors.push data Main.errors.push data
postErrors: ->
errors = Main.errors.map (d) -> d.message + ' ' + d.error.stack
$.ajax '<%= meta.page %>errors', {},
sync: true
form: $.formData
n: "<%= meta.name %> v#{g.VERSION}"
t: '<%= type %>'
ua: window.navigator.userAgent
url: window.location.href
e: errors.join '\n'
isThisPageLegit: -> isThisPageLegit: ->
# 404 error page or similar. # 404 error page or similar.
unless 'thisPageIsLegit' of Main unless 'thisPageIsLegit' of Main

View File

@ -30,7 +30,7 @@ Settings =
else else
$.on d, '4chanXInitFinished', Settings.open $.on d, '4chanXInitFinished', Settings.open
$.set $.set
lastupdate: Date.now() lastchecked: Date.now()
previousversion: g.VERSION previousversion: g.VERSION
Settings.addSection 'Style', Settings.style Settings.addSection 'Style', Settings.style
@ -291,34 +291,101 @@ Settings =
ta.value = item['QR.personas'] ta.value = item['QR.personas']
$.on ta, 'change', $.cb.value $.on ta, 'change', $.cb.value
# Archiver
archiver = $ 'select[name=archiver]', section
toSelect = Redirect.select g.BOARD.ID
toSelect = ['No Archive Available'] unless toSelect[0]
$.add archiver, $.el('option', {textContent: name}) for name in toSelect
if toSelect[1]
Conf['archivers'][g.BOARD]
archiver.value = Conf['archivers'][g.BOARD] or toSelect[0]
$.on archiver, 'change', ->
Conf['archivers'][g.BOARD] = @value
$.set 'archivers', Conf.archivers
$.get items, (items) -> $.get items, (items) ->
for key, val of items for key, val of items
continue if ['emojiPos', 'archiver'].contains key continue if ['emojiPos'].contains key
input = inputs[key] input = inputs[key]
input.value = val input.value = val
continue if key is 'usercss' continue if key is 'usercss'
$.on input, event, Settings[key] $.on input, event, Settings[key]
Settings[key].call input Settings[key].call input
Rice.nodes sectionreturn Rice.nodes section
$.on $('input[name=Interval]', section), 'change', ThreadUpdater.cb.interval $.on $('input[name=Interval]', section), 'change', ThreadUpdater.cb.interval
$.on $('input[name="Custom CSS"]', section), 'change', Settings.togglecss $.on $('input[name="Custom CSS"]', section), 'change', Settings.togglecss
$.on $.id('apply-css'), 'click', Settings.usercss $.on $.id('apply-css'), 'click', Settings.usercss
boards = {}
for name, archive of Redirect.archives
for boardID in archive.boards
data = boards[boardID] or= {
thread: []
post: []
file: []
}
data.thread.push name
data.post.push name if archive.software is 'foolfuuka'
data.file.push name if archive.files.contains boardID
rows = []
boardOptions = []
for boardID in Object.keys(boards).sort() # Alphabetical order
row = $.el 'tr',
className: "board-#{boardID}"
row.hidden = boardID isnt g.BOARD.ID
rows.push row
boardOptions.push $.el 'option',
textContent: "/#{boardID}/"
value: "board-#{boardID}"
selected: boardID is g.BOARD.ID
data = boards[boardID]
$.add row, [
Settings.addArchiveCell boardID, data, 'thread'
Settings.addArchiveCell boardID, data, 'post'
Settings.addArchiveCell boardID, data, 'file'
]
$.add $('tbody', section), rows
boardSelect = $('#archive-board-select', section)
$.add boardSelect, boardOptions
table = $.id 'archive-table'
$.on boardSelect, 'change', ->
$('tbody > :not([hidden])', table).hidden = true
$("tbody > .#{@value}", table).hidden = false
$.get 'selectedArchives', Conf['selectedArchives'], ({selectedArchives}) ->
for boardID, data of selectedArchives
for type, name of data
if option = $ "select[data-boardid='#{boardID}'][data-type='#{type}'] > option[value='#{name}']", section
option.selected = true
return
return
addArchiveCell: (boardID, data, type) ->
{length} = data[type]
td = $.el 'td',
className: 'archive-cell'
unless length
td.textContent = '--'
return td
options = []
i = 0
while i < length
archive = data[type][i++]
options.push $.el 'option',
textContent: archive
value: archive
td.innerHTML = '<select></select>'
select = td.firstElementChild
unless select.disabled = length is 1
# XXX GM can't into datasets
select.setAttribute 'data-boardid', boardID
select.setAttribute 'data-type', type
$.on select, 'change', Settings.saveSelectedArchive
$.add select, options
td
saveSelectedArchive: ->
$.get 'selectedArchives', Conf['selectedArchives'], ({selectedArchives}) =>
(selectedArchives[@dataset.boardid] or= {})[@dataset.type] = @value
$.set 'selectedArchives', selectedArchives
boardnav: -> boardnav: ->
Header.generateBoardList @value Header.generateBoardList @value

View File

@ -319,7 +319,8 @@ UI = do ->
o.hover o.latestEvent if el.parentNode o.hover o.latestEvent if el.parentNode
$.on root, endEvents, o.hoverend $.on root, endEvents, o.hoverend
$.on d, 'keydown', o.hoverend if $.x 'ancestor::div[contains(@class,"inline")][1]', root
$.on d, 'keydown', o.hoverend
$.on root, 'mousemove', o.hover $.on root, 'mousemove', o.hover
hover = (e) -> hover = (e) ->
@ -346,7 +347,7 @@ UI = do ->
style.right = right style.right = right
hoverend = (e) -> hoverend = (e) ->
return if e.type is 'keydown' and e.keyCode isnt 13 return if e.type is 'keydown' and e.keyCode isnt 13 or e.target.nodeName is "TEXTAREA"
$.rm @el $.rm @el
$.off @root, @endEvents, @hoverend $.off @root, @endEvents, @hoverend
$.off d, 'keydown', @hoverend $.off d, 'keydown', @hoverend

View File

@ -0,0 +1,58 @@
/* General */
:root.burichan .dialog {
background-color: #D6DAF0;
border-color: #B7C5D9;
}
:root.burichan .field:focus {
border-color: #98E;
}
/* Header */
:root.burichan #header-bar, :root.burichan #header-bar #notifications {
font-size: 11pt;
color: #89A;
}
:root.burichan #header-bar a, :root.burichan #header-bar #notifications a {
color: #34345C;
}
/* Settings */
:root.burichan #fourchanx-settings fieldset {
border-color: #B7C5D9;
}
/* Quote */
:root.burichan .backlink.deadlink {
color: #34345C !important;
}
:root.burichan .inline {
border-color: #B7C5D9;
background-color: rgba(255, 255, 255, .14);
}
/* QR */
.burichan #dump-list::-webkit-scrollbar-thumb {
background-color: #D6DAF0;
border-color: #B7C5D9;
}
:root.burichan .qr-preview {
background-color: rgba(0, 0, 0, .15);
}
/* Menu */
:root.burichan #menu {
color: #000000;
}
:root.burichan .entry {
border-bottom: 1px solid #B7C5D9;
font-size: 12pt;
}
:root.burichan .focused.entry {
background: rgba(255, 255, 255, .33);
}
/* Watcher Favicon */
:root.burichan .watch-thread-link
{
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

@ -0,0 +1,58 @@
/* General */
:root.futaba .dialog {
background-color: #F0E0D6;
border-color: #D9BFB7;
}
:root.futaba .field:focus {
border-color: #EA8;
}
/* Header */
:root.futaba #header-bar, :root.futaba #notifications {
font-size: 11pt;
color: #B86;
}
:root.futaba #header-bar a, :root.futaba #notifications a {
color: #800000;
}
/* Settings */
:root.futaba #fourchanx-settings fieldset {
border-color: #D9BFB7;
}
/* Quote */
:root.futaba .backlink.deadlink {
color: #00E !important;
}
:root.futaba .inline {
border-color: #D9BFB7;
background-color: rgba(255, 255, 255, .14);
}
/* QR */
.futaba #dump-list::-webkit-scrollbar-thumb {
background-color: #F0E0D6;
border-color: #D9BFB7;
}
:root.futaba .qr-preview {
background-color: rgba(0, 0, 0, .15);
}
/* Menu */
:root.futaba #menu {
color: #800000;
}
:root.futaba .entry {
border-bottom: 1px solid #D9BFB7;
font-size: 12pt;
}
:root.futaba .focused.entry {
background: rgba(255, 255, 255, .33);
}
/* Watcher Favicon */
:root.futaba .watch-thread-link
{
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

@ -127,6 +127,9 @@ hr {
margin: 0 0 1px; margin: 0 0 1px;
#{if _conf['Hide Horizontal Rules'] then 'visibility: hidden;' else ''} #{if _conf['Hide Horizontal Rules'] then 'visibility: hidden;' else ''}
} }
th {
text-align: left;
}
.center { .center {
text-align: center; text-align: center;
} }
@ -716,6 +719,19 @@ hide: "
overflow: hidden; overflow: hidden;
} }
"} "}
.watch-thread-link {
padding-top: 18px;
width: 18px;
height: 0px;
display: inline-block;
background-repeat: no-repeat;
opacity: 0.2;
position: relative;
top: 1px;
}
.watch-thread-link.watched {
opacity: 1;
}
/* Announcements */ /* Announcements */
#globalMessage { #globalMessage {
text-align: center; text-align: center;
@ -1569,6 +1585,9 @@ a:only-of-type > .remove {
display: block; display: block;
margin: 0 auto 6px; margin: 0 auto 6px;
} }
.section-advanced .archive-cell {
min-width: 200px;
}
.section-advanced .selectrice { .section-advanced .selectrice {
display: inline-block; display: inline-block;
clear: both; clear: both;

View File

@ -0,0 +1,58 @@
/* General */
:root.photon .dialog {
background-color: #DDD;
border-color: #CCC;
}
:root.photon .field:focus {
border-color: #EA8;
}
/* Header */
:root.photon #header-bar, :root.photon #notifications {
font-size: 9pt;
color: #333;
}
:root.photon #header-bar a, :root.photon #notifications a {
color: #FF6600;
}
/* Settings */
:root.photon #fourchanx-settings fieldset {
border-color: #CCC;
}
/* Quote */
:root.photon .backlink.deadlink {
color: #F60 !important;
}
:root.photon .inline {
border-color: #CCC;
background-color: rgba(255, 255, 255, .14);
}
/* QR */
.photon #dump-list::-webkit-scrollbar-thumb {
background-color: #DDD;
border-color: #CCC;
}
:root.photon .qr-preview {
background-color: rgba(0, 0, 0, .15);
}
/* Menu */
:root.photon #menu {
color: #333;
}
:root.photon .entry {
border-bottom: 1px solid #CCC;
font-size: 10pt;
}
:root.photon .focused.entry {
background: rgba(255, 255, 255, .33);
}
/* Watcher Favicon */
:root.photon .watch-thread-link
{
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

@ -201,6 +201,9 @@ a[style="cursor: pointer; float: right;"] ~ div[style^="width: 100%;"] > table {
background: #{theme["Dialog Background"]}; background: #{theme["Dialog Background"]};
border: 1px solid #{theme["Dialog Border"]}; border: 1px solid #{theme["Dialog Border"]};
} }
.watch-thread-link {
background-image: url("data:image/svg+xml,<svg viewBox='0 0 26 26' preserveAspectRatio='true' xmlns='http://www.w3.org/2000/svg'><path fill='#{theme["Post Numbers"]}' 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>");
}
.deleteform::before, .deleteform::before,
.deleteform, .deleteform,
#qr .warning { #qr .warning {

View File

@ -0,0 +1,64 @@
/* General */
:root.tomorrow .dialog {
background-color: #282A2E;
border-color: #111;
}
:root.tomorrow .field:focus {
border-color: #000;
}
/* Header */
:root.tomorrow #header-bar, :root.tomorrow #notifications {
font-size: 9pt;
color: #C5C8C6;
}
:root.tomorrow #header-bar a, :root.tomorrow #notifications a {
color: #81A2BE;
}
/* Settings */
:root.tomorrow #fourchanx-settings fieldset {
border-color: #111;
}
/* Quote */
:root.tomorrow .backlink.deadlink {
color: #81A2BE !important;
}
:root.tomorrow .inline {
border-color: #111;
background-color: rgba(0, 0, 0, .14);
}
/* QR */
.tomorrow #dump-list::-webkit-scrollbar-thumb {
background-color: #282A2E;
border-color: #111;
}
:root.tomorrow #qr select {
color: #C5C8C6;
}
:root.tomorrow #qr option {
color: #000;
}
:root.tomorrow .qr-preview {
background-color: rgba(255, 255, 255, .15);
}
/* Menu */
:root.tomorrow #menu {
color: #C5C8C6;
}
:root.tomorrow .entry {
border-bottom: 1px solid #111;
font-size: 10pt;
}
:root.tomorrow .focused.entry {
background: rgba(0, 0, 0, .33);
}
/* Watcher Favicon */
:root.tomorrow .watch-thread-link
{
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

@ -0,0 +1,58 @@
/* General */
:root.yotsuba-b .dialog {
background-color: #D6DAF0;
border-color: #B7C5D9;
}
:root.yotsuba-b .field:focus {
border-color: #98E;
}
/* Header */
:root.yotsuba-b #header-bar, :root.yotsuba-b #notifications {
font-size: 9pt;
color: #89A;
}
:root.yotsuba-b #header-bar a, :root.yotsuba-b #notifications a {
color: #34345C;
}
/* Settings */
:root.yotsuba-b #fourchanx-settings fieldset {
border-color: #B7C5D9;
}
/* Quote */
:root.yotsuba-b .backlink.deadlink {
color: #34345C !important;
}
:root.yotsuba-b .inline {
border-color: #B7C5D9;
background-color: rgba(255, 255, 255, .14);
}
/* QR */
.yotsuba-b #dump-list::-webkit-scrollbar-thumb {
background-color: #D6DAF0;
border-color: #B7C5D9;
}
:root.yotsuba-b .qr-preview {
background-color: rgba(0, 0, 0, .15);
}
/* Menu */
:root.yotsuba-b #menu {
color: #000;
}
:root.yotsuba-b .entry {
border-bottom: 1px solid #B7C5D9;
font-size: 10pt;
}
:root.yotsuba-b .focused.entry {
background: rgba(255, 255, 255, .33);
}
/* Watcher Favicon */
:root.yotsuba-b .watch-thread-link
{
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

@ -0,0 +1,58 @@
/* General */
:root.yotsuba .dialog {
background-color: #F0E0D6;
border-color: #D9BFB7;
}
:root.yotsuba .field:focus {
border-color: #EA8;
}
/* Header */
:root.yotsuba #header-bar, :root.yotsuba #notifications {
font-size: 9pt;
color: #B86;
}
:root.yotsuba #header-bar a, :root.yotsuba #notifications a {
color: #800000;
}
/* Settings */
:root.yotsuba #fourchanx-settings fieldset {
border-color: #D9BFB7;
}
/* Quote */
:root.yotsuba .backlink.deadlink {
color: #00E !important;
}
:root.yotsuba .inline {
border-color: #D9BFB7;
background-color: rgba(255, 255, 255, .14);
}
/* QR */
.yotsuba #dump-list::-webkit-scrollbar-thumb {
background-color: #F0E0D6;
border-color: #D9BFB7;
}
:root.yotsuba .qr-preview {
background-color: rgba(0, 0, 0, .15);
}
/* Menu */
:root.yotsuba #menu {
color: #800000;
}
:root.yotsuba .entry {
border-bottom: 1px solid #D9BFB7;
font-size: 10pt;
}
:root.yotsuba .focused.entry {
background: rgba(255, 255, 255, .33);
}
/* Watcher Favicon */
:root.yotsuba .watch-thread-link
{
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

@ -1,10 +1,18 @@
<fieldset> <fieldset>
<legend>Archiver</legend> <legend>Archiver</legend>
<div> <div class="warning" #{if Conf['404 Redirect'] then 'hidden' else ''}><code>404 Redirect</code> is disabled.</div>
Select an Archiver for this board: <div><select id='archive-board-select'></select></div>
<select name=archiver></select> <table id='archive-table'>
</div> <thead>
<th>Thread redirection</th>
<th>Post fetching</th>
<th>File redirection</th>
</thead>
<tbody></tbody>
</table>
<span class=note>Disabled selections indicate that only one archive is available for that board and redirection type.</span>
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>Custom Board Navigation</legend> <legend>Custom Board Navigation</legend>
<div><textarea name=boardnav class=field spellcheck=false></textarea></div> <div><textarea name=boardnav class=field spellcheck=false></textarea></div>

View File

@ -141,7 +141,10 @@ ImageExpand =
src = @src.split '/' src = @src.split '/'
if src[2] is 'images.4chan.org' if src[2] is 'images.4chan.org'
if URL = Redirect.image src[3], src[5] URL = Redirect.to 'file',
boardID: src[3]
filename: src[5]
if URL
setTimeout ImageExpand.expand, 10000, post, URL setTimeout ImageExpand.expand, 10000, post, URL
return return
if g.DEAD or post.isDead or post.file.isDead if g.DEAD or post.isDead or post.file.isDead

View File

@ -28,7 +28,10 @@ ImageHover =
src = @src.split '/' src = @src.split '/'
if src[2] is 'images.4chan.org' if src[2] is 'images.4chan.org'
if URL = Redirect.image src[3], src[5].replace /\?.+$/, '' URL = Redirect.to 'file',
boardID: src[3]
filename: src[5].replace /\?.+$/, ''
if URL
@src = URL @src = URL
return return
if g.DEAD or post.isDead or post.file.isDead if g.DEAD or post.isDead or post.file.isDead

View File

@ -10,8 +10,7 @@ ArchiveLink =
el: div el: div
order: 90 order: 90
open: ({ID, thread, board}) -> open: ({ID, thread, board}) ->
redirect = Redirect.to {postID: ID, threadID: thread.ID, boardID: board.ID} !!Redirect.to 'thread', {postID: ID, threadID: thread.ID, boardID: board.ID}
redirect isnt "//boards.4chan.org/#{board}/"
subEntries: [] subEntries: []
for type in [ for type in [
@ -35,14 +34,14 @@ ArchiveLink =
open = if type is 'post' open = if type is 'post'
({ID, thread, board}) -> ({ID, thread, board}) ->
el.href = Redirect.to {postID: ID, threadID: thread.ID, boardID: board.ID} el.href = Redirect.to 'thread', {postID: ID, threadID: thread.ID, boardID: board.ID}
true true
else else
(post) -> (post) ->
value = Filter[type] post value = Filter[type] post
# We want to parse the exact same stuff as the filter does already. # We want to parse the exact same stuff as the filter does already.
return false unless value return false unless value
el.href = Redirect.to el.href = Redirect.to 'search',
boardID: post.board.ID boardID: post.board.ID
type: type type: type
value: value value: value

View File

@ -5,7 +5,7 @@ CatalogLinks =
el = $.el 'label', el = $.el 'label',
id: 'toggleCatalog' id: 'toggleCatalog'
href: 'javascript:;' href: 'javascript:;'
innerHTML: "<input type=checkbox #{if Conf['Header catalog links'] then 'checked' else ''}>Catalog Links" innerHTML: "<input type=checkbox #{if Conf['Header catalog links'] then 'checked' else ''}> Catalog Links"
title: "Turn catalog links #{if Conf['Header catalog links'] then 'off' else 'on'}." title: "Turn catalog links #{if Conf['Header catalog links'] then 'off' else 'on'}."
input = $ 'input', el input = $ 'input', el
@ -41,7 +41,6 @@ CatalogLinks =
"//boards.4chan.org/#{board}/" "//boards.4chan.org/#{board}/"
else else
a.pathname = "/#{board}/#{path}" a.pathname = "/#{board}/#{path}"
a.title = if useCatalog then "#{a.title} - Catalog" else a.title.replace(/\ -\ Catalog$/, '')
@title = "Turn catalog links #{if useCatalog then 'off' else 'on'}." @title = "Turn catalog links #{if useCatalog then 'off' else 'on'}."
external: (board) -> external: (board) ->

View File

@ -84,7 +84,7 @@ ExpandThread =
if post = thread.posts[reply.no] if post = thread.posts[reply.no]
nodes.push post.nodes.root nodes.push post.nodes.root
continue continue
node = Build.postFromObject reply, thread.board node = Build.postFromObject reply, thread.board.ID
post = new Post node, thread, thread.board post = new Post node, thread, thread.board
link = $ 'a[title="Highlight this post"]', node link = $ 'a[title="Highlight this post"]', node
link.href = "res/#{thread}#p#{post}" link.href = "res/#{thread}#p#{post}"

View File

@ -53,5 +53,4 @@ Favicon =
Favicon.unread = Favicon.unreadNSFW Favicon.unread = Favicon.unreadNSFW
Favicon.unreadY = Favicon.unreadNSFWY Favicon.unreadY = Favicon.unreadNSFWY
empty: 'data:image/png;base64,<%= grunt.file.read("src/General/img/favicons/empty.png", {encoding: "base64"}) %>'
dead: 'data:image/png;base64,<%= grunt.file.read("src/General/img/favicons/dead.png", {encoding: "base64"}) %>' dead: 'data:image/png;base64,<%= grunt.file.read("src/General/img/favicons/dead.png", {encoding: "base64"}) %>'

View File

@ -9,6 +9,7 @@ ThreadWatcher =
$.sync 'WatchedThreads', @refresh $.sync 'WatchedThreads', @refresh
$.on $('.move>.close', ThreadWatcher.dialog), 'click', @toggleWatcher $.on $('.move>.close', ThreadWatcher.dialog), 'click', @toggleWatcher
$.ready -> $.ready ->
ThreadWatcher.refresh() ThreadWatcher.refresh()
$.add d.body, ThreadWatcher.dialog $.add d.body, ThreadWatcher.dialog
@ -18,8 +19,9 @@ ThreadWatcher =
cb: @node cb: @node
node: -> node: ->
favicon = $.el 'img', favicon = $.el 'a',
className: 'favicon' className: 'watch-thread-link'
href: 'javascript:;'
$.on favicon, 'click', ThreadWatcher.cb.toggle $.on favicon, 'click', ThreadWatcher.cb.toggle
$.before $('input', @OP.nodes.post), favicon $.before $('input', @OP.nodes.post), favicon
return if g.VIEW isnt 'thread' return if g.VIEW isnt 'thread'
@ -53,11 +55,11 @@ ThreadWatcher =
watched = watched[g.BOARD] or {} watched = watched[g.BOARD] or {}
for ID, thread of g.BOARD.threads for ID, thread of g.BOARD.threads
favicon = $ '.favicon', thread.OP.nodes.post favicon = $ '.watch-thread-link', thread.OP.nodes.post
favicon.src = if ID of watched if ID of watched
Favicon.default $.addClass favicon, 'watched'
else else
Favicon.empty $.rmClass favicon, 'watched'
return return
toggleWatcher: -> toggleWatcher: ->
@ -79,7 +81,7 @@ ThreadWatcher =
ThreadWatcher.watch board.threads[threadID] ThreadWatcher.watch board.threads[threadID]
toggle: (thread) -> toggle: (thread) ->
if $('.favicon', thread.OP.nodes.post).src is Favicon.empty unless $.hasClass $('.watch-thread-link', thread.OP.nodes.post), 'watched'
ThreadWatcher.watch thread ThreadWatcher.watch thread
else else
ThreadWatcher.unwatch thread.board, thread.ID ThreadWatcher.unwatch thread.board, thread.ID

View File

@ -62,6 +62,7 @@ QR =
$.on $('a[title="Quote this post"]', @nodes.info), 'click', QR.quote $.on $('a[title="Quote this post"]', @nodes.info), 'click', QR.quote
persist: -> persist: ->
return unless QR.postingIsEnabled
QR.open() QR.open()
QR.hide() if Conf['Auto Hide QR'] QR.hide() if Conf['Auto Hide QR']

View File

@ -51,14 +51,14 @@ Quotify =
a.setAttribute 'data-boardid', boardID a.setAttribute 'data-boardid', boardID
a.setAttribute 'data-threadid', post.thread.ID a.setAttribute 'data-threadid', post.thread.ID
a.setAttribute 'data-postid', postID a.setAttribute 'data-postid', postID
else if redirect = Redirect.to {boardID, threadID: 0, postID} else if redirect = Redirect.to 'thread', {boardID, threadID: 0, postID}
# Replace the .deadlink span if we can redirect. # Replace the .deadlink span if we can redirect.
a = $.el 'a', a = $.el 'a',
href: redirect href: redirect
className: 'deadlink' className: 'deadlink'
target: '_blank' target: '_blank'
textContent: "#{quote}\u00A0(Dead)" textContent: "#{quote}\u00A0(Dead)"
if Redirect.post boardID, postID if Redirect.to 'post', {boardID, postID}
# Make it function as a normal quote if we can fetch the post. # Make it function as a normal quote if we can fetch the post.
$.addClass a, 'quotelink' $.addClass a, 'quotelink'
a.setAttribute 'data-boardid', boardID a.setAttribute 'data-boardid', boardID