Merge branch 'v3'

Conflicts:
	CHANGELOG.md
	Gruntfile.coffee
	LICENSE
	builds/4chan-X.user.js
	builds/crx/manifest.json
	builds/crx/script.js
	latest.js
	package.json
	src/General/Config.coffee
	src/General/Header.coffee
	src/General/Settings.coffee
	src/General/css/font-awesome.css
	src/Images/FappeTyme.coffee
	src/Images/Gallery.coffee
	src/Images/ImageExpand.coffee
	src/Monitoring/Favicon.coffee
	src/Monitoring/ThreadWatcher.coffee
	src/Posting/QuickReply.coffee
This commit is contained in:
Zixaphir 2013-08-24 13:57:27 -07:00
commit d4a3bc8a17
29 changed files with 12574 additions and 1663 deletions

View File

@ -21,17 +21,19 @@
*2013-08-18*
**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.
- Added new option: `Desktop Notifications`
- Implement filename editing
- Replace shortcuts with icons
![filename editing](img/changelog/3.8.0/0.gif)
**seaweedchan**:
- Made shortcut icons optional under the Header submenu, disabled by default, as well as edited some of the icons
- Disabled desktop notifications by default
- Edited filename editing to require a ctrl+click, so otherwise the file input will look and behave the same as before
- Added `.you` class to quotelinks that quote you
- The QR now allows you to edit the filename on the fly:
**Zixaphir**:
- Forked and minimized the Font Awesome CSS used for the shortcut icons
- Some more linkifier improvements
**seaweedchan**:
- Optimizations for the banner and board title code

View File

@ -99,7 +99,6 @@ module.exports = (grunt) ->
commit:
options: shellOptions
command: [
'git checkout <%= pkg.meta.mainBranch %>'
'git commit -am "Release <%= pkg.meta.name %> v<%= pkg.version %>."'
'git tag -a <%= pkg.version %> -m "<%= pkg.meta.name %> v<%= pkg.version %>."'
'git tag -af stable -m "<%= pkg.meta.name %> v<%= pkg.version %>."'
@ -146,40 +145,31 @@ module.exports = (grunt) ->
pkg.type = type;
grunt.config 'pkg', pkg
pkg.sizing = if type is 'crx'
[
pkg.sizing
pkg.filter
pkg.flex
pkg.order
pkg.align
pkg.justify
pkg.transform
] = if type is 'crx' then [
'box-sizing'
else
'-moz-box-sizing'
pkg.filter = if type is 'crx'
'-webkit-filter'
else
'filter'
pkg.transform = if type is 'crx'
'-webkit-transform'
else
'transform'
pkg.flex = if type is 'crx'
'-webkit-flex'
else
'flex'
pkg.order = if type is 'crx'
'-webkit-order'
else
'order'
pkg.align = if type is 'crx'
'-webkit-align'
else
'align'
pkg.justify = if type is 'crx'
'-webkit-justify-content'
else
'-webkit-transform'
] else [
'-moz-box-sizing'
'filter'
'flex'
'order'
'align'
'justify-content'
'transform'
]
grunt.log.ok 'pkg.type = %s', type

View File

@ -1,5 +1,5 @@
/*
* appchan x - Version 2.3.6 - 2013-08-22
* appchan x - Version 2.3.6 - 2013-08-24
*
* Licensed under the MIT license.
* https://github.com/zixaphir/appchan-x/blob/master/LICENSE

View File

@ -1,6 +1,6 @@
// ==UserScript==
// @name 4chan X
// @version 1.2.32
// @version 1.2.35
// @minGMVer 1.13
// @minFFVer 22
// @namespace 4chan-X

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 690 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

View File

@ -14,7 +14,7 @@ Config =
'Add button to hide 4chan announcements.'
]
'Desktop Notifications': [
true
false
'Enables desktop notifications across various <%= meta.name %> features.'
]
'404 Redirect': [
@ -129,7 +129,7 @@ Config =
]
'Gallery': [
true
'Adds a cute gallery.'
'Adds a simple and cute image gallery.'
]
'Sauce': [
true
@ -161,7 +161,7 @@ Config =
]
'Werk Tyme': [
false
'Hide images when toggled.'
'Hide all post images when toggled.'
]
'Menu':
@ -379,6 +379,12 @@ Config =
false
'Advance to next post when contracting an expanded image.'
]
gallery:
# Gallery mostly gets its config from imageExpansion
'Hide thumbnails': [
false
]
style:

View File

@ -305,7 +305,8 @@ Header =
el = $.el 'span',
innerHTML: """
Desktop notification permissions are not granted:<br>
Desktop notification permissions are not granted.
[<a href='https://github.com/MayhemYDG/4chan-x/wiki/FAQ#desktop-notifications' target=_blank>FAQ</a>]<br>
<button>Authorize</button> or <button>Disable</button>
"""
[authorize, disable] = $$ 'button', el

View File

@ -43,7 +43,7 @@ Main =
# Track resolution of this bug.
Main.logError
message: 'Chrome Storage API bug'
error: new Error chrome.runtime.lastError.message or 'no lastError.message'
error: new Error '~'
<% } %>
Main.initFeatures()
@ -238,6 +238,7 @@ Main =
localStorage.getItem '4chan-settings'
catch err
new Notice 'warning', 'Cookies need to be enabled on 4chan for <%= meta.name %> to properly function.', 30
Main.disableReports = true
$.event '4chanXInitFinished'

View File

@ -40,4 +40,4 @@
}
#shortcuts .icon {
color: rgb(130,130,130) !important;
}
}

View File

@ -2239,17 +2239,16 @@ article li {
bottom: 0;
left: 0;
right: 0;
z-index: 30;
z-index: 20;
display: <%= flex %>;
<%= flex %>-direction: row;
background: linear-gradient(rgba(0,0,0,1), rgba(0,0,0,0.5));
background: rgba(0,0,0,0.7);
}
.gal-viewport {
display: <%= flex %>;
<%= align %>-items: stretch;
<%= flex %>-direction: row;
<%= flex %>: 1 1 auto;
position: relative;
}
.gal-thumbnails {
<%= flex %>: 0 0 150px;
@ -2258,56 +2257,64 @@ article li {
<%= flex %>-direction: column;
<%= align %>-items: stretch;
text-align: center;
background: rgba(0,0,0,.5);
border-left: 1px solid #222;
}
.gal-thumbnails img {
.hide-thumbnails .gal-thumbnails {
display: none;
}
.gal-thumb img {
max-width: 125px;
max-height: 125px;
height: auto;
width: auto;
}
.gal-thumbnails a {
.gal-thumb {
<%= flex %>: 0 0 auto;
padding: 3px;
line-height: 0;
transition: background .3s linear;
transition: background .2s linear;
}
.gal-highlight {
background: rgb(190, 190, 255);
background: rgba(0, 190, 255,.8);
}
.gal-prev {
order: 0;
border-right: 1px solid #222;
}
.gal-next {
order: 2;
border-left: 1px solid #222;
}
.gal-prev,
.gal-next {
<%= flex %>: 0 0 20px;
background-color: #000;
position: relative;
cursor: pointer;
opacity: 0.5;
opacity: 0.7;
background-color: rgba(0, 0, 0, 0.3);
}
.gal-prev:hover,
.gal-next:hover {
opacity: 0.9;
opacity: 1;
}
.gal-prev::after,
.gal-next::after {
position: absolute;
top: 50%;
right: 2px;
top: 48.6%;
<%= transform %>: translateY(-50%)
display: inline-block;
border-top: 15px solid transparent;
border-bottom: 15px solid transparent;
border-top: 11px solid transparent;
border-bottom: 11px solid transparent;
content: "";
}
.gal-prev::after {
border-right: 15px solid #fff;
border-right: 12px solid #fff;
right: 5px;
}
.gal-next::after {
border-left: 15px solid #fff;
border-left: 12px solid #fff;
right: 3px;
}
.gal-image {
order: 1;
@ -2318,37 +2325,53 @@ article li {
overflow: auto;
/* Flex > Non-Flex child max-width and overflow fix (Firefox only?) */
width: 1%;
padding-top: 2.4em;
}
.gal-image a {
margin: auto;
line-height: 0;
}
.gal-image img {
.fit-width .gal-image img {
max-width: 100%;
}
.gal-info {
position: absolute;
top: 0;
right: 40px;
left: 20px;
height: 2.4em;
background-color: rgba(0,0,0,0.3);
text-align: center;
text-overflow: ellipsis;
overflow: hidden;
padding: 0 20px;
.fit-height .gal-image img {
max-height: calc(100vh - 25px);
}
.gal-close {
.gal-buttons {
font-size: 2em;
float: right;
padding-right: 8px;
margin-right: 10px;
top: 5px;
color: #ffffff;
text-shadow: 0px 0px 1px #000000;
}
.gal-buttons,
.gal-name,
.gal-count {
position: fixed;
right: 178px;
}
.hide-thumbnails .gal-buttons,
.hide-thumbnails .gal-count,
.hide-thumbnails .gal-name {
right: 28px;
}
.gal-name {
font-size: 2em;
color: #ddd;
bottom: 5px;
background: rgba(0,0,0,0.6) !important;
border-radius: 3px;
padding: 1px 5px 2px 5px;
text-decoration: none !important;
color: white !important;
}
.gal-name:hover,
.gal-close:hover {
color: rgb(95, 95, 101) !important;
}
.gal-count {
color: #ddd;
bottom: 26px;
background: rgba(0,0,0,0.6) !important;
border-radius: 3px;
padding: 1px 5px 2px 5px;
color: #ffffff !important;
}
/* Catalog */
#content .navLinks,

File diff suppressed because it is too large Load Diff

View File

@ -9,9 +9,9 @@
</div>
<form>
<div class=persona>
<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>
<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>
</div>
<div class=textarea>
<textarea data-name=com placeholder=Comment class=field tabindex=40></textarea>

View File

@ -74,18 +74,15 @@ class DataBoard
ajaxClean: (boardID) ->
$.cache "//api.4chan.org/#{boardID}/threads.json", (e) =>
if e.target.status is 404
# Deleted board.
@delete boardID
else if e.target.status is 200
board = @data.boards[boardID]
threads = {}
for page in JSON.parse e.target.response
for thread in page.threads
if thread.no of board
threads[thread.no] = board[thread.no]
@data.boards[boardID] = threads
@deleteIfEmpty {boardID}
return if e.target.status isnt 200
board = @data.boards[boardID]
threads = {}
for page in JSON.parse e.target.response
for thread in page.threads
if thread.no of board
threads[thread.no] = board[thread.no]
@data.boards[boardID] = threads
@deleteIfEmpty {boardID}
@save()
onSync: (data) =>

View File

@ -22,7 +22,7 @@ Polyfill =
# DataUrl to Binary code from Aeosynth's 4chan X repo
l = data.length
ui8a = new Uint8Array l
for i in [0...l]
for i in [0...l] by 1
ui8a[i] = data.charCodeAt i
cb new Blob [ui8a], type: 'image/png'
visibility: ->

View File

@ -62,6 +62,8 @@ class Post
@kill() if that.isArchived
parseComment: ->
# Merge text nodes and remove empty ones.
@nodes.comment.normalize()
# Get the comment's text.
# <br> -> \n
# Remove:

View File

@ -12,7 +12,7 @@ FappeTyme =
$.on el, 'click', FappeTyme.cb.fappe
Header.addShortcut el, true
if Conf['Werk Tyme']
el = $.el 'a',
href: 'javascript:;'
@ -24,7 +24,6 @@ FappeTyme =
$.on el, 'click', FappeTyme.cb.werk
Header.addShortcut el, true
Post::callbacks.push
name: 'Fappe Tyme'
@ -34,9 +33,8 @@ FappeTyme =
return if @file
$.addClass @nodes.root, "noFile"
cb:
cb:
fappe: ->
$.toggleClass doc, 'fappeTyme'
werk: ->
$.toggleClass doc, 'werkTyme'

View File

@ -1,7 +1,7 @@
Gallery =
init: ->
return if g.VIEW is 'catalog' or g.BOARD is 'f' or !Conf['Gallery']
el = $.el 'a',
href: 'javascript:;'
id: 'appchan-gal'
@ -19,23 +19,29 @@ Gallery =
node: ->
return unless @file?.isImage
if Gallery.el
if Gallery.nodes
Gallery.generateThumb $ '.file', @nodes.root
Gallery.total.textContent = Gallery.images.length
Gallery.nodes.total.textContent = Gallery.images.length
unless Conf['Image Expansion']
$.on @file.thumb.parentNode, 'click', Gallery.cb.image
build: (image) ->
Gallery.el = dialog = $.el 'div',
Gallery.images = []
nodes = Gallery.nodes = {}
nodes.el = dialog = $.el 'div',
id: 'a-gallery'
innerHTML: """
<div class=gal-viewport>
<div class=gal-info>
<span class=gal-buttons>
<a class="menu-button" href="javascript:;"><i class=drop-marker></i></a>
<a href=javascript:; class=gal-close></a>
<a class=gal-name></a>
<span class=gal-count>(<span class='count'></span> / <span class='total'></span>)</a>
</div>
</span>
<a class=gal-name target="_blank"></a>
<span class=gal-count>
<span class='count'></span> / <span class='total'></span>
</span>
<div class=gal-prev></div>
<div class=gal-image>
<a href=javascript:;><img></a>
@ -44,7 +50,8 @@ Gallery =
</div>
<div class=gal-thumbnails></div>
"""
Gallery[key] = $ value, dialog for key, value of {
nodes[key] = $ value, dialog for key, value of {
frame: '.gal-image'
name: '.gal-name'
count: '.count'
@ -54,26 +61,41 @@ Gallery =
current: '.gal-image img'
}
Gallery.images = []
menuButton = $ '.menu-button', dialog
nodes.menu = new UI.Menu 'gallery'
$.on Gallery.frame, 'click', Gallery.cb.blank
$.on Gallery.current, 'click', Gallery.cb.download
$.on Gallery.next, 'click', Gallery.cb.next
$.on ($ '.gal-prev', dialog), 'click', Gallery.cb.prev
$.on ($ '.gal-next', dialog), 'click', Gallery.cb.next
$.on ($ '.gal-close', dialog), 'click', Gallery.cb.close
{cb} = Gallery
$.on nodes.frame, 'click', cb.blank
$.on nodes.current, 'click', cb.download
$.on nodes.next, 'click', cb.next
$.on ($ '.gal-prev', dialog), 'click', cb.prev
$.on ($ '.gal-next', dialog), 'click', cb.next
$.on ($ '.gal-close', dialog), 'click', cb.close
$.on d, 'keydown', Gallery.cb.keybinds
$.on menuButton, 'click', (e) ->
nodes.menu.toggle e, @, g
{createSubEntry} = Gallery.menu
for name in ['Fit width', 'Fit height', 'Hide thumbnails']
{el} = createSubEntry name
$.event 'AddMenuEntry',
type: 'gallery'
el: el
order: 0
$.on d, 'keydown', cb.keybinds
$.off d, 'keydown', Keybinds.keydown
i = 0
files = $$ '.post .file'
while file = files[i++]
continue if $ '.fileDeletedRes, .fileDeleted', file
Gallery.generateThumb file
$.add d.body, dialog
Gallery.thumbs.scrollTop = 0
Gallery.current.parentElement.scrollTop = 0
nodes.thumbs.scrollTop = 0
nodes.current.parentElement.scrollTop = 0
Gallery.cb.open.call if image
$ "[href='#{image.href.replace /https?:/, ''}']", Gallery.thumbs
@ -81,7 +103,7 @@ Gallery =
Gallery.images[0]
d.body.style.overflow = 'hidden'
Gallery.total.textContent = --i
nodes.total.textContent = --i
generateThumb: (file) ->
title = ($ '.fileText a', file).textContent
@ -89,7 +111,7 @@ Gallery =
if double = $ 'img + img', thumb
$.rm double
thumb.className = 'a-thumb'
thumb.className = 'gal-thumb'
thumb.title = title
thumb.dataset.id = Gallery.images.length
thumb.firstElementChild.style.cssText = ''
@ -97,7 +119,7 @@ Gallery =
$.on thumb, 'click', Gallery.cb.open
Gallery.images.push thumb
$.add Gallery.thumbs, thumb
$.add Gallery.nodes.thumbs, thumb
cb:
keybinds: (e) ->
@ -115,38 +137,70 @@ Gallery =
e.stopPropagation()
e.preventDefault()
cb()
open: (e) ->
e.preventDefault() if e
{nodes} = Gallery
{name} = nodes
$.rmClass el, 'gal-highlight' if el = $ '.gal-highlight', Gallery.thumbs
$.rmClass el, 'gal-highlight' if el = $ '.gal-highlight', nodes.thumbs
$.addClass @, 'gal-highlight'
img = $.el 'img',
src: Gallery.name.href = @href
title: Gallery.name.download = Gallery.name.textContent = @title
src: name.href = @href
title: name.download = name.textContent = @title
img.dataset.id = @dataset.id
$.replace Gallery.current, img
Gallery.count.textContent = +@dataset.id + 1
Gallery.current = img
Gallery.frame.scrollTop = 0
Gallery.current.focus()
$.replace nodes.current, img
nodes.count.textContent = +@dataset.id + 1
nodes.current = img
nodes.frame.scrollTop = 0
nodes.next.focus()
image: (e) ->
e.preventDefault()
e.stopPropagation()
Gallery.build @
prev: -> Gallery.cb.open.call Gallery.images[+Gallery.current.dataset.id - 1]
next: -> Gallery.cb.open.call Gallery.images[+Gallery.current.dataset.id + 1]
toggle: -> (if Gallery.el then Gallery.cb.close else Gallery.build)()
prev: -> Gallery.cb.open.call Gallery.images[+Gallery.nodes.current.dataset.id - 1]
next: -> Gallery.cb.open.call Gallery.images[+Gallery.nodes.current.dataset.id + 1]
toggle: -> (if Gallery.nodes then Gallery.cb.close else Gallery.build)()
blank: (e) -> Gallery.cb.close() if e.target is @
close: ->
$.rm Gallery.el
delete Gallery.el
$.rm Gallery.nodes.el
delete Gallery.nodes
d.body.style.overflow = ''
$.off d, 'keydown', Gallery.cb.keybinds
$.on d, 'keydown', Keybinds.keydown
$.on d, 'keydown', Keybinds.keydown
menu:
init: ->
return if g.VIEW is 'catalog' or !Conf['Gallery'] or Conf['Image Expansion']
el = $.el 'span',
textContent: 'Gallery'
className: 'gallery-link'
{createSubEntry} = Gallery.menu
subEntries = []
for name in ['Fit width', 'Fit height', 'Hide thumbnails']
subEntries.push createSubEntry name
$.event 'AddMenuEntry',
type: 'header'
el: el
order: 105
subEntries: subEntries
createSubEntry: (name) ->
label = $.el 'label',
innerHTML: "<input type=checkbox name='#{name}'> #{name}"
input = label.firstElementChild
# Reusing ImageExpand here because this code doesn't need any auditing to work for what we need
$.on input, 'change', ImageExpand.cb.setFitness
input.checked = Conf[name]
$.event 'change', null, input
$.on input, 'change', $.cb.checked
el: label

View File

@ -192,6 +192,3 @@ ImageExpand =
$.event 'change', null, input
$.on input, 'change', $.cb.checked
el: label
menuToggle: (e) ->
ImageExpand.opmenu.toggle e, @, g

View File

@ -81,6 +81,7 @@ Linkify =
for link in links.reverse()
@nodes.links.push Linkify.makeLink link, @
link.detach()
return unless Conf['Embedding'] or Conf['Link Title']
@ -398,4 +399,5 @@ Linkify =
src: "//www.youtube.com/embed/#{a.dataset.uid}#{if a.dataset.option then '#' + a.dataset.option else ''}?wmode=opaque"
title:
api: (uid) -> "https://gdata.youtube.com/feeds/api/videos/#{uid}?alt=json&fields=title/text(),yt:noembed,app:control/yt:state/@reasonCode"
text: (data) -> data.entry.title.$t
text: (data) -> data.entry.title.$t

View File

@ -1,7 +1,6 @@
IDColor =
init: ->
return if g.VIEW is 'catalog' or !Conf['Color User IDs']
return if g.VIEW is 'catalog' or not Conf['Color User IDs']
@ids = {}
Post::callbacks.push
@ -9,30 +8,34 @@ IDColor =
cb: @node
node: ->
return if @isClone or not str = @info.uniqueID
uid = $ '.hand', @nodes.uniqueID
return unless uid and uid.nodeName is 'SPAN'
uid.style.cssText = IDColor.css IDColor.ids[str] or IDColor.compute str
return if @isClone or not uid = @info.uniqueID
span = $ '.hand', @nodes.uniqueID
return unless span and span.nodeName is 'SPAN'
rgb = IDColor.compute uid
{style} = span
style.color = rgb[3]
style.backgroundColor = "rgb(#{rgb[0]},#{rgb[1]},#{rgb[2]})"
$.addClass span, 'painted'
span.title = 'Highlight posts by this ID'
compute: (str) ->
hash = IDColor.hash str
compute: (uid) ->
return IDColor.ids[uid] if IDColor.ids[uid]
hash = IDColor.hash uid
rgb = [
(hash >> 24) & 0xFF
(hash >> 16) & 0xFF
(hash >> 8) & 0xFF
(hash >> 8) & 0xFF
]
rgb[3] = if (rgb[0] * 0.299 + rgb[1] * 0.587 + rgb[2] * 0.114) > 125
'#000'
else
'#fff'
@ids[uid] = rgb
rgb[3] = ((rgb[0] * 0.299) + (rgb[1] * 0.587) + (rgb[2] * 0.114)) > 125
@ids[str] = rgb
rgb
css: (rgb) -> "background-color: rgb(#{rgb[0]},#{rgb[1]},#{rgb[2]}); color: #{if rgb[3] then "#000" else "#fff"}; border-radius: 3px; padding: 0px 2px;"
hash: (str) ->
hash: (uid) ->
msg = 0
i = 0
while i < 8
msg = ((msg << 5) - msg) + str.charCodeAt i++
msg
msg = (msg << 5) - msg + uid.charCodeAt i++
msg

View File

@ -60,4 +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"}) %>'
logo: 'data:image/png;base64,<%= grunt.file.read("src/General/img/icon128.png", {encoding: "base64"}) %>'

View File

@ -167,7 +167,9 @@ ThreadWatcher =
for threadID, thread of g.BOARD.threads
toggler = $ '.watch-thread-link', thread.OP.nodes.post
watched = ThreadWatcher.db.get {boardID: thread.board.ID, threadID}
$[if watched then 'addClass' else 'rmClass'] toggler, 'watched'
helper = if watched then ['addClass', 'Unwatch'] else ['rmClass', 'Watch']
$[helper[0]] toggler, 'watched'
toggler.title = "#{helper[1]} Thread"
for refresher in ThreadWatcher.menu.refreshers
refresher()

View File

@ -1,6 +1,6 @@
Unread =
init: ->
return if g.VIEW isnt 'thread' or !Conf['Unread Count'] and !Conf['Unread Favicon']
return if g.VIEW isnt 'thread' or !Conf['Unread Count'] and !Conf['Unread Favicon'] and !Conf['Desktop Notifications']
@db = new DataBoard 'lastReadPosts', @sync
@hr = $.el 'hr',
@ -30,14 +30,14 @@ Unread =
for ID, post of Unread.thread.posts
posts.push post if post.isReply
Unread.addPosts posts
Unread.scroll() if Conf['Scroll to Last Read Post']
Unread.scroll()
scroll: ->
return unless Conf['Scroll to Last Read Post']
# Let the header's onload callback handle it.
return if (hash = location.hash.match /\d+/) and hash[0] of Unread.thread.posts
if Unread.posts.length
if post = Unread.posts[0]
# Scroll to a non-hidden, non-OP post that's before the first unread post.
post = Unread.posts[0]
while root = $.x 'preceding-sibling::div[contains(@class,"replyContainer")][1]', post.nodes.root
break unless (post = Get.postFromRoot root).isHidden
return unless root
@ -48,11 +48,8 @@ Unread =
{root} = Unread.thread.posts[posts[posts.length - 1]].nodes
onload = -> Header.scrollToPost root if checkPosition root
checkPosition = (target) ->
# Don't scroll to the target if
# - it's visible.
# - we've scrolled past it.
{top, height} = target.getBoundingClientRect()
top + height - doc.clientHeight > 0
# Scroll to the target unless we scrolled past it.
target.getBoundingClientRect().bottom > doc.clientHeight
# Prevent the browser to scroll back to
# the previous scroll location on page load.
$.on window, 'load', onload
@ -66,7 +63,7 @@ Unread =
Unread.lastReadPost = lastReadPost
Unread.readArray Unread.posts
Unread.readArray Unread.postsQuotingYou
Unread.setLine()
Unread.setLine() if Conf['Unread Line']
Unread.update()
addPosts: (posts) ->
@ -93,7 +90,8 @@ Unread =
for quotelink in post.nodes.quotelinks when QR.db.get Get.postDataFromLink quotelink
Unread.postsQuotingYou.push post
Unread.openNotification post
return
return
openNotification: (post) ->
return unless Header.areNotificationsEnabled
name = if Conf['Anonymize']
@ -108,7 +106,7 @@ Unread =
window.focus()
setTimeout ->
notif.close()
, 5 * $.SECOND
, 7 * $.SECOND
onUpdate: (e) ->
if e.detail[404]
@ -138,8 +136,7 @@ Unread =
i = 0
while post = posts[i]
{bottom} = post.nodes.root.getBoundingClientRect()
if bottom < height # post is completely read
if post.nodes.root.getBoundingClientRect().bottom < height # post is not completely read
{ID} = post
if Conf['Mark Quotes of You']
if post.info.yours
@ -152,9 +149,8 @@ Unread =
break
i++
unless Conf['Quote Threading']
if i
posts.splice 0, i
if i and !Conf['Quote Threading']
posts.splice 0, i
return unless ID
@ -172,12 +168,9 @@ Unread =
setLine: (force) ->
return unless d.hidden or force is true
if post = Unread.posts[0]
{root} = post.nodes
if root isnt $ '.thread > .replyContainer', root.parentNode # not the first reply
$.before root, Unread.hr
else
$.rm Unread.hr
return $.rm Unread.hr unless post = Unread.posts[0]
if $.x 'preceding-sibling::div[contains(@class,"replyContainer")]', post.nodes.root # not the first reply
$.before post.nodes.root, Unread.hr
update: <% if (type === 'crx') { %>(dontrepeat) <% } %>->
count = Unread.posts.length
@ -189,18 +182,18 @@ Unread =
# crbug.com/124381
# Call it one second later,
# but don't display outdated unread count.
unless dontrepeat
setTimeout ->
d.title = ''
Unread.update true
, $.SECOND
return if dontrepeat
setTimeout ->
d.title = ''
Unread.update true
, $.SECOND
<% } %>
return unless Conf['Unread Favicon']
Favicon.el.href =
if g.DEAD
if Unread.postsQuotingYou.length
if Unread.postsQuotingYou[0]
Favicon.unreadDeadY
else if count
Favicon.unreadDead
@ -208,7 +201,7 @@ Unread =
Favicon.dead
else
if count
if Unread.postsQuotingYou.length
if Unread.postsQuotingYou[0]
Favicon.unreadY
else
Favicon.unread

View File

@ -71,7 +71,7 @@ QR =
persist: ->
return unless QR.postingIsEnabled
QR.open()
QR.hide() if Conf['Auto Hide QR']
QR.hide() if Conf['Auto-Hide QR'] or g.VIEW is 'catalog'
open: ->
if QR.nodes
@ -165,7 +165,7 @@ QR =
setTimeout ->
notif.onclose = null
notif.close()
, 5 * $.SECOND
, 7 * $.SECOND
<% } %>
notifications: []
@ -449,20 +449,52 @@ QR =
return unless e.dataTransfer.files.length
e.preventDefault()
QR.open()
QR.fileInput e.dataTransfer.files
QR.handleFiles e.dataTransfer.files
$.addClass QR.nodes.el, 'dump'
paste: (e) ->
files = []
for item in e.clipboardData.items
if item.kind is 'file'
blob = item.getAsFile()
blob.name = 'file'
blob.name += '.' + blob.type.split('/')[1] if blob.type
files.push blob
for item in e.clipboardData.items when item.kind is 'file'
blob = item.getAsFile()
blob.name = 'file'
blob.name += '.' + blob.type.split('/')[1] if blob.type
files.push blob
return unless files.length
QR.open()
QR.fileInput files
QR.handleFiles files
$.addClass QR.nodes.el, 'dump'
handleFiles: (files) ->
if @ isnt QR # file input
files = [@files...]
@value = null
return unless files.length
max = QR.nodes.fileInput.max
isSingle = files.length is 1
QR.cleanNotifications()
for file in files
QR.handleFile file, isSingle, max
$.addClass QR.nodes.el, 'dump' unless isSingle
handleFile: (file, isSingle, max) ->
if file.size > max
QR.error "#{file.name}: File too large (file: #{$.bytesToString file.size}, max: #{$.bytesToString max})."
return
else unless QR.mimeTypes.contains file.type
unless /^text/.test file.type
QR.error "#{file.name}: Unsupported file type."
return
if isSingle
post = QR.selected
else if (post = QR.posts[QR.posts.length - 1]).com
post = new QR.post()
post.pasteText file
return
if isSingle
post = QR.selected
else if (post = QR.posts[QR.posts.length - 1]).file
post = new QR.post()
post.setFile file
openFileInput: (e) ->
e.stopPropagation()
@ -476,42 +508,6 @@ QR =
e.preventDefault()
QR.nodes.fileInput.click()
fileInput: (files) ->
if files instanceof Event # file input, revert to "files instanceof Event" after a Pale Moon update
files = [@files...]
QR.nodes.fileInput.value = null # Don't hold the files from being modified on windows
{length} = files
return unless length
max = QR.nodes.fileInput.max
QR.cleanNotifications()
# Set or change current post's file.
if length is 1
file = files[0]
if /^text/.test file.type
QR.selected.pasteText file
else if file.size > max
QR.error "File too large (file: #{$.bytesToString file.size}, max: #{$.bytesToString max})."
else unless QR.mimeTypes.contains file.type
QR.error 'Unsupported file type.'
else
QR.selected.setFile file
return
# Create new posts with these files.
for file in files
if /^text/.test file.type
if (post = QR.posts[QR.posts.length - 1]).com
post = new QR.post()
post.pasteText file
else if file.size > max
QR.error "#{file.name}: File too large (file: #{$.bytesToString file.size}, max: #{$.bytesToString max})."
else unless QR.mimeTypes.contains file.type
QR.error "#{file.name}: Unsupported file type."
else
if (post = QR.posts[QR.posts.length - 1]).file
post = new QR.post()
post.setFile file
$.addClass QR.nodes.el, 'dump'
posts: []
post: class
@ -752,7 +748,9 @@ QR =
@nodes.span.textContent = @com
reader.readAsText file
dragStart: -> $.addClass @, 'drag'
dragStart: (e) ->
e.dataTransfer.setDragImage @, e.layerX, e.layerY
$.addClass @, 'drag'
dragEnd: -> $.rmClass @, 'drag'
dragEnter: -> $.addClass @, 'over'
dragLeave: -> $.rmClass @, 'over'
@ -998,7 +996,7 @@ QR =
$.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
$.on nodes.fileInput, 'change', QR.handleFiles
# mouseover descriptions
items = ['spoilerPar', 'dumpButton', 'fileRM']
@ -1243,7 +1241,19 @@ QR =
}
# Enable auto-posting if we have stuff to post, disable it otherwise.
QR.cooldown.auto = QR.posts.length > 1 and isReply
postsCount = QR.posts.length
QR.cooldown.auto = postsCount > 1 and isReply
if QR.cooldown.auto and QR.captcha.isEnabled and (captchasCount = QR.captcha.captchas.length) < 3 and captchasCount < postsCount
notif = new Notification 'Quick reply warning',
body: "You are running low on cached captchas. Cache count: #{captchasCount}."
icon: Favicon.logo
notif.onclick = ->
QR.open()
QR.captcha.nodes.input.focus()
window.focus()
setTimeout ->
notif.close()
, 7 * $.SECOND
unless Conf['Persistent QR'] or QR.cooldown.auto
QR.close()