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* *2013-08-18*
**MayhemYDG**: **MayhemYDG**:
- **New feature**: `Desktop Notifications` - Added new option: `Desktop Notifications`
- Enabled by default, but you will have to grant your browser permissions to display them or disable them altogether:<br> - Implement filename editing
![authorize or disable](img/changelog/3.9.0/0.png) - Replace shortcuts with icons
- 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) **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**: **seaweedchan**:
- Optimizations for the banner and board title code - Optimizations for the banner and board title code

View File

@ -99,7 +99,6 @@ module.exports = (grunt) ->
commit: commit:
options: shellOptions options: shellOptions
command: [ command: [
'git checkout <%= pkg.meta.mainBranch %>'
'git commit -am "Release <%= pkg.meta.name %> v<%= pkg.version %>."' 'git commit -am "Release <%= pkg.meta.name %> v<%= pkg.version %>."'
'git tag -a <%= pkg.version %> -m "<%= 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 %>."' 'git tag -af stable -m "<%= pkg.meta.name %> v<%= pkg.version %>."'
@ -146,40 +145,31 @@ module.exports = (grunt) ->
pkg.type = type; pkg.type = type;
grunt.config 'pkg', pkg 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' 'box-sizing'
else
'-moz-box-sizing'
pkg.filter = if type is 'crx'
'-webkit-filter' '-webkit-filter'
else
'filter'
pkg.transform = if type is 'crx'
'-webkit-transform'
else
'transform'
pkg.flex = if type is 'crx'
'-webkit-flex' '-webkit-flex'
else
'flex'
pkg.order = if type is 'crx'
'-webkit-order' '-webkit-order'
else
'order'
pkg.align = if type is 'crx'
'-webkit-align' '-webkit-align'
else
'align'
pkg.justify = if type is 'crx'
'-webkit-justify-content' '-webkit-justify-content'
else '-webkit-transform'
] else [
'-moz-box-sizing'
'filter'
'flex'
'order'
'align'
'justify-content' 'justify-content'
'transform'
]
grunt.log.ok 'pkg.type = %s', type 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. * Licensed under the MIT license.
* https://github.com/zixaphir/appchan-x/blob/master/LICENSE * https://github.com/zixaphir/appchan-x/blob/master/LICENSE

View File

@ -1,6 +1,6 @@
// ==UserScript== // ==UserScript==
// @name 4chan X // @name 4chan X
// @version 1.2.32 // @version 1.2.35
// @minGMVer 1.13 // @minGMVer 1.13
// @minFFVer 22 // @minFFVer 22
// @namespace 4chan-X // @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.' 'Add button to hide 4chan announcements.'
] ]
'Desktop Notifications': [ 'Desktop Notifications': [
true false
'Enables desktop notifications across various <%= meta.name %> features.' 'Enables desktop notifications across various <%= meta.name %> features.'
] ]
'404 Redirect': [ '404 Redirect': [
@ -129,7 +129,7 @@ Config =
] ]
'Gallery': [ 'Gallery': [
true true
'Adds a cute gallery.' 'Adds a simple and cute image gallery.'
] ]
'Sauce': [ 'Sauce': [
true true
@ -161,7 +161,7 @@ Config =
] ]
'Werk Tyme': [ 'Werk Tyme': [
false false
'Hide images when toggled.' 'Hide all post images when toggled.'
] ]
'Menu': 'Menu':
@ -380,6 +380,12 @@ Config =
'Advance to next post when contracting an expanded image.' 'Advance to next post when contracting an expanded image.'
] ]
gallery:
# Gallery mostly gets its config from imageExpansion
'Hide thumbnails': [
false
]
style: style:
# Style Options are either booleans, select options, or text, depending on the value of optionName[2]. # Style Options are either booleans, select options, or text, depending on the value of optionName[2].

View File

@ -305,7 +305,8 @@ Header =
el = $.el 'span', el = $.el 'span',
innerHTML: """ 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> <button>Authorize</button> or <button>Disable</button>
""" """
[authorize, disable] = $$ 'button', el [authorize, disable] = $$ 'button', el

View File

@ -43,7 +43,7 @@ Main =
# Track resolution of this bug. # Track resolution of this bug.
Main.logError Main.logError
message: 'Chrome Storage API bug' message: 'Chrome Storage API bug'
error: new Error chrome.runtime.lastError.message or 'no lastError.message' error: new Error '~'
<% } %> <% } %>
Main.initFeatures() Main.initFeatures()
@ -238,6 +238,7 @@ Main =
localStorage.getItem '4chan-settings' localStorage.getItem '4chan-settings'
catch err catch err
new Notice '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
Main.disableReports = true
$.event '4chanXInitFinished' $.event '4chanXInitFinished'

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -9,9 +9,9 @@
</div> </div>
<form> <form>
<div class=persona> <div class=persona>
<input data-name=name list="list-name" placeholder=Name class=field size=1 tabindex=10> <input name=name data-name=name list="list-name" placeholder=Name class=field size=1 tabindex=10>
<input data-name=email list="list-email" placeholder=E-mail class=field size=1 tabindex=20> <input name=email data-name=email list="list-email" placeholder=E-mail class=field size=1 tabindex=20>
<input data-name=sub list="list-sub" placeholder=Subject class=field size=1 tabindex=30> <input name=sub data-name=sub list="list-sub" placeholder=Subject class=field size=1 tabindex=30>
</div> </div>
<div class=textarea> <div class=textarea>
<textarea data-name=com placeholder=Comment class=field tabindex=40></textarea> <textarea data-name=com placeholder=Comment class=field tabindex=40></textarea>

View File

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

View File

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

View File

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

View File

@ -25,7 +25,6 @@ FappeTyme =
Header.addShortcut el, true Header.addShortcut el, true
Post::callbacks.push Post::callbacks.push
name: 'Fappe Tyme' name: 'Fappe Tyme'
cb: @node cb: @node
@ -39,4 +38,3 @@ FappeTyme =
$.toggleClass doc, 'fappeTyme' $.toggleClass doc, 'fappeTyme'
werk: -> werk: ->
$.toggleClass doc, 'werkTyme' $.toggleClass doc, 'werkTyme'

View File

@ -19,23 +19,29 @@ Gallery =
node: -> node: ->
return unless @file?.isImage return unless @file?.isImage
if Gallery.el if Gallery.nodes
Gallery.generateThumb $ '.file', @nodes.root Gallery.generateThumb $ '.file', @nodes.root
Gallery.total.textContent = Gallery.images.length Gallery.nodes.total.textContent = Gallery.images.length
unless Conf['Image Expansion'] unless Conf['Image Expansion']
$.on @file.thumb.parentNode, 'click', Gallery.cb.image $.on @file.thumb.parentNode, 'click', Gallery.cb.image
build: (image) -> build: (image) ->
Gallery.el = dialog = $.el 'div', Gallery.images = []
nodes = Gallery.nodes = {}
nodes.el = dialog = $.el 'div',
id: 'a-gallery' id: 'a-gallery'
innerHTML: """ innerHTML: """
<div class=gal-viewport> <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 href=javascript:; class=gal-close></a>
<a class=gal-name></a> </span>
<span class=gal-count>(<span class='count'></span> / <span class='total'></span>)</a> <a class=gal-name target="_blank"></a>
</div> <span class=gal-count>
<span class='count'></span> / <span class='total'></span>
</span>
<div class=gal-prev></div> <div class=gal-prev></div>
<div class=gal-image> <div class=gal-image>
<a href=javascript:;><img></a> <a href=javascript:;><img></a>
@ -44,7 +50,8 @@ Gallery =
</div> </div>
<div class=gal-thumbnails></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' frame: '.gal-image'
name: '.gal-name' name: '.gal-name'
count: '.count' count: '.count'
@ -54,26 +61,41 @@ Gallery =
current: '.gal-image img' current: '.gal-image img'
} }
Gallery.images = [] menuButton = $ '.menu-button', dialog
nodes.menu = new UI.Menu 'gallery'
$.on Gallery.frame, 'click', Gallery.cb.blank {cb} = Gallery
$.on Gallery.current, 'click', Gallery.cb.download $.on nodes.frame, 'click', cb.blank
$.on Gallery.next, 'click', Gallery.cb.next $.on nodes.current, 'click', cb.download
$.on ($ '.gal-prev', dialog), 'click', Gallery.cb.prev $.on nodes.next, 'click', cb.next
$.on ($ '.gal-next', dialog), 'click', Gallery.cb.next $.on ($ '.gal-prev', dialog), 'click', cb.prev
$.on ($ '.gal-close', dialog), 'click', Gallery.cb.close $.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 $.off d, 'keydown', Keybinds.keydown
i = 0 i = 0
files = $$ '.post .file' files = $$ '.post .file'
while file = files[i++] while file = files[i++]
continue if $ '.fileDeletedRes, .fileDeleted', file
Gallery.generateThumb file Gallery.generateThumb file
$.add d.body, dialog $.add d.body, dialog
Gallery.thumbs.scrollTop = 0 nodes.thumbs.scrollTop = 0
Gallery.current.parentElement.scrollTop = 0 nodes.current.parentElement.scrollTop = 0
Gallery.cb.open.call if image Gallery.cb.open.call if image
$ "[href='#{image.href.replace /https?:/, ''}']", Gallery.thumbs $ "[href='#{image.href.replace /https?:/, ''}']", Gallery.thumbs
@ -81,7 +103,7 @@ Gallery =
Gallery.images[0] Gallery.images[0]
d.body.style.overflow = 'hidden' d.body.style.overflow = 'hidden'
Gallery.total.textContent = --i nodes.total.textContent = --i
generateThumb: (file) -> generateThumb: (file) ->
title = ($ '.fileText a', file).textContent title = ($ '.fileText a', file).textContent
@ -89,7 +111,7 @@ Gallery =
if double = $ 'img + img', thumb if double = $ 'img + img', thumb
$.rm double $.rm double
thumb.className = 'a-thumb' thumb.className = 'gal-thumb'
thumb.title = title thumb.title = title
thumb.dataset.id = Gallery.images.length thumb.dataset.id = Gallery.images.length
thumb.firstElementChild.style.cssText = '' thumb.firstElementChild.style.cssText = ''
@ -97,7 +119,7 @@ Gallery =
$.on thumb, 'click', Gallery.cb.open $.on thumb, 'click', Gallery.cb.open
Gallery.images.push thumb Gallery.images.push thumb
$.add Gallery.thumbs, thumb $.add Gallery.nodes.thumbs, thumb
cb: cb:
keybinds: (e) -> keybinds: (e) ->
@ -118,35 +140,67 @@ Gallery =
open: (e) -> open: (e) ->
e.preventDefault() if 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' $.addClass @, 'gal-highlight'
img = $.el 'img', img = $.el 'img',
src: Gallery.name.href = @href src: name.href = @href
title: Gallery.name.download = Gallery.name.textContent = @title title: name.download = name.textContent = @title
img.dataset.id = @dataset.id img.dataset.id = @dataset.id
$.replace Gallery.current, img $.replace nodes.current, img
Gallery.count.textContent = +@dataset.id + 1 nodes.count.textContent = +@dataset.id + 1
Gallery.current = img nodes.current = img
Gallery.frame.scrollTop = 0 nodes.frame.scrollTop = 0
Gallery.current.focus() nodes.next.focus()
image: (e) -> image: (e) ->
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
Gallery.build @ Gallery.build @
prev: -> Gallery.cb.open.call Gallery.images[+Gallery.current.dataset.id - 1] prev: -> Gallery.cb.open.call Gallery.images[+Gallery.nodes.current.dataset.id - 1]
next: -> Gallery.cb.open.call Gallery.images[+Gallery.current.dataset.id + 1] next: -> Gallery.cb.open.call Gallery.images[+Gallery.nodes.current.dataset.id + 1]
toggle: -> (if Gallery.el then Gallery.cb.close else Gallery.build)() toggle: -> (if Gallery.nodes then Gallery.cb.close else Gallery.build)()
blank: (e) -> Gallery.cb.close() if e.target is @ blank: (e) -> Gallery.cb.close() if e.target is @
close: -> close: ->
$.rm Gallery.el $.rm Gallery.nodes.el
delete Gallery.el delete Gallery.nodes
d.body.style.overflow = '' d.body.style.overflow = ''
$.off d, 'keydown', Gallery.cb.keybinds $.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 $.event 'change', null, input
$.on input, 'change', $.cb.checked $.on input, 'change', $.cb.checked
el: label el: label
menuToggle: (e) ->
ImageExpand.opmenu.toggle e, @, g

View File

@ -81,6 +81,7 @@ Linkify =
for link in links.reverse() for link in links.reverse()
@nodes.links.push Linkify.makeLink link, @ @nodes.links.push Linkify.makeLink link, @
link.detach()
return unless Conf['Embedding'] or Conf['Link Title'] return unless Conf['Embedding'] or Conf['Link Title']
@ -399,3 +400,4 @@ Linkify =
title: title:
api: (uid) -> "https://gdata.youtube.com/feeds/api/videos/#{uid}?alt=json&fields=title/text(),yt:noembed,app:control/yt:state/@reasonCode" 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 = IDColor =
init: -> 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 = {} @ids = {}
Post::callbacks.push Post::callbacks.push
@ -9,30 +8,34 @@ IDColor =
cb: @node cb: @node
node: -> node: ->
return if @isClone or not str = @info.uniqueID return if @isClone or not uid = @info.uniqueID
uid = $ '.hand', @nodes.uniqueID span = $ '.hand', @nodes.uniqueID
return unless uid and uid.nodeName is 'SPAN' return unless span and span.nodeName is 'SPAN'
uid.style.cssText = IDColor.css IDColor.ids[str] or IDColor.compute str 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) -> compute: (uid) ->
hash = IDColor.hash str return IDColor.ids[uid] if IDColor.ids[uid]
hash = IDColor.hash uid
rgb = [ rgb = [
(hash >> 24) & 0xFF (hash >> 24) & 0xFF
(hash >> 16) & 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 hash: (uid) ->
@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) ->
msg = 0 msg = 0
i = 0 i = 0
while i < 8 while i < 8
msg = ((msg << 5) - msg) + str.charCodeAt i++ msg = (msg << 5) - msg + uid.charCodeAt i++
msg msg

View File

@ -167,7 +167,9 @@ ThreadWatcher =
for threadID, thread of g.BOARD.threads for threadID, thread of g.BOARD.threads
toggler = $ '.watch-thread-link', thread.OP.nodes.post toggler = $ '.watch-thread-link', thread.OP.nodes.post
watched = ThreadWatcher.db.get {boardID: thread.board.ID, threadID} 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 for refresher in ThreadWatcher.menu.refreshers
refresher() refresher()

View File

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

View File

@ -71,7 +71,7 @@ QR =
persist: -> persist: ->
return unless QR.postingIsEnabled return unless QR.postingIsEnabled
QR.open() QR.open()
QR.hide() if Conf['Auto Hide QR'] QR.hide() if Conf['Auto-Hide QR'] or g.VIEW is 'catalog'
open: -> open: ->
if QR.nodes if QR.nodes
@ -165,7 +165,7 @@ QR =
setTimeout -> setTimeout ->
notif.onclose = null notif.onclose = null
notif.close() notif.close()
, 5 * $.SECOND , 7 * $.SECOND
<% } %> <% } %>
notifications: [] notifications: []
@ -449,20 +449,52 @@ QR =
return unless e.dataTransfer.files.length return unless e.dataTransfer.files.length
e.preventDefault() e.preventDefault()
QR.open() QR.open()
QR.fileInput e.dataTransfer.files QR.handleFiles e.dataTransfer.files
$.addClass QR.nodes.el, 'dump' $.addClass QR.nodes.el, 'dump'
paste: (e) -> paste: (e) ->
files = [] files = []
for item in e.clipboardData.items for item in e.clipboardData.items when item.kind is 'file'
if item.kind is 'file' blob = item.getAsFile()
blob = item.getAsFile() blob.name = 'file'
blob.name = 'file' blob.name += '.' + blob.type.split('/')[1] if blob.type
blob.name += '.' + blob.type.split('/')[1] if blob.type files.push blob
files.push blob
return unless files.length return unless files.length
QR.open() 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) -> openFileInput: (e) ->
e.stopPropagation() e.stopPropagation()
@ -476,42 +508,6 @@ QR =
e.preventDefault() e.preventDefault()
QR.nodes.fileInput.click() 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: [] posts: []
post: class post: class
@ -752,7 +748,9 @@ QR =
@nodes.span.textContent = @com @nodes.span.textContent = @com
reader.readAsText file reader.readAsText file
dragStart: -> $.addClass @, 'drag' dragStart: (e) ->
e.dataTransfer.setDragImage @, e.layerX, e.layerY
$.addClass @, 'drag'
dragEnd: -> $.rmClass @, 'drag' dragEnd: -> $.rmClass @, 'drag'
dragEnter: -> $.addClass @, 'over' dragEnter: -> $.addClass @, 'over'
dragLeave: -> $.rmClass @, 'over' dragLeave: -> $.rmClass @, 'over'
@ -998,7 +996,7 @@ QR =
$.on nodes.fileRM, 'click', -> QR.selected.rmFile() $.on nodes.fileRM, 'click', -> QR.selected.rmFile()
$.on nodes.fileExtras, 'click', (e) -> e.stopPropagation() $.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.handleFiles
# mouseover descriptions # mouseover descriptions
items = ['spoilerPar', 'dumpButton', 'fileRM'] items = ['spoilerPar', 'dumpButton', 'fileRM']
@ -1243,7 +1241,19 @@ QR =
} }
# Enable auto-posting if we have stuff to post, disable it otherwise. # 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 unless Conf['Persistent QR'] or QR.cooldown.auto
QR.close() QR.close()