Merge branch 'v3'

Conflicts:
	.gitignore
	CHANGELOG.md
	CONTRIBUTING.md
	LICENSE
	builds/appchan-x.user.js
	builds/crx/manifest.json
	builds/crx/script.js
	latest.js
	package.json
	src/General/Header.coffee
	src/General/Index.coffee
	src/General/Settings.coffee
	src/General/UI.coffee
	src/General/html/Settings/Settings.html
	src/Images/Gallery.coffee
	src/Images/ImageExpand.coffee
	src/Monitoring/ThreadWatcher.coffee
This commit is contained in:
Zixaphir 2014-04-11 00:29:52 -07:00
commit ea47aefd93
46 changed files with 1387 additions and 15752 deletions

5
.gitignore vendored
View File

@ -3,6 +3,11 @@ node_modules/
*.db *.db
tmp-crx/ tmp-crx/
tmp-userscript/ tmp-userscript/
<<<<<<< HEAD
=======
testbuilds/
builds/4chan-X.zip
>>>>>>> v3
Gruntfile.js Gruntfile.js
builds/4chan-* builds/4chan-*
Gruntfile.js Gruntfile.js

View File

@ -1,3 +1,26 @@
**ccd0**
- `Loop in New Tab` (enabled by default) causes videos opened in a separate tab to loop, and applies your settings for inline expanded videos to them.
- Add WebM support to gallery (currently no controls).
- Add PDF support to gallery, disabled by default. Enable with `PDF in Gallery`.
- Fix behavior of .webm videos expanded within inline quotes.
- Contract thumbnails in quoted previews to avoid crashes caused by videos in quoted previews on some systems.
- Change interface when both `Autoplay` and `Show Controls` are unchecked. In this mode, videos are now activated by clicking on them. The first click expands the video, the second click plays the video, and the third click contracts it.
- Add item `Expand videos` in `Image Expansion` menu, which enables expansion of videos by `Expand All Images`. Disabled by default. Previously videos were expanded.
- Disable autoplay for videos expanded by `Expand All Images`.
- Fix minimum width bug.
**fgts**
- Update archive list.
**Nebukazar**
- `Quote Threading` disabled by default
- Added missing titles to Header icons
**Zixaphir**:
- WebM Thumbnail Replacement. Use at your own risk.
- Bugfixes.
### v2.9.15 ### v2.9.15
*2014-04-05* *2014-04-05*
@ -187,8 +210,8 @@
**Spittie** **Spittie**
- Check image dimension before uploading - Check image dimension before uploading
**Vampiricwulf** ## v1.7.0
- Flash embedding and other Flash features. *2014-04-06*
**Zixaphir** **Zixaphir**
- Update Custom Navigation legend to reflect index mode changes. - Update Custom Navigation legend to reflect index mode changes.

View File

@ -1,5 +1,6 @@
## Reporting bugs and suggestions ## Reporting bugs and suggestions
<<<<<<< HEAD
Reporting bugs: (note that some of these links refer to Mayhem's 4chan X repo to avoid duplication of information resources. Bugs MUST be filed on [our issue tracker](https://github.com/zixaphir/appchan-x/issues), not Mayhem's) Reporting bugs: (note that some of these links refer to Mayhem's 4chan X repo to avoid duplication of information resources. Bugs MUST be filed on [our issue tracker](https://github.com/zixaphir/appchan-x/issues), not Mayhem's)
1. Make sure both your **browser** and **Appchan X** are up to date.<br> 1. Make sure both your **browser** and **Appchan X** are up to date.<br>
@ -12,6 +13,19 @@ Reporting bugs: (note that some of these links refer to Mayhem's 4chan X repo to
1. Precise steps to reproduce the problem, with the expected and actual results. 1. Precise steps to reproduce the problem, with the expected and actual results.
2. [Console errors](https://github.com/MayhemYDG/4chan-x/wiki/FAQ#console-errors), if any. 2. [Console errors](https://github.com/MayhemYDG/4chan-x/wiki/FAQ#console-errors), if any.
3. Appchan X version, browser variant, browser version, and Greasemonkey version if you are using it. 3. Appchan X version, browser variant, browser version, and Greasemonkey version if you are using it.
=======
Reporting bugs:
1. Make sure both your **browser** and **4chan X** are up to date.<br>
Only **Chrome**, **Firefox** and **Opera** are supported.<br>
**SRWare Iron**, **Firefox ESR**, **Pale Moon**, **Waterfox**, and other derivatives are not supported, use them at your own risk.
2. Look at the list of [known problems and solutions](https://github.com/MayhemYDG/4chan-x/wiki/FAQ#known-problems).
3. Disable your other extensions & scripts to identify conflicts.
4. If your issue persists, open a [new issue](https://github.com/ccd0/4chan-x/issues) with the following information:
1. Precise steps to reproduce the problem, with the expected and actual results.
2. [Console errors](https://github.com/MayhemYDG/4chan-x/wiki/FAQ#console-errors), if any.
3. 4chan X version, browser variant, browser version, and Greasemonkey version if you are using it.
>>>>>>> v3
4. Your exported settings. If your settings contains sensible information (e.g. personas), edit the text file manually. 4. Your exported settings. If your settings contains sensible information (e.g. personas), edit the text file manually.
Respect these guidelines: Respect these guidelines:

View File

@ -44,8 +44,7 @@ module.exports = (grunt) ->
meta: meta:
files: files:
'LICENSE': 'src/General/meta/banner.js', 'LICENSE': 'src/General/meta/banner.js'
'latest.js': 'src/General/meta/latest.js'
crx: crx:
files: files:

View File

@ -1,5 +1,5 @@
/* /*
* appchan x - Version 2.9.15 - 2014-04-05 * appchan x - Version 2.9.15 - 2014-04-11
* *
* 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.5.1 // @version 1.7.7
// @minGMVer 1.14 // @minGMVer 1.14
// @minFFVer 26 // @minFFVer 26
// @namespace 4chan-X // @namespace 4chan-X

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?> <?xml version='1.0' encoding='UTF-8'?>
<gupdate xmlns='http://www.google.com/update2/response' protocol='2.0'> <gupdate xmlns='http://www.google.com/update2/response' protocol='2.0'>
<app appid='lacclbnghgdicfifcamcmcnilckjamag'> <app appid='lacclbnghgdicfifcamcmcnilckjamag'>
<updatecheck codebase='https://ccd0.github.io/4chan-x/builds/crx.crx' version='1.5.1' /> <updatecheck codebase='https://ccd0.github.io/4chan-x/builds/crx.crx' version='1.7.7' />
</app> </app>
</gupdate> </gupdate>

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@ -1,62 +0,0 @@
function GM_openInTab(_url) {
self.port.emit("GM_openInTab", _url);
return; // Should return the Window object
};
function GM_setValue(_name, _value) {
localStorage[_name] = _value;
return;
};
function GM_getValue(_name, _default) {
if (localStorage[_name] === null && _default === null) return null;
return (localStorage[_name] || _default);
};
function GM_deleteValue(_name) {
localStorage.removeItem(_name);
return;
};
function GM_listValues() {
return Object.keys(localStorage);
};
function GM_setClipboard(_text) {
self.port.emit("GM_setClipboard", _text);
};
//Deprecated
function GM_log(_message) {
console.log(_message);
return;
};
function GM_xmlhttpRequest(_details) {
//Ugly hack? Race condition? Memory leak?
_onload = _details.onload;
_context = _details.context;
self.port.emit("GM_xmlhttpRequest", _details);
};
self.port.on("callback_GM_xmlhttpRequest", function(_response) {
_response.context = _context;
_onload(_response);
});
function GM_addStyle(_css) {
self.port.emit("GM_addStyle", _css);
}
var GM_info = new Object();
GM_info.version = '1.15';
GM_info.scriptWillUpdate = true;
//To do
function GM_registerMenuCommand(_caption, _commandFunc, _accessKey) {
return;
}
self.port.on("load-userscript", function(_script) {
eval(_script);
});

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 232 B

View File

@ -1,82 +0,0 @@
var data = require("sdk/self").data;
var Request = require("sdk/request").Request;
var tabs = require("sdk/tabs");
var system = require("sdk/system");
if (system.platform !== 'android') {
// Clipboard is not supported on Android
var clipboard = require("sdk/clipboard");
}
var pageMod = require("sdk/page-mod");
pageMod.PageMod({
include: ["*.4chan.org", "*.4cdn.org"],
contentScriptFile: data.url("greaseshim.js"),
contentScriptWhen: "start",
onAttach: function(worker) {
worker.port.emit("load-userscript", data.load("4chan-X.user.js"));
//GM_openInTab
worker.port.on("GM_openInTab", function(url) {
tabs.open(url);
});
//GM_setClipboard
worker.port.on("GM_setClipboard", function(text) {
if (system.platform !== 'android') {
// Clipboard is not supported on Android
clipboard.set(text);
}
});
//GM_xmlhttpRequest
worker.port.on("GM_xmlhttpRequest", function(details) {
request = new Object();
request.url = details.url;
if (details.headers) {
request.headers = details.headers;
if (details.headers["Content-Type"]) {request.contentType = details.headers["Content-Type"]};
};
if (details.data) {request.content = encodeURIComponent(details.data)};
if (details.overrideMimeType) {request.overrideMimeType = details.overrideMimeType};
request.onComplete = function(response) {
response.finalUrl = details.url;
response.responseText = response.text;
for (var headerName in response.headers) {
_string = headerName + ": " + response.headers[headerName] + " \n";
response.responseHeaders += _string;
}
response.readyState = 4;
worker.port.emit("callback_GM_xmlhttpRequest", response);
}
xhr = Request(request);
switch(details.method){
case "GET":
xhr.get();
break;
case "POST":
xhr.post();
break;
case "HEAD":
xhr.head();
break;
case "PUT":
xhr.put();
break;
default: xhr.get();
}
});
//GM_addStyle
worker.port.on("GM_addStyle", function(css) {
tabs.activeTab.attach({
contentScript: "var style = document.createElement('style');" +
"style.type = 'text/css';" +
"style.innerHTML = '" + css + "';" +
"document.head.appendChild(style);"
});
});
}
});

View File

@ -1,9 +0,0 @@
{
"name": "4chanx",
"title": "4chan X",
"id": "72DAF86E-9689-11E3-8BA3-F4B66188709B",
"description": "Adds various features to 4chan.",
"author": "Spittie",
"license": "MIT",
"version": "1.4.1"
}

View File

@ -1,12 +0,0 @@
var main = require("./main");
exports["test main"] = function(assert) {
assert.pass("Unit test running!");
};
exports["test main async"] = function(assert, done) {
assert.pass("async Unit test running!");
done();
};
require("sdk/test").run(exports);

File diff suppressed because it is too large Load Diff

View File

@ -1,128 +0,0 @@
[{
"uid": 0,
"name": "Foolz",
"domain": "archive.foolz.us",
"http": true,
"https": true,
"software": "foolfuuka",
"boards": ["a", "biz", "co", "diy", "gd", "jp", "m", "sci", "sp", "tg", "tv", "v", "vg", "vp", "vr", "wsg"],
"files": ["a", "biz", "gd", "diy", "jp", "m", "sci", "tg", "vg", "vp", "vr", "wsg"]
}, {
"uid": 1,
"name": "NSFW Foolz",
"domain": "nsfw.foolz.us",
"http": true,
"https": true,
"software": "foolfuuka",
"boards": ["u"],
"files": ["u"]
}, {
"uid": 2,
"name": "The Dark Cave",
"domain": "archive.thedarkcave.org",
"http": true,
"https": true,
"software": "foolfuuka",
"boards": ["c", "int", "out", "po"],
"files": ["c", "po"]
}, {
"uid": 3,
"name": "4plebs Archive",
"domain": "archive.4plebs.org",
"http": true,
"https": true,
"software": "foolfuuka",
"boards": ["adv", "hr", "o", "pol", "s4s", "tg", "trv", "tv", "x"],
"files": ["adv", "hr", "o", "pol", "s4s", "tg", "trv", "tv", "x"]
}, {
"uid": 18,
"name": "4plebs Flash Archive",
"domain": "flash.4plebs.org",
"http": true,
"https": true,
"software": "foolfuuka",
"boards": ["f"],
"files": ["f"]
}, {
"uid": 4,
"name": "Nyafuu",
"domain": "archive.nyafuu.org",
"http": true,
"https": true,
"software": "foolfuuka",
"boards": ["c", "e", "w", "wg"],
"files": ["c", "e", "w", "wg"]
}, {
"uid": 5,
"name": "Love is Over",
"domain": "loveisover.me",
"http": true,
"https": true,
"software": "foolfuuka",
"boards": ["d", "i"],
"files": ["d", "i"]
}, {
"uid": 8,
"name": "Rebecca Black Tech",
"domain": "archive.rebeccablacktech.com",
"http": true,
"https": true,
"software": "fuuka",
"boards": ["cgl", "g", "mu", "w"],
"files": ["cgl", "g", "mu", "w"]
}, {
"uid": 9,
"name": "Heinessen",
"domain": "archive.heinessen.com",
"http": true,
"https": false,
"software": "fuuka",
"boards": ["an", "fit", "k", "mlp", "r9k", "toy"],
"files": ["an", "fit", "k", "r9k", "toy"]
}, {
"uid": 10,
"name": "warosu",
"domain": "fuuka.warosu.org",
"http": false,
"https": true,
"software": "fuuka",
"boards": ["3", "biz", "cgl", "ck", "diy", "fa", "g", "ic", "jp", "lit", "sci", "tg", "vr"],
"files": ["3", "biz", "cgl", "ck", "diy", "fa", "ic", "jp", "lit", "sci", "tg", "vr"]
}, {
"uid": 15,
"name": "fgts",
"domain": "fgts.eu",
"http": true,
"https": true,
"software": "foolfuuka",
"boards": ["cm", "hm", "r", "soc", "y"],
"files": ["cm", "hm", "r", "soc", "y"]
}, {
"uid": 16,
"name": "maware",
"domain": "archive.mawa.re",
"http": true,
"https": false,
"software": "foolfuuka",
"boards": ["t"],
"files": ["t"]
}, {
"uid": 17,
"name": "installgentoo.com",
"domain": "chan.installgentoo.com",
"http": true,
"https": false,
"software": "foolfuuka",
"boards": ["g", "t"],
"files": ["g", "t"]
}, {
"uid": 13,
"name": "Foolz Beta",
"domain": "beta.foolz.us",
"http": true,
"https": true,
"withCredentials": true,
"software": "foolfuuka",
"boards": ["a", "biz", "co", "d", "diy", "gd", "jp", "m", "s4s", "sci", "sp", "tg", "tv", "u", "v", "vg", "vp", "vr", "wsg"],
"files": ["a", "biz", "d", "diy", "gd", "jp", "m", "s4s", "sci", "tg", "u", "vg", "vp", "vr", "wsg"]
}]

View File

@ -95,8 +95,8 @@
"http": true, "http": true,
"https": true, "https": true,
"software": "foolfuuka", "software": "foolfuuka",
"boards": ["cm", "hm", "r", "soc", "y"], "boards": ["cm", "h", "hc", "hm", "r", "s", "soc", "y"],
"files": ["cm", "hm", "r", "soc", "y"] "files": ["cm", "h", "hc", "hm", "r", "s", "soc", "y"]
}, { }, {
"uid": 16, "uid": 16,
"name": "maware", "name": "maware",

View File

@ -2,7 +2,7 @@ Config =
main: main:
'Miscellaneous': 'Miscellaneous':
'JSON Navigation' : [ 'JSON Navigation' : [
true false
'Use JSON for loading the Board Index and Threads. Also allows searching and sorting the board index and infinite scolling.' 'Use JSON for loading the Board Index and Threads. Also allows searching and sorting the board index and infinite scolling.'
] ]
'Catalog Links': [ 'Catalog Links': [
@ -69,6 +69,10 @@ Config =
false false
'Indicate spoilers if Remove Spoilers is enabled, or make the text appear hovered if Remove Spoiler is disabled.' 'Indicate spoilers if Remove Spoilers is enabled, or make the text appear hovered if Remove Spoiler is disabled.'
] ]
'Show Support Message': [
true
'Warn if your browser is unsupported. 4chan X may not operate correctly on unsupported browser versions.'
]
'Linkification': 'Linkification':
'Linkify': [ 'Linkify': [
@ -131,6 +135,10 @@ Config =
true true
'Adds a simple and cute image gallery.' 'Adds a simple and cute image gallery.'
] ]
'PDF in Gallery': [
false
'Show PDF files in gallery.'
]
'Sauce': [ 'Sauce': [
true true
'Add sauce links to images.' 'Add sauce links to images.'
@ -141,15 +149,19 @@ Config =
] ]
'Replace GIF': [ 'Replace GIF': [
false false
'Replace thumbnail of gifs with its actual image.' 'Replace gif thumbnails with the actual image.'
]
'Replace PNG': [
false
'Replace pngs.'
] ]
'Replace JPG': [ 'Replace JPG': [
false false
'Replace jpgs.' 'Replace jpg thumbnails with the actual image.'
]
'Replace PNG': [
false
'Replace png thumbnails with the actual image.'
]
'Replace WEBM': [
false
'Replace webm thumbnails with the actual webm video. Probably will degrade browser performance ;)'
] ]
'Image Prefetching': [ 'Image Prefetching': [
false false
@ -165,15 +177,19 @@ Config =
] ]
'Autoplay': [ 'Autoplay': [
true true
'Videos begin playing immediately when opened inline.' 'Videos begin playing immediately when opened.'
] ]
'Show Controls': [ 'Show Controls': [
true true
'Show native seek and volume controls on videos. Contract videos when dragged to the left.' 'Show controls on videos expanded inline. Turn this off if you want to contract videos by clicking on them.'
] ]
'Allow Sound': [ 'Allow Sound': [
true true
'Allow sound in inline videos.' 'Allow sound in videos.'
]
'Loop in New Tab': [
true
'Loop videos opened in their own tabs, and apply settings for inline expanded videos to them.'
] ]
'Menu': 'Menu':
@ -366,6 +382,7 @@ Config =
'Add \'(Cross-thread)\' to cross-threads quotes.' 'Add \'(Cross-thread)\' to cross-threads quotes.'
'Highlights own posts if Quote Markers are enabled.' 'Highlights own posts if Quote Markers are enabled.'
] ]
imageExpansion: imageExpansion:
'Fit width': [ 'Fit width': [
false false
@ -379,6 +396,10 @@ Config =
true true
'Expand all images along with spoilers.' 'Expand all images along with spoilers.'
] ]
'Expand videos': [
false
'Expand all images also expands videos (no autoplay).'
]
'Expand from here': [ 'Expand from here': [
false false
'Expand all images only from current position to thread end.' 'Expand all images only from current position to thread end.'

View File

@ -62,6 +62,7 @@ Get =
quotelinks.filter (quotelink) -> quotelinks.filter (quotelink) ->
{boardID, postID} = Get.postDataFromLink quotelink {boardID, postID} = Get.postDataFromLink quotelink
boardID is post.board.ID and postID is post.ID boardID is post.board.ID and postID is post.ID
postClone: (boardID, threadID, postID, root, context) -> postClone: (boardID, threadID, postID, root, context) ->
if post = g.posts["#{boardID}.#{postID}"] if post = g.posts["#{boardID}.#{postID}"]
Get.insert post, root, context Get.insert post, root, context
@ -80,7 +81,7 @@ Get =
insert: (post, root, context) -> insert: (post, root, context) ->
# Stop here if the container has been removed while loading. # Stop here if the container has been removed while loading.
return unless root.parentNode return unless root.parentNode
clone = post.addClone context clone = post.addClone context, ($.hasClass root, 'dialog')
Clone.callbacks.execute [clone] Clone.callbacks.execute [clone]
# Get rid of the side arrows/stubs. # Get rid of the side arrows/stubs.
@ -104,6 +105,7 @@ Get =
$.cache url, $.cache url,
-> Get.archivedPost @, boardID, postID, root, context -> Get.archivedPost @, boardID, postID, root, context
, ,
responseType: 'json'
withCredentials: url.archive.withCredentials withCredentials: url.archive.withCredentials
else else
$.addClass root, 'warning' $.addClass root, 'warning'

View File

@ -362,6 +362,10 @@ Header =
bottom -= clientHeight - headRect.bottom + headRect.height - 10 bottom -= clientHeight - headRect.bottom + headRect.height - 10
bottom bottom
isNodeVisible: (node) ->
{height} = node.getBoundingClientRect()
Header.getTopOf(node) + height >= 0 and Header.getBottomOf(node) + height >= 0
isHidden: -> isHidden: ->
{top} = Header.bar.getBoundingClientRect() {top} = Header.bar.getBoundingClientRect()
if Conf['Bottom header'] if Conf['Bottom header']

View File

@ -171,9 +171,9 @@ Index =
scroll: -> scroll: ->
return if Index.req or Conf['Index Mode'] isnt 'infinite' or (window.scrollY <= doc.scrollHeight - (300 + window.innerHeight)) or g.VIEW is 'thread' return if Index.req or Conf['Index Mode'] isnt 'infinite' or (window.scrollY <= doc.scrollHeight - (300 + window.innerHeight)) or g.VIEW is 'thread'
Index.pageNum = (Index.pageNum or Index.getCurrentPage()) + 1 # Avoid having to pushState to keep track of the current page Index.currentPage = (Index.currentPage or Index.getCurrentPage()) + 1 # Avoid having to pushState to keep track of the current page
return Index.endNotice() if Index.pageNum >= Index.pagesNum return Index.endNotice() if Index.currentPage >= Index.pagesNum
Index.buildIndex true Index.buildIndex true
@ -419,8 +419,8 @@ Index =
Header.scrollToIfNeeded Index.navLinks Header.scrollToIfNeeded Index.navLinks
getCurrentPage: -> getCurrentPage: ->
if Conf['Index Mode'] is 'infinite' and Index.pageNum if Conf['Index Mode'] is 'infinite' and Index.currentPage
return Index.pageNum return Index.currentPage
+window.location.pathname.split('/')[2] +window.location.pathname.split('/')[2]
userPageNav: (pageNum) -> userPageNav: (pageNum) ->
@ -469,8 +469,8 @@ Index =
$.add pagesRoot, nodes $.add pagesRoot, nodes
Index.togglePagelist() Index.togglePagelist()
setPage: (pageNum) -> setPage: (pageNum = Index.getCurrentPage()) ->
pageNum or= Index.getCurrentPage() Index.currentPage = pageNum
maxPageNum = Index.getMaxPageNum() maxPageNum = Index.getMaxPageNum()
pagesRoot = $ '.pages', Index.pagelist pagesRoot = $ '.pages', Index.pagelist
# Previous/Next buttons # Previous/Next buttons
@ -513,7 +513,7 @@ Index =
return return
unless d.readyState is 'loading' or Index.root.parentElement unless d.readyState is 'loading' or Index.root.parentElement
$.replace $('.board'), Index.root $.replace $('.board'), Index.root
delete Index.pageNum Index.currentPage = 0
Index.req?.abort() Index.req?.abort()
Index.notice?.close() Index.notice?.close()
@ -564,10 +564,14 @@ Index =
Navigate.title() Navigate.title()
try try
pageNum or= 0
if req.status is 200 if req.status is 200
Index.parse req.response, pageNum Index.parse req.response, pageNum
else if req.status is 304 else if req.status is 304
Index.pageNav pageNum or 0 if Index.currentPage is pageNum
Index.buildIndex()
else
Index.pageNav pageNum
catch err catch err
c.error "Index failure: #{err.message}", err.stack c.error "Index failure: #{err.message}", err.stack
# network error or non-JSON content for example. # network error or non-JSON content for example.
@ -588,7 +592,7 @@ Index =
Index.parseThreadList pages Index.parseThreadList pages
Index.buildThreads() Index.buildThreads()
Index.sort() Index.sort()
if pageNum? if pageNum? and Index.currentPage isnt pageNum
Index.pageNav pageNum Index.pageNav pageNum
return return
Index.buildIndex() Index.buildIndex()
@ -752,16 +756,12 @@ Index =
buildIndex: (infinite) -> buildIndex: (infinite) ->
{sortedThreads} = Index {sortedThreads} = Index
nodes = []
switch Conf['Index Mode'] switch Conf['Index Mode']
when 'paged', 'infinite' when 'paged', 'infinite'
pageNum = Index.getCurrentPage() pageNum = Index.getCurrentPage()
if pageNum > Index.getMaxPageNum()
# Go to the last available page if we were past the limit.
Index.pageNav Index.getMaxPageNum()
return
threadsPerPage = Index.getThreadsNumPerPage() threadsPerPage = Index.getThreadsNumPerPage()
nodes = []
threads = [] threads = []
i = threadsPerPage * pageNum i = threadsPerPage * pageNum
max = i + threadsPerPage max = i + threadsPerPage
@ -777,8 +777,7 @@ Index =
nodes = Index.buildCatalogViews() nodes = Index.buildCatalogViews()
else else
nodes = [] i = 0
i = 0
while thread = sortedThreads[i++] while thread = sortedThreads[i++]
nodes.push thread.OP.nodes.root.parentNode, $.el 'hr' nodes.push thread.OP.nodes.root.parentNode, $.el 'hr'
Index.buildReplies thread Index.buildReplies thread
@ -799,9 +798,11 @@ Index =
unless Index.searchInput.dataset.searching unless Index.searchInput.dataset.searching
Index.searchInput.dataset.searching = 1 Index.searchInput.dataset.searching = 1
Index.pageBeforeSearch = Index.getCurrentPage() Index.pageBeforeSearch = Index.getCurrentPage()
pageNum = 0 Index.setPage pageNum = 0
else else
pageNum = Index.getCurrentPage() unless Conf['Index Mode'] is 'infinite'
pageNum = Index.getCurrentPage()
else else
return unless Index.searchInput.dataset.searching return unless Index.searchInput.dataset.searching
pageNum = Index.pageBeforeSearch pageNum = Index.pageBeforeSearch
@ -832,7 +833,6 @@ Index =
filtered.push thread if Index.searchMatch thread, keywords filtered.push thread if Index.searchMatch thread, keywords
Index.sortedThreads = filtered Index.sortedThreads = filtered
searchMatch: (thread, keywords) -> searchMatch: (thread, keywords) ->
{info, file} = thread.OP {info, file} = thread.OP
text = [] text = []

View File

@ -83,6 +83,11 @@ Main =
Report.init() Report.init()
return return
when 'i.4cdn.org' when 'i.4cdn.org'
if Conf['Loop in New Tab'] and video = $ 'video'
Video.configure video
$.on video, 'click', ->
if !video.controls
if video.paused then video.play() else video.pause()
$.ready -> $.ready ->
if Conf['404 Redirect'] and d.title in ['4chan - Temporarily Offline', '4chan - 404 Not Found'] if Conf['404 Redirect'] and d.title in ['4chan - Temporarily Offline', '4chan - 404 Not Found']
Redirect.init() Redirect.init()
@ -204,7 +209,7 @@ Main =
<% if (type === 'userscript') { %> <% if (type === 'userscript') { %>
test = $.el 'span' test = $.el 'span'
test.classList.add 'a', 'b' test.classList.add 'a', 'b'
if test.className isnt 'a b' if test.className isnt 'a b' and Conf['Show Support Message']
new Notice 'warning', "Your version of Firefox is outdated (v<%= meta.min.firefox %> minimum) and <%= meta.name %> may not operate correctly.", 30 new Notice 'warning', "Your version of Firefox is outdated (v<%= meta.min.firefox %> minimum) and <%= meta.name %> may not operate correctly.", 30
GMver = GM_info.version.split '.' GMver = GM_info.version.split '.'

View File

@ -115,11 +115,15 @@ Navigate =
index: -> index: ->
delete g.THREADID delete g.THREADID
Index.cb.toggleCatalogMode() if Conf['Index Mode'] is 'catalog' Index.cb.toggleCatalogMode() if Conf['Index Mode'] is 'catalog'
QR.posts[0]?.thread = 'new'
thread: -> thread: ->
$.rmClass doc, 'catalog-mode' if Conf['Index Mode'] is 'catalog' $.rmClass doc, 'catalog-mode' if Conf['Index Mode'] is 'catalog'
QR.posts[0]?.thread = g.THREADID
}[view]() }[view]()
QR.status() # Re-enable the QR in the case of a 404'd thread or something.
g.VIEW = view g.VIEW = view
updateBoard: (boardID) -> updateBoard: (boardID) ->

View File

@ -3,6 +3,7 @@ Settings =
# Appchan X settings link # Appchan X settings link
el = $.el 'a', el = $.el 'a',
className: 'settings-link' className: 'settings-link'
title: 'Appchan X Settings'
href: 'javascript:;' href: 'javascript:;'
textContent: 'Settings' textContent: 'Settings'
$.on el, 'click', @open $.on el, 'click', @open

View File

@ -309,7 +309,7 @@ UI = do ->
$.off d, 'mouseup', @up $.off d, 'mouseup', @up
$.set "#{@id}.position", @style.cssText $.set "#{@id}.position", @style.cssText
hoverstart = ({root, el, latestEvent, endEvents, asapTest, cb, offsetX, offsetY}) -> hoverstart = ({root, el, latestEvent, endEvents, asapTest, cb, offsetX, offsetY, noRemove}) ->
o = { o = {
root root
el el
@ -322,6 +322,7 @@ UI = do ->
clientWidth: doc.clientWidth clientWidth: doc.clientWidth
offsetX: offsetX or 45 offsetX: offsetX or 45
offsetY: offsetY or -120 offsetY: offsetY or -120
noRemove
} }
o.hover = hover.bind o o.hover = hover.bind o
o.hoverend = hoverend.bind o o.hoverend = hoverend.bind o
@ -338,7 +339,7 @@ UI = do ->
$.on root, 'mousemove', o.hover $.on root, 'mousemove', o.hover
<% if (type === 'userscript') { %> <% if (type === 'userscript') { %>
# Workaround for https://github.com/MayhemYDG/4chan-x/issues/377 # Workaround for https://github.com/MayhemYDG/4chan-x/issues/377
o.workaround = (e) -> o.hoverend() unless root.contains e.target o.workaround = (e) -> o.hoverend(e) unless root.contains e.target
$.on doc, 'mousemove', o.workaround $.on doc, 'mousemove', o.workaround
<% } %> <% } %>

View File

@ -223,7 +223,7 @@ div.center:not(.ad-cnt) {
height: 10px; height: 10px;
position: absolute; position: absolute;
} }
:root:not(.autohide) #scroll-marker { :root #header-bar:not(.autohide) #scroll-marker {
pointer-events: none; pointer-events: none;
} }
#header-bar #scroll-marker { #header-bar #scroll-marker {
@ -824,20 +824,26 @@ span.hide-announcement {
/* File */ /* File */
.fileText:hover .fntrunc, .fileText:hover .fntrunc,
.fileText:not(:hover) .fnfull, .fileText:not(:hover) .fnfull,
.expanded-image > .post > .file > .fileThumb > img[data-md5], .expanded-image > .post > .file > .fileThumb > video[data-md5],
:not(.expanded-image) > .post > .file .full-image { .expanded-image > .post > .file > .fileThumb > img[data-md5] {
display: none; display: none;
} }
.full-image:not(#ihover) {
display: none;
}
.expanded-image > .post > .file > .fileThumb > .full-image:not(#ihover) {
display: inline;
}
.expanding { .expanding {
opacity: .5; opacity: .5;
} }
:root.fit-height .full-image { :root.fit-height .full-image:not(#ihover) {
max-height: 100vh; max-height: 100vh;
} }
:root.fit-width .full-image { :root.fit-width .full-image:not(#ihover) {
max-width: 100%; max-width: 100%;
} }
:root.gecko.fit-width .full-image { :root.gecko.fit-width .full-image:not(#ihover) {
width: 100%; width: 100%;
} }
#ihover { #ihover {
@ -1459,34 +1465,44 @@ div.boardTitle {
/* 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%;
} }
:root:not(.gal-fit-height) .gal-image { :root:not(.gal-fit-height):not(.gal-pdf) .gal-image {
overflow-y: scroll !important; overflow-y: scroll !important;
} }
:root:not(.gal-fit-width) .gal-image { :root:not(.gal-fit-width):not(.gal-pdf) .gal-image {
overflow-x: scroll !important; overflow-x: scroll !important;
} }
.gal-image a { .gal-image a {
margin: auto; margin: auto;
line-height: 0; line-height: 0;
} }
:root.gal-pdf .gal-image a {
width: 100%;
height: 100%;
}
.gal-fit-width .gal-image video,
.gal-fit-width .gal-image img { .gal-fit-width .gal-image img {
max-width: 100%; max-width: 100%;
} }
.gal-fit-height .gal-image video,
.gal-fit-height .gal-image img { .gal-fit-height .gal-image img {
/*
Chrome doesn't support viewpoint units in calc()
http://bugs.chromium.org/168840
"It looks like the original author of viewport units in WebKit is not coming back to fix this stuff."
Well, fuck.
*/
max-height: 95vh; max-height: 95vh;
max-height: calc(100vh - 25px); }
.gal-image iframe {
width: 100%;
height: 100%;
} }
.gal-buttons { .gal-buttons {
font-size: 2em; font-size: 2em;
margin-right: 10px; margin-right: 3px;
padding-left: 7px;
padding-right: 7px;
top: 5px; top: 5px;
} }
.gal-pdf .gal-buttons {
top: 40px;
background: rgba(0,0,0,0.6) !important;
border-radius: 3px;
}
.gal-buttons i { .gal-buttons i {
vertical-align: baseline; vertical-align: baseline;
border-top-width: .4em; border-top-width: .4em;
@ -1506,12 +1522,12 @@ div.boardTitle {
.gal-name, .gal-name,
.gal-count { .gal-count {
position: fixed; position: fixed;
right: 178px; right: 195px;
} }
.gal-hide-thumbnails .gal-buttons, .gal-hide-thumbnails .gal-buttons,
.gal-hide-thumbnails .gal-count, .gal-hide-thumbnails .gal-count,
.gal-hide-thumbnails .gal-name { .gal-hide-thumbnails .gal-name {
right: 28px; right: 44px;
} }
.gal-name { .gal-name {
bottom: 6px; bottom: 6px;
@ -1526,6 +1542,10 @@ div.boardTitle {
.gal-buttons .menu-button:hover { .gal-buttons .menu-button:hover {
color: rgb(95, 95, 101) !important; color: rgb(95, 95, 101) !important;
} }
:root.gal-pdf .gal-close:hover,
:root.gal-pdf .gal-buttons .menu-button:hover {
color: rgb(204, 204, 204) !important;
}
.gal-count { .gal-count {
bottom: 27px; bottom: 27px;
background: rgba(0,0,0,0.6) !important; background: rgba(0,0,0,0.6) !important;
@ -1533,21 +1553,21 @@ div.boardTitle {
padding: 1px 5px 2px 5px; padding: 1px 5px 2px 5px;
color: #ffffff !important; color: #ffffff !important;
} }
:root:not(.gal-fit-width) .gal-name { :root:not(.gal-fit-width):not(.gal-pdf) .gal-name {
bottom: 23px !important; bottom: 23px !important;
} }
:root:not(.gal-fit-width) .gal-count { :root:not(.gal-fit-width):not(.gal-pdf) .gal-count {
bottom: 44px !important; bottom: 44px !important;
} }
:root:not(.gal-fit-height):not(.gal-hide-thumbnails) .gal-buttons, :root.gal-fit-height:not(.gal-pdf):not(.gal-hide-thumbnails) .gal-buttons,
:root:not(.gal-fit-height):not(.gal-hide-thumbnails) .gal-name, :root.gal-fit-height:not(.gal-pdf):not(.gal-hide-thumbnails) .gal-name,
:root:not(.gal-fit-height):not(.gal-hide-thumbnails) .gal-count { :root.gal-fit-height:not(.gal-pdf):not(.gal-hide-thumbnails) .gal-count {
right: 195px !important; right: 178px !important;
} }
:root.gal-hide-thumbnails:not(.gal-fit-height) .gal-buttons, :root.gal-hide-thumbnails:.gal-fit-height:not(.gal-pdf) .gal-buttons,
:root.gal-hide-thumbnails:not(.gal-fit-height) .gal-name, :root.gal-hide-thumbnails:.gal-fit-height:not(.gal-pdf) .gal-name,
:root.gal-hide-thumbnails:not(.gal-fit-height) .gal-count { :root.gal-hide-thumbnails:.gal-fit-height:not(.gal-pdf) .gal-count {
right: 44px !important; right: 28px !important;
} }
@media screen and (resolution: 1dppx) { @media screen and (resolution: 1dppx) {
.fa-bars { .fa-bars {

View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -1,11 +1,14 @@
class Clone extends Post class Clone extends Post
constructor: (@origin, @context) -> constructor: (@origin, @context, contractThumb) ->
for key in ['ID', 'fullID', 'board', 'thread', 'info', 'quotes', 'isReply'] for key in ['ID', 'fullID', 'board', 'thread', 'info', 'quotes', 'isReply']
# Copy or point to the origin's key value. # Copy or point to the origin's key value.
@[key] = origin[key] @[key] = origin[key]
{nodes} = origin {nodes} = origin
root = nodes.root.cloneNode true root = if contractThumb
@cloneWithoutVideo nodes.root
else
nodes.root.cloneNode true
post = $ '.post', root post = $ '.post', root
info = $ '.postInfo', post info = $ '.postInfo', post
@nodes = @nodes =
@ -56,6 +59,29 @@ class Clone extends Post
@file.thumb = $ 'img[data-md5]', file @file.thumb = $ 'img[data-md5]', file
@file.fullImage = $ '.full-image', file @file.fullImage = $ '.full-image', file
# Contract thumbnails in quote preview
if contractThumb
$.rmClass root, 'expanded-image'
$.rmClass @file.thumb, 'expanding'
@file.isExpanded = $.hasClass root, 'expanded-image'
# Remove any #ihover ID
@file.fullImage?.removeAttribute 'id'
# Remove video controls.
($ '.video-controls', @file.text)?.remove()
@isDead = true if origin.isDead @isDead = true if origin.isDead
@isClone = true @isClone = true
root.dataset.clone = origin.clones.push(@) - 1 root.dataset.clone = origin.clones.push(@) - 1
cloneWithoutVideo: (node) ->
if node.tagName is 'VIDEO'
[]
else if node.nodeType is Node.ELEMENT_NODE and $ 'video', node
clone = node.cloneNode false
$.add clone, @cloneWithoutVideo child for child in node.childNodes
clone
else
node.cloneNode true

View File

@ -290,8 +290,8 @@ class Post
@thread.posts.rm @ @thread.posts.rm @
@board.posts.rm @ @board.posts.rm @
addClone: (context) -> addClone: (context, contractThumb) ->
new Clone @, context new Clone @, context, contractThumb
rmClone: (index) -> rmClone: (index) ->
@clones.splice index, 1 @clones.splice index, 1

View File

@ -1 +0,0 @@
postMessage({version:'<%= version %>'},'*')

View File

@ -1,34 +0,0 @@
AutoGIF =
init: ->
return if !Conf['Auto-GIF'] or g.BOARD.ID in ['gif', 'wsg']
Post.callbacks.push
name: 'Auto-GIF'
cb: @node
CatalogThread.callbacks.push
name: 'Auto-GIF'
cb: @catalogNode
node: ->
return if @isClone or @isHidden or @thread.isHidden or !@file?.isImage
{thumb, URL} = @file
return unless /gif$/.test(URL) and !/spoiler/.test thumb.src
if @file.isSpoiler
# Revealed spoilers do not have height/width set, this fixes auto-gifs dimensions.
{style} = thumb
style.maxHeight = style.maxWidth = if @isReply then '125px' else '250px'
AutoGIF.replaceThumbnail thumb, URL
catalogNode: ->
{OP} = @thread
return unless OP.file?.isImage
{URL} = OP.file
return unless /gif$/.test URL
AutoGIF.replaceThumbnail @nodes.thumb, URL, true
replaceThumbnail: (thumb, URL, isBackground) ->
gif = $.el 'img'
$.on gif, 'load', ->
# Replace the thumbnail once the GIF has finished loading.
if isBackground
thumb.style.backgroundImage = "url(#{URL})"
else
thumb.src = URL
gif.src = URL

View File

@ -15,10 +15,10 @@ Gallery =
Post.callbacks.push Post.callbacks.push
name: 'Gallery' name: 'Gallery'
cb: @node cb: @node
node: -> node: ->
return unless @file?.isImage return unless @file
if Gallery.nodes if Gallery.nodes
Gallery.generateThumb $ '.file', @nodes.root Gallery.generateThumb $ '.file', @nodes.root
Gallery.nodes.total.textContent = Gallery.images.length Gallery.nodes.total.textContent = Gallery.images.length
@ -33,23 +33,23 @@ Gallery =
nodes.el = dialog = $.el 'div', nodes.el = dialog = $.el 'div',
id: 'a-gallery' id: 'a-gallery'
innerHTML: """ innerHTML: """
<div class=gal-viewport> <div class=gal-viewport>
<span class=gal-buttons> <span class=gal-buttons>
<a class="menu-button" href="javascript:;"><i class="fa">\uf107</i></a> <a class="menu-button" href="javascript:;"><i></i></a>
<a href=javascript:; class='gal-close fa'>\uf00d</a> <a href=javascript:; class=gal-close>×</a>
</span> </span>
<a class=gal-name target="_blank"></a> <a class=gal-name target="_blank"></a>
<span class=gal-count> <span class=gal-count>
<span class='count'></span> / <span class='total'></span> <span class='count'></span> / <span class='total'></span>
</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>
</div> </div>
<div class=gal-next></div> <div class=gal-next></div>
</div> </div>
<div class=gal-thumbnails></div> <div class=gal-thumbnails></div>
""" """
nodes[key] = $ value, dialog for key, value of { nodes[key] = $ value, dialog for key, value of {
frame: '.gal-image' frame: '.gal-image'
@ -65,12 +65,11 @@ Gallery =
nodes.menu = new UI.Menu 'gallery' nodes.menu = new UI.Menu 'gallery'
{cb} = Gallery {cb} = Gallery
$.on nodes.frame, 'click', cb.blank $.on nodes.frame, 'click', cb.blank
$.on nodes.current, 'click', cb.download $.on nodes.next, 'click', cb.advance
$.on nodes.next, 'click', cb.next $.on $('.gal-prev', dialog), 'click', cb.prev
$.on ($ '.gal-prev', dialog), 'click', cb.prev $.on $('.gal-next', dialog), 'click', cb.next
$.on ($ '.gal-next', dialog), 'click', cb.next $.on $('.gal-close', dialog), 'click', cb.close
$.on ($ '.gal-close', dialog), 'click', cb.close
$.on menuButton, 'click', (e) -> $.on menuButton, 'click', (e) ->
nodes.menu.toggle e, @, g nodes.menu.toggle e, @, g
@ -86,12 +85,7 @@ Gallery =
$.on d, 'keydown', cb.keybinds $.on d, 'keydown', cb.keybinds
$.off d, 'keydown', Keybinds.keydown $.off d, 'keydown', Keybinds.keydown
Gallery.generateThumb file for file, i in $$ '.post .file' when !$ '.fileDeletedRes, .fileDeleted', file
i = 0
files = $$ '.post .file'
while file = files[i++]
continue if $ '.fileDeletedRes, .fileDeleted', file
Gallery.generateThumb file
$.add d.body, dialog $.add d.body, dialog
nodes.thumbs.scrollTop = 0 nodes.thumbs.scrollTop = 0
@ -107,16 +101,22 @@ Gallery =
generateThumb: (file) -> generateThumb: (file) ->
post = Get.postFromNode file post = Get.postFromNode file
return unless post.file and (post.file.isImage or post.file.isVideo or Conf['PDF in Gallery'])
title = ($ '.fileText a', file).textContent title = ($ '.fileText a', file).textContent
thumb = post.file.thumb.parentNode.cloneNode true
if dupe = $ 'img + img', thumb
$.rm dupe
thumb.className = 'gal-thumb' thumb = $.el 'a',
thumb.title = title className: 'gal-thumb'
thumb.dataset.id = Gallery.images.length href: post.file.URL
thumb.dataset.post = $('a[title="Highlight this post"]', post.nodes.info).href target: '_blank'
thumb.firstElementChild.style.cssText = '' title: title
thumb.dataset.id = Gallery.images.length
thumb.dataset.post = $('a[title="Highlight this post"]', post.nodes.info).href
thumb.dataset.isVideo = true if post.file.isVideo
thumbImg = post.file.thumb.cloneNode false
thumbImg.style.cssText = ''
$.add thumb, thumbImg
$.on thumb, 'click', Gallery.cb.open $.on thumb, 'click', Gallery.cb.open
@ -130,8 +130,10 @@ Gallery =
cb = switch key cb = switch key
when 'Esc', Conf['Open Gallery'] when 'Esc', Conf['Open Gallery']
Gallery.cb.close Gallery.cb.close
when 'Right', 'Enter' when 'Right'
Gallery.cb.next Gallery.cb.next
when 'Enter'
Gallery.cb.advance
when 'Left', '' when 'Left', ''
Gallery.cb.prev Gallery.cb.prev
@ -150,15 +152,20 @@ Gallery =
$.rmClass el, 'gal-highlight' if el = $ '.gal-highlight', nodes.thumbs $.rmClass el, 'gal-highlight' if el = $ '.gal-highlight', nodes.thumbs
$.addClass @, 'gal-highlight' $.addClass @, 'gal-highlight'
img = $.el 'img', elType = if @dataset.isVideo then 'video' else if /\.pdf$/.test(@href) then 'iframe' else 'img'
$[if elType is 'iframe' then 'addClass' else 'rmClass'] nodes.el, 'gal-pdf'
file = $.el elType,
src: name.href = @href src: name.href = @href
title: name.download = name.textContent = @title title: name.download = name.textContent = @title
$.extend img.dataset, @dataset $.extend file.dataset, @dataset
$.replace nodes.current, img nodes.current.pause?()
$.replace nodes.current, file
Video.configure file if @dataset.isVideo
nodes.count.textContent = +@dataset.id + 1 nodes.count.textContent = +@dataset.id + 1
nodes.current = img nodes.current = file
nodes.frame.scrollTop = 0 nodes.frame.scrollTop = 0
nodes.next.focus() nodes.next.focus()
# Scroll # Scroll
@ -170,8 +177,8 @@ Gallery =
nodes.thumbs.scrollTop += top nodes.thumbs.scrollTop += top
$.on img, 'error', -> $.on file, 'error', ->
Gallery.cb.error img, thumb Gallery.cb.error file, thumb
image: (e) -> image: (e) ->
e.preventDefault() e.preventDefault()
@ -183,24 +190,20 @@ Gallery =
delete post.file.fullImage delete post.file.fullImage
src = @src.split '/' src = @src.split '/'
if src[2] is 'images.4chan.org' if src[2] is 'i.4cdn.org'
URL = Redirect.to 'file', URL = Redirect.to 'file',
boardID: src[3] boardID: src[3]
filename: src[5] filename: src[5]
if URL if URL
thumb.href = URL thumb.href = URL
return unless Gallery.nodes.current is img return unless Gallery.nodes.current is img
revived = $.el 'img', img.src = URL
src: URL
title: img.title
$.extend revived.dataset, img.dataset
$.replace img, revived
return return
if g.DEAD or post.isDead or post.file.isDead if g.DEAD or post.isDead or post.file.isDead
return return
# XXX CORS for images.4chan.org WHEN? # XXX CORS for images.4chan.org WHEN?
$.ajax "//api.4chan.org/#{post.board}/res/#{post.thread}.json", onload: -> $.ajax "//a.4cdn.org/#{post.board}/res/#{post.thread}.json", onload: ->
return if @status isnt 200 return if @status isnt 200
i = 0 i = 0
{posts} = @response {posts} = @response
@ -211,12 +214,22 @@ Gallery =
if postObj.filedeleted if postObj.filedeleted
post.kill true post.kill true
prev: -> Gallery.cb.open.call Gallery.images[+Gallery.nodes.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.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)() 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 @
advance: ->
if Gallery.nodes.current.controls then return
if Gallery.nodes.current.paused then return Gallery.nodes.current.play()
Gallery.cb.next()
pause: ->
{current} = Gallery.nodes
current[if current.paused then 'play' else 'pause']() if current.nodeType is 'VIDEO'
close: -> close: ->
Gallery.nodes.current.pause?()
$.rm Gallery.nodes.el $.rm Gallery.nodes.el
delete Gallery.nodes delete Gallery.nodes
d.body.style.overflow = '' d.body.style.overflow = ''

View File

@ -1,6 +1,6 @@
ImageExpand = ImageExpand =
init: -> init: ->
return if !Conf['Image Expansion'] return if g.VIEW is 'catalog' or !Conf['Image Expansion']
@EAI = $.el 'a', @EAI = $.el 'a',
id: 'img-controls' id: 'img-controls'
@ -17,51 +17,66 @@ ImageExpand =
cb: @node cb: @node
node: -> node: ->
return unless @file and (@file.isImage or @file.isVideo) return unless @file?.isImage or @file?.isVideo
{thumb} = @file {thumb} = @file
$.on thumb.parentNode, 'click', ImageExpand.cb.toggle $.on thumb.parentNode, 'click', ImageExpand.cb.toggle
if @isClone and $.hasClass thumb, 'expanding' if @isClone
# If we clone a post where the image is still loading, if $.hasClass thumb, 'expanding'
# make it loading in the clone too. # If we clone a post where the image is still loading,
ImageExpand.contract @ # make it loading in the clone too.
ImageExpand.expand @ ImageExpand.contract @
ImageExpand.expand @
else if @file.isExpanded and @file.isVideo
clone = @
ImageExpand.setupVideoControls clone
unless clone.origin.file.fullImage.paused
$.queueTask -> Video.start clone.file.fullImage
return return
if ImageExpand.on and !@isHidden and (Conf['Expand spoilers'] or !@file.isSpoiler)
ImageExpand.expand @ else if ImageExpand.on and !@isHidden and
(Conf['Expand spoilers'] or !@file.isSpoiler) and
(Conf['Expand videos'] or !@file.isVideo)
ImageExpand.expand @, null, true
cb: cb:
toggle: (e) -> toggle: (e) ->
return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or e.button isnt 0 return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or e.button isnt 0
post = Get.postFromNode @
return if post.file.isExpanded and post.file.fullImage?.controls
e.preventDefault() e.preventDefault()
ImageExpand.toggle Get.postFromNode @ ImageExpand.toggle post
toggleAll: -> toggleAll: ->
$.event 'CloseMenu' $.event 'CloseMenu'
toggle = (post) ->
{file} = post
return unless file and (file.isImage or file.isVideo) and doc.contains post.nodes.root
if ImageExpand.on and
(!Conf['Expand spoilers'] and file.isSpoiler or
!Conf['Expand videos'] and file.isVideo or
Conf['Expand from here'] and Header.getTopOf(file.thumb) < 0)
return
$.queueTask func, post
if ImageExpand.on = $.hasClass ImageExpand.EAI, 'expand-all-shortcut' if ImageExpand.on = $.hasClass ImageExpand.EAI, 'expand-all-shortcut'
ImageExpand.EAI.className = 'contract-all-shortcut a-icon' ImageExpand.EAI.className = 'contract-all-shortcut a-icon'
ImageExpand.EAI.title = 'Contract All Images' ImageExpand.EAI.title = 'Contract All Images'
func = ImageExpand.expand func = (post) -> ImageExpand.expand post, null, true
else else
ImageExpand.EAI.className = 'expand-all-shortcut a-icon' ImageExpand.EAI.className = 'expand-all-shortcut a-icon'
ImageExpand.EAI.title = 'Expand All Images' ImageExpand.EAI.title = 'Expand All Images'
func = ImageExpand.contract func = ImageExpand.contract
g.posts.forEach (post) -> g.posts.forEach (post) ->
for post in [post].concat post.clones toggle post for post in [post, post.clones...]
{file} = post
return unless file and (file.isImage or file.isVideo) and doc.contains post.nodes.root
if ImageExpand.on and !post.isHidden and
(!Conf['Expand spoilers'] and file.isSpoiler or
Conf['Expand from here'] and Header.getTopOf(file.thumb) < 0)
return
$.queueTask func, post
return return
setFitness: -> setFitness: ->
(if @checked then $.addClass else $.rmClass) doc, @name.toLowerCase().replace /\s+/g, '-' (if @checked then $.addClass else $.rmClass) doc, @name.toLowerCase().replace /\s+/g, '-'
toggle: (post) -> toggle: (post) ->
{thumb} = post.file unless post.file.isExpanded or $.hasClass post.file.thumb, 'expanding'
unless post.file.isExpanded or $.hasClass thumb, 'expanding'
ImageExpand.expand post ImageExpand.expand post
return return
@ -90,87 +105,94 @@ ImageExpand =
ImageExpand.contract post ImageExpand.contract post
contract: (post) -> contract: (post) ->
{thumb} = post.file
if post.file.isVideo and video = post.file.fullImage
video.pause()
TrashQueue.add video, post
thumb.parentNode.href = video.src
thumb.parentNode.target = '_blank'
for eventName, cb of ImageExpand.videoCB
$.off video, eventName, cb
$.rm post.file.videoControls
delete post.file.videoControls
$.rmClass post.nodes.root, 'expanded-image' $.rmClass post.nodes.root, 'expanded-image'
$.rmClass post.file.thumb, 'expanding' $.rmClass thumb, 'expanding'
post.file.isExpanded = false post.file.isExpanded = false
post.file.fullImage.pause() if post.file.isVideo
post.file.videoControls?.map($.rm)
delete post.file.videoControls
expand: (post, src) -> expand: (post, src, disableAutoplay) ->
# Do not expand images of hidden/filtered replies, or already expanded pictures. # Do not expand images of hidden/filtered replies, or already expanded pictures.
{thumb, isVideo} = post.file {thumb, isVideo} = post.file
return if post.isHidden or post.file.isExpanded or $.hasClass thumb, 'expanding' return if post.isHidden or post.file.isExpanded or $.hasClass thumb, 'expanding'
$.addClass thumb, 'expanding' $.addClass thumb, 'expanding'
if post.file.fullImage if el = post.file.fullImage
# Expand already-loaded/ing picture. # Expand already-loaded/ing picture.
$.asap (-> (file = post.file.fullImage) and (file.videoHeight or file.naturalHeight)), -> TrashQueue.remove el
ImageExpand.completeExpand post else
return el = post.file.fullImage = $.el (if isVideo then 'video' else 'img'),
file =
$.el (if post.file.isImage then 'img' else 'video'),
className: 'full-image' className: 'full-image'
src: src or post.file.URL $.on el, 'error', ImageExpand.error
post.file.fullImage = file el.src = src or post.file.URL
if isVideo
file.loop = true
file.controls = Conf['Show Controls']
$.on file, 'error', ImageExpand.error
$.asap (-> (file = post.file.fullImage) and (file.videoHeight or file.naturalHeight)), ->
if isVideo if isVideo
# XXX Firefox doesn't seem to size videos correctly? el.loop = true
file.style.maxHeight = file.videoHeight + "px" $.after thumb, el unless el is thumb.nextSibling
file.style.maxWidth = file.videoWidth + "px" $.asap (-> el.videoHeight or el.naturalHeight), ->
ImageExpand.completeExpand post ImageExpand.completeExpand post, disableAutoplay
$.after (if file.controls then thumb.parentNode else thumb), file
completeExpand: (post) -> completeExpand: (post, disableAutoplay) ->
{thumb} = post.file return unless $.hasClass post.file.thumb, 'expanding' # contracted before the image loaded
return unless $.hasClass thumb, 'expanding' # contracted before the image loaded
post.file.isExpanded = true
ImageExpand.setupVideo post if post.file.isVideo
$.addClass post.nodes.root, 'expanded-image'
$.rmClass post.file.thumb, 'expanding'
unless post.nodes.root.parentNode unless post.nodes.root.parentNode
# Image might start/finish loading before the post is inserted. # Image might start/finish loading before the post is inserted.
# Don't scroll when it's expanded in a QP for example. # Don't scroll when it's expanded in a QP for example.
ImageExpand.completeExpand2 post
return return
{bottom} = post.nodes.root.getBoundingClientRect() {bottom} = post.nodes.root.getBoundingClientRect()
$.queueTask -> $.queueTask ->
ImageExpand.completeExpand2 post, disableAutoplay
return unless bottom <= 0 return unless bottom <= 0
window.scrollBy 0, post.nodes.root.getBoundingClientRect().bottom - bottom window.scrollBy 0, post.nodes.root.getBoundingClientRect().bottom - bottom
setupVideo: (post) -> completeExpand2: (post, disableAutoplay) ->
{file} = post $.addClass post.nodes.root, 'expanded-image'
video = file.fullImage $.rmClass post.file.thumb, 'expanding'
file.videoControls = [] post.file.isExpanded = true
video.muted = not Conf['Allow Sound'] if post.file.isVideo
if video.controls ImageExpand.setupVideoControls post
# contract link in file info Video.configure post.file.fullImage, disableAutoplay
videoCB: do ->
# dragging to the left contracts the video
mousedown = false
mouseover: -> mousedown = false
mousedown: (e) -> mousedown = true if e.button is 0
mouseup: (e) -> mousedown = false if e.button is 0
mouseout: (e) -> ImageExpand.contract(Get.postFromNode @) if mousedown and e.clientX <= @getBoundingClientRect().left
click: (e) ->
if @paused and not @controls
@play()
e.preventDefault()
setupVideoControls: (post) ->
{file} = post
{thumb} = file
video = file.fullImage
# disable link to file so native controls can work
file.thumb.parentNode.removeAttribute 'href'
file.thumb.parentNode.removeAttribute 'target'
# setup callbacks on video element
$.on video, eventName, cb for eventName, cb of ImageExpand.videoCB
# setup controls in file info
file.videoControls = $.el 'span',
className: 'video-controls'
if Conf['Show Controls']
contract = $.el 'a', contract = $.el 'a',
textContent: 'contract' textContent: 'contract'
href: 'javascript:;' href: 'javascript:;'
title: 'You can also contract the video by dragging it to the left.' title: 'You can also contract the video by dragging it to the left.'
$.on contract, 'click', (e) -> ImageExpand.contract post $.on contract, 'click', (e) -> ImageExpand.contract post
file.videoControls.push $.tn('\u00A0'), contract $.add file.videoControls, [$.tn('\u00A0'), contract]
# drag left to contract
file.mousedown = false
$.on video, 'mousedown', (e) -> file.mousedown = true if e.button is 0
$.on video, 'mouseup', (e) -> file.mousedown = false if e.button is 0
$.on video, 'mouseover', (e) -> file.mousedown = false
$.on video, 'mouseout', (e) ->
if file.mousedown and e.clientX <= video.getBoundingClientRect().left
ImageExpand.contract post
if Conf['Autoplay']
video.play()
else unless video.controls
play = $.el 'a',
textContent: 'play'
href: 'javascript:;'
$.on play, 'click', (e) ->
video[@textContent]()
@textContent = if @textContent is 'play' then 'pause' else 'play'
file.videoControls.push $.tn('\u00A0'), play
$.add file.text, file.videoControls $.add file.text, file.videoControls
error: -> error: ->
@ -198,7 +220,7 @@ ImageExpand =
timeoutID = setTimeout ImageExpand.expand, 10000, post timeoutID = setTimeout ImageExpand.expand, 10000, post
<% if (type === 'crx') { %> <% if (type === 'crx') { %>
$.ajax post.file.URL, $.ajax @src,
onloadend: -> onloadend: ->
return if @status isnt 404 return if @status isnt 404
clearTimeout timeoutID clearTimeout timeoutID
@ -221,7 +243,7 @@ ImageExpand =
menu: menu:
init: -> init: ->
return if !Conf['Image Expansion'] return if g.VIEW is 'catalog' or !Conf['Image Expansion']
el = $.el 'span', el = $.el 'span',
textContent: 'Image Expansion' textContent: 'Image Expansion'

View File

@ -9,36 +9,50 @@ ImageHover =
name: 'Image Hover' name: 'Image Hover'
cb: @catalogNode cb: @catalogNode
node: -> node: ->
return unless @file and (@file.isImage or @file.isVideo) return unless @file?.isImage or @file?.isVideo
$.on @file.thumb, 'mouseover', ImageHover.mouseover $.on @file.thumb, 'mouseover', ImageHover.mouseover
catalogNode: -> catalogNode: ->
return unless @thread.OP.file and (@thread.OP.file.isImage or @thread.OP.file.isVideo) return unless (file = @thread.OP.file) and (file.isImage or file.isVideo)
$.on @nodes.thumb, 'mouseover', ImageHover.mouseover $.on @nodes.thumb, 'mouseover', ImageHover.mouseover
mouseover: (e) -> mouseover: (e) ->
post = if $.hasClass @, 'thumb' post = if $.hasClass @, 'thumb'
g.posts[@parentNode.dataset.fullID] g.posts[@parentNode.dataset.fullID]
else else
Get.postFromNode @ Get.postFromNode @
el = if post.file.isImage {isVideo} = post.file
$.el 'img', if post.file.fullImage
id: 'ihover' el = post.file.fullImage
src: post.file.URL TrashQueue.remove el
else else
$.el 'video', el = $.el (if isVideo then 'video' else 'img'),
controls: false className: 'full-image'
id: 'ihover'
src: post.file.URL src: post.file.URL
autoplay: Conf['Autoplay'] post.file.fullImage = el
muted: !Conf['Allow Sound'] {thumb} = post.file
loop: true
$.add Header.hover, el if d.body.contains thumb
$.after thumb, el unless el is thumb.nextSibling
else
$.add Header.hover, el if el.parentNode isnt Header.hover
el.id = 'ihover'
el.dataset.fullID = post.fullID el.dataset.fullID = post.fullID
if isVideo
el.loop = true
el.controls = false
el.muted = not Conf['Allow Sound']
el.play() if Conf['Autoplay']
UI.hover UI.hover
root: @ root: @
el: el el: el
latestEvent: e latestEvent: e
endEvents: 'mouseout click' endEvents: 'mouseout click'
asapTest: -> post.file.isVideo or el.naturalHeight asapTest: -> (el.videoHeight or el.naturalHeight)
noRemove: true
cb: ->
if isVideo
el.pause()
TrashQueue.add el, post
el.removeAttribute 'id'
$.on el, 'error', ImageHover.error $.on el, 'error', ImageHover.error
error: -> error: ->
return unless doc.contains @ return unless doc.contains @

View File

@ -1,6 +1,6 @@
ImageLoader = ImageLoader =
init: -> init: ->
return unless Conf["Image Prefetching"] or Conf["Replace JPG"] or Conf["Replace PNG"] or Conf["Replace GIF"] return unless Conf["Image Prefetching"] or Conf["Replace JPG"] or Conf["Replace PNG"] or Conf["Replace GIF"] or Conf["Replace WEBM"]
Post.callbacks.push Post.callbacks.push
name: 'Image Replace' name: 'Image Replace'
@ -10,8 +10,6 @@ ImageLoader =
name: 'Image Replace' name: 'Image Replace'
cb: @thread cb: @thread
return unless Conf['Image Prefetching'] and g.VIEW is 'thread'
prefetch = $.el 'label', prefetch = $.el 'label',
innerHTML: '<input type=checkbox name="prefetch"> Prefetch Images' innerHTML: '<input type=checkbox name="prefetch"> Prefetch Images'
@ -27,22 +25,44 @@ ImageLoader =
ImageLoader.thread = @ ImageLoader.thread = @
node: -> node: ->
return if @isClone or @isHidden or @thread.isHidden or !@file?.isImage return unless @file
{isImage, isVideo} = @file
return if @isClone or @isHidden or @thread.isHidden or !(isImage or isVideo)
{thumb, URL} = @file {thumb, URL} = @file
return unless (Conf[string = "Replace #{if (type = (URL.match /\w{3}$/)[0].toUpperCase()) is 'PEG' then 'JPG' else type}"] and !/spoiler/.test thumb.src) or Conf['prefetch'] {style} = thumb
type = if (match = URL.match(/\.([^.]+)$/)[1].toUpperCase()) is 'JPEG' then 'JPG' else match
replace = "Replace #{type}"
return unless (Conf[replace] and !/spoiler/.test thumb.src) or (Conf['prefetch'] and g.VIEW is 'thread')
if @file.isSpoiler if @file.isSpoiler
# Revealed spoilers do not have height/width set, this fixes the image's dimensions. # Revealed spoilers do not have height/width set, this fixes the image's dimensions.
{style} = thumb
style.maxHeight = style.maxWidth = if @isReply then '125px' else '250px' style.maxHeight = style.maxWidth = if @isReply then '125px' else '250px'
img = $.el 'img' file = $.el if isImage then 'img' else 'video'
if Conf[string] if Conf[replace]
$.on img, 'load', -> if isVideo
# Replace the thumbnail once the GIF has finished loading.
file.alt = thumb.alt
file.dataset.md5 = thumb.dataset.md5
file.style.height = style.height
file.style.width = style.width
file.style.maxHeight = style.maxHeight
file.style.maxWidth = style.maxWidth
file.loop = true
file.autoplay = Conf['Autoplay']
if Conf['Image Hover']
$.on file, 'mouseover', ImageHover.mouseover
cb = =>
$.off file, 'load loadedmetadata', cb
# Replace the thumbnail once the file has finished loading.
if isVideo
$.replace thumb, file
@file.thumb = file # XXX expanding requires the post.file.thumb node.
return
thumb.src = URL thumb.src = URL
img.src = URL $.on file, 'load loadedmetadata', cb
file.src = URL
toggle: -> toggle: ->
enabled = Conf['prefetch'] = @checked enabled = Conf['prefetch'] = @checked
if enabled if enabled
ImageLoader.thread.posts.forEach ImageLoader.node.call g.BOARD.posts.forEach ImageLoader.node.call
return return

View File

@ -0,0 +1,14 @@
TrashQueue =
init: -> return
add: (video, post) ->
if @killNext and video isnt @killNext
delete @killNextPost?.file?.fullImage
$.rm @killNext
@killNext = video
@killNextPost = post
remove: (video) ->
if video is @killNext
delete @killNext

21
src/Images/Video.coffee Normal file
View File

@ -0,0 +1,21 @@
Video =
configure: (video, disableAutoplay) ->
video.loop = true
video.muted = !Conf['Allow Sound']
video.controls = Conf['Show Controls']
video.autoplay = false
if Conf['Autoplay'] and not disableAutoplay
Video.start video
else
video.pause()
start: (video) ->
{controls} = video
video.controls = false
video.play()
# Hacky workaround for Firefox forever-loading bug for very short videos
if controls
$.asap (-> (video.readyState >= 3 and video.currentTime <= Math.max 0.1, (video.duration - 0.5)) or !d.contains video), ->
video.controls = true
, 500

View File

@ -8,7 +8,7 @@ ExpandThread =
return unless summary = $.x 'following-sibling::*[contains(@class,"summary")][1]', thread.OP.nodes.root return unless summary = $.x 'following-sibling::*[contains(@class,"summary")][1]', thread.OP.nodes.root
a = $.el 'a', a = $.el 'a',
textContent: ExpandThread.text '+', summary.textContent.match(/\d+/g)... textContent: ExpandThread.text '+', summary.textContent.match(/\d+/g)...
href: "#{thread.board.ID}/res/#{thread.ID}" href: "res/#{thread.ID}"
className: 'summary' className: 'summary'
$.on a, 'click', ExpandThread.cbToggle $.on a, 'click', ExpandThread.cbToggle
$.replace summary, a $.replace summary, a

View File

@ -299,13 +299,11 @@ QR =
blob.name = url.substr(url.lastIndexOf('/')+1, url.length) blob.name = url.substr(url.lastIndexOf('/')+1, url.length)
name_start = header.indexOf('name="') + 6 name_start = header.indexOf('name="') + 6
if (name_start - 6 != -1) if (name_start - 6 != -1)
name_end = header.substr(name_start, header.length).indexOf('"') name_end = header.substr(name_start, header.length).indexOf('"')
blob.name = header.substr(name_start, name_end) blob.name = header.substr(name_start, name_end)
return if blob.type is null return if blob.type is null
QR.error "Unsupported file type." QR.error "Unsupported file type."
return unless blob.type in QR.mimeTypes
QR.error "Unsupported file type."
QR.handleFiles([blob]) QR.handleFiles([blob])
handleUrl: -> handleUrl: ->
@ -778,6 +776,7 @@ QR =
QR.cooldown.auto = false QR.cooldown.auto = false
QR.status() QR.status()
QR.error err QR.error err
QR.captcha.setup() if QR.captcha.isEnabled
return return
h1 = $ 'h1', resDoc h1 = $ 'h1', resDoc
@ -812,10 +811,13 @@ QR =
# Enable auto-posting if we have stuff left to post, disable it otherwise. # Enable auto-posting if we have stuff left to post, disable it otherwise.
postsCount = QR.posts.length - 1 postsCount = QR.posts.length - 1
QR.cooldown.auto = postsCount and isReply QR.cooldown.auto = postsCount and isReply
QR.captcha.setup() if QR.captcha.isEnabled and QR.cooldown.auto
unless Conf['Persistent QR'] or QR.cooldown.auto unless Conf['Persistent QR'] or QR.cooldown.auto
QR.close() QR.close()
else else
if QR.posts.length > 1
QR.captcha.setup()
post.rm() post.rm()
QR.cooldown.set {req, post, isReply, threadID} QR.cooldown.set {req, post, isReply, threadID}

View File

@ -80,8 +80,6 @@ QR.post = class
$.rmClass QR.nodes.el, 'dump' $.rmClass QR.nodes.el, 'dump'
else if @ is QR.selected else if @ is QR.selected
(QR.posts[index-1] or QR.posts[index+1]).select() (QR.posts[index-1] or QR.posts[index+1]).select()
if QR.captcha.isEnabled
QR.captcha.setup()
QR.posts.splice index, 1 QR.posts.splice index, 1
QR.status() QR.status()
delete: -> delete: ->