Merge branch 'v3' into Av2

Conflicts:
	LICENSE
	builds/4chan-X.js
	builds/4chan-X.meta.js
	builds/4chan-X.user.js
	builds/crx/manifest.json
	builds/crx/script.js
	package.json
	src/General/Main.coffee
	src/General/css/style.css
	src/General/html/Features/QuickReply.html
	src/Posting/QuickReply.coffee
This commit is contained in:
Zixaphir 2013-05-03 07:17:59 -07:00
commit ee00d5a3aa
42 changed files with 22701 additions and 617 deletions

View File

@ -1,11 +1,75 @@
### 1.1.9 - 2013-05-02
seaweedchan
- Fix boards with previously deleted archives not switching to new archives
ihavenoface:
- 4chan Pass link by the style selector
zixaphir:
- Make Allow False Positives option more efficient
### 1.1.8 - 2013-05-01
seaweedchan:
- Fix QR not clearing on submit with Posting Success Notifications disabled
- New archives for /h/, /v/, and /vg/
### 1.1.7 - 2013-05-01
seaweedchan:
- External image embedding
- Account for time options in youtube links for embedding
- Once again remove /v/ and /vg/ archiving... ;_;
- Add paste.installgentoo.com embedding
- Added `Posting Success Notifications` option to make "Post Successful!" and "_____ uploaded" notifications optional
- Added `Allow False Positives` option under Linkification, giving the user more control over what's linkified.
- Fix URL for update checking
### 1.1.6 - 2013-05-01
seaweedchan:
- Fix Gist links if no username is specificed
MayhemYDG:
- Access it in the `Advanced` tab of the Settings window.
zixaphir:
- Add Gist link titles
### 1.1.5 - 2013-04-30
seaweedchan:
- Fix various embedding issues
- Fix Link Title depending on Embedding
- Added favicons to links that can be embedded
- Add gist embedding
### 1.1.4 - 2013-04-29
seaweedchan:
- Change ESC functionality in QR to autohide if Persistent QR is enabled
- Add /v/ and /vg/ archiving to archive.nihil-ad-rem.net, and make sure Archiver Selection settings actually switch to it
- Add option to toggle between updater and stats fixed in header or floating
MayhemYDG:
- Add nyafuu archiving for /w/
- Add /d/ archive
### 1.1.3 - 2013-04-28 ### 1.1.3 - 2013-04-28
seaweedchan: seaweedchan:
- Chrome doesn't get .null, so don't style it - Chrome doesn't get .null, so don't style it
- Fix count when auto update is disabled and set updater text to "Update" - Fix count when auto update is disabled and set updater text to "Update"
- Remove /v/ and /vg/ redirection. See https://archive.foolz.us/foolz/thread/509388/ for news and how you can donate to bring /v/ and /vg/ archiving back. - Remove /v/ and /vg/ redirection from Foolz.
- Toggle keybind for header auto-hiding - Toggle keybind for header auto-hiding
MayhemYDG: MayhemYDG:
=======
- Access it in the `QR` tab of the Settings window.
- Updated archive redirection for /h/, /v/ and /vg/.
### 3.2.3 - *2013-04-30*
- Update archive redirection for /c/, /d/, /v/, /vg/, /w/ and /wg/.
- Minor fixes.
### 3.2.2 - *2013-04-27*
>>>>>>> b74e0c92fdf2d755d996cb574dddb3c8d964e91a
- Fix Unread Count taking into account hidden posts. - Fix Unread Count taking into account hidden posts.
### 1.1.2 - 2013-04-26 ### 1.1.2 - 2013-04-26
@ -22,7 +86,7 @@ zixaphir:
- Fix preview with favicons and emoji - Fix preview with favicons and emoji
- Fix NaN error on Thread Updater Interval - Fix NaN error on Thread Updater Interval
- Draggable UI can no longer overlap the Header. - Draggable UI can no longer overlap the Header.
-- Setting the header to Autohide also increases its z-index to overlap other UI - Setting the header to Autohide also increases its z-index to overlap other UI
### 1.1.1 - 2013-04-26 ### 1.1.1 - 2013-04-26
zixaphir: zixaphir:
@ -35,29 +99,30 @@ MayhemYDG:
seaweedchan: seaweedchan:
- Allow thread watcher to load on catalog - Allow thread watcher to load on catalog
### 1.0.10: ### 1.0.10 - 2013-04-23
- Add message pertaining to rewrite - Add message pertaining to rewrite
### 1.0.9: ### 1.0.9 - 2013-04-17
ihavenoface: ihavenoface:
- Implement Announcement Hiding - Implement Announcement Hiding
seaweedchan: seaweedchan:
- Change #options back to inheriting colors from replies - Change #options back to inheriting colors from replies
- Fix script breaking when disabling image expansion - Fix script breaking when disabling image expansion
### 1.0.8: ### 1.0.8 - 2013-04-15
seaweedchan: seaweedchan:
- Redo settings menu styling - Redo settings menu styling
- Move Export/Import buttons and dialog - Move Export/Import buttons and dialog
- Update license and use banner.js for license - Update license and use banner.js for license
### 1.0.7: ### 1.0.7 - 2013-04-14
qqueue: qqueue:
- Relative post dates - Relative post dates
MayhemYDG: MayhemYDG:
- Exporting/importing settings - Exporting/importing settings
### 1.0.6 ### 1.0.6 - 2013-04-13
seaweedchan: seaweedchan:
- Update supported boards for archive redirection and custom navigation - Update supported boards for archive redirection and custom navigation
- Point to github.io instead of github.com for pages - Point to github.io instead of github.com for pages
@ -65,40 +130,44 @@ seaweedchan:
- Make InstallGentoo default for /g/ - Make InstallGentoo default for /g/
- Fix embedding issues - Fix embedding issues
### 1.0.5: ### 1.0.5 - 2013-04-09
seaweedchan: seaweedchan:
- Added keybind to toggle Fappe Tyme - Added keybind to toggle Fappe Tyme
- Fix code tag keybind - Fix code tag keybind
Zixaphir: Zixaphir:
- Add 'yourPost' class to own replies - Add 'yourPost' class to own replies
### 1.0.4: ### 1.0.4 - 2013-04-08
seaweedchan: seaweedchan:
- Fix Fappe Tyme - Fix Fappe Tyme
- Re- add label for image expanding - Re- add label for image expanding
- Move restore button to left side as per RiDeag - Move restore button to left side as per RiDeag
### 1.0.3 ### 1.0.3 - 2013-03-23
seaweedchan: seaweedchan:
- Add ad- blocking CSS into Custom CSS examples - Add ad- blocking CSS into Custom CSS examples
Zixaphir: Zixaphir:
- Fix ctrl+s bringing up save dialog - Fix ctrl+s bringing up save dialog
- Fix issues with soundcloud embedding - Fix issues with soundcloud embedding
### 1.0.2: ### 1.0.2 - 2013-03-14
seaweedchan: seaweedchan:
- New Rice option: Emoji Position - New Rice option: Emoji Position
- New layout for Rice tab - New layout for Rice tab
- No more Yotsuba / Yotsuba B in options - No more Yotsuba / Yotsuba B in options
### 1.0.1: ### 1.0.1 - 2013-03-14
- New option: Emoji - New option: Emoji
- New Rice option: Sage Emoji - New Rice option: Sage Emoji
seaweedchan: seaweedchan:
- Prettier error messages - Prettier error messages
### 1.0.0 ### 1.0.0 - 2013-03-13
- Initial release - Initial release
zixaphir: zixaphir:
- Fix unread post count for filtered posts - Fix unread post count for filtered posts
- Fix issues when switching from ihavenoface's fork - Fix issues when switching from ihavenoface's fork

View File

@ -39,10 +39,11 @@ module.exports = (grunt) ->
] ]
dest: 'tmp-<%= pkg.type %>/script.coffee' dest: 'tmp-<%= pkg.type %>/script.coffee'
license: meta:
options: concatOptions options: concatOptions
files: files:
'LICENSE': 'src/General/meta/banner.js' 'LICENSE': 'src/General/meta/banner.js',
'builds/version': 'src/General/meta/version.js'
crx: crx:
options: concatOptions options: concatOptions
@ -86,6 +87,7 @@ module.exports = (grunt) ->
concurrent: concurrent:
build: [ build: [
'concat:meta'
'build-crx' 'build-crx'
'build-userjs' 'build-userjs'
'build-userscript' 'build-userscript'
@ -153,7 +155,6 @@ module.exports = (grunt) ->
grunt.registerTask 'build', [ grunt.registerTask 'build', [
'concurrent:build' 'concurrent:build'
'concat:license'
] ]
grunt.registerTask 'build-crx', [ grunt.registerTask 'build-crx', [

View File

@ -1,5 +1,5 @@
/* /*
* appchan x - Version 2.0.0 - 2013-04-28 * appchan x - Version 2.0.0 - 2013-05-03
* *
* 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

View File

@ -8,7 +8,7 @@
1. Precise steps to reproduce the problem, with the expected and actual results. 1. Precise steps to reproduce the problem, with the expected and actual results.
2. Console errors, if any. 2. Console errors, if any.
3. Browser version. 3. Browser version.
4. Your exported settings. 4. Your exported settings. If your settings contains sensitive information (e.g. personas), edit the text file manually.
Open your console with: Open your console with:
- `Ctrl + Shift + J` on Chrome. - `Ctrl + Shift + J` on Chrome.

10166
builds/4chan-X.js Normal file

File diff suppressed because one or more lines are too long

19
builds/4chan-X.meta.js Normal file
View File

@ -0,0 +1,19 @@
// ==UserScript==
// @name 4chan X
// @version 1.1.9
// @namespace 4chan-X
// @description Cross-browser userscript for maximum lurking on 4chan.
// @license MIT; https://github.com/seaweedchan/4chan-x/blob/master/LICENSE
// @match *://api.4chan.org/*
// @match *://boards.4chan.org/*
// @match *://images.4chan.org/*
// @match *://sys.4chan.org/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_openInTab
// @run-at document-start
// @updateURL https://github.com/seaweedchan/4chan-x/raw/stable/builds/4chan_X.meta.js
// @downloadURL https://github.com/seaweedchan/4chan-x/raw/stable/builds/4chan_X.user.js
// @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwAgMAAAAqbBEUAAAACVBMVEUAAGcAAABmzDNZt9VtAAAAAXRSTlMAQObYZgAAAHFJREFUKFOt0LENACEIBdBv4Qju4wgWanEj3D6OcIVMKaitYHEU/jwTCQj8W75kiVCSBvdQ5/AvfVHBin11BgdRq3ysBgfwBDRrj3MCIA+oAQaku/Q1cNctrAmyDl577tOThYt/Y1RBM4DgOHzM0HFTAyLukH/cmRnqAAAAAElFTkSuQmCC
// ==/UserScript==

10189
builds/4chan-X.user.js Normal file

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

File diff suppressed because one or more lines are too long

1
builds/version Normal file
View File

@ -0,0 +1 @@
2.0.0

View File

@ -1,30 +1,39 @@
Redirect = Redirect =
init: -> init: ->
$.sync 'archs', @updateArchives $.sync 'archivers', @updateArchives
updateArchives: -> updateArchives: ->
$.get 'archivers', {}, ({archivers}) -> $.get 'archivers', {}, ({archivers}) ->
Conf['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/"
hr: "http://archive.4plebs.org/"
u: "//nsfw.foolz.us/"
po: "//archive.thedarkcave.org/"
vg: "http://nth.pensivenonsen.se/"
c: "//archive.nyafuu.org/"
o.gd = o.jp = o.m = o.q = o.tg = o.vp = o.vr = o.wsg = o.a
o.fa = o.lit = o.s4s = 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) -> image: (boardID, filename) ->
# Do not use g.BOARD, the image url can originate from a cross-quote. # Do not use g.BOARD, the image url can originate from a cross-quote.
switch boardID # Fuck. Your. Shit.
when 'a', 'gd', 'jp', 'm', 'q', 'tg', 'vp', 'vr', 'wsg' "#{Redirect.imageArchives[boardID]}#{boardID}/full_image/#{filename}"
"//archive.foolz.us/#{boardID}/full_image/#{filename}"
when 'u'
"//nsfw.foolz.us/#{boardID}/full_image/#{filename}"
when 'po'
"//archive.thedarkcave.org/#{boardID}/full_image/#{filename}"
when 'hr', 'tv'
"http://archive.4plebs.org/#{boardID}/full_image/#{filename}"
when 'ck', 'fa', 'lit', 's4s'
"//fuuka.warosu.org/#{boardID}/full_image/#{filename}"
when 'cgl', 'g', 'mu', 'w'
"//rbt.asia/#{boardID}/full_image/#{filename}"
when 'an', 'k', 'toy', 'x'
"http://archive.heinessen.com/#{boardID}/full_image/#{filename}"
when 'c'
"//archive.nyafuu.org/#{boardID}/full_image/#{filename}"
post: (boardID, postID) -> post: (boardID, postID) ->
unless Redirect.post[boardID]? unless Redirect.post[boardID]?
@ -58,6 +67,9 @@ Redirect =
else else
null) null)
unless archive.boards.contains g.BOARD.ID
Conf['archivers'] = archive
archiver: archiver:
'Foolz': 'Foolz':
base: 'https://archive.foolz.us' base: 'https://archive.foolz.us'
@ -75,9 +87,21 @@ Redirect =
base: 'http://archive.4plebs.org' base: 'http://archive.4plebs.org'
boards: ['hr', 'tg', 'tv', 'x'] boards: ['hr', 'tg', 'tv', 'x']
base: 'foolfuuka' base: 'foolfuuka'
'NyaFuu':
base: '//archive.nyafuu.org'
boards: ['c', 'w', 'wg']
type: 'foolfuuka'
'LoveIsOver':
base: '//loveisover.me'
boards: ['d', 'h', 'v']
type: 'foolfuuka'
'PensiveNonsen':
base: 'http://nth.pensivenonsen.se'
boards: ['vg']
type: 'foolfuuka'
'Warosu': 'Warosu':
base: '//fuuka.warosu.org' base: '//fuuka.warosu.org'
boards: ['cgl', 'ck', 'fa', 'jp', 'lit', 's4s', 'q', 'tg'] boards: ['cgl', 'ck', 'fa', 'jp', 'lit', 's4s', 'q', 'tg', 'vr']
type: 'fuuka' type: 'fuuka'
'InstallGentoo': 'InstallGentoo':
base: '//archive.installgentoo.net' base: '//archive.installgentoo.net'
@ -85,7 +109,7 @@ Redirect =
type: 'fuuka' type: 'fuuka'
'RebeccaBlackTech': 'RebeccaBlackTech':
base: '//rbt.asia' base: '//rbt.asia'
boards: ['an', 'cgl', 'g', 'mu', 'w'] boards: ['cgl', 'g', 'mu', 'w']
type: 'fuuka_mail' type: 'fuuka_mail'
'Heinessen': 'Heinessen':
base: 'http://archive.heinessen.com' base: 'http://archive.heinessen.com'
@ -95,10 +119,6 @@ Redirect =
base: '//www.cliché.net/4chan/cgi-board.pl' base: '//www.cliché.net/4chan/cgi-board.pl'
boards: ['e'] boards: ['e']
type: 'fuuka' type: 'fuuka'
'NyaFuu':
base: '//archive.nyafuu.org'
boards: ['c', 'w']
type: 'fuuka'
path: (base, archiver, data) -> path: (base, archiver, data) ->
if data.isSearch if data.isSearch

View File

@ -19,12 +19,7 @@ ThreadHiding =
hiddenThreads = ThreadHiding.db.get hiddenThreads = ThreadHiding.db.get
boardID: g.BOARD.ID boardID: g.BOARD.ID
defaultValue: {} defaultValue: {}
# XXX tmp fix hiddenThreadsOnCatalog = JSON.parse(localStorage.getItem "4chan-hide-t-#{g.BOARD}") or {}
try
hiddenThreadsOnCatalog = JSON.parse(localStorage.getItem "4chan-hide-t-#{g.BOARD}") or {}
catch e
localStorage.setItem "4chan-hide-t-#{g.BOARD}", JSON.stringify {}
return ThreadHiding.syncCatalog()
# Add threads that were hidden in the catalog. # Add threads that were hidden in the catalog.
for threadID of hiddenThreadsOnCatalog for threadID of hiddenThreadsOnCatalog
@ -130,10 +125,10 @@ ThreadHiding =
return if thread.isHidden return if thread.isHidden
{OP} = thread {OP} = thread
threadRoot = OP.nodes.root.parentNode threadRoot = OP.nodes.root.parentNode
threadRoot.hidden = thread.isHidden = true thread.isHidden = true
unless makeStub unless makeStub
threadRoot.nextElementSibling.hidden = true # <hr> threadRoot.hidden = threadRoot.nextElementSibling.hidden = true # <hr>
return return
numReplies = 0 numReplies = 0
@ -154,7 +149,7 @@ ThreadHiding =
$.add thread.stub, a $.add thread.stub, a
if Conf['Menu'] if Conf['Menu']
$.add thread.stub, [$.tn(' '), Menu.makeButton OP] $.add thread.stub, [$.tn(' '), Menu.makeButton OP]
$.before threadRoot, thread.stub $.prepend threadRoot, thread.stub
show: (thread) -> show: (thread) ->
if thread.stub if thread.stub

View File

@ -79,6 +79,10 @@ Config =
true true
'Convert text into links where applicable.' 'Convert text into links where applicable.'
] ]
'Allow False Positives': [
false
'Linkify everything, allowing more false positives but reducing missed links'
]
'Embedding': [ 'Embedding': [
true true
'Embed supported services.' 'Embed supported services.'
@ -89,7 +93,7 @@ Config =
] ]
'Link Title': [ 'Link Title': [
true true
'Replace the link of a supported site with its actual title. Currently Supported: YouTube, Vimeo, SoundCloud' 'Replace the link of a supported site with its actual title. Currently Supported: YouTube, Vimeo, SoundCloud, and Github gists'
] ]
'Filtering': 'Filtering':
@ -217,6 +221,10 @@ Config =
true true
'Display reply and image count.' 'Display reply and image count.'
] ]
'Updater and Stats in Header': [
true,
'Places the thread updater and thread stats in the header instead of floating them.'
]
'Thread Watcher': [ 'Thread Watcher': [
true true
'Bookmark threads.' 'Bookmark threads.'
@ -277,6 +285,10 @@ Config =
true true
'Decrease the cooldown time by taking into account upload speed. Disable it if it\'s inaccurate for you.' 'Decrease the cooldown time by taking into account upload speed. Disable it if it\'s inaccurate for you.'
] ]
'Posting Success Notifications': [
true
'Show notifications on successful post creation or file uploading.'
]
'Quote Links': 'Quote Links':
'Quote Backlinks': [ 'Quote Backlinks': [
@ -766,6 +778,11 @@ http://iqdb.org/?url=%TURL
boardnav: '[ toggle-all ] [current-title]' boardnav: '[ toggle-all ] [current-title]'
QR:
'QR.personas': [
'#email:"sage";boards:jp;always'
].join '\n'
time: '%m/%d/%y(%a)%H:%M:%S' time: '%m/%d/%y(%a)%H:%M:%S'
backlink: '>>%id' backlink: '>>%id'

View File

@ -8,6 +8,8 @@ Get =
if excerpt.length > 70 if excerpt.length > 70
excerpt = "#{excerpt[...67]}..." excerpt = "#{excerpt[...67]}..."
"/#{thread.board}/ - #{excerpt}" "/#{thread.board}/ - #{excerpt}"
threadFromRoot: (root) ->
g.threads["#{g.BOARD}.#{root.id[1..]}"]
postFromRoot: (root) -> postFromRoot: (root) ->
link = $ 'a[title="Highlight this post"]', root link = $ 'a[title="Highlight this post"]', root
boardID = link.pathname.split('/')[1] boardID = link.pathname.split('/')[1]

View File

@ -135,22 +135,22 @@ Header =
for a in as for a in as
if a.textContent is board if a.textContent is board
a = a.cloneNode true a = a.cloneNode true
if /-title/.test t
a.textContent = a.title a.textContent = if /-title/.test(t) or /-replace/.test(t) and $.hasClass a, 'current'
else if /-replace/.test t a.title
if $.hasClass a, 'current'
a.textContent = a.title
else if /-full/.test t else if /-full/.test t
a.textContent = "/#{board}/ - #{a.title}" "/#{board}/ - #{a.title}"
else if /-(index|catalog|text)/.test t else if m = t.match /-text:"(.+)"/
if m = t.match /-(index|catalog)/ m[1]
a.setAttribute 'data-only', m[1] else
a.href = "//boards.4chan.org/#{board}/" a.textContent
a.href += 'catalog' if m[1] is 'catalog'
if m = t.match /-text:"(.+)"/ if m = t.match /-(index|catalog)/
a.textContent = m[1] a.setAttribute 'data-only', m[1]
else if board is '@' a.href = "//boards.4chan.org/#{board}/"
$.addClass a, 'navSmall' a.href += 'catalog' if m[1] is 'catalog'
$.addClass a, 'navSmall' if board is '@'
return a return a
$.tn t $.tn t
$.add list, nodes $.add list, nodes

View File

@ -93,7 +93,7 @@ Main =
'Rice': Rice 'Rice': Rice
'Banner': Banner 'Banner': Banner
'Announcements': GlobalMessage 'Announcements': GlobalMessage
'Redirection': Redirect 'Archive Redirection': Redirect
'Header': Header 'Header': Header
'Catalog Links': CatalogLinks 'Catalog Links': CatalogLinks
'Settings': Settings 'Settings': Settings
@ -190,8 +190,19 @@ Main =
$.event '4chanXInitFinished' $.event '4chanXInitFinished'
Main.checkUpdate() Main.checkUpdate()
if styleSelector = $.id 'styleSelector'
passLink = $.el 'a',
textContent: '4chan Pass'
href: 'javascript:;'
$.on passLink, 'click', ->
window.open '//sys.4chan.org/auth',
'This will steal your data.'
'left=0,top=0,width=500,height=255,toolbar=0,resizable=0'
$.before styleSelector.previousSibling, [$.tn '['; passLink, $.tn ']\u00A0\u00A0']
return return
$.event '4chanXInitFinished' $.event '4chanXInitFinished'
Main.checkUpdate() Main.checkUpdate()
@ -280,7 +291,7 @@ Main =
$.get items, (items) -> $.get items, (items) ->
if items.lastupdate > now - freq or items.lastchecked > now - $.DAY if items.lastupdate > now - freq or items.lastchecked > now - $.DAY
return return
$.ajax '<%= meta.page %><%= meta.buildsPath %>version', onload: -> $.ajax '<%= meta.repo %>raw/<%= meta.mainBranch %>/<%= meta.buildsPath %>version', onload: ->
return unless @status is 200 return unless @status is 200
version = @response version = @response
return unless /^\d\.\d+\.\d+$/.test version return unless /^\d\.\d+\.\d+$/.test version

View File

@ -334,10 +334,10 @@ Settings =
section.innerHTML = """ section.innerHTML = """
<%= grunt.file.read('src/General/html/Settings/Sauce.html').replace(/>\s+</g, '><').trim() %> <%= grunt.file.read('src/General/html/Settings/Sauce.html').replace(/>\s+</g, '><').trim() %>
""" """
sauce = $ 'textarea', section ta = $ 'textarea', section
$.get 'sauces', Conf['sauces'], (item) -> $.get 'sauces', Conf['sauces'], (item) ->
sauce.value = item['sauces'] ta.value = item['sauces']
$.on sauce, 'change', $.cb.value $.on ta, 'change', $.cb.value
advanced: (section) -> advanced: (section) ->
section.innerHTML = """<%= grunt.file.read('src/General/html/Settings/Advanced.html').replace(/>\s+</g, '><').trim() %>""" section.innerHTML = """<%= grunt.file.read('src/General/html/Settings/Advanced.html').replace(/>\s+</g, '><').trim() %>"""
@ -353,6 +353,12 @@ Settings =
'input' 'input'
$.on input, event, $.cb.value $.on input, event, $.cb.value
# Quick Reply Personas
ta = $ '.personafield', section
$.get 'QR.personas', Conf['QR.personas'], (item) ->
ta.value = item['QR.personas']
$.on ta, 'change', $.cb.value
# Archiver # Archiver
archiver = $ 'select[name=archiver]', section archiver = $ 'select[name=archiver]', section
toSelect = Redirect.select g.BOARD.ID toSelect = Redirect.select g.BOARD.ID

941
src/General/css/style.css Normal file
View File

@ -0,0 +1,941 @@
/* General */
.dialog {
box-shadow: 0 1px 2px rgba(0, 0, 0, .15);
border: 1px solid;
display: block;
padding: 0;
}
.captcha-img,
.field {
background-color: #FFF;
border: 1px solid #CCC;
-moz-box-sizing: border-box;
box-sizing: border-box;
color: #333;
font: 13px sans-serif;
outline: none;
transition: color .25s, border-color .25s;
transition: color .25s, border-color .25s;
}
.field::-moz-placeholder,
.field:hover::-moz-placeholder {
color: #AAA !important;
font-size: 13px !important;
opacity: 1.0 !important;
}
.captch-img:hover,
.field:hover {
border-color: #999;
}
.field:hover, .field:focus {
color: #000;
}
.field[disabled] {
background-color: #F2F2F2;
color: #888;
}
.move {
cursor: move;
overflow: hidden;
}
label, .favicon {
cursor: pointer;
}
a[href="javascript:;"] {
text-decoration: none;
}
.warning {
color: red;
}
#boardNavDesktop {
display: none !important;
}
/* 4chan style fixes */
.opContainer, .op {
display: block !important;
overflow: visible !important;
}
[hidden] {
display: none !important;
}
/* fixed, z-index */
#overlay,
#fourchanx-settings,
#qp, #ihover,
#navlinks, .fixed #header-bar,
#watcher,
:root.float #updater,
:root.float #thread-stats,
#qr {
position: fixed;
}
#fourchanx-settings {
z-index: 999;
}
#overlay {
z-index: 900;
}
#notifications {
z-index: 70;
}
#qp, #ihover {
z-index: 60;
}
#menu {
z-index: 50;
}
#navlinks, #updater, #thread-stats {
z-index: 40;
}
.fixed #header-bar.autohide {
z-index: 35;
}
#qr {
z-index: 30;
}
#watcher {
z-index: 20;
}
.fixed #header-bar {
z-index: 10;
}
/* Header */
.fixed.top body {
padding-top: 2em;
}
.fixed.bottom body {
padding-bottom: 2em;
}
.fixed #header-bar {
right: 0;
left: 0;
padding: 3px 4px 4px;
}
.fixed.top #header-bar {
top: 0;
}
.fixed.bottom #header-bar {
bottom: 0;
}
#header-bar {
border-width: 0;
transition: all .1s .05s ease-in-out;
}
.fixed.top #header-bar {
border-bottom-width: 1px;
}
.fixed.bottom #header-bar {
box-shadow: 0 -1px 2px rgba(0, 0, 0, .15);
border-top-width: 1px;
}
.fixed.bottom #header-bar .menu-button i {
border-top: none;
border-bottom: 6px solid;
}
#board-list {
text-align: center;
}
.fixed #header-bar.autohide:not(:hover) {
box-shadow: none;
transition: all .8s .6s cubic-bezier(.55, .055, .675, .19);
}
.fixed.top #header-bar.autohide:not(:hover) {
margin-bottom: -1em;
-webkit-transform: translateY(-100%);
transform: translateY(-100%);
}
.fixed.bottom #header-bar.autohide:not(:hover) {
-webkit-transform: translateY(100%);
transform: translateY(100%);
}
#scroll-marker {
left: 0;
right: 0;
height: 10px;
position: absolute;
}
#header-bar #scroll-marker {
display: none;
}
.fixed #header-bar #scroll-marker {
display: block;
}
.fixed.top #header-bar #scroll-marker {
top: 100%;
}
.fixed.bottom #header-bar #scroll-marker {
bottom: 100%;
}
#header-bar a:not(.entry):not(.close) {
text-decoration: none;
padding: 1px;
}
#header-bar input {
margin: 0;
vertical-align: bottom;
}
#shortcuts:empty {
display: none;
}
.brackets-wrap::before {
content: "\\00a0[";
}
.brackets-wrap::after {
content: "]\\00a0";
}
.disabled,
.expand-all-shortcut {
opacity: .45;
}
#shortcuts {
float: right;
}
#navbotright,
#navtopright {
display: none;
}
#toggleMsgBtn {
display: none !important;
}
/* Notifications */
#notifications {
position: fixed;
top: 0;
height: 0;
text-align: center;
right: 0;
left: 0;
transition: all .8s .6s cubic-bezier(.55, .055, .675, .19);
}
.fixed.top #header-bar #notifications {
position: absolute;
top: 100%;
}
.notification {
color: #FFF;
font-weight: 700;
text-shadow: 0 1px 2px rgba(0, 0, 0, .5);
box-shadow: 0 1px 2px rgba(0, 0, 0, .15);
border-radius: 2px;
margin: 1px auto;
width: 500px;
max-width: 100%;
position: relative;
transition: all .25s ease-in-out;
}
.notification.error {
background-color: hsla(0, 100%, 38%, .9);
}
.notification.warning {
background-color: hsla(36, 100%, 38%, .9);
}
.notification.info {
background-color: hsla(200, 100%, 38%, .9);
}
.notification.success {
background-color: hsla(104, 100%, 38%, .9);
}
.notification a {
color: white;
}
.notification > .close {
padding: 6px;
top: 0;
right: 5px;
position: absolute;
}
.message {
-moz-box-sizing: border-box;
box-sizing: border-box;
padding: 6px 20px;
max-height: 200px;
width: 100%;
overflow: auto;
}
/* Settings */
:root.fourchan-x body {
-moz-box-sizing: border-box;
box-sizing: border-box;
}
#overlay {
background-color: rgba(0, 0, 0, .5);
top: 0;
left: 0;
height: 100%;
width: 100%;
}
#fourchanx-settings {
-moz-box-sizing: border-box;
box-sizing: border-box;
box-shadow: 0 0 15px rgba(0, 0, 0, .15);
height: 600px;
min-height: 0;
max-height: 100%;
width: 900px;
min-width: 0;
max-width: 100%;
margin: auto;
padding: 3px;
top: 50%;
left: 50%;
-moz-transform: translate(-50%, -50%);
-webkit-transform: translate(-50%, -50%);
-o-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
#fourchanx-settings > nav {
padding: 2px 2px 0;
height: 15px;
}
#fourchanx-settings > nav a {
text-decoration: underline;
}
#fourchanx-settings > nav a.close {
text-decoration: none;
padding: 2px;
}
.section-container {
overflow: auto;
position: absolute;
top: 2.1em;
right: 5px;
bottom: 5px;
left: 5px;
padding-right: 5px;
}
.sections-list {
padding: 0 3px;
float: left;
}
.credits {
float: right;
}
.tab-selected {
font-weight: 700;
}
.section-sauce ul,
.section-advanced ul {
list-style: none;
margin: 0;
}
.section-sauce ul {
padding: 8px;
}
.section-advanced ul {
padding: 0px;
}
.section-sauce li,
.section-advanced li {
padding-left: 4px;
}
.section-main label {
text-decoration: underline;
}
.section-filter ul {
padding: 0;
}
.section-filter li {
margin: 10px 40px;
}
.section-filter textarea {
height: 500px;
}
.section-sauce textarea {
height: 350px;
}
.section-advanced .field[name="boardnav"] {
width: 100%;
}
.section-advanced textarea {
height: 150px;
}
#fourchanx-settings fieldset {
border: 1px solid;
border-radius: 3px;
}
#fourchanx-settings legend {
font-weight: 700;
}
#fourchanx-settings textarea {
font-family: monospace;
min-width: 100%;
max-width: 100%;
}
#fourchanx-settings code {
color: #000;
background-color: #FFF;
padding: 0 2px;
}
.unscroll {
overflow: hidden;
}
/* Announcement Hiding */
:root.hide-announcement #globalMessage {
display: none;
}
a.hide-announcement {
float: left;
}
/* Unread */
#unread-line {
margin: 0;
border-color: rgb(255,0,0);
}
/* Thread Updater */
#updater {
background: none;
border: none;
box-shadow: none;
}
#updater > .move {
padding: 5px 3px 0px;
margin-bottom: -3px;
}
#updater > div:last-child {
text-align: center;
}
#updater input[type=number] {
width: 4em;
}
:root.float #updater {
padding: 0px 3px;
}
.new {
color: limegreen;
}
#update-status.warning, #update-status.new {
margin-right: 5px;
}
#update-timer {
cursor: pointer;
}
/* Thread Watcher */
#watcher {
padding-bottom: 3px;
overflow: hidden;
white-space: nowrap;
}
#watcher:not(:hover) {
max-height: 220px;
}
#watcher > .move {
padding-top: 3px;
}
#watcher > div {
max-width: 200px;
overflow: hidden;
padding-left: 3px;
padding-right: 3px;
text-overflow: ellipsis;
}
#watcher a {
text-decoration: none;
}
/* Thread Stats */
#thread-stats {
background: none;
border: none;
box-shadow: none;
}
:root.float #post-count, :root.float #file-count {
pointer-events: none;
}
:root.float #thread-stats {
padding: 0px 3px;
}
/* Quote */
.deadlink {
text-decoration: none !important;
}
.backlink.deadlink:not(.forwardlink), .quotelink.deadlink:not(.forwardlink) {
text-decoration: underline !important;
}
.inlined {
opacity: .5;
}
#qp input, .forwarded {
display: none;
}
.quotelink.forwardlink,
.backlink.forwardlink {
text-decoration: none;
border-bottom: 1px dashed;
}
.filtered {
text-decoration: underline line-through;
}
.inline {
border: 1px solid;
display: table;
margin: 2px 0;
}
.inline .post {
border: 0 !important;
background-color: transparent !important;
display: table !important;
margin: 0 !important;
padding: 1px 2px !important;
}
#qp > .opContainer::after {
content: '';
clear: both;
display: table;
}
#qp .post {
border: none;
margin: 0;
padding: 2px 2px 5px;
}
#qp img {
max-height: 300px;
max-width: 500px;
max-height: 80vh;
max-width: 50vw;
}
.qphl {
outline: 2px solid rgba(216, 94, 49, .7);
}
.highlight-own .yourPost>.reply {
border-left: 2px solid rgba(221,0,0,.5);
}
/* Quote Threading */
.threadContainer {
margin-left: 20px;
border-left: 1px solid rgba(128,128,128,.3);
}
.threadOP {
clear: both;
}
/* File */
.fileText:hover .fntrunc,
.fileText:not(:hover) .fnfull,
.expanded-image > .post > .file > .fileThumb > img[data-md5],
:not(.expanded-image) > .post > .file > .fileThumb > .full-image {
display: none;
}
.expanding {
opacity: .5;
}
.expanded-image {
clear: both;
}
.expanded-image > .op > .file::after {
content: '';
clear: both;
display: table;
}
:root.fit-width .full-image {
max-width: 100%;
}
:root.gecko.fit-width .full-image,
:root.presto.fit-width .full-image {
width: 100%;
}
#ihover {
-moz-box-sizing: border-box;
box-sizing: border-box;
max-height: 100%;
max-width: 75%;
padding-bottom: 16px;
}
.fappeTyme .thread > .noFile,
.fappeTyme .threadContainer > .noFile {
display: none;
}
/* Index/Reply Navigation */
#navlinks {
font-size: 16px;
top: 25px;
right: 10px;
}
/* Filter */
.opContainer.filter-highlight {
box-shadow: inset 5px 0 rgba(255, 0, 0, .5);
}
.filter-highlight > .reply {
box-shadow: -5px 0 rgba(255, 0, 0, .5);
}
/* Thread & Reply Hiding */
.hide-thread-button,
.hide-reply-button {
float: left;
margin-right: 2px;
}
.stub ~ * {
display: none !important;
}
.stub input {
display: inline-block;
}
/* QR */
:root.hide-original-post-form #postForm,
:root.hide-original-post-form .postingMode,
:root.hide-original-post-form #togglePostForm,
#qr.autohide:not(.has-focus):not(:hover) > form,
.postingMode ~ #qr select,
#file-n-submit:not(.has-file) #qr-filerm {
display: none;
}
#qr select, #dump-button, .remove, .captcha-img {
cursor: pointer;
}
#qr {
z-index: 20;
position: fixed;
padding: 1px;
border: 1px solid transparent;
min-width: 248px;
border-radius: 3px 3px 0 0;
}
#qrtab {
border-radius: 3px 3px 0 0;
}
#qrtab {
margin-bottom: 1px;
}
#qr .close {
float: right;
padding: 0 3px;
}
#qr .warning {
min-height: 1.6em;
vertical-align: middle;
padding: 0 1px;
border-width: 1px;
border-style: solid;
}
.qr-link {
text-align: center;
}
.persona {
width: 248px;
max-width: 100%;
min-width: 100%;
}
#dump-button {
background: linear-gradient(#EEE, #CCC);
border: 1px solid #CCC;
width: 10%;
margin: 0;
font: 13px sans-serif;
padding: 1px 0px 2px;
}
.persona .field:not(#dump) {
width: 95px;
min-width: 30%;
max-width: 30%;
}
#qr textarea.field {
height: 14.8em;
min-height: 9em;
}
#qr.has-captcha textarea.field {
height: 9em;
}
input.field.tripped:not(:hover):not(:focus) {
color: transparent !important; text-shadow: none !important;
}
#qr textarea {
resize: both;
}
.captcha-img {
margin: 0px;
text-align: center;
background-image: #fff;
font-size: 0px;
min-height: 59px;
min-width: 302px;
}
.captcha-input {
width: 100%;
margin: 1px 0 0;
}
.field {
-moz-box-sizing: border-box;
margin: 0px;
padding: 2px 4px 3px;
}
#qr textarea {
min-width: 100%;
}
#qr [type='submit'] {
width: 25%;
vertical-align: top;
}
/* Fake File Input */
#qr-filename,
.has-file #qr-no-file {
display: none;
}
#qr-no-file,
.has-file #qr-filename {
display: block;
padding: 0px 4px;
margin-bottom: 2px;
overflow: hidden;
text-overflow: ellipsis;
}
#qr-no-file {
color: #AAA;
}
#qr-filename-container {
-moz-box-sizing: border-box;
display: inline-block;
position: relative;
width: 100px;
min-width: 74.6%;
max-width: 74.6%;
margin-right: 0.4%;
margin-top: 1px;
overflow: hidden;
padding: 2px 1px 0;
height: 22px;
}
#qr-filename-container:hover {
cursor: text;
}
#qr-filerm {
position: relative;
right: 14px;
bottom: 6px;
margin-right: -8px;
z-index: 2;
}
#file-n-submit {
height: 23px;
}
#qr input[type=file] {
display: none;
}
/* Thread Select / Spoiler Label */
#qr select {
float: right;
}
/* Dumping UI */
.dump #dump-list-container {
display: block;
}
#dump-list-container {
display: none;
position: relative;
overflow-y: hidden;
margin-top: 1px;
}
#dump-list {
overflow-x: auto;
overflow-y: hidden;
white-space: pre;
width: 248px;
max-width: 100%;
min-width: 100%;
}
#dump-list:hover {
overflow-x: auto;
}
.qr-preview {
-moz-box-sizing: border-box;
counter-increment: thumbnails;
cursor: move;
display: inline-block;
height: 90px;
width: 90px;
padding: 2px;
opacity: .5;
overflow: hidden;
position: relative;
text-shadow: 0 1px 1px #000;
-moz-transition: opacity .25s ease-in-out;
vertical-align: top;
}
.qr-preview:hover,
.qr-preview:focus {
opacity: .9;
}
.qr-preview::before {
content: counter(thumbnails);
color: #fff;
position: absolute;
top: 3px;
right: 3px;
text-shadow: 0 0 3px #000, 0 0 8px #000;
}
.qr-preview#selected {
opacity: 1;
}
.qr-preview.drag {
box-shadow: 0 0 10px rgba(0,0,0,.5);
}
.qr-preview.over {
border-color: #fff;
}
.qr-preview > span {
color: #fff;
}
.remove {
background: none;
color: #e00;
font-weight: 700;
padding: 3px;
}
a:only-of-type > .remove {
display: none;
}
.remove:hover::after {
content: " Remove";
}
.qr-preview > label {
background: rgba(0,0,0,.5);
color: #fff;
right: 0; bottom: 0; left: 0;
position: absolute;
text-align: center;
}
.qr-preview > label > input {
margin: 0;
}
#add-post {
cursor: pointer;
font-size: 2em;
position: absolute;
top: 50%;
right: 10px;
-moz-transform: translateY(-50%);
}
.textarea {
position: relative;
}
#char-count {
color: #000;
background: hsla(0, 0%, 100%, .5);
font-size: 8pt;
position: absolute;
bottom: 1px;
right: 1px;
pointer-events: none;
}
/* Menu */
.menu-button {
display: inline-block;
position: relative;
cursor: pointer;
}
.menu-button i {
border-top: 6px solid;
border-right: 4px solid transparent;
border-left: 4px solid transparent;
display: inline-block;
margin: 2px;
vertical-align: middle;
}
#menu {
position: fixed;
outline: none;
}
.entry {
border-bottom: 1px solid rgba(0,0,0,.25);
cursor: pointer;
display: block;
outline: none;
padding: 3px 7px;
position: relative;
text-decoration: none;
white-space: nowrap;
}
.left>.entry.has-submenu {
padding-right: 17px !important;
}
.entry:last-child {
border-bottom: 0;
}
.has-submenu::after {
content: "";
border-left: .5em solid;
border-top: .3em solid transparent;
border-bottom: .3em solid transparent;
display: inline-block;
margin: .3em;
position: absolute;
right: 3px;
}
.left .has-submenu::after {
border-left: 0;
border-right: .5em solid;
}
.submenu {
display: none;
position: absolute;
left: 100%;
top: -1px;
}
.focused .submenu {
display: block;
}
.imp-exp-result {
position: absolute;
text-align: center;
margin: auto;
right: 0px;
left: 0px;
width: 200px;
}
.export, .import {
cursor: pointer;
text-decoration: none !important;
}
/* Link Title Favicons */
.linkify.YouTube {
background: transparent url('data:image/png;base64,<%= grunt.file.read("src/General/img/links/youtube.png", {encoding: "base64"}) %>') center left no-repeat!important;
padding-left: 18px;
}
.linkify.Vimeo {
background: transparent url('data:image/png;base64,<%= grunt.file.read("src/General/img/links/vimeo.png", {encoding: "base64"}) %>') center left no-repeat!important;
padding-left: 18px;
}
.linkify.SoundCloud {
background: transparent url('data:image/png;base64,<%= grunt.file.read("src/General/img/links/soundcloud.png", {encoding: "base64"}) %>') center left no-repeat!important;
padding-left: 18px;
}
.linkify.audio {
background: transparent url('data:image/png;base64,<%= grunt.file.read("src/General/img/links/audio.png", {encoding: "base64"}) %>') center left no-repeat!important;
padding-left: 18px;
}
.linkify.LiveLeak {
background: transparent url('data:image/png;base64,<%= grunt.file.read("src/General/img/links/liveleak.png", {encoding: "base64"}) %>') center left no-repeat!important;
padding-left: 18px;
}
.linkify.Vocaroo {
background: transparent url('data:image/png;base64,<%= grunt.file.read("src/General/img/links/vocaroo.png", {encoding: "base64"}) %>') center left no-repeat!important;
padding-left: 18px;
}
.linkify.pastebin {
background: transparent url('data:image/png;base64,<%= grunt.file.read("src/General/img/links/pastebin.png", {encoding: "base64"}) %>') center left no-repeat!important;
padding-left: 18px;
}
.linkify.gist {
background: transparent url('data:image/png;base64,<%= grunt.file.read("src/General/img/links/gist.png", {encoding: "base64"}) %>') center left no-repeat!important;
padding-left: 18px;
}
.linkify.image {
background: transparent url('data:image/png;base64,<%= grunt.file.read("src/General/img/links/image.png", {encoding: "base64"}) %>') center left no-repeat!important;
padding-left: 18px;
}
.linkify.InstallGentoo {
background: transparent url('data:image/png;base64,<%= grunt.file.read("src/General/img/links/installgentoo.png", {encoding: "base64"}) %>') center left no-repeat!important;
padding-left: 18px;
}

View File

@ -5,12 +5,12 @@
<form> <form>
<div class=persona> <div class=persona>
<input id=dump-button type=button title='Dump list' value=+ tabindex=0> <input id=dump-button type=button title='Dump list' value=+ tabindex=0>
<input name=name data-name=name title=Name placeholder=Name class=field size=1 tabindex=10> <input name=name data-name=name list="list-name" placeholder=Name class=field size=1 tabindex=10>
<input name=email data-name=email title=E-mail placeholder=E-mail class=field size=1 tabindex=20> <input name=email data-name=email list="list-email" placeholder=E-mail class=field size=1 tabindex=20>
<input name=sub data-name=sub title=Subject placeholder=Subject class=field size=1 tabindex=30> <input name=sub data-name=sub list="list-sub" placeholder=Subject class=field size=1 tabindex=30>
</div> </div>
<div class=textarea> <div class=textarea>
<textarea data-name=com title=Comment placeholder=Comment class=field tabindex=40></textarea> <textarea data-name=com placeholder=Comment class=field tabindex=40></textarea>
<span id=char-count></span> <span id=char-count></span>
</div> </div>
<div id=dump-list-container> <div id=dump-list-container>
@ -35,3 +35,6 @@
<input type=checkbox id=qr-file-spoiler title='Spoiler image' tabindex=90>Spoiler? <input type=checkbox id=qr-file-spoiler title='Spoiler image' tabindex=90>Spoiler?
</label> </label>
</form> </form>
<datalist id="list-name"></datalist>
<datalist id="list-email"></datalist>
<datalist id="list-sub"></datalist>

View File

@ -54,6 +54,23 @@
<div>Resolution: <code>%r</code> (Displays 'PDF' for PDF files)</div> <div>Resolution: <code>%r</code> (Displays 'PDF' for PDF files)</div>
</fieldset> </fieldset>
<fieldset>
<legend>Quick Reply Personas <span class="warning" #{if Conf['Quick Reply'] then 'hidden' else ''}>is disabled.</span></legend>
<textarea class=personafield name="QR.personas" class="field" spellcheck="false"></textarea>
<p>
One item per line.<br>
Items will be added in the relevant input's auto-completion list.<br>
Password items will always be used, since there is no password input.<br>
Lines starting with a <code>#</code> will be ignored.
</p>
<ul>You can use these settings with each item, separate them with semicolons:
<li>Possible items are: <code>name</code>, <code>email</code>, <code>subject</code> and <code>password</code>.</li>
<li>Wrap values of items with quotes, like this: <code>email:"sage"</code>.</li>
<li>Force values as defaults with the <code>always</code> keyword, for example: <code>email:"sage";always</code>.</li>
<li>Select specific boards for an item, separated with commas, for example: <code>email:"sage";boards:jp;always</code>.</li>
</ul>
</fieldset>
<fieldset> <fieldset>
<legend>Unread Favicon <span class=warning #{if Conf['Unread Favicon'] then 'hidden' else ''}>is disabled.</span></legend> <legend>Unread Favicon <span class=warning #{if Conf['Unread Favicon'] then 'hidden' else ''}>is disabled.</span></legend>
<div> <div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 700 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 683 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 863 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 730 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 952 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 871 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 491 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 435 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 928 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 B

View File

@ -16,14 +16,14 @@ Array::contains = (object) ->
Array::indexOf = (object) -> Array::indexOf = (object) ->
i = @length i = @length
while i-- while i--
break if @[i] is object return i if @[i] is object
return i return i
Array::pushArrays = -> Array::pushArrays = ->
args = arguments args = arguments
for arg in args for arg in args
@push.apply @, arg @push.apply @, arg
return return @
Array::remove = (object) -> Array::remove = (object) ->
if (index = @indexOf object) > -1 if (index = @indexOf object) > -1
@ -133,7 +133,8 @@ $.x = (path, root) ->
$.X = (path, root) -> $.X = (path, root) ->
root or= d.body root or= d.body
d.evaluate path, root, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null # XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE === 6
d.evaluate path, root, null, 6, null
$.addClass = (el, className) -> $.addClass = (el, className) ->
el.classList.add className el.classList.add className
@ -148,7 +149,7 @@ $.hasClass = (el, className) ->
el.classList.contains className el.classList.contains className
$.rm = do -> $.rm = do ->
if 'remove' of Element.prototype if 'remove' of Element::
(el) -> el.remove() (el) -> el.remove()
else else
(el) -> el.parentNode?.removeChild el (el) -> el.parentNode?.removeChild el
@ -373,7 +374,7 @@ $.set = do ->
items = {} items = {}
localItems = {} localItems = {}
catch err catch err
c.error err c.error err.stack
(key, val) -> (key, val) ->
if typeof key is 'string' if typeof key is 'string'

View File

@ -0,0 +1 @@
<%= version %>

View File

@ -2,6 +2,26 @@ Linkify =
init: -> init: ->
return if g.VIEW is 'catalog' or not Conf['Linkify'] return if g.VIEW is 'catalog' or not Conf['Linkify']
@regString = if Conf['Allow False Positives']
///(
\b(
[a-z]+://
|
[a-z]{3,}\.[-a-z0-9]+\.[a-z]+
|
[-a-z0-9]+\.[a-z]
|
[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+
|
[a-z]{3,}:[a-z0-9?]
|
[a-z0-9._%+-:]+@[a-z0-9.-]+\.[a-z0-9]
)
[^\s'"]+
)///gi
else
/(((magnet|mailto)\:|(www\.)|(news|(ht|f)tp(s?))\:\/\/){1}\S+)/gi
if Conf['Comment Expansion'] if Conf['Comment Expansion']
ExpandComment.callbacks.push @node ExpandComment.callbacks.push @node
@ -9,23 +29,6 @@ Linkify =
name: 'Linkify' name: 'Linkify'
cb: @node cb: @node
regString: ///(
\b(
[a-z]+://
|
[a-z]{3,}\.[-a-z0-9]+\.[a-z]+
|
[-a-z0-9]+\.[a-z]
|
[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+
|
[a-z]{3,}:[a-z0-9?]
|
[a-z0-9._%+-:]+@[a-z0-9.-]+\.[a-z0-9]
)
[^\s'"]+
)///gi
cypher: $.el 'div' cypher: $.el 'div'
node: -> node: ->
@ -44,6 +47,7 @@ Linkify =
data = node.data data = node.data
# Test for valid links # Test for valid links
continue unless node.parentNode and Linkify.regString.test data continue unless node.parentNode and Linkify.regString.test data
Linkify.regString.lastIndex = 0 Linkify.regString.lastIndex = 0
@ -125,21 +129,17 @@ Linkify =
textContent: @getAttribute("data-title") or url textContent: @getAttribute("data-title") or url
@textContent = '(embed)' @textContent = '(embed)'
$.addClass el, "#{@getAttribute 'data-service'}"
# Embed
else else
# We create an element to embed # We create an element to embed
el = (type = Linkify.types[@getAttribute("data-service")]).el.call @ el = (type = Linkify.types[@getAttribute("data-service")]).el.call @
# Set style values. # Set style values.
if style = type.style el.style.cssText = if style = type.style
el.style.cssText = style style
else else
items = "border: 0; width: 640px; height: 390px"
'embedWidth': Config['embedWidth']
'embedHeight': Config['embedHeight']
$.get items, (items) ->
el.style.cssText = "border: 0; width: #{items['embedWidth']}px; height: #{items['embedHeight']}px"
@textContent = '(unembed)' @textContent = '(unembed)'
@ -148,10 +148,10 @@ Linkify =
types: types:
YouTube: YouTube:
regExp: /.*(?:youtu.be\/|youtube.*v=|youtube.*\/embed\/|youtube.*\/v\/|youtube.*videos\/)([^#\&\?]*).*/ regExp: /.*(?:youtu.be\/|youtube.*v=|youtube.*\/embed\/|youtube.*\/v\/|youtube.*videos\/)([^#\&\?]*)\??(t\=.*)?/
el: -> el: ->
$.el 'iframe', $.el 'iframe',
src: "//www.youtube.com/embed/#{@name}" src: "//www.youtube.com/embed/#{@name}#{if @option then '#' + @option else ''}"
title: title:
api: -> "https://gdata.youtube.com/feeds/api/videos/#{@name}?alt=json&fields=title/text(),yt:noembed,app:control/yt:state/@reasonCode" api: -> "https://gdata.youtube.com/feeds/api/videos/#{@name}?alt=json&fields=title/text(),yt:noembed,app:control/yt:state/@reasonCode"
text: -> JSON.parse(@responseText).entry.title.$t text: -> JSON.parse(@responseText).entry.title.$t
@ -186,33 +186,63 @@ Linkify =
preload: 'auto' preload: 'auto'
src: @name src: @name
image:
regExp: /(http|www).*\.(gif|png|jpg|jpeg|bmp)$/
style: 'border: 0; width: auto; height: auto;'
el: ->
$.el 'div',
innerHTML: "<a target=_blank href='#{@getAttribute 'data-originalURL'}'><img src='#{@getAttribute 'data-originalURL'}'></a>"
SoundCloud: SoundCloud:
regExp: /.*(?:soundcloud.com\/|snd.sc\/)([^#\&\?]*).*/ regExp: /.*(?:soundcloud.com\/|snd.sc\/)([^#\&\?]*).*/
style: 'height: auto; width: 500px; display: inline-block;'
el: -> el: ->
div = $.el 'div', div = $.el 'div',
className: "soundcloud" className: "soundcloud"
name: "soundcloud" name: "soundcloud"
$.ajax( $.ajax(
"//soundcloud.com/oembed?show_artwork=false&&maxwidth=500px&show_comments=false&format=json&url=#{@getAttribute 'data-originalURL'}&color=#{Style.colorToHex Themes[Conf['theme']]['Background Color']}" "//soundcloud.com/oembed?show_artwork=false&&maxwidth=500px&show_comments=false&format=json&url=https://www.soundcloud.com/#{@name}"
div: div div: div
onloadend: -> onloadend: ->
@div.innerHTML = JSON.parse(@responseText).html @div.innerHTML = JSON.parse(@responseText).html
false) false)
div
title:
api: -> "//soundcloud.com/oembed?show_artwork=false&&maxwidth=500px&show_comments=false&format=json&url=https://www.soundcloud.com/#{@name}"
text: -> JSON.parse(@responseText).title
pastebin: pastebin:
regExp: /.*(?:pastebin.com\/)([^#\&\?]*).*/ regExp: /.*(?:pastebin.com\/(?!u\/))([^#\&\?]*).*/
el: -> el: ->
div = $.el 'iframe', div = $.el 'iframe',
src: "http://pastebin.com/embed_iframe.php?i=#{@name}" src: "http://pastebin.com/embed_iframe.php?i=#{@name}"
gist:
regExp: /.*(?:gist.github.com.*\/)([^\/][^\/]*)$/
el: ->
div = $.el 'iframe',
# Github doesn't allow embedding straight from the site, so we use an external site to bypass that.
src: "http://www.purplegene.com/script?url=https://gist.github.com/#{@name}.js"
title:
api: -> "https://api.github.com/gists/#{@name}"
text: ->
response = JSON.parse(@responseText).files
return file for file of response when response.hasOwnProperty file
InstallGentoo:
regExp: /.*(?:paste.installgentoo.com\/view\/)([0-9a-z_]+)/
el: ->
$.el 'iframe',
src: "http://paste.installgentoo.com/view/embed/#{@name}"
embedder: (a) -> embedder: (a) ->
return [a] unless Conf['Embedding'] return [a] unless Conf['Link Title']
titles = {} titles = {}
callbacks = -> callbacks = ->
a.textContent = switch @status a.textContent = switch @status
when 200, 304 when 200, 304
title = "[#{embed.getAttribute 'data-service'}] #{service.text.call @}" title = "#{service.text.call @}"
embed.setAttribute 'data-title', title embed.setAttribute 'data-title', title
titles[embed.name] = [title, Date.now()] titles[embed.name] = [title, Date.now()]
$.set 'CachedTitles', titles $.set 'CachedTitles', titles
@ -229,19 +259,23 @@ Linkify =
embed = $.el 'a', embed = $.el 'a',
name: (a.name = match[1]) name: (a.name = match[1])
option: match[2]
className: 'embedder' className: 'embedder'
href: 'javascript:;' href: 'javascript:;'
textContent: '(embed)' textContent: '(embed)'
embed.setAttribute 'data-service', key embed.setAttribute 'data-service', key
embed.setAttribute 'data-originalURL', a.href embed.setAttribute 'data-originalURL', a.href
$.addClass a, "#{embed.getAttribute 'data-service'}"
$.on embed, 'click', Linkify.toggle $.on embed, 'click', Linkify.toggle
unless Conf['Embedding']
embed.hidden = true
if Conf['Link Title'] and (service = type.title) if Conf['Link Title'] and (service = type.title)
$.get 'CachedTitles', {}, (item) -> $.get 'CachedTitles', {}, (item) ->
titles = item['CachedTitles'] titles = item['CachedTitles']
if title = titles[match[1]] if title = titles[match[1]]
a.textContent = title[0] a.textContent = title[0]
embed.setAttribute 'data-title', title[0] embed.setAttribute 'data-title', title[0]

View File

@ -46,18 +46,12 @@ DeleteLink =
$.off @, 'click', DeleteLink.delete $.off @, 'click', DeleteLink.delete
@textContent = "Deleting #{@textContent}..." @textContent = "Deleting #{@textContent}..."
pwd =
if m = d.cookie.match /4chan_pass=([^;]+)/
decodeURIComponent m[1]
else
$.id('delPassword').value
fileOnly = $.hasClass @, 'delete-file' fileOnly = $.hasClass @, 'delete-file'
form = form =
mode: 'usrdel' mode: 'usrdel'
onlyimgdel: fileOnly onlyimgdel: fileOnly
pwd: pwd pwd: QR.persona.getPassword()
form[post.ID] = 'delete' form[post.ID] = 'delete'
link = @ link = @

View File

@ -12,7 +12,7 @@ ExpandComment =
cb: @node cb: @node
node: -> node: ->
if a = $ '.abbr > a', @nodes.comment if a = $ '.abbr > a:not([onclick])', @nodes.comment
$.on a, 'click', ExpandComment.cb $.on a, 'click', ExpandComment.cb
callbacks: [] callbacks: []

View File

@ -41,7 +41,10 @@ Keybinds =
for notification in notifications for notification in notifications
$('.close', notification).click() $('.close', notification).click()
else if QR.nodes else if QR.nodes
QR.close() if Conf['Persistent QR']
QR.hide()
else
QR.close()
when Conf['Spoiler tags'] when Conf['Spoiler tags']
return if target.nodeName isnt 'TEXTAREA' return if target.nodeName isnt 'TEXTAREA'
Keybinds.tags 'spoiler', target Keybinds.tags 'spoiler', target

View File

@ -42,7 +42,9 @@ Nav =
else else
headRect = Header.bar.getBoundingClientRect() headRect = Header.bar.getBoundingClientRect()
topMargin = headRect.top + headRect.height topMargin = headRect.top + headRect.height
threads = $$ '.thread:not([hidden])' threads = $$('.thread').filter (thread) ->
thread = Get.threadFromRoot thread
!(thread.isHidden and !thread.stub)
for thread, i in threads for thread, i in threads
rect = thread.getBoundingClientRect() rect = thread.getBoundingClientRect()
if rect.bottom > topMargin # not scrolled past if rect.bottom > topMargin # not scrolled past

View File

@ -1,15 +1,21 @@
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 = sc = $.el 'span',
innerHTML: "<span id=post-count>0</span> / <span id=file-count>0</span></div>" if Conf['Updater and Stats in Header']
id: 'thread-stats' @dialog = sc = $.el 'span',
innerHTML: "<span id=post-count>0</span> / <span id=file-count>0</span>"
id: 'thread-stats'
Header.addShortcut sc
else
@dialog = sc = UI.dialog 'thread-stats', 'bottom: 0px; right: 0px;',
"<div class=move><span id=post-count>0</span> / <span id=file-count>0</span></div>"
$.ready =>
$.add d.body, sc
@postCountEl = $ '#post-count', sc @postCountEl = $ '#post-count', sc
@fileCountEl = $ '#file-count', sc @fileCountEl = $ '#file-count', sc
Header.addShortcut sc
Thread::callbacks.push Thread::callbacks.push
name: 'Thread Stats' name: 'Thread Stats'
cb: @node cb: @node

View File

@ -3,20 +3,28 @@ ThreadUpdater =
return if g.VIEW isnt 'thread' or !Conf['Thread Updater'] return if g.VIEW isnt 'thread' or !Conf['Thread Updater']
checked = if Conf['Auto Update'] then 'checked' else '' checked = if Conf['Auto Update'] then 'checked' else ''
@dialog = sc = $.el 'span',
innerHTML: "<span id=update-status></span><span id=update-timer title='Update now'></span>"
id: 'updater'
@timer = $ '#update-timer', sc if Conf['Updater and Stats in Header']
@dialog = sc = $.el 'span',
innerHTML: "<span id=update-status></span><span id=update-timer title='Update now'></span>"
id: 'updater'
Header.addShortcut sc
else
@dialog = sc = UI.dialog 'updater', 'bottom: 0px; left: 0px;',
"<div class=move></div><span id=update-status></span><span id=update-timer title='Update now'></span>"
$.addClass doc, 'float'
$.ready =>
$.addClass doc, 'float'
$.add d.body, sc
@checkPostCount = 0
@timer = $ '#update-timer', sc
@status = $ '#update-status', sc @status = $ '#update-status', sc
$.on @timer, 'click', ThreadUpdater.update $.on @timer, 'click', ThreadUpdater.update
$.on @status, 'click', ThreadUpdater.update $.on @status, 'click', ThreadUpdater.update
@checkPostCount = 0
Header.addShortcut sc
subEntries = [] subEntries = []
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 ''

View File

@ -39,6 +39,7 @@ ThreadWatcher =
for id, props of watched[board] for id, props of watched[board]
x = $.el 'a', x = $.el 'a',
textContent: '×' textContent: '×'
className: 'close'
href: 'javascript:;' href: 'javascript:;'
$.on x, 'click', ThreadWatcher.cb.x $.on x, 'click', ThreadWatcher.cb.x
link = $.el 'a', props link = $.el 'a', props

View File

@ -149,6 +149,73 @@ QR =
value value
status.disabled = disabled or false status.disabled = disabled or false
persona:
pwd: ''
always: {}
init: ->
QR.persona.getPassword()
$.get 'QR.personas', Conf['QR.personas'], ({'QR.personas': personas}) ->
types =
name: []
email: []
sub: []
for item in personas.split '\n'
QR.persona.parseItem item.trim(), types
for type, arr of types
QR.persona.loadPersonas type, arr
return
parseItem: (item, types) ->
return if item[0] is '#'
return unless match = item.match /(name|email|subject|password):"(.*)"/i
[match, type, val] = match
# Don't mix up item settings with val.
item = item.replace match, ''
boards = item.match(/boards:([^;]+)/i)?[1].toLowerCase() or 'global'
if boards isnt 'global' and not (g.BOARD.ID in boards.split ',')
return
if type is 'password'
QR.persona.pwd = val
return
type = 'sub' if type is 'subject'
if /always/i.test item
QR.persona.always[type] = val
unless val in types[type]
types[type].push val
loadPersonas: (type, arr) ->
list = $ "#list-#{type}", QR.nodes.el
for val in arr
$.add list, $.el 'option',
textContent: val
return
getPassword: ->
unless QR.persona.pwd
QR.persona.pwd = if m = d.cookie.match /4chan_pass=([^;]+)/
decodeURIComponent m[1]
else if input = $.id 'postPassword'
input.value
else
# If we're in a closed thread, #postPassword isn't available.
# And since #delPassword.value is only filled on window.onload
# we'd rather use #postPassword when we can.
$.id('delPassword').value
return QR.persona.pwd
get: (cb) ->
$.get 'QR.persona', {}, ({'QR.persona': persona}) ->
cb persona
set: (post) ->
$.get 'QR.persona', {}, ({'QR.persona': persona}) ->
persona =
name: post.name
email: if /^sage$/.test post.email then persona.email else post.email
sub: if Conf['Remember Subject'] then post.sub else undefined
$.set 'QR.persona', persona
cooldown: cooldown:
init: -> init: ->
return unless Conf['Cooldown'] return unless Conf['Cooldown']
@ -428,18 +495,27 @@ QR =
prev.spoiler prev.spoiler
else else
false false
$.get 'QR.persona', {}, (item) => QR.persona.get (persona) =>
persona = item['QR.persona'] @name = if 'name' of QR.persona.always
@name = if prev QR.persona.always.name
else if prev
prev.name prev.name
else else
persona.name persona.name
@email = if prev and !/^sage$/.test prev.email
@email = if 'email' of QR.persona.always
QR.persona.always.email
else if prev and !/^sage$/.test prev.email
prev.email prev.email
else else
persona.email persona.email
if Conf['Remember Subject']
@sub = if prev then prev.sub else persona.sub @sub = if 'sub' of QR.persona.always
QR.persona.always.sub
else if Conf['Remember Subject']
if prev then prev.sub else persona.sub
else
''
@load() if QR.selected is @ # load persona @load() if QR.selected is @ # load persona
@select() if select @select() if select
@unlock() @unlock()
@ -878,8 +954,9 @@ QR =
return if e.button isnt 0 return if e.button isnt 0
$.set 'QR Size', @style.cssText $.set 'QR Size', @style.cssText
<% } %> <% } %>
new QR.post true
QR.persona.init()
new QR.post true
QR.status() QR.status()
QR.cooldown.init() QR.cooldown.init()
QR.captcha.init() QR.captcha.init()
@ -970,7 +1047,7 @@ QR =
spoiler: post.spoiler spoiler: post.spoiler
textonly: textOnly textonly: textOnly
mode: 'regist' mode: 'regist'
pwd: if m = d.cookie.match(/4chan_pass=([^;]+)/) then decodeURIComponent m[1] else $.id('postPassword').value pwd: QR.persona.pwd
recaptcha_challenge_field: challenge recaptcha_challenge_field: challenge
recaptcha_response_field: response recaptcha_response_field: response
@ -1069,17 +1146,14 @@ QR =
QR.error err QR.error err
return return
h1 = $ 'h1', tmpDoc
QR.cleanNotifications()
QR.notifications.push new Notification 'success', h1.textContent, 5
$.get 'QR.persona', {}, (item) -> QR.cleanNotifications()
persona = item['QR.persona'] h1 = $ 'h1', tmpDoc
persona =
name: post.name if Conf['Posting Success Notifications']
email: if /^sage$/.test post.email then persona.email else post.email QR.notifications.push new Notification 'success', h1.textContent, 5
sub: if Conf['Remember Subject'] then post.sub else null
$.set 'QR.persona', persona QR.persona.set post
[_, threadID, postID] = h1.nextSibling.textContent.match /thread:(\d+),no:(\d+)/ [_, threadID, postID] = h1.nextSibling.textContent.match /thread:(\d+),no:(\d+)/
postID = +postID postID = +postID