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:
commit
d4a3bc8a17
20
CHANGELOG.md
20
CHANGELOG.md
@ -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>
|
||||

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

|
||||
**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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
2
LICENSE
2
LICENSE
@ -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
|
||||
|
||||
@ -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
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 |
@ -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:
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
2
src/General/css/font-awesome.css
vendored
2
src/General/css/font-awesome.css
vendored
@ -40,4 +40,4 @@
|
||||
}
|
||||
#shortcuts .icon {
|
||||
color: rgb(130,130,130) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
@ -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>
|
||||
|
||||
@ -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) =>
|
||||
|
||||
@ -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: ->
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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
|
||||
|
||||
@ -192,6 +192,3 @@ ImageExpand =
|
||||
$.event 'change', null, input
|
||||
$.on input, 'change', $.cb.checked
|
||||
el: label
|
||||
|
||||
menuToggle: (e) ->
|
||||
ImageExpand.opmenu.toggle e, @, g
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
@ -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"}) %>'
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user