Merge branch 'v3'

Conflicts:
	CHANGELOG.md
	Gruntfile.coffee
	LICENSE
	builds/appchan-x.user.js
	builds/crx/manifest.json
	builds/crx/script.js
	package.json
	src/General/Config.coffee
	src/General/Header.coffee
	src/General/Main.coffee
	src/General/Settings.coffee
	src/General/html/Features/QuickReply.html
	src/General/img/favicons/xat-/unreadDead.png
	src/General/img/favicons/xat-/unreadDeadY.png
	src/General/img/favicons/xat-/unreadNSFW.png
	src/General/img/favicons/xat-/unreadNSFWY.png
	src/General/img/favicons/xat-/unreadSFW.png
	src/General/img/favicons/xat-/unreadSFWY.png
	src/General/meta/banner.js
	src/General/meta/metadata.js
	src/Images/ImageExpand.coffee
	src/Monitoring/Favicon.coffee
	src/Monitoring/ThreadWatcher.coffee
	src/Posting/QuickReply.coffee
This commit is contained in:
Zixaphir 2013-08-18 11:17:07 -07:00
commit 72e3fc2585
55 changed files with 2088 additions and 522 deletions

View File

@ -1,3 +1,16 @@
**MayhemYDG**:
- **New feature**: `Desktop Notifications`
- Enabled by default, but you will have to grant your browser permissions to display them or disable them altogether:<br>
![authorize or disable](img/changelog/3.9.0/0.png)
- Clicking on a notification will bring up the relevant tab. (Does not work on Firefox unfortunately, [see bug 874050](https://bugzilla.mozilla.org/show_bug.cgi?id=874050).)
- Notifications will appear when someone quotes you, clicking such notification will also scroll the thread to the relevant post.
- Notifications will appear for posting errors instead of alert popups.
- Opera does *not* support desktop notifications yet.
![filename editing](img/changelog/3.8.0/0.gif)
- The QR now allows you to edit the filename on the fly:
**seaweedchan**: **seaweedchan**:
- Optimizations for the banner and board title code - Optimizations for the banner and board title code

View File

@ -135,7 +135,7 @@ module.exports = (grunt) ->
tmpcrx: 'tmp-crx' tmpcrx: 'tmp-crx'
tmpuserscript: 'tmp-userscript' tmpuserscript: 'tmp-userscript'
require('matchdep').filterDev('grunt-*').forEach grunt.loadNpmTasks require('load-grunt-tasks') grunt
grunt.registerTask 'default', [ grunt.registerTask 'default', [
'build' 'build'

View File

@ -78,15 +78,6 @@
* *
* license: https://github.com/4chan/4chan-JS/blob/master/LICENSE * license: https://github.com/4chan/4chan-JS/blob/master/LICENSE
* *
* Linkify: (http://userscripts.org/scripts/show/1352)
* Copyright (c) 2011, Anthony Lieuallen
* All rights reserved.
* Originally written by Anthony Lieuallen of http://arantius.com/
* Licensed for unlimited modification and redistribution as long as
* this notice is kept intact.
*
* license: http://userscripts.org/scripts/review/1352
*
* jsColor: (http://jscolor.com/) * jsColor: (http://jscolor.com/)
* Copyright (c) Jan Odvarko, http://odvarko.cz * Copyright (c) Jan Odvarko, http://odvarko.cz
* *

View File

@ -1,6 +1,8 @@
// ==UserScript== // ==UserScript==
// @name 4chan X // @name 4chan X
// @version 1.2.32 // @version 1.2.32
// @minGMVer 1.13
// @minFFVer 22
// @namespace 4chan-X // @namespace 4chan-X
// @description Cross-browser userscript for maximum lurking on 4chan. // @description Cross-browser userscript for maximum lurking on 4chan.
// @license MIT; https://github.com/seaweedchan/4chan-x/blob/master/LICENSE // @license MIT; https://github.com/seaweedchan/4chan-x/blob/master/LICENSE
@ -16,4 +18,4 @@
// @updateURL https://github.com/seaweedchan/4chan-x/raw/stable/builds/4chan-X.meta.js // @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 // @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 // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwAgMAAAAqbBEUAAAACVBMVEUAAGcAAABmzDNZt9VtAAAAAXRSTlMAQObYZgAAAHFJREFUKFOt0LENACEIBdBv4Qju4wgWanEj3D6OcIVMKaitYHEU/jwTCQj8W75kiVCSBvdQ5/AvfVHBin11BgdRq3ysBgfwBDRrj3MCIA+oAQaku/Q1cNctrAmyDl577tOThYt/Y1RBM4DgOHzM0HFTAyLukH/cmRnqAAAAAElFTkSuQmCC
// ==/UserScript== // ==/UserScript==

View File

@ -1,6 +1,8 @@
// ==UserScript== // ==UserScript==
// @name appchan x // @name appchan x
// @version 2.3.3 // @version 2.3.3
// @minGMVer 1.13
// @minFFVer 22
// @namespace zixaphir // @namespace zixaphir
// @description The most comprehensive 4chan userscript. // @description The most comprehensive 4chan userscript.
// @license MIT; https://github.com/zixaphir/appchan-x/blob/master/LICENSE // @license MIT; https://github.com/zixaphir/appchan-x/blob/master/LICENSE

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

BIN
img/changelog/3.8.0/0.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 690 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View File

@ -19,6 +19,7 @@
} }
}, },
"devDependencies": { "devDependencies": {
"font-awesome": "git://github.com/MayhemYDG/Font-Awesome.git#df4285951124f9ca1f3907438462e5ba9e464bcb",
"grunt": "~0.4.1", "grunt": "~0.4.1",
"grunt-bump": "~0.0.11", "grunt-bump": "~0.0.11",
"grunt-concurrent": "~0.3.0", "grunt-concurrent": "~0.3.0",
@ -29,7 +30,7 @@
"grunt-contrib-copy": "~0.4.1", "grunt-contrib-copy": "~0.4.1",
"grunt-contrib-watch": "~0.5.0", "grunt-contrib-watch": "~0.5.0",
"grunt-shell": "~0.3.1", "grunt-shell": "~0.3.1",
"matchdep": "~0.1.2" "load-grunt-tasks": "~0.1.0"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -33,7 +33,7 @@ Filter =
regexp = RegExp regexp[1], regexp[2] regexp = RegExp regexp[1], regexp[2]
catch err catch err
# I warned you, bro. # I warned you, bro.
new Notification 'warning', err.message, 60 new Notice 'warning', err.message, 60
continue continue
# Filter OPs along with their threads, replies only, or both. # Filter OPs along with their threads, replies only, or both.

View File

@ -13,6 +13,10 @@ Config =
true true
'Add button to hide 4chan announcements.' 'Add button to hide 4chan announcements.'
] ]
'Desktop Notifications': [
true
'Enables desktop notifications across various <%= meta.name %> features.'
]
'404 Redirect': [ '404 Redirect': [
true true
'Redirect dead threads and images.' 'Redirect dead threads and images.'
@ -826,6 +830,7 @@ http://iqdb.org/?url=%TURL
'Bottom Header': false 'Bottom Header': false
'Header catalog links': false 'Header catalog links': false
'Bottom Board List': true 'Bottom Board List': true
'Shortcut Icons': false
'Custom Board Navigation': true 'Custom Board Navigation': true
boardnav: """ boardnav: """

View File

@ -156,11 +156,8 @@ Get =
# https://github.com/eksopl/asagi/blob/master/src/main/java/net/easymodo/asagi/Yotsuba.java#L109-138 # https://github.com/eksopl/asagi/blob/master/src/main/java/net/easymodo/asagi/Yotsuba.java#L109-138
bq.innerHTML = bq.innerHTML.replace /// bq.innerHTML = bq.innerHTML.replace ///
\n \n
| \[/?b\] |
| \[/?spoiler\] \[/?[a-z]+(:lit)?\]
| \[/?code\]
| \[/?moot\]
| \[/?banned\]
///g, Get.parseMarkup ///g, Get.parseMarkup
comment = bq.innerHTML comment = bq.innerHTML
@ -234,6 +231,8 @@ Get =
when '[/moot]' when '[/moot]'
'</div>' '</div>'
when '[banned]' when '[banned]'
'<b style="color: red;">' '<strong style="color: red;">'
when '[/banned]' when '[/banned]'
'</b>' '</strong>'
else
text.replace ':lit', ''

View File

@ -36,6 +36,7 @@ Header =
$.sync 'Fixed Header', Header.setBarFixed $.sync 'Fixed Header', Header.setBarFixed
$.sync 'Bottom Header', Header.setBarPosition $.sync 'Bottom Header', Header.setBarPosition
$.event 'AddMenuEntry', $.event 'AddMenuEntry',
type: 'header' type: 'header'
el: $.el 'span', el: $.el 'span',
@ -60,6 +61,7 @@ Header =
$.prepend d.body, @bar $.prepend d.body, @bar
$.add d.body, Header.hover $.add d.body, Header.hover
@setBarPosition Conf['Bottom Header'] @setBarPosition Conf['Bottom Header']
@
bar: $.el 'div', bar: $.el 'div',
id: 'header-bar' id: 'header-bar'
@ -200,6 +202,21 @@ Header =
Conf['Fixed Header'] = @checked Conf['Fixed Header'] = @checked
$.set 'Fixed Header', @checked $.set 'Fixed Header', @checked
setShortcutIcons: (show) ->
Header.shortcutToggler.checked = show
if show
$.addClass doc, 'shortcut-icons'
else
$.rmClass doc, 'shortcut-icons'
toggleShortcutIcons: ->
$.event 'CloseMenu'
Header.setShortcutIcons @checked
Conf['Shortcut Icons'] = @checked
$.set 'Shortcut Icons', @checked
setBarVisibility: (hide) -> setBarVisibility: (hide) ->
Header.headerToggler.checked = hide Header.headerToggler.checked = hide
$.event 'CloseMenu' $.event 'CloseMenu'
@ -220,8 +237,7 @@ Header =
'automatically hide itself.' 'automatically hide itself.'
else else
'remain visible.'}" 'remain visible.'}"
new Notice 'info', message, 2
new Notification 'info', message, 2
setCustomNav: (show) -> setCustomNav: (show) ->
Header.customNavToggler.checked = show Header.customNavToggler.checked = show
@ -265,5 +281,33 @@ Header =
createNotification: (e) -> createNotification: (e) ->
{type, content, lifetime, cb} = e.detail {type, content, lifetime, cb} = e.detail
notif = new Notification type, content, lifetime notif = new Notice type, content, lifetime
cb notif if cb cb notif if cb
areNotificationsEnabled: false
enableDesktopNotifications: ->
return unless window.Notification and Conf['Desktop Notifications']
switch Notification.permission
when 'granted'
Header.areNotificationsEnabled = true
return
when 'denied'
# requestPermission doesn't work if status is 'denied',
# but it'll still work if status is 'default'.
return
el = $.el 'span',
innerHTML: """
Desktop notification permissions are not granted:<br>
<button>Authorize</button> or <button>Disable</button>
"""
[authorize, disable] = $$ 'button', el
$.on authorize, 'click', ->
Notification.requestPermission (status) ->
Header.areNotificationsEnabled = status is 'granted'
return if status is 'default'
notice.close()
$.on disable, 'click', ->
$.set 'Desktop Notifications', false
notice.close()
notice = new Notice 'info', el

View File

@ -14,8 +14,7 @@ Main =
flatten null, Config flatten null, Config
# Unflattened Config. for db in DataBoard.keys
for db in DataBoards
Conf[db] = boards: {} Conf[db] = boards: {}
$.extend Conf, $.extend Conf,
@ -33,7 +32,7 @@ Main =
$.extend Conf, items $.extend Conf, items
<% if (type === 'crx') { %> <% if (type === 'crx') { %>
unless items unless items
new Notification 'error', $.el 'span', new Notice 'error', $.el 'span',
innerHTML: """ innerHTML: """
It seems like your <%= meta.name %> settings became corrupted due to a <a href="https://code.google.com/p/chromium/issues/detail?id=261623" target=_blank>Chrome bug</a>.<br> It seems like your <%= meta.name %> settings became corrupted due to a <a href="https://code.google.com/p/chromium/issues/detail?id=261623" target=_blank>Chrome bug</a>.<br>
Unfortunately, you'll have to <a href="https://github.com/MayhemYDG/4chan-x/wiki/FAQ#known-problems" target=_blank>fix it yourself</a>. Unfortunately, you'll have to <a href="https://github.com/MayhemYDG/4chan-x/wiki/FAQ#known-problems" target=_blank>fix it yourself</a>.
@ -234,7 +233,7 @@ Main =
try try
localStorage.getItem '4chan-settings' localStorage.getItem '4chan-settings'
catch err catch err
new Notification 'warning', 'Cookies need to be enabled on 4chan for <%= meta.name %> to properly function.', 30 new Notice 'warning', 'Cookies need to be enabled on 4chan for <%= meta.name %> to properly function.', 30
$.event '4chanXInitFinished' $.event '4chanXInitFinished'
@ -316,7 +315,7 @@ Main =
else if errors.length is 1 else if errors.length is 1
error = errors[0] error = errors[0]
if error if error
new Notification 'error', Main.parseError(error), 15 new Notice 'error', Main.parseError(error), 15
return return
div = $.el 'div', div = $.el 'div',
@ -332,7 +331,7 @@ Main =
for error in errors for error in errors
$.add logs, Main.parseError error $.add logs, Main.parseError error
new Notification 'error', [div, logs], 30 new Notice 'error', [div, logs], 30
parseError: (data) -> parseError: (data) ->
Main.logError data Main.logError data

View File

@ -2,7 +2,7 @@ Settings =
init: -> init: ->
# Appchan X settings link # Appchan X settings link
el = $.el 'a', el = $.el 'a',
className: 'settings-link' className: 'settings-link icon icon-wrench'
href: 'javascript:;' href: 'javascript:;'
textContent: 'Settings' textContent: 'Settings'
$.on el, 'click', Settings.open $.on el, 'click', Settings.open
@ -25,7 +25,7 @@ Settings =
el = $.el 'span', el = $.el 'span',
innerHTML: "<%= meta.name %> has been updated to <a href='#{changelog}' target=_blank>version #{g.VERSION}</a>." innerHTML: "<%= meta.name %> has been updated to <a href='#{changelog}' target=_blank>version #{g.VERSION}</a>."
if Conf['Show Updated Notifications'] if Conf['Show Updated Notifications']
new Notification 'info', el, 30 new Notice 'info', el, 30
else else
$.on d, '4chanXInitFinished', Settings.open $.on d, '4chanXInitFinished', Settings.open
$.set 'previousversion', g.VERSION $.set 'previousversion', g.VERSION
@ -171,7 +171,7 @@ Settings =
data = data =
version: g.VERSION version: g.VERSION
date: now date: now
for db in DataBoards for db in DataBoard.keys
Conf[db] = boards: {} Conf[db] = boards: {}
# Make sure to export the most recent data. # Make sure to export the most recent data.
$.get Conf, (Conf) -> $.get Conf, (Conf) ->

View File

@ -19,8 +19,7 @@ UI = do ->
constructor: (@type) -> constructor: (@type) ->
# Doc here: https://github.com/MayhemYDG/4chan-x/wiki/Menu-API # Doc here: https://github.com/MayhemYDG/4chan-x/wiki/Menu-API
$.on d, 'AddMenuEntry', @addEntry.bind @ $.on d, 'AddMenuEntry', @addEntry
@close = close.bind @
@entries = [] @entries = []
makeMenu: -> makeMenu: ->
@ -29,7 +28,7 @@ UI = do ->
id: 'menu' id: 'menu'
tabIndex: 0 tabIndex: 0
$.on menu, 'click', (e) -> e.stopPropagation() $.on menu, 'click', (e) -> e.stopPropagation()
$.on menu, 'keydown', @keybinds.bind @ $.on menu, 'keydown', @keybinds
menu menu
toggle: (e, button, data) -> toggle: (e, button, data) ->
@ -111,7 +110,7 @@ UI = do ->
$.add entry.el, submenu $.add entry.el, submenu
return return
close = -> close: =>
$.rm currentMenu $.rm currentMenu
$.rmClass lastToggledButton, 'active' $.rmClass lastToggledButton, 'active'
currentMenu = null currentMenu = null
@ -124,7 +123,7 @@ UI = do ->
+(first.style.order or first.style.webkitOrder) - +(second.style.order or second.style.webkitOrder) +(first.style.order or first.style.webkitOrder) - +(second.style.order or second.style.webkitOrder)
entries[entries.indexOf(entry) + direction] entries[entries.indexOf(entry) + direction]
keybinds: (e) -> keybinds: (e) =>
entry = $ '.focused', currentMenu entry = $ '.focused', currentMenu
while subEntry = $ '.focused', entry while subEntry = $ '.focused', entry
entry = subEntry entry = subEntry
@ -182,7 +181,7 @@ UI = do ->
style.left = left style.left = left
style.right = right style.right = right
addEntry: (e) -> addEntry: (e) =>
entry = e.detail entry = e.detail
return if entry.type isnt @type return if entry.type isnt @type
@parseEntry entry @parseEntry entry

1167
src/General/css/font-awesome.css vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1501,6 +1501,19 @@ input:not([type=radio]) {
.has-file #qr-filename { .has-file #qr-filename {
display: block; display: block;
} }
#qr-filename {
border: medium none;
vertical-align: top;
padding: 0;
margin: 0;
height: auto;
background: transparent none;
overflow: hidden;
text-overflow: ellipsis;
}
#qr-filename:not(.edit) {
pointer-events: none;
}
.has-file #qr-filerm { .has-file #qr-filerm {
display: inline-block; display: inline-block;
} }

View File

@ -206,8 +206,7 @@ a {
content: "]\\00a0"; content: "]\\00a0";
} }
.dead-thread, .dead-thread,
.disabled, .disabled {
.expand-all-shortcut {
opacity: .45; opacity: .45;
} }
#shortcuts { #shortcuts {
@ -773,21 +772,34 @@ input.field.tripped:not(:hover):not(:focus) {
height: 24px; height: 24px;
} }
/* Fake File Input */ /* Fake File Input */
input#qr-filename {
border: none !important;
width: 80%;
padding: 0px 4px;
position: relative;
bottom: 1px;
background: none !important;
}
input#qr-filename:not(.edit) {
pointer-events: none;
}
#qr-filename, #qr-filename,
#qr-filesize,
.has-file #qr-no-file { .has-file #qr-no-file {
display: none; display: none;
} }
#qr-no-file, #qr-no-file,
.has-file #qr-filename { .has-file #qr-filename,
.has-file #qr-filesize {
display: inline-block; display: inline-block;
padding: 0px 4px; margin: 0 0 2px;
margin-bottom: 2px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
max-width: 88%; vertical-align: top;
} }
#qr-no-file { #qr-no-file {
color: #AAA; color: #AAA;
padding: 1px 4px;
} }
#qr-filename-container { #qr-filename-container {
-moz-box-sizing: border-box; -moz-box-sizing: border-box;
@ -1026,7 +1038,6 @@ a:only-of-type > .remove {
.boardSubtitle[contenteditable="true"] { .boardSubtitle[contenteditable="true"] {
cursor: text !important; cursor: text !important;
} }
/* Link Title Favicons */ /* Link Title Favicons */
.linkify.YouTube { .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; background: transparent url('data:image/png;base64,<%= grunt.file.read("src/General/img/links/youtube.png", {encoding: "base64"}) %>') center left no-repeat!important;

View File

@ -9,9 +9,9 @@
</div> </div>
<form> <form>
<div class=persona> <div class=persona>
<input name=name data-name=name list="list-name" placeholder=Name class=field size=1 tabindex=10> <input data-name=name list="list-name" placeholder=Name class=field size=1 tabindex=10>
<input name=email data-name=email list="list-email" placeholder=E-mail class=field size=1 tabindex=20> <input data-name=email list="list-email" placeholder=E-mail class=field size=1 tabindex=20>
<input name=sub data-name=sub list="list-sub" placeholder=Subject class=field size=1 tabindex=30> <input 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 placeholder=Comment class=field tabindex=40></textarea> <textarea data-name=com placeholder=Comment class=field tabindex=40></textarea>
@ -24,7 +24,7 @@
<div id=file-n-submit> <div id=file-n-submit>
<span id=qr-filename-container class=field tabindex=60> <span id=qr-filename-container class=field tabindex=60>
<span id=qr-no-file>No selected file</span> <span id=qr-no-file>No selected file</span>
<span id=qr-filename></span> <input id="qr-filename" data-name="filename" spellcheck="false">
<span id=qr-extras-container> <span id=qr-extras-container>
<label id=qr-spoiler-label> <label id=qr-spoiler-label>
<input type=checkbox id=qr-file-spoiler title='Spoiler image' tabindex=70> <input type=checkbox id=qr-file-spoiler title='Spoiler image' tabindex=70>
@ -43,4 +43,4 @@
<datalist id="list-name"></datalist> <datalist id="list-name"></datalist>
<datalist id="list-email"></datalist> <datalist id="list-email"></datalist>
<datalist id="list-sub"></datalist> <datalist id="list-sub"></datalist>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 346 B

After

Width:  |  Height:  |  Size: 335 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 456 B

After

Width:  |  Height:  |  Size: 384 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 323 B

After

Width:  |  Height:  |  Size: 309 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 450 B

After

Width:  |  Height:  |  Size: 376 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 321 B

After

Width:  |  Height:  |  Size: 309 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 450 B

After

Width:  |  Height:  |  Size: 377 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 232 B

After

Width:  |  Height:  |  Size: 189 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 232 B

After

Width:  |  Height:  |  Size: 216 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 232 B

After

Width:  |  Height:  |  Size: 216 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 B

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 B

After

Width:  |  Height:  |  Size: 133 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 274 B

After

Width:  |  Height:  |  Size: 254 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 B

After

Width:  |  Height:  |  Size: 133 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 274 B

After

Width:  |  Height:  |  Size: 256 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 B

After

Width:  |  Height:  |  Size: 133 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 B

After

Width:  |  Height:  |  Size: 253 B

View File

@ -41,10 +41,8 @@ $.formData = (form) ->
return new FormData form return new FormData form
fd = new FormData() fd = new FormData()
for key, val of form when val for key, val of form when val
# XXX GM bug if typeof val is 'object' and 'newName' of val
# if val instanceof Blob fd.append key, val, val.newName
if val.size and val.name
fd.append key, val, val.name
else else
fd.append key, val fd.append key, val
fd fd

View File

@ -3,4 +3,4 @@
<%= grunt.file.read('src/General/lib/post.class') %> <%= grunt.file.read('src/General/lib/post.class') %>
<%= grunt.file.read('src/General/lib/clone.class') %> <%= grunt.file.read('src/General/lib/clone.class') %>
<%= grunt.file.read('src/General/lib/databoard.class') %> <%= grunt.file.read('src/General/lib/databoard.class') %>
<%= grunt.file.read('src/General/lib/notification.class') %> <%= grunt.file.read('src/General/lib/notice.class') %>

View File

@ -1,9 +1,9 @@
DataBoards = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads']
class DataBoard class DataBoard
@keys = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads']
constructor: (@key, sync, dontClean) -> constructor: (@key, sync, dontClean) ->
@data = Conf[key] @data = Conf[key]
$.sync key, @onSync.bind @ $.sync key, @onSync
@clean() unless dontClean @clean() unless dontClean
return unless sync return unless sync
# Chrome also fires the onChanged callback on the current tab, # Chrome also fires the onChanged callback on the current tab,
@ -88,6 +88,6 @@ class DataBoard
@deleteIfEmpty {boardID} @deleteIfEmpty {boardID}
@save() @save()
onSync: (data) -> onSync: (data) =>
@data = data or boards: {} @data = data or boards: {}
@sync?() @sync?()

View File

@ -1,4 +1,4 @@
class Notification class Notice
constructor: (type, content, @timeout) -> constructor: (type, content, @timeout) ->
@el = $.el 'div', @el = $.el 'div',
innerHTML: '<a href=javascript:; class=close title=Close>✖</a><div class=message></div>' innerHTML: '<a href=javascript:; class=close title=Close>✖</a><div class=message></div>'
@ -25,4 +25,5 @@ class Notification
setTimeout @close, @timeout * $.SECOND if @timeout setTimeout @close, @timeout * $.SECOND if @timeout
close: => close: =>
$.rm @el $.off d, 'visibilitychange', @add
$.rm @el

View File

@ -1,9 +1,21 @@
Polyfill = Polyfill =
init: -> init: ->
<% if (type === 'crx') { %> <% if (type === 'crx') { %>
Polyfill.toBlob() @notificationPermission()
Polyfill.visibility() @toBlob()
@visibility()
<% } %> <% } %>
notificationPermission: ->
return if !window.Notification or 'permission' of Notification
Object.defineProperty Notification, 'permission',
get: ->
switch webkitNotifications.checkPermission()
when 0
'granted'
when 1
'default'
when 2
'denied'
toBlob: -> toBlob: ->
HTMLCanvasElement::toBlob or= (cb) -> HTMLCanvasElement::toBlob or= (cb) ->
data = atob @toDataURL()[22..] data = atob @toDataURL()[22..]

View File

@ -78,15 +78,6 @@
* *
* license: https://github.com/4chan/4chan-JS/blob/master/LICENSE * license: https://github.com/4chan/4chan-JS/blob/master/LICENSE
* *
* Linkify: (http://userscripts.org/scripts/show/1352)
* Copyright (c) 2011, Anthony Lieuallen
* All rights reserved.
* Originally written by Anthony Lieuallen of http://arantius.com/
* Licensed for unlimited modification and redistribution as long as
* this notice is kept intact.
*
* license: http://userscripts.org/scripts/review/1352
*
* jsColor: (http://jscolor.com/) * jsColor: (http://jscolor.com/)
* Copyright (c) Jan Odvarko, http://odvarko.cz * Copyright (c) Jan Odvarko, http://odvarko.cz
* *

View File

@ -1,6 +1,8 @@
// ==UserScript== // ==UserScript==
// @name <%= meta.name %> // @name <%= meta.name %>
// @version <%= version %> // @version <%= version %>
// @minGMVer 1.13
// @minFFVer 22
// @namespace <%= meta.namespace %> // @namespace <%= meta.namespace %>
// @description <%= description %> // @description <%= description %>
// @license MIT; <%= meta.repo %>blob/<%= meta.mainBranch %>/LICENSE // @license MIT; <%= meta.repo %>blob/<%= meta.mainBranch %>/LICENSE

View File

@ -4,9 +4,9 @@ ImageExpand =
@EAI = $.el 'a', @EAI = $.el 'a',
id: 'img-controls' id: 'img-controls'
className: 'expand-all-shortcut' className: 'expand-all-shortcut icon icon-resize-full'
title: 'Expand All Images' title: 'Expand All Images'
href: 'javascript:;' href: 'javascript:;'
$.on @EAI, 'click', ImageExpand.cb.toggleAll $.on @EAI, 'click', ImageExpand.cb.toggleAll
@ -27,7 +27,7 @@ ImageExpand =
ImageExpand.contract @ ImageExpand.contract @
ImageExpand.expand @ ImageExpand.expand @
return return
if ImageExpand.on and !@isHidden if ImageExpand.on and !@isHidden and (Conf['Expand spoilers'] or !@file.isSpoiler)
ImageExpand.expand @ ImageExpand.expand @
cb: cb:
toggle: (e) -> toggle: (e) ->
@ -37,11 +37,11 @@ ImageExpand =
toggleAll: -> toggleAll: ->
$.event 'CloseMenu' $.event 'CloseMenu'
if ImageExpand.on = $.hasClass ImageExpand.EAI, 'expand-all-shortcut' if ImageExpand.on = $.hasClass ImageExpand.EAI, 'expand-all-shortcut'
ImageExpand.EAI.className = 'contract-all-shortcut' ImageExpand.EAI.className = 'contract-all-shortcut icon icon-resize-small'
ImageExpand.EAI.title = 'Contract All Images' ImageExpand.EAI.title = 'Contract All Images'
func = ImageExpand.expand func = ImageExpand.expand
else else
ImageExpand.EAI.className = 'expand-all-shortcut' ImageExpand.EAI.className = 'expand-all-shortcut icon icon-resize-full'
ImageExpand.EAI.title = 'Expand All Images' ImageExpand.EAI.title = 'Expand All Images'
func = ImageExpand.contract func = ImageExpand.contract
for ID, post of g.posts for ID, post of g.posts

View File

@ -2,24 +2,23 @@ Linkify =
init: -> init: ->
return if g.VIEW is 'catalog' or not Conf['Linkify'] return if g.VIEW is 'catalog' or not Conf['Linkify']
@regString = @regString = ///(
///( # http, magnet, ftp, etc
# http, magnet, ftp, etc (https?|mailto|git|magnet|ftp|irc):(
(https?|mailto|git|magnet|ftp|irc):( [a-z\d%/]
[a-z\d%/] )
) |
| # This should account for virtually all links posted without http:
# This should account for virtually all links posted without http: [-a-z\d]+[.](
[-a-z\d]+[.]( aero|asia|biz|cat|com|coop|info|int|jobs|mobi|museum|name|net|org|post|pro|tel|travel|xxx|edu|gov|mil|[a-z]{2}
aero|asia|biz|cat|com|coop|info|int|jobs|mobi|museum|name|net|org|post|pro|tel|travel|xxx|edu|gov|mil|[a-z]{2} )(/|(?!.))
)(/|(?!.)) |
| # IPv4 Addresses
# IPv4 Addresses [\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}
[\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}\.[\d]{1,3} |
| # E-mails
# E-mails [-\w\d.@]+@[a-z\d.-]+\.[a-z\d]
[-\w\d.@]+@[a-z\d.-]+\.[a-z\d] )///i
)///i
if Conf['Comment Expansion'] if Conf['Comment Expansion']
ExpandComment.callbacks.push @node ExpandComment.callbacks.push @node
@ -55,40 +54,40 @@ Linkify =
while result = test.exec data while result = test.exec data
{index} = result {index} = result
endNode = node endNode = node
word = result[0]
# End of node, not necessarily end of space-delimited string # End of node, not necessarily end of space-delimited string
if (length = index + result[0].length) is data.length if (length = index + word.length) is data.length
test.lastIndex = 0
while (saved = snapshot.snapshotItem i++) while (saved = snapshot.snapshotItem i++)
break if saved.nodeName is 'BR' if saved.nodeName is 'BR'
break
endNode = saved endNode = saved
{length} = saved.data {data} = saved
word += data
{length} = data
if end = space.exec saved.data if end = space.exec data
# Set our snapshot and regex to start on this node at this position when the loop resumes # Set our snapshot and regex to start on this node at this position when the loop resumes
test.lastIndex = length = end.index test.lastIndex = length = end.index
i-- i--
break break
test.lastIndex = 0 if length is endNode.data.length if Linkify.regString.exec word
range = Linkify.makeRange node, endNode, index, length links.push Linkify.makeRange node, endNode, index, length
links.push range if link = Linkify.regString.exec range.toString()
break
else break unless test.lastIndex and node is endNode
if link = Linkify.regString.exec result[0]
range = Linkify.makeRange node, node, index, length
links.push range
for range in links.reverse() for link in links.reverse()
@nodes.links.push Linkify.makeLink range, @ @nodes.links.push Linkify.makeLink link, @
return unless Conf['Embedding'] or Conf['Link Title'] return unless Conf['Embedding'] or Conf['Link Title']
items = @nodes.links {links} = @nodes
i = 0 i = 0
while range = items[i++] while link = links[i++]
if data = Linkify.services range if data = Linkify.services link
Linkify.embed data if Conf['Embedding'] Linkify.embed data if Conf['Embedding']
Linkify.title data if Conf['Link Title'] Linkify.title data if Conf['Link Title']
@ -126,12 +125,9 @@ Linkify =
if i if i
range.setEnd range.endContainer, range.endOffset - i range.setEnd range.endContainer, range.endOffset - i
# This is the only piece of code left based on Anthony Lieuallen's Linkify unless /(https?|mailto|git|magnet|ftp|irc):/.test text
text = text = (
if text.contains ':' if /@/.test text
text
else (
if text.contains '@'
'mailto:' 'mailto:'
else else
'http://' 'http://'

View File

@ -32,13 +32,9 @@ Fourchan =
cb: @math cb: @math
code: -> code: ->
return if @isClone return if @isClone
for pre in $$ '.prettyprint', @nodes.comment for pre in $$ '.prettyprint:not(.prettyprinted)', @nodes.comment
# Don't pretty print twice: $.event 'prettyprint', pre, window
# Might need a better way to detect if a .prettyprint $.addClass pre, 'prettyprinted'
# is already pretty-printed. We can't just look for spans
# since 4chan inserts its quotes and whatnot inside.
unless $ '.pln', pre
$.event 'prettyprint', pre, window
return return
math: -> math: ->
return if @isClone or !$ '.math', @nodes.comment return if @isClone or !$ '.math', @nodes.comment

View File

@ -9,43 +9,49 @@ Favicon =
Favicon.switch() Favicon.switch()
switch: -> switch: ->
unreadDead = Favicon.unreadDeadY = Favicon.unreadSFW = Favicon.unreadSFWY = Favicon.unreadNSFW = Favicon.unreadNSFWY = 'data:image/png;base64,' t = 'data:image/png;base64,'
switch Conf['favicon'] f = Favicon
when 'ferongr' [f.unreadDead, funreadDeadY, f.unreadSFW, f.unreadSFWY, f.unreadNSFW, f.unreadNSFWY] = switch Conf['favicon']
Favicon.unreadDead += '<%= grunt.file.read("src/General/img/favicons/ferongr/unreadDead.png", {encoding: "base64"}) %>' when 'ferongr' then [
Favicon.unreadDeadY += '<%= grunt.file.read("src/General/img/favicons/ferongr/unreadDeadY.png", {encoding: "base64"}) %>' t + '<%= grunt.file.read("src/General/img/favicons/ferongr/unreadDead.png", {encoding: "base64"}) %>'
Favicon.unreadSFW += '<%= grunt.file.read("src/General/img/favicons/ferongr/unreadSFW.png", {encoding: "base64"}) %>' t + '<%= grunt.file.read("src/General/img/favicons/ferongr/unreadDeadY.png", {encoding: "base64"}) %>'
Favicon.unreadSFWY += '<%= grunt.file.read("src/General/img/favicons/ferongr/unreadSFWY.png", {encoding: "base64"}) %>' t + '<%= grunt.file.read("src/General/img/favicons/ferongr/unreadSFW.png", {encoding: "base64"}) %>'
Favicon.unreadNSFW += '<%= grunt.file.read("src/General/img/favicons/ferongr/unreadNSFW.png", {encoding: "base64"}) %>' t + '<%= grunt.file.read("src/General/img/favicons/ferongr/unreadSFWY.png", {encoding: "base64"}) %>'
Favicon.unreadNSFWY += '<%= grunt.file.read("src/General/img/favicons/ferongr/unreadNSFWY.png", {encoding: "base64"}) %>' t + '<%= grunt.file.read("src/General/img/favicons/ferongr/unreadNSFW.png", {encoding: "base64"}) %>'
when 'xat-' t + '<%= grunt.file.read("src/General/img/favicons/ferongr/unreadNSFWY.png", {encoding: "base64"}) %>'
Favicon.unreadDead += '<%= grunt.file.read("src/General/img/favicons/xat-/unreadDead.png", {encoding: "base64"}) %>' ]
Favicon.unreadDeadY += '<%= grunt.file.read("src/General/img/favicons/xat-/unreadDeadY.png", {encoding: "base64"}) %>' when 'xat-' then [
Favicon.unreadSFW += '<%= grunt.file.read("src/General/img/favicons/xat-/unreadSFW.png", {encoding: "base64"}) %>' t + '<%= grunt.file.read("src/General/img/favicons/xat-/unreadDead.png", {encoding: "base64"}) %>'
Favicon.unreadSFWY += '<%= grunt.file.read("src/General/img/favicons/xat-/unreadSFWY.png", {encoding: "base64"}) %>' t + '<%= grunt.file.read("src/General/img/favicons/xat-/unreadDeadY.png", {encoding: "base64"}) %>'
Favicon.unreadNSFW += '<%= grunt.file.read("src/General/img/favicons/xat-/unreadNSFW.png", {encoding: "base64"}) %>' t + '<%= grunt.file.read("src/General/img/favicons/xat-/unreadSFW.png", {encoding: "base64"}) %>'
Favicon.unreadNSFWY += '<%= grunt.file.read("src/General/img/favicons/xat-/unreadNSFWY.png", {encoding: "base64"}) %>' t + '<%= grunt.file.read("src/General/img/favicons/xat-/unreadSFWY.png", {encoding: "base64"}) %>'
when 'Mayhem' t + '<%= grunt.file.read("src/General/img/favicons/xat-/unreadNSFW.png", {encoding: "base64"}) %>'
Favicon.unreadDead += '<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadDead.png", {encoding: "base64"}) %>' t + '<%= grunt.file.read("src/General/img/favicons/xat-/unreadNSFWY.png", {encoding: "base64"}) %>'
Favicon.unreadDeadY += '<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadDeadY.png", {encoding: "base64"}) %>' ]
Favicon.unreadSFW += '<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadSFW.png", {encoding: "base64"}) %>' when 'Mayhem' then [
Favicon.unreadSFWY += '<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadSFWY.png", {encoding: "base64"}) %>' t + '<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadDead.png", {encoding: "base64"}) %>'
Favicon.unreadNSFW += '<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadNSFW.png", {encoding: "base64"}) %>' t + '<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadDeadY.png", {encoding: "base64"}) %>'
Favicon.unreadNSFWY += '<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadNSFWY.png", {encoding: "base64"}) %>' t + '<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadSFW.png", {encoding: "base64"}) %>'
when '4chanJS' t + '<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadSFWY.png", {encoding: "base64"}) %>'
Favicon.unreadDead += '<%= grunt.file.read("src/General/img/favicons/4chanJS/unreadDead.png", {encoding: "base64"}) %>' t + '<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadNSFW.png", {encoding: "base64"}) %>'
Favicon.unreadDeadY += '<%= grunt.file.read("src/General/img/favicons/4chanJS/unreadDeadY.png", {encoding: "base64"}) %>' t + '<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadNSFWY.png", {encoding: "base64"}) %>'
Favicon.unreadSFW += '<%= grunt.file.read("src/General/img/favicons/4chanJS/unreadSFW.png", {encoding: "base64"}) %>' ]
Favicon.unreadSFWY += '<%= grunt.file.read("src/General/img/favicons/4chanJS/unreadSFWY.png", {encoding: "base64"}) %>' when '4chanJS' then [
Favicon.unreadNSFW += '<%= grunt.file.read("src/General/img/favicons/4chanJS/unreadNSFW.png", {encoding: "base64"}) %>' t + '<%= grunt.file.read("src/General/img/favicons/4chanJS/unreadDead.png", {encoding: "base64"}) %>'
Favicon.unreadNSFWY += '<%= grunt.file.read("src/General/img/favicons/4chanJS/unreadNSFWY.png", {encoding: "base64"}) %>' t + '<%= grunt.file.read("src/General/img/favicons/4chanJS/unreadDeadY.png", {encoding: "base64"}) %>'
when 'Original' t + '<%= grunt.file.read("src/General/img/favicons/4chanJS/unreadSFW.png", {encoding: "base64"}) %>'
Favicon.unreadDead += '<%= grunt.file.read("src/General/img/favicons/Original/unreadDead.png", {encoding: "base64"}) %>' t + '<%= grunt.file.read("src/General/img/favicons/4chanJS/unreadSFWY.png", {encoding: "base64"}) %>'
Favicon.unreadDeadY += '<%= grunt.file.read("src/General/img/favicons/Original/unreadDeadY.png", {encoding: "base64"}) %>' t + '<%= grunt.file.read("src/General/img/favicons/4chanJS/unreadNSFW.png", {encoding: "base64"}) %>'
Favicon.unreadSFW += '<%= grunt.file.read("src/General/img/favicons/Original/unreadSFW.png", {encoding: "base64"}) %>' t + '<%= grunt.file.read("src/General/img/favicons/4chanJS/unreadNSFWY.png", {encoding: "base64"}) %>'
Favicon.unreadSFWY += '<%= grunt.file.read("src/General/img/favicons/Original/unreadSFWY.png", {encoding: "base64"}) %>' ]
Favicon.unreadNSFW += '<%= grunt.file.read("src/General/img/favicons/Original/unreadNSFW.png", {encoding: "base64"}) %>' when 'Original' then [
Favicon.unreadNSFWY += '<%= grunt.file.read("src/General/img/favicons/Original/unreadNSFWY.png", {encoding: "base64"}) %>' t + '<%= grunt.file.read("src/General/img/favicons/Original/unreadDead.png", {encoding: "base64"}) %>'
t + '<%= grunt.file.read("src/General/img/favicons/Original/unreadDeadY.png", {encoding: "base64"}) %>'
t + '<%= grunt.file.read("src/General/img/favicons/Original/unreadSFW.png", {encoding: "base64"}) %>'
t + '<%= grunt.file.read("src/General/img/favicons/Original/unreadSFWY.png", {encoding: "base64"}) %>'
t + '<%= grunt.file.read("src/General/img/favicons/Original/unreadNSFW.png", {encoding: "base64"}) %>'
t + '<%= grunt.file.read("src/General/img/favicons/Original/unreadNSFWY.png", {encoding: "base64"}) %>'
]
if Favicon.SFW if Favicon.SFW
Favicon.unread = Favicon.unreadSFW Favicon.unread = Favicon.unreadSFW
Favicon.unreadY = Favicon.unreadSFWY Favicon.unreadY = Favicon.unreadSFWY
@ -54,3 +60,4 @@ Favicon =
Favicon.unreadY = Favicon.unreadNSFWY Favicon.unreadY = Favicon.unreadNSFWY
dead: 'data:image/png;base64,<%= grunt.file.read("src/General/img/favicons/dead.png", {encoding: "base64"}) %>' dead: 'data:image/png;base64,<%= grunt.file.read("src/General/img/favicons/dead.png", {encoding: "base64"}) %>'
logo: 'data:image/png;base64,<%= grunt.file.read("src/General/img/icon128.png", {encoding: "base64"}) %>'

View File

@ -220,14 +220,14 @@ ThreadUpdater =
'The thread is not a sticky anymore.' 'The thread is not a sticky anymore.'
else else
'The thread is not closed anymore.' 'The thread is not closed anymore.'
new Notification 'info', message, 30 new Notice 'info', message, 30
$.rm $ ".#{titleLC}Icon", ThreadUpdater.thread.OP.nodes.info $.rm $ ".#{titleLC}Icon", ThreadUpdater.thread.OP.nodes.info
return return
message = if title is 'Sticky' message = if title is 'Sticky'
'The thread is now a sticky.' 'The thread is now a sticky.'
else else
'The thread is now closed.' 'The thread is now closed.'
new Notification 'info', message, 30 new Notice 'info', message, 30
icon = $.el 'img', icon = $.el 'img',
src: "//static.4chan.org/image/#{titleLC}.gif" src: "//static.4chan.org/image/#{titleLC}.gif"
alt: title alt: title

View File

@ -90,10 +90,25 @@ Unread =
addPostQuotingYou: (post) -> addPostQuotingYou: (post) ->
return unless QR.db return unless QR.db
for quotelink in post.nodes.quotelinks for quotelink in post.nodes.quotelinks when QR.db.get Get.postDataFromLink quotelink
if QR.db.get Get.postDataFromLink quotelink Unread.postsQuotingYou.push post
Unread.postsQuotingYou.push post Unread.openNotification post
return return
openNotification: (post) ->
return unless Header.areNotificationsEnabled
name = if Conf['Anonymize']
'Anonymous'
else
$('.nameBlock', post.nodes.info).textContent.trim()
notif = new Notification "#{name} replied to you",
body: post.info.comment
icon: Favicon.logo
notif.onclick = ->
Header.scrollToPost post.nodes.root
window.focus()
setTimeout ->
notif.close()
, 5 * $.SECOND
onUpdate: (e) -> onUpdate: (e) ->
if e.detail[404] if e.detail[404]

View File

@ -17,7 +17,7 @@ QR =
return unless Conf['Header Shortcut'] or Conf['Page Shortcut'] return unless Conf['Header Shortcut'] or Conf['Page Shortcut']
sc = $.el 'a', sc = $.el 'a',
className: "qr-shortcut #{unless Conf['Persistent QR'] then 'disabled' else ''}" className: "qr-shortcut icon icon-comment #{unless Conf['Persistent QR'] then 'disabled' else ''}"
textContent: 'QR' textContent: 'QR'
title: 'Quick Reply' title: 'Quick Reply'
href: 'javascript:;' href: 'javascript:;'
@ -94,7 +94,7 @@ QR =
d.activeElement.blur() d.activeElement.blur()
$.rmClass QR.nodes.el, 'dump' $.rmClass QR.nodes.el, 'dump'
unless Conf['Captcha Warning Notifications'] unless Conf['Captcha Warning Notifications']
$.rmClass QR.captcha.nodes.input, 'error' $.rmClass QR.captcha.nodes.input, 'error' if QR.captcha.isEnabled
if Conf['QR Shortcut'] if Conf['QR Shortcut']
$.toggleClass $('.qr-shortcut'), 'disabled' $.toggleClass $('.qr-shortcut'), 'disabled'
new QR.post true new QR.post true
@ -107,8 +107,13 @@ QR =
$.addClass QR.nodes.el, 'has-focus' $.addClass QR.nodes.el, 'has-focus'
focusout: -> focusout: ->
<% if (type === 'crx') { %>
$.rmClass QR.nodes.el, 'has-focus' $.rmClass QR.nodes.el, 'has-focus'
<% } else { %>
$.queueTask ->
return if $.x 'ancestor::div[@id="qr"]', d.activeElement
$.rmClass QR.nodes.el, 'has-focus'
<% } %>
hide: -> hide: ->
d.activeElement.blur() d.activeElement.blur()
$.addClass QR.nodes.el, 'autohide' $.addClass QR.nodes.el, 'autohide'
@ -135,15 +140,33 @@ QR =
# Focus the captcha input on captcha error. # Focus the captcha input on captcha error.
QR.captcha.nodes.input.focus() QR.captcha.nodes.input.focus()
if Conf['Captcha Warning Notifications'] if Conf['Captcha Warning Notifications']
QR.notifications.push new Notification 'warning', el QR.notify el
else else
$.addClass QR.captcha.nodes.input, 'error' $.addClass QR.captcha.nodes.input, 'error'
$.on QR.captcha.nodes.input, 'keydown', -> $.on QR.captcha.nodes.input, 'keydown', ->
$.rmClass QR.captcha.nodes.input, 'error' $.rmClass QR.captcha.nodes.input, 'error'
else else
QR.notifications.push new Notification 'warning', el QR.notify el
alert el.textContent if d.hidden alert el.textContent if d.hidden
notify: (el) ->
notice = new Notice 'warning', el
QR.notifications.push notice
return unless Header.areNotificationsEnabled
notif = new Notification el.textContent,
body: el.textContent
icon: Favicon.logo
notif.onclick = -> window.focus()
<% if (type === 'crx') { %>
# Firefox automatically closes notifications
# so we can't control the onclose properly.
notif.onclose = -> notice.close()
setTimeout ->
notif.onclose = null
notif.close()
, 5 * $.SECOND
<% } %>
notifications: [] notifications: []
cleanNotifications: -> cleanNotifications: ->
@ -441,10 +464,15 @@ QR =
QR.fileInput files QR.fileInput files
openFileInput: (e) -> openFileInput: (e) ->
e.preventDefault() e.stopPropagation()
return if e.keyCode and not [32, 13].contains e.keyCode if e.shiftKey and e.type is 'click'
if e.shiftKey and not e.keyCode
return QR.selected.rmFile() return QR.selected.rmFile()
if e.ctrlKey and e.type is 'click'
$.addClass QR.nodes.filename, 'edit'
QR.nodes.filename.focus()
return
return if e.target.nodeName is 'INPUT' or (e.keyCode and not [32, 13].contains e.keyCode) or e.ctrlKey
e.preventDefault()
QR.nodes.fileInput.click() QR.nodes.fileInput.click()
fileInput: (files) -> fileInput: (files) ->
@ -505,8 +533,8 @@ QR =
for elm in $$ '*', el for elm in $$ '*', el
$.on elm, 'blur', QR.focusout $.on elm, 'blur', QR.focusout
$.on elm, 'focus', QR.focusin $.on elm, 'focus', QR.focusin
<% } %> <% } %>
$.on el, 'click', @select.bind @ $.on el, 'click', @select
$.on @nodes.rm, 'click', (e) => e.stopPropagation(); @rm() $.on @nodes.rm, 'click', (e) => e.stopPropagation(); @rm()
$.on @nodes.label, 'click', (e) => e.stopPropagation() $.on @nodes.label, 'click', (e) => e.stopPropagation()
$.on @nodes.spoiler, 'change', (e) => $.on @nodes.spoiler, 'change', (e) =>
@ -570,18 +598,17 @@ QR =
lock: (lock=true) -> lock: (lock=true) ->
@isLocked = lock @isLocked = lock
return unless @ is QR.selected return unless @ is QR.selected
for name in ['thread', 'name', 'email', 'sub', 'com', 'spoiler'] for name in ['thread', 'name', 'email', 'sub', 'com', 'filename', 'spoiler']
QR.nodes[name].disabled = lock QR.nodes[name].disabled = lock
@nodes.rm.style.visibility = @nodes.rm.style.visibility = if lock then 'hidden' else ''
QR.nodes.fileRM.style.visibility = if lock then 'hidden' else '' (if lock then $.off else $.on) QR.nodes.filename.previousElementSibling, 'click', QR.openFileInput
(if lock then $.off else $.on) QR.nodes.filename.parentNode, 'click', QR.openFileInput
@nodes.spoiler.disabled = lock @nodes.spoiler.disabled = lock
@nodes.el.draggable = !lock @nodes.el.draggable = !lock
unlock: -> unlock: ->
@lock false @lock false
select: -> select: =>
if QR.selected if QR.selected
QR.selected.nodes.el.id = null QR.selected.nodes.el.id = null
QR.selected.forceSave() QR.selected.forceSave()
@ -598,7 +625,7 @@ QR =
load: -> load: ->
# Load this post's values. # Load this post's values.
for name in ['thread', 'name', 'email', 'sub', 'com'] for name in ['thread', 'name', 'email', 'sub', 'com', 'filename']
QR.nodes[name].value = @[name] or null QR.nodes[name].value = @[name] or null
QR.tripcodeHider.call QR.nodes['name'] QR.tripcodeHider.call QR.nodes['name']
@ -621,18 +648,27 @@ QR =
# during the last 5 seconds of the cooldown. # during the last 5 seconds of the cooldown.
if QR.cooldown.auto and @ is QR.posts[0] and 0 < QR.cooldown.seconds <= 5 if QR.cooldown.auto and @ is QR.posts[0] and 0 < QR.cooldown.seconds <= 5
QR.cooldown.auto = false QR.cooldown.auto = false
when 'filename'
return unless @file
@file.newName = @filename.replace /[/\\]/g, '-'
unless /\.(jpe?g|png|gif|pdf|swf)$/i.test @filename
# 4chan will truncate the filename if it has no extension,
# but it will always replace the extension by the correct one,
# so we suffix it with '.jpg' when needed.
@file.newName += '.jpg'
@updateFilename()
forceSave: -> forceSave: ->
return unless @ is QR.selected return unless @ is QR.selected
# Do this in case people use extensions # Do this in case people use extensions
# that do not trigger the `input` event. # that do not trigger the `input` event.
for name in ['thread', 'name', 'email', 'sub', 'com', 'spoiler'] for name in ['thread', 'name', 'email', 'sub', 'com', 'filename', 'spoiler']
@save QR.nodes[name] @save QR.nodes[name]
return return
setFile: (@file) -> setFile: (@file) ->
@filename = "#{file.name} (#{$.bytesToString file.size})" @filename = file.name
@nodes.el.title = @filename @filesize = $.bytesToString file.size
@nodes.label.hidden = false if QR.spoiler @nodes.label.hidden = false if QR.spoiler
URL.revokeObjectURL @URL URL.revokeObjectURL @URL
@showFileData() @showFileData()
@ -676,18 +712,27 @@ QR =
img.src = fileURL img.src = fileURL
rmFile: -> rmFile: ->
return if @isLocked
delete @file delete @file
delete @filename delete @filename
delete @filesize
@nodes.el.title = null @nodes.el.title = null
QR.nodes.fileContainer.title = ''
@nodes.el.style.backgroundImage = null @nodes.el.style.backgroundImage = null
@nodes.label.hidden = true if QR.spoiler @nodes.label.hidden = true if QR.spoiler
@showFileData() @showFileData()
URL.revokeObjectURL @URL URL.revokeObjectURL @URL
updateFilename: ->
long = "#{@filename} (#{@filesize})\nCtrl+click to edit filename. Shift+click to clear."
@nodes.el.title = long
return unless @ is QR.selected
QR.nodes.fileContainer.title = long
showFileData: -> showFileData: ->
if @file if @file
QR.nodes.filename.textContent = @filename @updateFilename()
QR.nodes.filename.title = @filename QR.nodes.filename.value = @filename
QR.nodes.spoiler.checked = @spoiler QR.nodes.spoiler.checked = @spoiler
$.addClass QR.nodes.fileSubmit, 'has-file' $.addClass QR.nodes.fileSubmit, 'has-file'
else else
@ -860,28 +905,29 @@ QR =
dialog = UI.dialog 'qr', 'top:0;right:0;', """<%= grunt.file.read('src/General/html/Features/QuickReply.html').replace(/>\s+</g, '><').trim() %>""" dialog = UI.dialog 'qr', 'top:0;right:0;', """<%= grunt.file.read('src/General/html/Features/QuickReply.html').replace(/>\s+</g, '><').trim() %>"""
nodes[key] = $ value, dialog for key, value of { nodes[key] = $ value, dialog for key, value of {
move: '.move' move: '.move'
autohide: '#autohide' autohide: '#autohide'
thread: 'select' thread: 'select'
threadPar: '#qr-thread-select' threadPar: '#qr-thread-select'
close: '.close' close: '.close'
form: 'form' form: 'form'
dumpButton: '#dump-button' dumpButton: '#dump-button'
name: '[data-name=name]' name: '[data-name=name]'
email: '[data-name=email]' email: '[data-name=email]'
sub: '[data-name=sub]' sub: '[data-name=sub]'
com: '[data-name=com]' com: '[data-name=com]'
dumpList: '#dump-list' dumpList: '#dump-list'
addPost: '#add-post' addPost: '#add-post'
charCount: '#char-count' charCount: '#char-count'
fileSubmit: '#file-n-submit' fileSubmit: '#file-n-submit'
filename: '#qr-filename' filename: '#qr-filename'
fileRM: '#qr-filerm' fileContainer: '#qr-filename-container'
fileExtras: '#qr-extras-container' fileRM: '#qr-filerm'
spoiler: '#qr-file-spoiler' fileExtras: '#qr-extras-container'
spoilerPar: '#qr-spoiler-label' spoiler: '#qr-file-spoiler'
status: '[type=submit]' spoilerPar: '#qr-spoiler-label'
fileInput: '[type=file]' status: '[type=submit]'
fileInput: '[type=file]'
} }
check = check =
@ -939,15 +985,17 @@ QR =
$.on elm, 'focus', QR.focusin $.on elm, 'focus', QR.focusin
<% } %> <% } %>
$.on dialog, 'focusin', QR.focusin $.on dialog, 'focusin', QR.focusin
$.on dialog, 'focusout', QR.focusout $.on dialog, 'focusout', QR.focusout
$.on nodes.autohide, 'change', QR.toggleHide $.on nodes.autohide, 'change', QR.toggleHide
$.on nodes.close, 'click', QR.close $.on nodes.close, 'click', QR.close
$.on nodes.dumpButton, 'click', -> nodes.el.classList.toggle 'dump' $.on nodes.dumpButton, 'click', -> nodes.el.classList.toggle 'dump'
$.on nodes.addPost, 'click', -> new QR.post true $.on nodes.addPost, 'click', -> new QR.post true
$.on nodes.form, 'submit', QR.submit $.on nodes.form, 'submit', QR.submit
$.on nodes.fileRM, 'click', -> QR.selected.rmFile() $.on nodes.filename, 'blur', -> $.rmClass @, 'edit'
$.on nodes.fileExtras, 'click', (e) -> e.stopPropagation() $.on nodes.fileRM, 'click', -> QR.selected.rmFile()
$.on nodes.fileExtras, 'click', (e) -> e.stopPropagation()
$.on nodes.spoiler, 'change', -> QR.selected.nodes.spoiler.click() $.on nodes.spoiler, 'change', -> QR.selected.nodes.spoiler.click()
$.on nodes.fileInput, 'change', QR.fileInput $.on nodes.fileInput, 'change', QR.fileInput
@ -958,7 +1006,7 @@ QR =
$.on nodes[name], 'mouseover', QR.mouseover $.on nodes[name], 'mouseover', QR.mouseover
# save selected post's data # save selected post's data
items = ['name', 'email', 'sub', 'com'] items = ['name', 'email', 'sub', 'com', 'filename']
i = 0 i = 0
while name = items[i++] while name = items[i++]
$.on nodes[name], 'input', -> QR.selected.save @ $.on nodes[name], 'input', -> QR.selected.save @
@ -1169,7 +1217,7 @@ QR =
QR.cleanNotifications() QR.cleanNotifications()
if Conf['Posting Success Notifications'] if Conf['Posting Success Notifications']
QR.notifications.push new Notification 'success', h1.textContent, 5 QR.notifications.push new Notice 'success', h1.textContent, 5
QR.persona.set post QR.persona.set post
@ -1221,7 +1269,8 @@ QR =
QR.req.abort() QR.req.abort()
delete QR.req delete QR.req
QR.posts[0].unlock() QR.posts[0].unlock()
QR.notifications.push new Notification 'info', 'QR upload aborted.', 5 QR.cooldown.auto = false
QR.notifications.push new Notice 'info', 'QR upload aborted.', 5
QR.status() QR.status()
mouseover: (e) -> mouseover: (e) ->

View File

@ -26,6 +26,7 @@ QuoteOP =
{fullID} = (if @isClone then @context else @).thread {fullID} = (if @isClone then @context else @).thread
# add (OP) to quotes quoting this context's OP. # add (OP) to quotes quoting this context's OP.
return unless quotes.contains fullID return unless quotes.contains fullID
i = 0 i = 0
while quotelink = quotelinks[i++] while quotelink = quotelinks[i++]

View File

@ -26,6 +26,7 @@ QuoteYou =
for quotelink in @nodes.quotelinks when QR.db.get Get.postDataFromLink quotelink for quotelink in @nodes.quotelinks when QR.db.get Get.postDataFromLink quotelink
$.add quotelink, $.tn '\u00A0(You)' $.add quotelink, $.tn '\u00A0(You)'
$.addClass quotelink, 'you'
$.addClass @nodes.root, 'quotesYou' $.addClass @nodes.root, 'quotesYou'
return return
@ -36,7 +37,7 @@ QuoteYou =
unless QuoteYou.lastRead unless QuoteYou.lastRead
unless post = QuoteYou.lastRead = $ '.quotesYou' unless post = QuoteYou.lastRead = $ '.quotesYou'
new Notification 'warning', 'No posts are currently quoting you, loser.', 20 new Notice 'warning', 'No posts are currently quoting you, loser.', 20
return return
return if QuoteYou.cb.scroll post return if QuoteYou.cb.scroll post
else else

View File

@ -18,7 +18,7 @@ Quotify =
return return
parseDeadlink: (deadlink) -> parseDeadlink: (deadlink) ->
if deadlink.parentNode.className is 'prettyprint' if $.hasClass deadlink.parentNode, 'prettyprint'
# Don't quotify deadlinks inside code tags, # Don't quotify deadlinks inside code tags,
# un-`span` them. # un-`span` them.
# This won't be necessary once 4chan # This won't be necessary once 4chan