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
13
CHANGELOG.md
@ -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>
|
||||

|
||||
- 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.
|
||||
|
||||

|
||||
|
||||
- The QR now allows you to edit the filename on the fly:
|
||||
|
||||
**seaweedchan**:
|
||||
- Optimizations for the banner and board title code
|
||||
|
||||
|
||||
@ -135,7 +135,7 @@ module.exports = (grunt) ->
|
||||
tmpcrx: 'tmp-crx'
|
||||
tmpuserscript: 'tmp-userscript'
|
||||
|
||||
require('matchdep').filterDev('grunt-*').forEach grunt.loadNpmTasks
|
||||
require('load-grunt-tasks') grunt
|
||||
|
||||
grunt.registerTask 'default', [
|
||||
'build'
|
||||
|
||||
9
LICENSE
@ -78,15 +78,6 @@
|
||||
*
|
||||
* 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/)
|
||||
* Copyright (c) Jan Odvarko, http://odvarko.cz
|
||||
*
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
// ==UserScript==
|
||||
// @name 4chan X
|
||||
// @version 1.2.32
|
||||
// @minGMVer 1.13
|
||||
// @minFFVer 22
|
||||
// @namespace 4chan-X
|
||||
// @description Cross-browser userscript for maximum lurking on 4chan.
|
||||
// @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
|
||||
// @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==
|
||||
// ==/UserScript==
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
// ==UserScript==
|
||||
// @name appchan x
|
||||
// @version 2.3.3
|
||||
// @minGMVer 1.13
|
||||
// @minFFVer 22
|
||||
// @namespace zixaphir
|
||||
// @description The most comprehensive 4chan userscript.
|
||||
// @license MIT; https://github.com/zixaphir/appchan-x/blob/master/LICENSE
|
||||
|
||||
BIN
img/changelog/3.2.0/0.png
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
img/changelog/3.8.0/0.gif
Normal file
|
After Width: | Height: | Size: 690 KiB |
BIN
img/changelog/3.9.0/0.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
@ -19,6 +19,7 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"font-awesome": "git://github.com/MayhemYDG/Font-Awesome.git#df4285951124f9ca1f3907438462e5ba9e464bcb",
|
||||
"grunt": "~0.4.1",
|
||||
"grunt-bump": "~0.0.11",
|
||||
"grunt-concurrent": "~0.3.0",
|
||||
@ -29,7 +30,7 @@
|
||||
"grunt-contrib-copy": "~0.4.1",
|
||||
"grunt-contrib-watch": "~0.5.0",
|
||||
"grunt-shell": "~0.3.1",
|
||||
"matchdep": "~0.1.2"
|
||||
"load-grunt-tasks": "~0.1.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@ -33,7 +33,7 @@ Filter =
|
||||
regexp = RegExp regexp[1], regexp[2]
|
||||
catch err
|
||||
# I warned you, bro.
|
||||
new Notification 'warning', err.message, 60
|
||||
new Notice 'warning', err.message, 60
|
||||
continue
|
||||
|
||||
# Filter OPs along with their threads, replies only, or both.
|
||||
|
||||
@ -13,6 +13,10 @@ Config =
|
||||
true
|
||||
'Add button to hide 4chan announcements.'
|
||||
]
|
||||
'Desktop Notifications': [
|
||||
true
|
||||
'Enables desktop notifications across various <%= meta.name %> features.'
|
||||
]
|
||||
'404 Redirect': [
|
||||
true
|
||||
'Redirect dead threads and images.'
|
||||
@ -826,6 +830,7 @@ http://iqdb.org/?url=%TURL
|
||||
'Bottom Header': false
|
||||
'Header catalog links': false
|
||||
'Bottom Board List': true
|
||||
'Shortcut Icons': false
|
||||
'Custom Board Navigation': true
|
||||
|
||||
boardnav: """
|
||||
|
||||
@ -156,11 +156,8 @@ Get =
|
||||
# https://github.com/eksopl/asagi/blob/master/src/main/java/net/easymodo/asagi/Yotsuba.java#L109-138
|
||||
bq.innerHTML = bq.innerHTML.replace ///
|
||||
\n
|
||||
| \[/?b\]
|
||||
| \[/?spoiler\]
|
||||
| \[/?code\]
|
||||
| \[/?moot\]
|
||||
| \[/?banned\]
|
||||
|
|
||||
\[/?[a-z]+(:lit)?\]
|
||||
///g, Get.parseMarkup
|
||||
|
||||
comment = bq.innerHTML
|
||||
@ -234,6 +231,8 @@ Get =
|
||||
when '[/moot]'
|
||||
'</div>'
|
||||
when '[banned]'
|
||||
'<b style="color: red;">'
|
||||
'<strong style="color: red;">'
|
||||
when '[/banned]'
|
||||
'</b>'
|
||||
'</strong>'
|
||||
else
|
||||
text.replace ':lit', ''
|
||||
|
||||
@ -36,6 +36,7 @@ Header =
|
||||
$.sync 'Fixed Header', Header.setBarFixed
|
||||
$.sync 'Bottom Header', Header.setBarPosition
|
||||
|
||||
|
||||
$.event 'AddMenuEntry',
|
||||
type: 'header'
|
||||
el: $.el 'span',
|
||||
@ -60,6 +61,7 @@ Header =
|
||||
$.prepend d.body, @bar
|
||||
$.add d.body, Header.hover
|
||||
@setBarPosition Conf['Bottom Header']
|
||||
@
|
||||
|
||||
bar: $.el 'div',
|
||||
id: 'header-bar'
|
||||
@ -200,6 +202,21 @@ Header =
|
||||
Conf['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) ->
|
||||
Header.headerToggler.checked = hide
|
||||
$.event 'CloseMenu'
|
||||
@ -220,8 +237,7 @@ Header =
|
||||
'automatically hide itself.'
|
||||
else
|
||||
'remain visible.'}"
|
||||
|
||||
new Notification 'info', message, 2
|
||||
new Notice 'info', message, 2
|
||||
|
||||
setCustomNav: (show) ->
|
||||
Header.customNavToggler.checked = show
|
||||
@ -265,5 +281,33 @@ Header =
|
||||
|
||||
createNotification: (e) ->
|
||||
{type, content, lifetime, cb} = e.detail
|
||||
notif = new Notification type, content, lifetime
|
||||
notif = new Notice type, content, lifetime
|
||||
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
|
||||
|
||||
@ -14,8 +14,7 @@ Main =
|
||||
|
||||
flatten null, Config
|
||||
|
||||
# Unflattened Config.
|
||||
for db in DataBoards
|
||||
for db in DataBoard.keys
|
||||
Conf[db] = boards: {}
|
||||
|
||||
$.extend Conf,
|
||||
@ -33,7 +32,7 @@ Main =
|
||||
$.extend Conf, items
|
||||
<% if (type === 'crx') { %>
|
||||
unless items
|
||||
new Notification 'error', $.el 'span',
|
||||
new Notice 'error', $.el 'span',
|
||||
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>
|
||||
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
|
||||
localStorage.getItem '4chan-settings'
|
||||
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'
|
||||
|
||||
@ -316,7 +315,7 @@ Main =
|
||||
else if errors.length is 1
|
||||
error = errors[0]
|
||||
if error
|
||||
new Notification 'error', Main.parseError(error), 15
|
||||
new Notice 'error', Main.parseError(error), 15
|
||||
return
|
||||
|
||||
div = $.el 'div',
|
||||
@ -332,7 +331,7 @@ Main =
|
||||
for error in errors
|
||||
$.add logs, Main.parseError error
|
||||
|
||||
new Notification 'error', [div, logs], 30
|
||||
new Notice 'error', [div, logs], 30
|
||||
|
||||
parseError: (data) ->
|
||||
Main.logError data
|
||||
|
||||
@ -2,7 +2,7 @@ Settings =
|
||||
init: ->
|
||||
# Appchan X settings link
|
||||
el = $.el 'a',
|
||||
className: 'settings-link'
|
||||
className: 'settings-link icon icon-wrench'
|
||||
href: 'javascript:;'
|
||||
textContent: 'Settings'
|
||||
$.on el, 'click', Settings.open
|
||||
@ -25,7 +25,7 @@ Settings =
|
||||
el = $.el 'span',
|
||||
innerHTML: "<%= meta.name %> has been updated to <a href='#{changelog}' target=_blank>version #{g.VERSION}</a>."
|
||||
if Conf['Show Updated Notifications']
|
||||
new Notification 'info', el, 30
|
||||
new Notice 'info', el, 30
|
||||
else
|
||||
$.on d, '4chanXInitFinished', Settings.open
|
||||
$.set 'previousversion', g.VERSION
|
||||
@ -171,7 +171,7 @@ Settings =
|
||||
data =
|
||||
version: g.VERSION
|
||||
date: now
|
||||
for db in DataBoards
|
||||
for db in DataBoard.keys
|
||||
Conf[db] = boards: {}
|
||||
# Make sure to export the most recent data.
|
||||
$.get Conf, (Conf) ->
|
||||
|
||||
@ -19,8 +19,7 @@ UI = do ->
|
||||
|
||||
constructor: (@type) ->
|
||||
# Doc here: https://github.com/MayhemYDG/4chan-x/wiki/Menu-API
|
||||
$.on d, 'AddMenuEntry', @addEntry.bind @
|
||||
@close = close.bind @
|
||||
$.on d, 'AddMenuEntry', @addEntry
|
||||
@entries = []
|
||||
|
||||
makeMenu: ->
|
||||
@ -29,7 +28,7 @@ UI = do ->
|
||||
id: 'menu'
|
||||
tabIndex: 0
|
||||
$.on menu, 'click', (e) -> e.stopPropagation()
|
||||
$.on menu, 'keydown', @keybinds.bind @
|
||||
$.on menu, 'keydown', @keybinds
|
||||
menu
|
||||
|
||||
toggle: (e, button, data) ->
|
||||
@ -111,7 +110,7 @@ UI = do ->
|
||||
$.add entry.el, submenu
|
||||
return
|
||||
|
||||
close = ->
|
||||
close: =>
|
||||
$.rm currentMenu
|
||||
$.rmClass lastToggledButton, 'active'
|
||||
currentMenu = null
|
||||
@ -124,7 +123,7 @@ UI = do ->
|
||||
+(first.style.order or first.style.webkitOrder) - +(second.style.order or second.style.webkitOrder)
|
||||
entries[entries.indexOf(entry) + direction]
|
||||
|
||||
keybinds: (e) ->
|
||||
keybinds: (e) =>
|
||||
entry = $ '.focused', currentMenu
|
||||
while subEntry = $ '.focused', entry
|
||||
entry = subEntry
|
||||
@ -182,7 +181,7 @@ UI = do ->
|
||||
style.left = left
|
||||
style.right = right
|
||||
|
||||
addEntry: (e) ->
|
||||
addEntry: (e) =>
|
||||
entry = e.detail
|
||||
return if entry.type isnt @type
|
||||
@parseEntry entry
|
||||
|
||||
1167
src/General/css/font-awesome.css
vendored
Normal file
@ -1501,6 +1501,19 @@ input:not([type=radio]) {
|
||||
.has-file #qr-filename {
|
||||
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 {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@ -206,8 +206,7 @@ a {
|
||||
content: "]\\00a0";
|
||||
}
|
||||
.dead-thread,
|
||||
.disabled,
|
||||
.expand-all-shortcut {
|
||||
.disabled {
|
||||
opacity: .45;
|
||||
}
|
||||
#shortcuts {
|
||||
@ -773,21 +772,34 @@ input.field.tripped:not(:hover):not(:focus) {
|
||||
height: 24px;
|
||||
}
|
||||
/* 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-filesize,
|
||||
.has-file #qr-no-file {
|
||||
display: none;
|
||||
}
|
||||
#qr-no-file,
|
||||
.has-file #qr-filename {
|
||||
.has-file #qr-filename,
|
||||
.has-file #qr-filesize {
|
||||
display: inline-block;
|
||||
padding: 0px 4px;
|
||||
margin-bottom: 2px;
|
||||
margin: 0 0 2px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 88%;
|
||||
vertical-align: top;
|
||||
}
|
||||
#qr-no-file {
|
||||
color: #AAA;
|
||||
padding: 1px 4px;
|
||||
}
|
||||
#qr-filename-container {
|
||||
-moz-box-sizing: border-box;
|
||||
@ -1026,7 +1038,6 @@ a:only-of-type > .remove {
|
||||
.boardSubtitle[contenteditable="true"] {
|
||||
cursor: text !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;
|
||||
|
||||
@ -9,9 +9,9 @@
|
||||
</div>
|
||||
<form>
|
||||
<div class=persona>
|
||||
<input name=name 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 name=sub data-name=sub list="list-sub" placeholder=Subject class=field size=1 tabindex=30>
|
||||
<input data-name=name list="list-name" placeholder=Name class=field size=1 tabindex=10>
|
||||
<input data-name=email list="list-email" placeholder=E-mail class=field size=1 tabindex=20>
|
||||
<input data-name=sub list="list-sub" placeholder=Subject class=field size=1 tabindex=30>
|
||||
</div>
|
||||
<div class=textarea>
|
||||
<textarea data-name=com placeholder=Comment class=field tabindex=40></textarea>
|
||||
@ -24,7 +24,7 @@
|
||||
<div id=file-n-submit>
|
||||
<span id=qr-filename-container class=field tabindex=60>
|
||||
<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>
|
||||
<label id=qr-spoiler-label>
|
||||
<input type=checkbox id=qr-file-spoiler title='Spoiler image' tabindex=70>
|
||||
@ -43,4 +43,4 @@
|
||||
<datalist id="list-name"></datalist>
|
||||
<datalist id="list-email"></datalist>
|
||||
<datalist id="list-sub"></datalist>
|
||||
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 346 B After Width: | Height: | Size: 335 B |
|
Before Width: | Height: | Size: 456 B After Width: | Height: | Size: 384 B |
|
Before Width: | Height: | Size: 323 B After Width: | Height: | Size: 309 B |
|
Before Width: | Height: | Size: 450 B After Width: | Height: | Size: 376 B |
|
Before Width: | Height: | Size: 321 B After Width: | Height: | Size: 309 B |
|
Before Width: | Height: | Size: 450 B After Width: | Height: | Size: 377 B |
|
Before Width: | Height: | Size: 232 B After Width: | Height: | Size: 189 B |
|
Before Width: | Height: | Size: 232 B After Width: | Height: | Size: 216 B |
|
Before Width: | Height: | Size: 232 B After Width: | Height: | Size: 216 B |
|
Before Width: | Height: | Size: 114 B After Width: | Height: | Size: 113 B |
|
Before Width: | Height: | Size: 172 B After Width: | Height: | Size: 133 B |
|
Before Width: | Height: | Size: 274 B After Width: | Height: | Size: 254 B |
|
Before Width: | Height: | Size: 172 B After Width: | Height: | Size: 133 B |
|
Before Width: | Height: | Size: 274 B After Width: | Height: | Size: 256 B |
|
Before Width: | Height: | Size: 172 B After Width: | Height: | Size: 133 B |
|
Before Width: | Height: | Size: 270 B After Width: | Height: | Size: 253 B |
@ -41,10 +41,8 @@ $.formData = (form) ->
|
||||
return new FormData form
|
||||
fd = new FormData()
|
||||
for key, val of form when val
|
||||
# XXX GM bug
|
||||
# if val instanceof Blob
|
||||
if val.size and val.name
|
||||
fd.append key, val, val.name
|
||||
if typeof val is 'object' and 'newName' of val
|
||||
fd.append key, val, val.newName
|
||||
else
|
||||
fd.append key, val
|
||||
fd
|
||||
|
||||
@ -3,4 +3,4 @@
|
||||
<%= grunt.file.read('src/General/lib/post.class') %>
|
||||
<%= grunt.file.read('src/General/lib/clone.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') %>
|
||||
@ -1,9 +1,9 @@
|
||||
DataBoards = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads']
|
||||
|
||||
class DataBoard
|
||||
@keys = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads']
|
||||
|
||||
constructor: (@key, sync, dontClean) ->
|
||||
@data = Conf[key]
|
||||
$.sync key, @onSync.bind @
|
||||
$.sync key, @onSync
|
||||
@clean() unless dontClean
|
||||
return unless sync
|
||||
# Chrome also fires the onChanged callback on the current tab,
|
||||
@ -88,6 +88,6 @@ class DataBoard
|
||||
@deleteIfEmpty {boardID}
|
||||
@save()
|
||||
|
||||
onSync: (data) ->
|
||||
onSync: (data) =>
|
||||
@data = data or boards: {}
|
||||
@sync?()
|
||||
|
||||
5
src/General/lib/notification.class → src/General/lib/notice.class
Executable file → Normal file
@ -1,4 +1,4 @@
|
||||
class Notification
|
||||
class Notice
|
||||
constructor: (type, content, @timeout) ->
|
||||
@el = $.el '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
|
||||
|
||||
close: =>
|
||||
$.rm @el
|
||||
$.off d, 'visibilitychange', @add
|
||||
$.rm @el
|
||||
@ -1,9 +1,21 @@
|
||||
Polyfill =
|
||||
init: ->
|
||||
<% if (type === 'crx') { %>
|
||||
Polyfill.toBlob()
|
||||
Polyfill.visibility()
|
||||
@notificationPermission()
|
||||
@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: ->
|
||||
HTMLCanvasElement::toBlob or= (cb) ->
|
||||
data = atob @toDataURL()[22..]
|
||||
|
||||
@ -78,15 +78,6 @@
|
||||
*
|
||||
* 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/)
|
||||
* Copyright (c) Jan Odvarko, http://odvarko.cz
|
||||
*
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
// ==UserScript==
|
||||
// @name <%= meta.name %>
|
||||
// @version <%= version %>
|
||||
// @minGMVer 1.13
|
||||
// @minFFVer 22
|
||||
// @namespace <%= meta.namespace %>
|
||||
// @description <%= description %>
|
||||
// @license MIT; <%= meta.repo %>blob/<%= meta.mainBranch %>/LICENSE
|
||||
|
||||
@ -4,9 +4,9 @@ ImageExpand =
|
||||
|
||||
@EAI = $.el 'a',
|
||||
id: 'img-controls'
|
||||
className: 'expand-all-shortcut'
|
||||
title: 'Expand All Images'
|
||||
href: 'javascript:;'
|
||||
className: 'expand-all-shortcut icon icon-resize-full'
|
||||
title: 'Expand All Images'
|
||||
href: 'javascript:;'
|
||||
|
||||
$.on @EAI, 'click', ImageExpand.cb.toggleAll
|
||||
|
||||
@ -27,7 +27,7 @@ ImageExpand =
|
||||
ImageExpand.contract @
|
||||
ImageExpand.expand @
|
||||
return
|
||||
if ImageExpand.on and !@isHidden
|
||||
if ImageExpand.on and !@isHidden and (Conf['Expand spoilers'] or !@file.isSpoiler)
|
||||
ImageExpand.expand @
|
||||
cb:
|
||||
toggle: (e) ->
|
||||
@ -37,11 +37,11 @@ ImageExpand =
|
||||
toggleAll: ->
|
||||
$.event 'CloseMenu'
|
||||
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'
|
||||
func = ImageExpand.expand
|
||||
else
|
||||
ImageExpand.EAI.className = 'expand-all-shortcut'
|
||||
ImageExpand.EAI.className = 'expand-all-shortcut icon icon-resize-full'
|
||||
ImageExpand.EAI.title = 'Expand All Images'
|
||||
func = ImageExpand.contract
|
||||
for ID, post of g.posts
|
||||
|
||||
@ -2,24 +2,23 @@ Linkify =
|
||||
init: ->
|
||||
return if g.VIEW is 'catalog' or not Conf['Linkify']
|
||||
|
||||
@regString =
|
||||
///(
|
||||
# http, magnet, ftp, etc
|
||||
(https?|mailto|git|magnet|ftp|irc):(
|
||||
[a-z\d%/]
|
||||
)
|
||||
|
|
||||
# This should account for virtually all links posted without http:
|
||||
[-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}
|
||||
)(/|(?!.))
|
||||
|
|
||||
# IPv4 Addresses
|
||||
[\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}
|
||||
|
|
||||
# E-mails
|
||||
[-\w\d.@]+@[a-z\d.-]+\.[a-z\d]
|
||||
)///i
|
||||
@regString = ///(
|
||||
# http, magnet, ftp, etc
|
||||
(https?|mailto|git|magnet|ftp|irc):(
|
||||
[a-z\d%/]
|
||||
)
|
||||
|
|
||||
# This should account for virtually all links posted without http:
|
||||
[-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}
|
||||
)(/|(?!.))
|
||||
|
|
||||
# IPv4 Addresses
|
||||
[\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}
|
||||
|
|
||||
# E-mails
|
||||
[-\w\d.@]+@[a-z\d.-]+\.[a-z\d]
|
||||
)///i
|
||||
|
||||
if Conf['Comment Expansion']
|
||||
ExpandComment.callbacks.push @node
|
||||
@ -55,40 +54,40 @@ Linkify =
|
||||
while result = test.exec data
|
||||
{index} = result
|
||||
endNode = node
|
||||
word = result[0]
|
||||
# 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++)
|
||||
break if saved.nodeName is 'BR'
|
||||
if saved.nodeName is 'BR'
|
||||
break
|
||||
|
||||
endNode = saved
|
||||
{length} = saved.data
|
||||
endNode = saved
|
||||
{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
|
||||
test.lastIndex = length = end.index
|
||||
i--
|
||||
break
|
||||
|
||||
test.lastIndex = 0 if length is endNode.data.length
|
||||
range = Linkify.makeRange node, endNode, index, length
|
||||
links.push range if link = Linkify.regString.exec range.toString()
|
||||
break
|
||||
if Linkify.regString.exec word
|
||||
links.push Linkify.makeRange node, endNode, index, length
|
||||
|
||||
else
|
||||
if link = Linkify.regString.exec result[0]
|
||||
range = Linkify.makeRange node, node, index, length
|
||||
links.push range
|
||||
break unless test.lastIndex and node is endNode
|
||||
|
||||
for range in links.reverse()
|
||||
@nodes.links.push Linkify.makeLink range, @
|
||||
for link in links.reverse()
|
||||
@nodes.links.push Linkify.makeLink link, @
|
||||
|
||||
return unless Conf['Embedding'] or Conf['Link Title']
|
||||
|
||||
items = @nodes.links
|
||||
{links} = @nodes
|
||||
i = 0
|
||||
while range = items[i++]
|
||||
if data = Linkify.services range
|
||||
while link = links[i++]
|
||||
if data = Linkify.services link
|
||||
Linkify.embed data if Conf['Embedding']
|
||||
Linkify.title data if Conf['Link Title']
|
||||
|
||||
@ -126,12 +125,9 @@ Linkify =
|
||||
if i
|
||||
range.setEnd range.endContainer, range.endOffset - i
|
||||
|
||||
# This is the only piece of code left based on Anthony Lieuallen's Linkify
|
||||
text =
|
||||
if text.contains ':'
|
||||
text
|
||||
else (
|
||||
if text.contains '@'
|
||||
unless /(https?|mailto|git|magnet|ftp|irc):/.test text
|
||||
text = (
|
||||
if /@/.test text
|
||||
'mailto:'
|
||||
else
|
||||
'http://'
|
||||
|
||||
@ -32,13 +32,9 @@ Fourchan =
|
||||
cb: @math
|
||||
code: ->
|
||||
return if @isClone
|
||||
for pre in $$ '.prettyprint', @nodes.comment
|
||||
# Don't pretty print twice:
|
||||
# Might need a better way to detect if a .prettyprint
|
||||
# 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
|
||||
for pre in $$ '.prettyprint:not(.prettyprinted)', @nodes.comment
|
||||
$.event 'prettyprint', pre, window
|
||||
$.addClass pre, 'prettyprinted'
|
||||
return
|
||||
math: ->
|
||||
return if @isClone or !$ '.math', @nodes.comment
|
||||
|
||||
@ -9,43 +9,49 @@ Favicon =
|
||||
Favicon.switch()
|
||||
|
||||
switch: ->
|
||||
unreadDead = Favicon.unreadDeadY = Favicon.unreadSFW = Favicon.unreadSFWY = Favicon.unreadNSFW = Favicon.unreadNSFWY = 'data:image/png;base64,'
|
||||
switch Conf['favicon']
|
||||
when 'ferongr'
|
||||
Favicon.unreadDead += '<%= grunt.file.read("src/General/img/favicons/ferongr/unreadDead.png", {encoding: "base64"}) %>'
|
||||
Favicon.unreadDeadY += '<%= grunt.file.read("src/General/img/favicons/ferongr/unreadDeadY.png", {encoding: "base64"}) %>'
|
||||
Favicon.unreadSFW += '<%= grunt.file.read("src/General/img/favicons/ferongr/unreadSFW.png", {encoding: "base64"}) %>'
|
||||
Favicon.unreadSFWY += '<%= grunt.file.read("src/General/img/favicons/ferongr/unreadSFWY.png", {encoding: "base64"}) %>'
|
||||
Favicon.unreadNSFW += '<%= grunt.file.read("src/General/img/favicons/ferongr/unreadNSFW.png", {encoding: "base64"}) %>'
|
||||
Favicon.unreadNSFWY += '<%= grunt.file.read("src/General/img/favicons/ferongr/unreadNSFWY.png", {encoding: "base64"}) %>'
|
||||
when 'xat-'
|
||||
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"}) %>'
|
||||
Favicon.unreadSFW += '<%= grunt.file.read("src/General/img/favicons/xat-/unreadSFW.png", {encoding: "base64"}) %>'
|
||||
Favicon.unreadSFWY += '<%= grunt.file.read("src/General/img/favicons/xat-/unreadSFWY.png", {encoding: "base64"}) %>'
|
||||
Favicon.unreadNSFW += '<%= grunt.file.read("src/General/img/favicons/xat-/unreadNSFW.png", {encoding: "base64"}) %>'
|
||||
Favicon.unreadNSFWY += '<%= grunt.file.read("src/General/img/favicons/xat-/unreadNSFWY.png", {encoding: "base64"}) %>'
|
||||
when 'Mayhem'
|
||||
Favicon.unreadDead += '<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadDead.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"}) %>'
|
||||
Favicon.unreadSFWY += '<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadSFWY.png", {encoding: "base64"}) %>'
|
||||
Favicon.unreadNSFW += '<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadNSFW.png", {encoding: "base64"}) %>'
|
||||
Favicon.unreadNSFWY += '<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadNSFWY.png", {encoding: "base64"}) %>'
|
||||
when '4chanJS'
|
||||
Favicon.unreadDead += '<%= grunt.file.read("src/General/img/favicons/4chanJS/unreadDead.png", {encoding: "base64"}) %>'
|
||||
Favicon.unreadDeadY += '<%= grunt.file.read("src/General/img/favicons/4chanJS/unreadDeadY.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"}) %>'
|
||||
Favicon.unreadNSFW += '<%= grunt.file.read("src/General/img/favicons/4chanJS/unreadNSFW.png", {encoding: "base64"}) %>'
|
||||
Favicon.unreadNSFWY += '<%= grunt.file.read("src/General/img/favicons/4chanJS/unreadNSFWY.png", {encoding: "base64"}) %>'
|
||||
when 'Original'
|
||||
Favicon.unreadDead += '<%= grunt.file.read("src/General/img/favicons/Original/unreadDead.png", {encoding: "base64"}) %>'
|
||||
Favicon.unreadDeadY += '<%= grunt.file.read("src/General/img/favicons/Original/unreadDeadY.png", {encoding: "base64"}) %>'
|
||||
Favicon.unreadSFW += '<%= grunt.file.read("src/General/img/favicons/Original/unreadSFW.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"}) %>'
|
||||
Favicon.unreadNSFWY += '<%= grunt.file.read("src/General/img/favicons/Original/unreadNSFWY.png", {encoding: "base64"}) %>'
|
||||
t = 'data:image/png;base64,'
|
||||
f = Favicon
|
||||
[f.unreadDead, funreadDeadY, f.unreadSFW, f.unreadSFWY, f.unreadNSFW, f.unreadNSFWY] = switch Conf['favicon']
|
||||
when 'ferongr' then [
|
||||
t + '<%= grunt.file.read("src/General/img/favicons/ferongr/unreadDead.png", {encoding: "base64"}) %>'
|
||||
t + '<%= grunt.file.read("src/General/img/favicons/ferongr/unreadDeadY.png", {encoding: "base64"}) %>'
|
||||
t + '<%= grunt.file.read("src/General/img/favicons/ferongr/unreadSFW.png", {encoding: "base64"}) %>'
|
||||
t + '<%= grunt.file.read("src/General/img/favicons/ferongr/unreadSFWY.png", {encoding: "base64"}) %>'
|
||||
t + '<%= grunt.file.read("src/General/img/favicons/ferongr/unreadNSFW.png", {encoding: "base64"}) %>'
|
||||
t + '<%= grunt.file.read("src/General/img/favicons/ferongr/unreadNSFWY.png", {encoding: "base64"}) %>'
|
||||
]
|
||||
when 'xat-' then [
|
||||
t + '<%= grunt.file.read("src/General/img/favicons/xat-/unreadDead.png", {encoding: "base64"}) %>'
|
||||
t + '<%= grunt.file.read("src/General/img/favicons/xat-/unreadDeadY.png", {encoding: "base64"}) %>'
|
||||
t + '<%= grunt.file.read("src/General/img/favicons/xat-/unreadSFW.png", {encoding: "base64"}) %>'
|
||||
t + '<%= grunt.file.read("src/General/img/favicons/xat-/unreadSFWY.png", {encoding: "base64"}) %>'
|
||||
t + '<%= grunt.file.read("src/General/img/favicons/xat-/unreadNSFW.png", {encoding: "base64"}) %>'
|
||||
t + '<%= grunt.file.read("src/General/img/favicons/xat-/unreadNSFWY.png", {encoding: "base64"}) %>'
|
||||
]
|
||||
when 'Mayhem' then [
|
||||
t + '<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadDead.png", {encoding: "base64"}) %>'
|
||||
t + '<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadDeadY.png", {encoding: "base64"}) %>'
|
||||
t + '<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadSFW.png", {encoding: "base64"}) %>'
|
||||
t + '<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadSFWY.png", {encoding: "base64"}) %>'
|
||||
t + '<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadNSFW.png", {encoding: "base64"}) %>'
|
||||
t + '<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadNSFWY.png", {encoding: "base64"}) %>'
|
||||
]
|
||||
when '4chanJS' then [
|
||||
t + '<%= grunt.file.read("src/General/img/favicons/4chanJS/unreadDead.png", {encoding: "base64"}) %>'
|
||||
t + '<%= grunt.file.read("src/General/img/favicons/4chanJS/unreadDeadY.png", {encoding: "base64"}) %>'
|
||||
t + '<%= grunt.file.read("src/General/img/favicons/4chanJS/unreadSFW.png", {encoding: "base64"}) %>'
|
||||
t + '<%= grunt.file.read("src/General/img/favicons/4chanJS/unreadSFWY.png", {encoding: "base64"}) %>'
|
||||
t + '<%= grunt.file.read("src/General/img/favicons/4chanJS/unreadNSFW.png", {encoding: "base64"}) %>'
|
||||
t + '<%= grunt.file.read("src/General/img/favicons/4chanJS/unreadNSFWY.png", {encoding: "base64"}) %>'
|
||||
]
|
||||
when 'Original' then [
|
||||
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
|
||||
Favicon.unread = Favicon.unreadSFW
|
||||
Favicon.unreadY = Favicon.unreadSFWY
|
||||
@ -54,3 +60,4 @@ Favicon =
|
||||
Favicon.unreadY = Favicon.unreadNSFWY
|
||||
|
||||
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"}) %>'
|
||||
@ -220,14 +220,14 @@ ThreadUpdater =
|
||||
'The thread is not a sticky anymore.'
|
||||
else
|
||||
'The thread is not closed anymore.'
|
||||
new Notification 'info', message, 30
|
||||
new Notice 'info', message, 30
|
||||
$.rm $ ".#{titleLC}Icon", ThreadUpdater.thread.OP.nodes.info
|
||||
return
|
||||
message = if title is 'Sticky'
|
||||
'The thread is now a sticky.'
|
||||
else
|
||||
'The thread is now closed.'
|
||||
new Notification 'info', message, 30
|
||||
new Notice 'info', message, 30
|
||||
icon = $.el 'img',
|
||||
src: "//static.4chan.org/image/#{titleLC}.gif"
|
||||
alt: title
|
||||
|
||||
@ -90,10 +90,25 @@ Unread =
|
||||
|
||||
addPostQuotingYou: (post) ->
|
||||
return unless QR.db
|
||||
for quotelink in post.nodes.quotelinks
|
||||
if QR.db.get Get.postDataFromLink quotelink
|
||||
Unread.postsQuotingYou.push post
|
||||
return
|
||||
for quotelink in post.nodes.quotelinks when QR.db.get Get.postDataFromLink quotelink
|
||||
Unread.postsQuotingYou.push post
|
||||
Unread.openNotification post
|
||||
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) ->
|
||||
if e.detail[404]
|
||||
|
||||
@ -17,7 +17,7 @@ QR =
|
||||
return unless Conf['Header Shortcut'] or Conf['Page Shortcut']
|
||||
|
||||
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'
|
||||
title: 'Quick Reply'
|
||||
href: 'javascript:;'
|
||||
@ -94,7 +94,7 @@ QR =
|
||||
d.activeElement.blur()
|
||||
$.rmClass QR.nodes.el, 'dump'
|
||||
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']
|
||||
$.toggleClass $('.qr-shortcut'), 'disabled'
|
||||
new QR.post true
|
||||
@ -107,8 +107,13 @@ QR =
|
||||
$.addClass QR.nodes.el, 'has-focus'
|
||||
|
||||
focusout: ->
|
||||
<% if (type === 'crx') { %>
|
||||
$.rmClass QR.nodes.el, 'has-focus'
|
||||
|
||||
<% } else { %>
|
||||
$.queueTask ->
|
||||
return if $.x 'ancestor::div[@id="qr"]', d.activeElement
|
||||
$.rmClass QR.nodes.el, 'has-focus'
|
||||
<% } %>
|
||||
hide: ->
|
||||
d.activeElement.blur()
|
||||
$.addClass QR.nodes.el, 'autohide'
|
||||
@ -135,15 +140,33 @@ QR =
|
||||
# Focus the captcha input on captcha error.
|
||||
QR.captcha.nodes.input.focus()
|
||||
if Conf['Captcha Warning Notifications']
|
||||
QR.notifications.push new Notification 'warning', el
|
||||
QR.notify el
|
||||
else
|
||||
$.addClass QR.captcha.nodes.input, 'error'
|
||||
$.on QR.captcha.nodes.input, 'keydown', ->
|
||||
$.rmClass QR.captcha.nodes.input, 'error'
|
||||
else
|
||||
QR.notifications.push new Notification 'warning', el
|
||||
QR.notify el
|
||||
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: []
|
||||
|
||||
cleanNotifications: ->
|
||||
@ -441,10 +464,15 @@ QR =
|
||||
QR.fileInput files
|
||||
|
||||
openFileInput: (e) ->
|
||||
e.preventDefault()
|
||||
return if e.keyCode and not [32, 13].contains e.keyCode
|
||||
if e.shiftKey and not e.keyCode
|
||||
e.stopPropagation()
|
||||
if e.shiftKey and e.type is 'click'
|
||||
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()
|
||||
|
||||
fileInput: (files) ->
|
||||
@ -505,8 +533,8 @@ QR =
|
||||
for elm in $$ '*', el
|
||||
$.on elm, 'blur', QR.focusout
|
||||
$.on elm, 'focus', QR.focusin
|
||||
<% } %>
|
||||
$.on el, 'click', @select.bind @
|
||||
<% } %>
|
||||
$.on el, 'click', @select
|
||||
$.on @nodes.rm, 'click', (e) => e.stopPropagation(); @rm()
|
||||
$.on @nodes.label, 'click', (e) => e.stopPropagation()
|
||||
$.on @nodes.spoiler, 'change', (e) =>
|
||||
@ -570,18 +598,17 @@ QR =
|
||||
lock: (lock=true) ->
|
||||
@isLocked = lock
|
||||
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
|
||||
@nodes.rm.style.visibility =
|
||||
QR.nodes.fileRM.style.visibility = if lock then 'hidden' else ''
|
||||
(if lock then $.off else $.on) QR.nodes.filename.parentNode, 'click', QR.openFileInput
|
||||
@nodes.rm.style.visibility = if lock then 'hidden' else ''
|
||||
(if lock then $.off else $.on) QR.nodes.filename.previousElementSibling, 'click', QR.openFileInput
|
||||
@nodes.spoiler.disabled = lock
|
||||
@nodes.el.draggable = !lock
|
||||
|
||||
unlock: ->
|
||||
@lock false
|
||||
|
||||
select: ->
|
||||
select: =>
|
||||
if QR.selected
|
||||
QR.selected.nodes.el.id = null
|
||||
QR.selected.forceSave()
|
||||
@ -598,7 +625,7 @@ QR =
|
||||
|
||||
load: ->
|
||||
# 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.tripcodeHider.call QR.nodes['name']
|
||||
@ -621,18 +648,27 @@ QR =
|
||||
# during the last 5 seconds of the cooldown.
|
||||
if QR.cooldown.auto and @ is QR.posts[0] and 0 < QR.cooldown.seconds <= 5
|
||||
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: ->
|
||||
return unless @ is QR.selected
|
||||
# Do this in case people use extensions
|
||||
# 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]
|
||||
return
|
||||
|
||||
setFile: (@file) ->
|
||||
@filename = "#{file.name} (#{$.bytesToString file.size})"
|
||||
@nodes.el.title = @filename
|
||||
@filename = file.name
|
||||
@filesize = $.bytesToString file.size
|
||||
@nodes.label.hidden = false if QR.spoiler
|
||||
URL.revokeObjectURL @URL
|
||||
@showFileData()
|
||||
@ -676,18 +712,27 @@ QR =
|
||||
img.src = fileURL
|
||||
|
||||
rmFile: ->
|
||||
return if @isLocked
|
||||
delete @file
|
||||
delete @filename
|
||||
delete @filesize
|
||||
@nodes.el.title = null
|
||||
QR.nodes.fileContainer.title = ''
|
||||
@nodes.el.style.backgroundImage = null
|
||||
@nodes.label.hidden = true if QR.spoiler
|
||||
@showFileData()
|
||||
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: ->
|
||||
if @file
|
||||
QR.nodes.filename.textContent = @filename
|
||||
QR.nodes.filename.title = @filename
|
||||
@updateFilename()
|
||||
QR.nodes.filename.value = @filename
|
||||
QR.nodes.spoiler.checked = @spoiler
|
||||
$.addClass QR.nodes.fileSubmit, 'has-file'
|
||||
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() %>"""
|
||||
|
||||
nodes[key] = $ value, dialog for key, value of {
|
||||
move: '.move'
|
||||
autohide: '#autohide'
|
||||
thread: 'select'
|
||||
threadPar: '#qr-thread-select'
|
||||
close: '.close'
|
||||
form: 'form'
|
||||
dumpButton: '#dump-button'
|
||||
name: '[data-name=name]'
|
||||
email: '[data-name=email]'
|
||||
sub: '[data-name=sub]'
|
||||
com: '[data-name=com]'
|
||||
dumpList: '#dump-list'
|
||||
addPost: '#add-post'
|
||||
charCount: '#char-count'
|
||||
fileSubmit: '#file-n-submit'
|
||||
filename: '#qr-filename'
|
||||
fileRM: '#qr-filerm'
|
||||
fileExtras: '#qr-extras-container'
|
||||
spoiler: '#qr-file-spoiler'
|
||||
spoilerPar: '#qr-spoiler-label'
|
||||
status: '[type=submit]'
|
||||
fileInput: '[type=file]'
|
||||
move: '.move'
|
||||
autohide: '#autohide'
|
||||
thread: 'select'
|
||||
threadPar: '#qr-thread-select'
|
||||
close: '.close'
|
||||
form: 'form'
|
||||
dumpButton: '#dump-button'
|
||||
name: '[data-name=name]'
|
||||
email: '[data-name=email]'
|
||||
sub: '[data-name=sub]'
|
||||
com: '[data-name=com]'
|
||||
dumpList: '#dump-list'
|
||||
addPost: '#add-post'
|
||||
charCount: '#char-count'
|
||||
fileSubmit: '#file-n-submit'
|
||||
filename: '#qr-filename'
|
||||
fileContainer: '#qr-filename-container'
|
||||
fileRM: '#qr-filerm'
|
||||
fileExtras: '#qr-extras-container'
|
||||
spoiler: '#qr-file-spoiler'
|
||||
spoilerPar: '#qr-spoiler-label'
|
||||
status: '[type=submit]'
|
||||
fileInput: '[type=file]'
|
||||
}
|
||||
|
||||
check =
|
||||
@ -939,15 +985,17 @@ QR =
|
||||
$.on elm, 'focus', QR.focusin
|
||||
<% } %>
|
||||
|
||||
$.on dialog, 'focusin', QR.focusin
|
||||
$.on dialog, 'focusout', QR.focusout
|
||||
$.on dialog, 'focusin', QR.focusin
|
||||
$.on dialog, 'focusout', QR.focusout
|
||||
|
||||
$.on nodes.autohide, 'change', QR.toggleHide
|
||||
$.on nodes.close, 'click', QR.close
|
||||
$.on nodes.dumpButton, 'click', -> nodes.el.classList.toggle 'dump'
|
||||
$.on nodes.addPost, 'click', -> new QR.post true
|
||||
$.on nodes.form, 'submit', QR.submit
|
||||
$.on nodes.fileRM, 'click', -> QR.selected.rmFile()
|
||||
$.on nodes.fileExtras, 'click', (e) -> e.stopPropagation()
|
||||
$.on nodes.filename, 'blur', -> $.rmClass @, 'edit'
|
||||
$.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.fileInput, 'change', QR.fileInput
|
||||
|
||||
@ -958,7 +1006,7 @@ QR =
|
||||
$.on nodes[name], 'mouseover', QR.mouseover
|
||||
|
||||
# save selected post's data
|
||||
items = ['name', 'email', 'sub', 'com']
|
||||
items = ['name', 'email', 'sub', 'com', 'filename']
|
||||
i = 0
|
||||
while name = items[i++]
|
||||
$.on nodes[name], 'input', -> QR.selected.save @
|
||||
@ -1169,7 +1217,7 @@ QR =
|
||||
QR.cleanNotifications()
|
||||
|
||||
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
|
||||
|
||||
@ -1221,7 +1269,8 @@ QR =
|
||||
QR.req.abort()
|
||||
delete QR.req
|
||||
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()
|
||||
|
||||
mouseover: (e) ->
|
||||
|
||||
@ -26,6 +26,7 @@ QuoteOP =
|
||||
|
||||
{fullID} = (if @isClone then @context else @).thread
|
||||
# add (OP) to quotes quoting this context's OP.
|
||||
|
||||
return unless quotes.contains fullID
|
||||
i = 0
|
||||
while quotelink = quotelinks[i++]
|
||||
|
||||
@ -26,6 +26,7 @@ QuoteYou =
|
||||
|
||||
for quotelink in @nodes.quotelinks when QR.db.get Get.postDataFromLink quotelink
|
||||
$.add quotelink, $.tn '\u00A0(You)'
|
||||
$.addClass quotelink, 'you'
|
||||
$.addClass @nodes.root, 'quotesYou'
|
||||
return
|
||||
|
||||
@ -36,7 +37,7 @@ QuoteYou =
|
||||
|
||||
unless QuoteYou.lastRead
|
||||
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 if QuoteYou.cb.scroll post
|
||||
else
|
||||
|
||||
@ -18,7 +18,7 @@ Quotify =
|
||||
return
|
||||
|
||||
parseDeadlink: (deadlink) ->
|
||||
if deadlink.parentNode.className is 'prettyprint'
|
||||
if $.hasClass deadlink.parentNode, 'prettyprint'
|
||||
# Don't quotify deadlinks inside code tags,
|
||||
# un-`span` them.
|
||||
# This won't be necessary once 4chan
|
||||
|
||||