Merge branch 'v3' into Av2

Conflicts:
	builds/4chan-X.meta.js
	css/style.css
	lib/ui.coffee
	package.json
	src/features.coffee
	src/qr.coffee
This commit is contained in:
Zixaphir 2013-04-14 15:28:57 -07:00
commit 30880e499a
25 changed files with 27878 additions and 9406 deletions

1
.gitignore vendored
View File

@ -1,4 +1,3 @@
builds/
node_modules/
tmp/
4chan_x.user.js

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,17 @@
- Fix resurrecting dead quotelinks on HTTP.
### 3.0.6 - *2013-04-14*
- Fix regression concerning thread selection when quoting on the index.
### 3.0.5 - *2013-04-14*
- `Scroll to Last Read Post` is now optional, enabled by default.
- The QR won't auto-hide when auto-hide is enabled and one of its input is focused. Doesn't work on Firefox.
- Added the `Remember QR Size` setting back in, disabled by default. Only on Firefox.
- Fix QR remembering the file spoiler state when it shouldn't.
- Fix QR cooldown in Opera.
### 3.0.4 - *2013-04-11*
- More minor fixes.
@ -9,7 +23,7 @@
### 3.0.2 - *2013-04-09*
- Added a setting in the Header's menu to move it at the bottom of the screen.
- Added Cooldown setting back in.
- Added the `Cooldown` setting back in.
- Fixed the Header going above posts when following quotelinks for example.
- Fixed a bug where dead quotelinks would disappear.

View File

@ -1,15 +1,16 @@
module.exports = (grunt) ->
pkg = grunt.file.readJSON 'package.json'
concatOptions =
process:
data: pkg
# Project configuration.
grunt.initConfig
pkg: pkg
concat:
coffee:
options:
process:
data: pkg
options: concatOptions
src: [
'src/config.coffee'
'src/globals.coffee'
@ -27,9 +28,7 @@ module.exports = (grunt) ->
dest: 'tmp/script.coffee'
crx:
options:
process:
data: pkg
options: concatOptions
files:
'builds/crx/manifest.json': 'src/manifest.json'
'builds/crx/script.js': [
@ -38,9 +37,7 @@ module.exports = (grunt) ->
]
userjs:
options:
process:
data: pkg
options: concatOptions
src: [
'src/metadata.js'
'src/banner.js'
@ -49,12 +46,10 @@ module.exports = (grunt) ->
dest: 'builds/<%= pkg.name %>.js'
userscript:
options:
process:
data: pkg
options: concatOptions
files:
'<%= pkg.name %>.meta.js': 'src/metadata.js'
'<%= pkg.name %>.user.js': [
'builds/<%= pkg.name %>.meta.js': 'src/metadata.js'
'builds/<%= pkg.name %>.user.js': [
'src/metadata.js'
'src/banner.js'
'tmp/script.js'
@ -170,22 +165,29 @@ module.exports = (grunt) ->
grunt.registerTask 'patch', [
'bump'
'reloadPkh'
'updcl:3'
]
grunt.registerTask 'minor', [
'bump:minor'
'reloadPkh'
'updcl:2'
]
grunt.registerTask 'major', [
'bump:major'
'reloadPkh'
'updcl:1'
]
grunt.registerTask 'updcl', 'Update the changelog', (i) ->
grunt.registerTask 'reloadPkg', 'Reload the package', ->
# Update the `pkg` object with the new version.
pkg = grunt.file.readJSON('package.json');
pkg = grunt.file.readJSON('package.json')
concatOptions.process.data = pkg
grunt.log.ok('pkg reloaded.')
grunt.registerTask 'updcl', 'Update the changelog', (i) ->
# i is the number of #s for markdown.
version = []
version.length = +i + 1

File diff suppressed because one or more lines are too long

13596
builds/appchan-x.user.js Normal file

File diff suppressed because one or more lines are too long

BIN
builds/crx/icon128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 B

BIN
builds/crx/icon16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 B

BIN
builds/crx/icon48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 B

21
builds/crx/manifest.json Normal file
View File

@ -0,0 +1,21 @@
{
"name": "appchan x",
"version": "2.0.0",
"manifest_version": 2,
"description": "The most comprehensive 4chan userscript.",
"icons": {
"16": "icon16.png",
"48": "icon48.png",
"128": "icon128.png"
},
"content_scripts": [{
"js": ["script.js"],
"matches": ["*://api.4chan.org/*","*://boards.4chan.org/*","*://images.4chan.org/*","*://sys.4chan.org/*"],
"run_at": "document_start"
}],
"homepage_url": "http://zixaphir.github.com/appchan-x/",
"minimum_chrome_version": "26",
"permissions": [
"storage"
]
}

13521
builds/crx/script.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -6,9 +6,7 @@ body > div.navLinks > a:first-of-type::after,
#{if Conf['Announcements'] is 'slideout' then '#globalMessage::after,' else ''}
#boardNavDesktopFoot::after,
body > a[style="cursor: pointer; float: right;"]::after,
#imgControls .expand-all-shortcut,
#imgControls .contract-all-shortcut,
#imgControls .menu-button,
#img-controls,
#catalog::after,
#fappeTyme {
z-index: 18;
@ -34,9 +32,7 @@ body > div.navLinks > a:first-of-type:hover,
#{if Conf['Announcements'] is 'slideout' then '#globalMessage:hover,' else ''}
#boardNavDesktopFoot:hover,
body > a[style="cursor: pointer; float: right;"]:hover,
#imgControls .expand-all-shortcut,
#imgControls .contract-all-shortcut,
#imgControls .menu-button,
#img-controls,
#catalog:hover {
z-index: 17;
}
@ -62,8 +58,7 @@ body > a[style="cursor: pointer; float: right;"]::after {
cursor: pointer;
background-position: 0 -75px;
}
#imgControls .expand-all-shortcut,
#imgControls .contract-all-shortcut {
#img-controls {
background-position: 0 -90px;
}
#navtopright .exlinksOptionsLink::after {
@ -78,9 +73,7 @@ body > a[style="cursor: pointer; float: right;"]::after {
}
#boardNavDesktopFoot:hover::after,
#globalMessage:hover::after,
#imgControls .expand-all-shortcut:hover,
#imgControls .contract-all-shortcut:hover,
#imgControls .menu-button:hover,
#img-controls:hover,
#navlinks a:hover,
#appchanOptions:hover,
#navtopright .exlinksOptionsLink:hover::after,

View File

@ -24,12 +24,7 @@ body > a[style="cursor: pointer; float: right;"]::after {
#{align}: #{position[i++]}px;
}
/* Expand Images */
#imgControls .expand-all-shortcut,
#imgControls .contract-all-shortcut {
#{align}: #{position[i++]}px;
}
/* Expand Images Menu */
#imgControls .menu-button {
#img-controls {
#{align}: #{position[i++]}px;
}
/* 4chan Catalog */
@ -66,9 +61,7 @@ div.navLinks > a:first-of-type::after {
#appchanOptions,
#watcher::after,
#globalMessage::after,
#imgControls .expand-all-shortcut,
#imgControls .contract-all-shortcut,
#imgControls .menu-button,
#img-controls,
#fappeTyme,
div.navLinks > a:first-of-type::after,
#catalog::after,

View File

@ -1,11 +1,6 @@
/* Expand Images */
#imgControls .expand-all-shortcut,
#imgControls .contract-all-shortcut {
top: #{position[i++]}px;
}
/* Expand Images Menu */
#imgControls .menu-button {
#img-controls {
top: #{position[i++]}px;
}
/* 4chan X Options */
@ -64,9 +59,7 @@ div.navLinks > a:first-of-type::after {
#appchanOptions,
#boardNavDesktopFoot::after,
#globalMessage::after,
#imgControls .expand-all-shortcut,
#imgControls .contract-all-shortcut,
#imgControls .menu-button,
#img-controls,
#fappeTyme,
#{if _conf["Slideout Watcher"] then "#watcher::after," else ""}
body > a[style="cursor: pointer; float: right;"]::after,

View File

@ -118,7 +118,7 @@ hr {
opacity: 0.5;
}
/* Symbols */
.dropmarker {
.drop-marker {
vertical-align: middle;
display: inline-block;
margin: 2px 2px 3px;
@ -126,6 +126,12 @@ hr {
border-right: .3em solid transparent;
border-left: .3em solid transparent;
}
.brackets-wrap::before {
content: "\\00a0[";
}
.brackets-wrap::after {
content: "]\\00a0";
}
/* Thread / Reply Nav */
#navlinks a {
position: fixed;
@ -140,30 +146,7 @@ hr {
#boardNavDesktop {
z-index: 6;
border-width: 1px;
#{{
"sticky top": "
position: fixed;
top: 0;
border-top-width: 0;
#{if _conf["Rounded Edges"] then "border-radius: 0 0 3px 3px;" else ""}"
"sticky bottom": "
position: fixed;
bottom: 0;
border-bottom-width: 0;
#{if _conf["Rounded Edges"] then "border-radius: 3px 3px 0 0;" else ""}"
"top": "
position: absolute;
top: 0;
border-top-width: 0;
#{if _conf["Rounded Edges"] then "border-radius: 0 0 3px 3px;" else ""}"
"hide": "
position: fixed;
top: 110%;"
}[_conf['Boards Navigation']]}
#{
if _conf['4chan SS Navigation'] then "
left: 0;
@ -182,6 +165,71 @@ else "
" else ""}
text-align: #{_conf["Navigation Alignment"]};
}
.fixed #boardNavDesktop {
position: fixed;
}
.top #boardNavDesktop {
top: 0;
border-top-width: 0;
#{if _conf["Rounded Edges"] then "border-radius: 0 0 3px 3px;" else ""}"
}
.fixed.bottom #boardNavDesktop {
bottom: 0;
border-bottom-width: 0;
#{if _conf["Rounded Edges"] then "border-radius: 3px 3px 0 0;" else ""}"
}
.hide #boardNavDesktop {
position: fixed;
top: 110%;
bottom: auto;
}
/* Header Autohide */
.fixed #boardNavDesktop.autohide:not(:hover) {
box-shadow: none;
transition: all .8s .6s cubic-bezier(.55, .055, .675, .19);
}
.fixed.top #boardNavDesktop.autohide:not(:hover) {
margin-bottom: -1em;
-webkit-transform: translateY(-100%);
transform: translateY(-100%);
}
.fixed.bottom #boardNavDesktop.autohide:not(:hover) {
-webkit-transform: translateY(100%);
transform: translateY(100%);
}
#toggle-header-bar {
left: 0;
right: 0;
height: 10px;
position: absolute;
}
#boardNavDesktop #toggle-header-bar {
display: none;
}
.fixed #boardNavDesktop #toggle-header-bar {
display: block;
}
.fixed #boardNavDesktop #toggle-header-bar {
cursor: n-resize;
}
.fixed.top boardNavDesktop #toggle-header-bar {
top: 100%;
}
.fixed.bottom #boardNavDesktop #toggle-header-bar {
bottom: 100%;
}
.fixed #boardNavDesktop #header-bar.autohide #toggle-header-bar {
cursor: s-resize;
}
/* Notifications */
.notification {
display: block;
overflow: hidden;
width: 300px;
}
.close {
float: right;
}
/* Pagination */
.pagelist {
border-width: 1px;
@ -443,27 +491,6 @@ else "
z-index: 10;
}
/* Image Expansion */
#imgControls a.menu-button {
margin: 0;
margin: 1px;
border: 2px solid;
border-radius: 10px;
height: 14px;
width: 14px;
#{Style.sizing}: border-box;
}
#imgControls .menu-button::after {
content: '';
font-size: 10px;
position: absolute;
bottom: 50%;
right: 50%;
#{agent}transform: translate(50%, 60%);
display: block;
border-top: 6px solid rgb(130, 130, 130);
border-left: 3px solid transparent;
border-right: 3px solid transparent;
}
.fit-width .full-image {
max-width: 100%;
width: 100%;
@ -691,7 +718,7 @@ hide: "
.menu-button {
position: relative;
}
.menu-button,
.post .menu-button,
.hide-thread-button,
.hide-reply-button {
float: right;
@ -1037,7 +1064,7 @@ input:checked + .rice {
#{agent}transform: translateX(#{xOffset}93%);
}
#qr:hover,
#qr.focus,
#qr.has-focus,
#qr.dump {
#{agent}transform: translate(0);
}"
@ -1051,7 +1078,7 @@ input:checked + .rice {
#{agent}transform: translateX(#{xOffset}100%);
}
#qr:hover,
#qr.focus,
#qr.has-focus,
#qr.dump {
#{agent}transform: translateX(0);
}
@ -1067,7 +1094,7 @@ input:checked + .rice {
cursor: default;
}
#qr:hover #qrtab,
#qr.focus #qrtab,
#qr.has-focus #qrtab,
#qr.dump #qrtab {
opacity: 0;
#{Style.sidebarLocation[0]}: #{252 + Style.sidebarOffset.W}px;
@ -1090,7 +1117,7 @@ input:checked + .rice {
#{agent}transition: opacity .3s ease-in-out 1s;
}
#qr:hover,
#qr.focus,
#qr.has-focus,
#qr.dump {
opacity: 1;
#{agent}transition: opacity .3s linear;
@ -1103,7 +1130,7 @@ unless _conf['Post Form Style'] is 'tabbed slideout'
unless _conf['Post Form Style'] is 'float' or _conf['Show Post Form Header']
"#qrtab { display: none; }"
else unless _conf['Post Form Style'] is 'slideout'
".autohide:not(:hover):not(.focus) > form { display: none !important; }"
".autohide:not(:hover):not(.has-focus) > form { display: none !important; }"
else ""
) + "#qrtab { margin-bottom: 1px; }"
else ""}
@ -1114,7 +1141,7 @@ if _conf['Post Form Style'] isnt 'float' and _conf["Post Form Slideout Transitio
#{agent}transition: #{agent}transform .3s ease-in-out 1s;
}
#qr:hover,
#qr.focus,
#qr.has-focus,
#qr.dump {
#{agent}transition: #{agent}transform .3s linear;
}

View File

@ -211,10 +211,6 @@ a[style="cursor: pointer; float: right;"] ~ div[style^="width: 100%;"] > table {
#navlinks a:last-of-type {
border-top: 11px solid rgb(130,130,130);
}
#imgControls .menu-button {
border-color: rgb(130,130,130);
color: rgb(130,130,130);
}
#charCount {
color: #{(if Style.lightTheme then "rgba(0,0,0,0.7)" else "rgba(255,255,255,0.7)")};
}
@ -396,8 +392,7 @@ a .name {
#globalMessage::after,
#boardNavDesktopFoot::after,
a[style="cursor: pointer; float: right;"]::after,
#imgControls .expand-all-shortcut,
#imgControls .contract-all-shortcut,
#img-controls,
#catalog::after,
#fappeTyme {
background-image: url('#{icons}');

View File

@ -140,8 +140,16 @@ $.extend $,
el.classList.toggle className
hasClass: (el, className) ->
el.classList.contains className
rm: (el) ->
el.parentNode.removeChild el
rm: do ->
if 'remove' of Element.prototype
(el) -> el.remove()
else
(el) -> el.parentNode?.removeChild el
rmAll: (root) ->
# jsperf.com/emptify-element
while node = root.firstChild
$.rm node
return
tn: (s) ->
d.createTextNode s
frag: ->

View File

@ -4,7 +4,8 @@ UI = do ->
className: 'dialog'
innerHTML: html
id: id
el.style.cssText = localStorage.getItem("#{g.NAMESPACE}#{id}.position") or position
$.get "#{id}.position", position, (item) ->
el.style.cssText = item["#{id}.position"]
move = $ '.move', el
$.on move, 'touchstart mousedown', dragstart
for child in move.children
@ -280,7 +281,7 @@ UI = do ->
else # mouseup
$.off d, 'mousemove', @move
$.off d, 'mouseup', @up
localStorage.setItem "#{g.NAMESPACE}#{@id}.position", @style.cssText
$.set "#{@id}.position", @style.cssText
hoverstart = ({root, el, latestEvent, endEvents, asapTest, cb, close}) ->
o = {

View File

@ -240,7 +240,6 @@ Style =
$ '#navtopright .exlinksOptionsLink', d.body
notCatalog and $ 'body > a[style="cursor: pointer; float: right;"]', d.body
notEither and _conf['Image Expansion']
notEither and _conf['Image Expansion']
notEither
g.VIEW is 'thread'
notEither and _conf['Fappe Tyme']
@ -265,7 +264,6 @@ Style =
position = aligner(
2 + (if _conf["4chan Banner"] is "at sidebar top" then (Style.logoOffset + 19) else 0)
[
notEither and _conf['Image Expansion']
notEither and _conf['Image Expansion']
notCatalog
_conf['Slideout Navigation'] isnt 'hide'
@ -314,12 +312,11 @@ Style =
Style.padding.pages.property = _conf["Pagination"].split(" ")
Style.padding.pages.property = Style.padding.pages.property[Style.padding.pages.property.length - 1]
css = "body::before {\n"
if _conf["4chan SS Emulation"]
if Style.padding.pages and (_conf["Pagination"] is "sticky top" or _conf["Pagination"] is "sticky bottom")
css += " #{Style.padding.pages.property}: #{Style.padding.pages.offsetHeight}px !important;\n"
if Style.padding.pages and (_conf["Pagination"] is "sticky top" or _conf["Pagination"] is "sticky bottom")
css += " #{Style.padding.pages.property}: #{Style.padding.pages.offsetHeight}px !important;\n"
if _conf["Boards Navigation"] is "sticky top" or _conf["Boards Navigation"] is "sticky bottom"
css += " #{Style.padding.nav.property}: #{Style.padding.nav.offsetHeight}px !important;\n"
if _conf["Boards Navigation"] is "sticky top" or _conf["Boards Navigation"] is "sticky bottom"
css += " #{Style.padding.nav.property}: #{Style.padding.nav.offsetHeight}px !important;\n"
css += """
}

View File

@ -13,6 +13,10 @@ Config =
false
'Compatibility between <%= meta.name %> and 4chan\'s inline extension is NOT guaranteed.'
]
'Fixed Header': [
false
'Mayhem X\'s Fixed Header (kinda).'
]
'Custom Board Navigation': [
false
'Show custom links instead of the full board list.'
@ -225,6 +229,12 @@ Config =
false
'Remember the subject field, instead of resetting after posting.'
]
<% if (type === 'userscript') { %>
'Remember QR Size': [
false
'Remember the size of the Quick reply.'
]
<% } %>
'Remember Spoiler': [
false
'Remember the spoiler state, instead of resetting after posting.'
@ -586,11 +596,6 @@ Config =
]
Navigation:
'Boards Navigation': [
'sticky top'
'The position of 4chan board navigation'
['sticky top', 'sticky bottom', 'top', 'hide']
]
'Navigation Alignment': [
'center'
'Change the text alignment of the navigation.'
@ -716,7 +721,11 @@ http://iqdb.org/?url=%TURL
#//archive.installgentoo.net/%board/image/%MD5;text:View same on installgentoo /%board/
"""
'Custom CSS': false
'Boards Navigation': 'sticky top'
'Custom CSS': false
'Bottom header': false
'Header auto-hide': false

View File

@ -1,13 +1,37 @@
Header =
init: ->
@bar = $.el 'div',
id: 'notifications'
@shortcuts = $.el 'span',
id: 'shortcuts'
@hover = $.el 'div',
id: 'hoverUI'
@menuButton = $.el 'span',
className: 'menu-button'
innerHTML: '<a class=brackets-wrap href=javascript:;><i class=drop-marker></i></a>'
$.on window, 'load hashchange', Header.hashScroll
@menu = new UI.Menu 'header'
$.on @menuButton, 'click', @menuToggle
$.on @toggle, 'mousedown', @toggleBarVisibility
$.on window, 'load hashchange', Header.hashScroll
@positionToggler = $.el 'span',
textContent: 'Header Position'
className: 'header-position-link'
{createSubEntry} = Header
subEntries = []
for setting in ['sticky top', 'sticky bottom', 'top', 'hide']
subEntries.push createSubEntry setting
$.event 'AddMenuEntry',
type: 'header'
el: @positionToggler
order: 108
subEntries: subEntries
@headerToggler = $.el 'label',
innerHTML: "<input type=checkbox #{if Conf['Header auto-hide'] then 'checked' else ''}> Auto-hide header"
$.on @headerToggler.firstElementChild, 'change', @toggleBarVisibility
$.event 'AddMenuEntry',
type: 'header'
el: @headerToggler
order: 109
$.on d, 'CreateNotification', @createNotification
@ -20,18 +44,48 @@ Header =
$.ready ->
$.add d.body, Header.hover
setBoardList: ->
bar: $.el 'div',
id: 'notifications'
shortcuts: $.el 'span',
id: 'shortcuts'
hover: $.el 'div',
id: 'hoverUI'
toggle: $.el 'div',
id: 'toggle-header-bar'
createSubEntry: (setting)->
label = $.el 'label',
textContent: "#{setting}"
$.on label, 'click', Header.setBarPosition
el: label
setBoardList: ->
Header.nav = nav = $.id 'boardNavDesktop'
if a = $ "a[href*='/#{g.BOARD}/']", nav
a.className = 'current'
fullBoardList = $.el 'span',
id: 'full-board-list'
hidden: true
customBoardList = $.el 'span',
id: 'custom-board-list'
Header.setBarPosition.call textContent: "#{Conf['Boards Navigation']}"
$.sync 'Boards Navigation', ->
Header.setBarPosition.call textContent: "#{Conf['Boards Navigation']}"
Header.setBarVisibility Conf['Header auto-hide']
$.sync 'Header auto-hide', Header.setBarVisibility
$.add fullBoardList, [nav.childNodes...]
$.add nav, [customBoardList, fullBoardList, Header.shortcuts, $ '#navtopright', fullBoardList, Header.bar]
$.add nav, [Header.menuButton, customBoardList, fullBoardList, Header.shortcuts, $('#navtopright', fullBoardList), Header.toggle]
$.add d.body, Header.bar
if Conf['Custom Board Navigation']
Header.generateBoardList Conf['boardnav']
@ -47,7 +101,7 @@ Header =
generateBoardList: (text) ->
list = $ '#custom-board-list', Header.nav
list.innerHTML = null
$.rmAll list
return unless text
as = $$('#full-board-list a', Header.nav)[0...-2] # ignore the Settings and Home links
nodes = text.match(/[\w@]+(-(all|title|full|index|catalog|text:"[^"]+"))*|[^\w@]+/g).map (t) ->
@ -92,8 +146,41 @@ Header =
custom.hidden = !showBoardList
full.hidden = showBoardList
setBarPosition: ->
$.event 'CloseMenu'
switch @textContent
when 'sticky top'
$.addClass doc, 'top'
$.addClass doc, 'fixed'
$.rmClass doc, 'bottom'
$.rmClass doc, 'hide'
when 'sticky bottom'
$.rmClass doc, 'top'
$.addClass doc, 'fixed'
$.addClass doc, 'bottom'
$.rmClass doc, 'hide'
when 'top'
$.addClass doc, 'top'
$.rmClass doc, 'fixed'
$.rmClass doc, 'bottom'
$.rmClass doc, 'hide'
when 'hide'
$.rmClass doc, 'top'
$.rmClass doc, 'fixed'
$.rmClass doc, 'bottom'
$.addClass doc, 'hide'
Conf['Boards Navigation'] = @textContent
$.set 'Boards Navigation', @textContent
setBarVisibility: (hide) ->
Header.headerToggler.firstElementChild.checked = hide
(if hide then $.addClass else $.rmClass) Header.nav, 'autohide'
hashScroll: ->
return unless post = $.id @location.hash[1..]
return if (Get.postFromRoot post).isHidden
Header.scrollToPost post
scrollToPost: (post) ->
@ -103,12 +190,29 @@ Header =
top += - headRect.top - headRect.height
(if $.engine is 'webkit' then d.body else doc).scrollTop += top
toggleBarVisibility: (e) ->
return if e.type is 'mousedown' and e.button isnt 0 # not LMB
hide = if @nodeName is 'INPUT'
@checked
else
!$.hasClass Header.nav, 'autohide'
Header.setBarVisibility hide
message = if hide
'The header bar will automatically hide itself.'
else
'The header bar will remain visible.'
new Notification 'info', message, 2
$.set 'Header auto-hide', hide
addShortcut: (el) ->
shortcut = $.el 'span',
className: 'shortcut'
$.add shortcut, [$.tn(' ['), el, $.tn(']')]
$.add Header.shortcuts, shortcut
menuToggle: (e) ->
Header.menu.toggle e, @, g
createNotification: (e) ->
{type, content, lifetime, cb} = e.detail
notif = new Notification type, content, lifetime
@ -144,7 +248,7 @@ class Notification
setTimeout @close, @timeout * $.SECOND if @timeout
close = ->
$.rm @el if @el.parentNode
$.rm @el
CatalogLinks =
init: ->
@ -193,7 +297,6 @@ CatalogLinks =
else
"//boards.4chan.org/#{board}/catalog"
)
ready: ->
if catalogLink = ($('.pages.cataloglink a', d.body) or $ '[href=".././catalog"]', d.body)
if g.VIEW isnt 'thread'
@ -967,7 +1070,7 @@ Menu =
(post) ->
a or= $.el 'a',
className: 'menu-button'
innerHTML: '[<span class=dropmarker></span>]'
innerHTML: '[<span class=drop-marker></span>]'
href: 'javascript:;'
clone = a.cloneNode true
clone.setAttribute 'data-postid', post.fullID
@ -1008,7 +1111,7 @@ ReportLink =
DeleteLink =
init: ->
return if g.VIEW is 'catalog' or !Conf['Menu'] or !Conf['Delete Link'] or !Conf['Quick Reply']
return if g.VIEW is 'catalog' or !Conf['Menu'] or !Conf['Delete Link']
div = $.el 'div',
className: 'delete-link'
@ -1060,20 +1163,22 @@ DeleteLink =
else
$.id('delPassword').value
fileOnly = $.hasClass @, 'delete-file'
form =
mode: 'usrdel'
onlyimgdel: $.hasClass @, 'delete-file'
onlyimgdel: fileOnly
pwd: pwd
form[post.ID] = 'delete'
link = @
$.ajax $.id('delform').action.replace("/#{g.BOARD}/", "/#{post.board}/"),
onload: -> DeleteLink.load link, post, @response
onload: -> DeleteLink.load link, post, fileOnly, @response
onerror: -> DeleteLink.error link
,
cred: true
form: $.formData form
load: (link, post, html) ->
load: (link, post, fileOnly, html) ->
tmpDoc = d.implementation.createHTMLDocument ''
tmpDoc.documentElement.innerHTML = html
if tmpDoc.title is '4chan - Banned' # Ban/warn check
@ -1084,7 +1189,7 @@ DeleteLink =
else
if tmpDoc.title is 'Updating index...'
# We're 100% sure.
(post.origin or post).kill()
(post.origin or post).kill fileOnly
s = 'Deleted'
link.textContent = s
error: (link) ->
@ -1360,7 +1465,7 @@ Keybinds =
location.href = url
hl: (delta, thread) ->
if Conf['Bottom header']
if Conf['Fixed Header'] and Conf['Bottom header']
topMargin = 0
else
headRect = Header.bar.getBoundingClientRect()
@ -1398,7 +1503,13 @@ Keybinds =
Nav =
init: ->
return if g.VIEW is 'index' and !Conf['Index Navigation'] or g.VIEW is 'thread' and !Conf['Reply Navigation']
switch g.VIEW
when 'index'
return unless Conf['Index Navigation']
when 'thread'
return unless Conf['Reply Navigation']
else # catalog
return
span = $.el 'span',
id: 'navlinks'
@ -1470,11 +1581,13 @@ Redirect =
when 'c'
"//archive.nyafuu.org/#{boardID}/full_image/#{filename}"
post: (boardID, postID) ->
# XXX foolz had HSTS set for 120 days, which broke XHR+CORS+Redirection when on HTTP.
# Remove necessary HTTPS procotol in September 2013.
switch boardID
when 'a', 'co', 'gd', 'jp', 'm', 'q', 'sp', 'tg', 'tv', 'v', 'vg', 'vp', 'vr', 'wsg'
"//archive.foolz.us/_/api/chan/post/?board=#{boardID}&num=#{postID}"
"https://archive.foolz.us/_/api/chan/post/?board=#{boardID}&num=#{postID}"
when 'u'
"//nsfw.foolz.us/_/api/chan/post/?board=#{boardID}&num=#{postID}"
"https://nsfw.foolz.us/_/api/chan/post/?board=#{boardID}&num=#{postID}"
when 'c', 'int', 'out', 'po'
"//archive.thedarkcave.org/_/api/chan/post/?board=#{boardID}&num=#{postID}"
# for fuuka-based archives:
@ -1490,9 +1603,9 @@ Redirect =
Redirect.path '//archive.thedarkcave.org', 'foolfuuka', data
when 'ck', 'fa', 'lit', 's4s'
Redirect.path '//fuuka.warosu.org', 'fuuka', data
when 'diy', 'sci'
when 'diy', 'g', 'sci'
Redirect.path '//archive.installgentoo.net', 'fuuka', data
when 'cgl', 'g', 'mu', 'w'
when 'cgl', 'mu', 'w'
Redirect.path '//rbt.asia', 'fuuka', data
when 'an', 'fit', 'k', 'mlp', 'r9k', 'toy', 'x'
Redirect.path 'http://archive.heinessen.com', 'fuuka', data
@ -1865,10 +1978,10 @@ Get =
# Get rid of the side arrows.
{nodes} = clone
nodes.root.innerHTML = null
$.rmAll nodes.root
$.add nodes.root, nodes.post
root.innerHTML = null
$.rmAll root
$.add root, nodes.root
fetchedPost: (req, boardID, threadID, postID, root, context) ->
# In case of multiple callbacks for the same request,
@ -2662,41 +2775,21 @@ ImageExpand =
init: ->
return if g.VIEW is 'catalog' or !Conf['Image Expansion']
wrapper = $.el 'div',
id: 'imgControls'
innerHTML: """
<a class='expand-all-shortcut' title='Expand All Images' href='javascript:;'></a>
<a class='menu-button' href='javascript:;'></a>
"""
@EAI = wrapper.firstElementChild
@EAI = $.el 'a',
id: 'img-controls'
className: 'expand-all-shortcut'
title: 'Expand All Images'
href: 'javascript:;'
$.on @EAI, 'click', ImageExpand.cb.toggleAll
@opmenu = new UI.Menu 'imageexpand'
$.on $('.menu-button', wrapper), 'click', @menuToggle
for type, config of Config.imageExpansion
label = $.el 'label',
innerHTML: "<input type=checkbox name='#{type}'> #{type}"
input = label.firstElementChild
if ['Fit width', 'Fit height'].contains type
$.on input, 'change', ImageExpand.cb.setFitness
if config
label.title = config[1]
input.checked = Conf[type]
$.event 'change', null, input
$.on input, 'change', $.cb.checked
$.event 'AddMenuEntry',
type: 'imageexpand'
el: label
$.asap (-> $.id 'delform'), ->
$.prepend $.id('delform'), wrapper
Post::callbacks.push
name: 'Image Expansion'
cb: @node
cb: @node
$.asap (-> $.id 'delform'), ->
$.prepend $.id('delform'), ImageExpand.EAI
node: ->
return unless @file?.isImage
{thumb} = @file
@ -2843,7 +2936,7 @@ ImageExpand =
el = $.el 'span',
textContent: 'Image Expansion'
className: 'image-expansion-link'
className: 'image-expansion-link'
{createSubEntry} = ImageExpand.menu
subEntries = []
@ -3190,18 +3283,22 @@ Unread =
threadID: @ID
defaultValue: 0
Unread.addPosts posts
if (hash = location.hash.match /\d+/) and post = @posts[hash[0]]
Header.scrollToPost post.nodes.root
else if Unread.posts.length
# Scroll to before the first unread post.
$.x('preceding-sibling::div[contains(@class,"postContainer")][1]', Unread.posts[0].nodes.root).scrollIntoView false
else if posts.length
# Scroll to the last read post.
Header.scrollToPost posts[posts.length - 1].nodes.root
$.on d, 'ThreadUpdate', Unread.onUpdate
$.on d, 'scroll visibilitychange', Unread.read
$.on d, 'visibilitychange', Unread.setLine if Conf['Unread Line']
return unless Conf['Scroll to Last Read Post']
# Let the header's onload callback handle it.
return if (hash = location.hash.match /\d+/) and hash[0] of @posts
if Unread.posts.length
# Scroll to before the first unread post.
while root = $.x 'preceding-sibling::div[contains(@class,"postContainer")][1]', Unread.posts[0].nodes.root
break unless (Get.postFromRoot root).isHidden
root.scrollIntoView false
else if posts.length
# Scroll to the last read post.
Header.scrollToPost posts[posts.length - 1].nodes.root
sync: ->
lastReadPost = Unread.db.get
boardID: Unread.thread.board.ID
@ -3287,7 +3384,7 @@ Unread =
{root} = post.nodes
if root isnt $ '.thread > .replyContainer', root.parentNode # not the first reply
$.before root, Unread.hr
else if Unread.hr.parentNode
else
$.rm Unread.hr
update: <% if (type === 'crx') { %>(dontrepeat) <% } %>->
@ -3581,7 +3678,7 @@ ThreadUpdater =
unless d.hidden
# Lower the max refresh rate limit on visible tabs.
j = Math.min j, 7
ThreadUpdater.seconds =
ThreadUpdater.seconds =
if Conf['Optional Increase']
Math.max i, [0, 5, 10, 15, 20, 30, 60, 90, 120, 240, 300][j]
else
@ -3781,7 +3878,7 @@ ThreadWatcher =
$.add div, [x, $.tn(' '), link]
nodes.push div
ThreadWatcher.dialog.innerHTML = ''
$.rmAll ThreadWatcher.dialog
$.add ThreadWatcher.dialog, nodes
watched = watched[g.BOARD] or {}
@ -4035,7 +4132,7 @@ Linkify =
embedder: (a) ->
return [a] unless Conf['Embedding']
callbacks = ->
a.textContent = switch @status
when 200, 304
@ -4066,16 +4163,17 @@ Linkify =
$.on embed, 'click', Linkify.toggle
if Conf['Link Title'] and (service = type.title)
titles = $.get 'CachedTitles', {}
$.get 'CachedTitles', {}, (item) ->
titles = item['CachedTitles']
if title = titles[match[1]]
a.textContent = title[0]
embed.setAttribute 'data-title', title[0]
else
try
$.cache service.api.call(a), callbacks
catch err
a.innerHTML = "[#{key}] <span class=warning>Title Link Blocked</span> (are you using NoScript?)</a>"
if title = titles[match[1]]
a.textContent = title[0]
embed.setAttribute 'data-title', title[0]
else
try
$.cache service.api.call(a), callbacks
catch err
a.innerHTML = "[#{key}] <span class=warning>Title Link Blocked</span> (are you using NoScript?)</a>"
return [a, $.tn(' '), embed]
return [a]

View File

@ -432,7 +432,7 @@ Main =
# c.timeEnd 'All initializations'
$.on d, 'AddCallback', Main.addCallback
$.on d, 'AddCallback', Main.addCallback
$.ready Main.initReady
initReady: ->

View File

@ -11,9 +11,9 @@ QR =
href: 'javascript:;'
$.on sc, 'click', ->
if !QR.nodes or QR.nodes.el.hidden
$.event 'CloseMenu'
QR.open()
QR.nodes.com.focus()
QR.resetThreadSelector()
else
QR.close()
$.toggleClass @, 'disabled'
@ -21,8 +21,7 @@ QR =
Header.addShortcut sc
if Conf['Hide Original Post Form']
$.asap (-> doc), ->
$.addClass doc, 'hide-original-post-form'
$.asap (-> doc), -> $.addClass doc, 'hide-original-post-form'
$.on d, '4chanXInitFinished', @initReady
@ -81,6 +80,10 @@ QR =
QR.status()
if !Conf['Remember Spoiler'] and QR.nodes.spoiler.checked
QR.nodes.spoiler.click()
focusin: ->
$.addClass QR.nodes.el, 'has-focus'
focusout: ->
$.rmClass QR.nodes.el, 'has-focus'
hide: ->
d.activeElement.blur()
$.addClass QR.nodes.el, 'autohide'
@ -213,7 +216,7 @@ QR =
now = Date.now()
post = QR.posts[0]
isReply = QR.nodes.thread.value isnt 'new'
isReply = post.thread isnt 'new'
isSage = /sage/i.test post.email
hasFile = !!post.file
seconds = null
@ -272,19 +275,19 @@ QR =
text += ">#{s}\n"
QR.open()
ta = QR.nodes.com
QR.nodes.thread.value = OP.ID unless ta.value
{com, thread} = QR.nodes
thread.value = OP.ID unless com.value
caretPos = ta.selectionStart
caretPos = com.selectionStart
# Replace selection for text.
ta.value = ta.value[...caretPos] + text + ta.value[ta.selectionEnd..]
com.value = com.value[...caretPos] + text + com.value[com.selectionEnd..]
# Move the caret to the end of the new quote.
range = caretPos + text.length
ta.setSelectionRange range, range
ta.focus()
com.setSelectionRange range, range
com.focus()
# Fire the 'input' event
$.event 'input', null, ta
QR.selected.save com
QR.selected.save thread
characterCount: ->
counter = QR.nodes.charCount
@ -362,12 +365,6 @@ QR =
post = new QR.post()
post.setFile file
$.addClass QR.nodes.el, 'dump'
resetThreadSelector: ->
if g.VIEW is 'thread'
QR.nodes.thread.value = g.THREADID
else
QR.nodes.thread.value = 'new'
posts: []
@ -386,8 +383,12 @@ QR =
spoiler: $ 'input', el
span: el.lastChild
@nodes.spoiler.checked = @spoiler
<% if (type === 'userscript') { %>
# XXX Firefox lacks focusin/focusout support.
for elm in $$ '*', el
$.on elm, 'blur', QR.focusout
$.on elm, 'focus', QR.focusin
<% } %>
$.on el, 'click', @select.bind @
$.on @nodes.rm, 'click', (e) => e.stopPropagation(); @rm()
$.on @nodes.label, 'click', (e) => e.stopPropagation()
@ -399,9 +400,14 @@ QR =
for event in ['dragStart', 'dragEnter', 'dragLeave', 'dragOver', 'dragEnd', 'drop']
$.on el, event.toLowerCase(), @[event]
@thread = if g.VIEW is 'thread'
g.THREADID
else
'new'
prev = QR.posts[QR.posts.length - 1]
QR.posts.push @
@spoiler = if prev and Conf['Remember Spoiler']
@nodes.spoiler.checked = @spoiler = if prev and Conf['Remember Spoiler']
prev.spoiler
else
false
@ -435,7 +441,7 @@ QR =
lock: (lock=true) ->
@isLocked = lock
return unless @ is QR.selected
for name in ['name', 'email', 'sub', 'com', 'spoiler']
for name in ['thread', 'name', 'email', 'sub', 'com', 'spoiler']
QR.nodes[name].disabled = lock
@nodes.rm.style.visibility =
QR.nodes.fileRM.style.visibility = if lock then 'hidden' else ''
@ -461,12 +467,15 @@ QR =
load: ->
# Load this post's values.
for name in ['name', 'email', 'sub', 'com']
for name in ['thread', 'name', 'email', 'sub', 'com']
QR.nodes[name].value = @[name] or null
@showFileData()
QR.characterCount()
save: (input) ->
if input.type is 'checkbox'
@spoiler = input.checked
return
{value} = input
@[input.dataset.name] = value
return if input.nodeName isnt 'TEXTAREA'
@ -481,7 +490,7 @@ QR =
return unless @ is QR.selected
# Do this in case people use extensions
# that do not trigger the `input` event.
for name in ['name', 'email', 'sub', 'com']
for name in ['thread', 'name', 'email', 'sub', 'com', 'spoiler']
@save QR.nodes[name]
return
@ -565,8 +574,8 @@ QR =
@showFileData()
return unless window.URL
URL.revokeObjectURL @URL
showFileData: (hide) ->
showFileData: ->
if @file
QR.nodes.filename.textContent = @filename
QR.nodes.filename.title = @filename
@ -661,6 +670,12 @@ QR =
# start with an uncached captcha
@reload()
<% if (type === 'userscript') { %>
# XXX Firefox lacks focusin/focusout support.
$.on input, 'blur', QR.focusout
$.on input, 'focus', QR.focusin
<% } %>
$.addClass QR.nodes.el, 'has-captcha'
$.after QR.nodes.dumpList.parentElement, [imgContainer, input]
@ -847,10 +862,17 @@ QR =
$.add nodes.thread, $.el 'option',
value: thread
textContent: "Thread No.#{thread}"
QR.resetThreadSelector()
$.on nodes.filename.parentNode, 'click keyup', QR.openFileInput
<% if (type === 'userscript') { %>
# XXX Firefox lacks focusin/focusout support.
for elm in $$ '*', QR.nodes.el
$.on elm, 'blur', QR.focusout
$.on elm, 'focus', QR.focusin
<% } %>
$.on QR.nodes.el, 'focusin', QR.focusin
$.on QR.nodes.el, 'focusout', QR.focusout
$.on nodes.autohide, 'change', QR.toggleHide
$.on nodes.close, 'click', QR.close
$.on nodes.dumpButton, 'click', -> nodes.el.classList.toggle 'dump'
@ -861,13 +883,21 @@ QR =
QR.selected.rmFile()
$.on nodes.spoiler, 'change', -> QR.selected.nodes.spoiler.click()
$.on nodes.fileInput, 'change', QR.fileInput
new QR.post true
# save selected post's data
for name in ['name', 'email', 'sub', 'com']
$.on nodes[name], 'input', -> QR.selected.save @
$.on nodes[name], 'focus', -> $.addClass nodes.el, 'focus'
$.on nodes[name], 'blur', -> $.rmClass nodes.el, 'focus'
$.on nodes[name], 'input', -> QR.selected.save @
$.on nodes.thread, 'change', -> QR.selected.save @
<% if (type === 'userscript') { %>
if Conf['Remember QR Size']
$.get 'QR Size', '', (item) ->
nodes.com.style.cssText = item['QR Size']
$.on nodes.com, 'mouseup', (e) ->
return if e.button isnt 0
$.set 'QR Size', @style.cssText
<% } %>
new QR.post true
QR.status()
QR.cooldown.init()
@ -897,7 +927,7 @@ QR =
post.forceSave()
if g.BOARD.ID is 'f'
filetag = QR.nodes.flashTag.value
threadID = QR.nodes.thread.value
threadID = post.thread
thread = g.BOARD.threads[threadID]
# prevent errors
@ -985,6 +1015,11 @@ QR =
QR.status()
response: ->
<% if (type === 'userjs') { %>
# The upload.onload callback is not called
# or at least not in time with Opera.
QR.req.upload.onload()
<% } %>
{req} = QR
delete QR.req
@ -1026,6 +1061,12 @@ QR =
# Too many frequent mistyped captchas will auto-ban you!
# On connection error, the post most likely didn't go through.
QR.cooldown.set delay: 2
else if err.textContent and m = err.textContent.match /wait\s(\d+)\ssecond/i
QR.cooldown.auto = if QR.captcha.isEnabled
!!QR.captcha.captchas.length
else
true
QR.cooldown.set delay: m[1]
else # stop auto-posting
QR.cooldown.auto = false
QR.status()

View File

@ -124,7 +124,7 @@ Settings =
$.rmClass selected, 'tab-selected'
$.addClass $(".tab-#{@hyphenatedTitle}", Settings.dialog), 'tab-selected'
section = $ 'section', Settings.dialog
section.innerHTML = null
$.rmAll section
section.className = "section-#{@hyphenatedTitle}"
@open section, mode
section.scrollTop = 0
@ -188,6 +188,13 @@ Settings =
$.delete ['hiddenThreads', 'hiddenPosts']
$.after $('input[name="Stubs"]', section).parentNode.parentNode, div
div = $.el 'div',
innerHTML: "<button>Reset Header</button><span class=description>: Unhide the navigation bar."
unhide = $ 'button', div
$.on unhide, 'click', ->
Header.setBarPosition.call textContent: "sticky top"
$.after $('input[name="Check for Updates"]', section).parentNode.parentNode, div
export: (now, data) ->
unless typeof now is 'number'
now = Date.now()
@ -213,7 +220,7 @@ Settings =
return
# XXX Firefox won't let us download automatically.
p = $ '.imp-exp-result', Settings.dialog
p.innerHTML = null
$.rmAll p
$.add p, a
import: ->
@ -340,7 +347,7 @@ Settings =
selectFilter: ->
div = @nextElementSibling
if (name = @value) isnt 'guide'
div.innerHTML = null
$.rmAll div
ta = $.el 'textarea',
name: name
className: 'field'
@ -824,7 +831,7 @@ Settings =
mascotHide = $.el "div",
id: "mascot_hide"
className: "reply"
innerHTML: "Hide Categories <span class=dropmarker></span><div></div>"
innerHTML: "Hide Categories <span class=drop-marker></span><div></div>"
keys = Object.keys Mascots
keys.sort()