Merge branch 'master' into postjumper
This commit is contained in:
commit
1b9378e809
81
CHANGELOG.md
81
CHANGELOG.md
@ -2,8 +2,89 @@
|
||||
|
||||
-Sometimes the changelog has notes (not comprehensive) acknowledging people's work. This does not mean the changes are their fault, only that their code was used. All changes to the script are chosen by and the fault of the maintainer (ccd0).
|
||||
|
||||
### v1.14.7
|
||||
|
||||
**v1.14.7.2** *(2019-04-11)* - [[Userscript](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.7.2/builds/4chan-X-noupdate.user.js)] [[Chrome extension](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.7.2/builds/4chan-X-noupdate.crx)]
|
||||
- Fix dragging left to contract WebMs in Firefox. #1547
|
||||
- Remove query string from filename in Post from URL feature.
|
||||
- Speed up Post from URL on some platforms.
|
||||
- Fix issue making WebM title fetching needlessly slow on Chrome extension.
|
||||
|
||||
**v1.14.7.1** *(2019-04-09)* - [[Userscript](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.7.1/builds/4chan-X-noupdate.user.js)] [[Chrome extension](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.7.1/builds/4chan-X-noupdate.crx)]
|
||||
- Tolerate broken HTML better.
|
||||
- Fix 4chan/4channel not being correct in certain links.
|
||||
- Use boards.json to determine whether to activate [code] and [math] tag related functions. #525
|
||||
|
||||
**v1.14.7.0** *(2019-04-07)* - [[Userscript](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.7.0/builds/4chan-X-noupdate.user.js)] [[Chrome extension](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.7.0/builds/4chan-X-noupdate.crx)]
|
||||
- Based on v1.14.6.8.
|
||||
- (Teasday) Hotkey to toggle quote threading, `Shift+t` by default.
|
||||
- Show what pages watched threads are on. Can be disabled by unchecking `Show Page` in the thread watcher menu. #1030
|
||||
- Move Thread Watcher settings out of submenu.
|
||||
- Restore filtering on the email field. #2171
|
||||
- Support specifying the sites that filters apply to. #2171
|
||||
- Make per-board filtering work on boards with unusual characters in the name (e.g. certain lainchan boards).
|
||||
- Board names in filters are now case-sensitive.
|
||||
|
||||
### v1.14.6
|
||||
|
||||
**v1.14.6.8** *(2019-04-06)* - [[Userscript](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.6.8/builds/4chan-X-noupdate.user.js)] [[Chrome extension](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.6.8/builds/4chan-X-noupdate.crx)]
|
||||
- Update list of boards on https://catalog.neet.tv/.
|
||||
|
||||
**v1.14.6.7** *(2019-04-05)* - [[Userscript](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.6.7/builds/4chan-X-noupdate.user.js)] [[Chrome extension](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.6.7/builds/4chan-X-noupdate.crx)]
|
||||
- Update .crx files to CRX3. This should fix the errors when attempting to install them on newer versions of Chromium.
|
||||
|
||||
**v1.14.6.6** *(2019-04-05)* - [[Userscript](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.6.6/builds/4chan-X-noupdate.user.js)] [[Chrome extension](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.6.6/builds/4chan-X-noupdate.crx)]
|
||||
- Sauce: Update DeviantArt filename format. #2237
|
||||
- Sauce: Replace unmatched regex groups with empty string, not 'undefined'
|
||||
- Whether to add parameter to avoid cache should be based on site being queried, not site currenly on.
|
||||
|
||||
**v1.14.6.5** *(2019-04-04)* - [[Userscript](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.6.5/builds/4chan-X-noupdate.user.js)] [[Chrome extension](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.6.5/builds/4chan-X-noupdate.crx)]
|
||||
- Fix Thread Watcher bug that in certain circumstances caused the last check of an archived thread for new replies to be skipped.
|
||||
|
||||
**v1.14.6.4** *(2019-04-02)* - [[Userscript](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.6.4/builds/4chan-X-noupdate.user.js)] [[Chrome extension](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.6.4/builds/4chan-X-noupdate.crx)]
|
||||
- Merge v1.14.5.16: Remove score/perks message. Fix Posting Success Notifications.
|
||||
- Merge v1.14.5.16: Remove like buttons. Continue to show like counts and scores where given in API.
|
||||
- Bugfix: Account for posts added by thread expansion when marking read from index.
|
||||
|
||||
**v1.14.6.3** *(2019-04-01)* - [[Userscript](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.6.3/builds/4chan-X-noupdate.user.js)] [[Chrome extension](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.6.3/builds/4chan-X-noupdate.crx)]
|
||||
- Merge v1.14.5.15: Show info relating to April 2019 event. #2266
|
||||
|
||||
**v1.14.6.2** *(2019-03-31)* - [[Userscript](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.6.2/builds/4chan-X-noupdate.user.js)] [[Chrome extension](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.6.2/builds/4chan-X-noupdate.crx)]
|
||||
- Support filters that apply to multiple post fields joined by newline characters.
|
||||
|
||||
**v1.14.6.1** *(2019-03-30)* - [[Userscript](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.6.1/builds/4chan-X-noupdate.user.js)] [[Chrome extension](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.6.1/builds/4chan-X-noupdate.crx)]
|
||||
- Fix errors in certain userscript managers introduced in v1.14.6.0. #2256
|
||||
|
||||
**v1.14.6.0** *(2019-03-25)* - [[Userscript](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.6.0/builds/4chan-X-noupdate.user.js)] [[Chrome extension](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.6.0/builds/4chan-X-noupdate.crx)]
|
||||
- Based on v1.14.5.14.
|
||||
- (ebinBuddha) Added desktop notification for filters (`notify` option).
|
||||
- Make it possible to filter posts without ID (use `//`). #1578
|
||||
- Add `file` option to filter only posts with/without files.
|
||||
- Improvements in Thread Watcher efficiency, particularly when using it with multiple sites.
|
||||
- Allow image hover previews to use full width of screen even in cases where it covers the thumbnail.
|
||||
- Make movement of image hover / quote preview with mouse optional; option is `Follow Cursor`. #471, #2245
|
||||
- Fix image/video hover in case where dimensions are not available. #2197
|
||||
- Implement pruning of data for dead threads on vichan sites with JSON API. #2171
|
||||
- Override 4chan CSS causing sauce links to get cut off. #2193
|
||||
- Change export URL from data: to blob: so larger settings files can be exported. #2255
|
||||
- Unbreak warning in Chrome extension to reload the page after an update.
|
||||
- Various minor bugfixes.
|
||||
|
||||
### v1.14.5
|
||||
|
||||
**v1.14.5.16** *(2019-04-02)* - [[Userscript](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.5.16/builds/4chan-X-noupdate.user.js)] [[Chrome extension](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.5.16/builds/4chan-X-noupdate.crx)]
|
||||
- Remove score/perks message. Fix Posting Success Notifications.
|
||||
- Remove like buttons. Continue to show like counts and scores where given in API.
|
||||
|
||||
**v1.14.5.15** *(2019-04-01)* - [[Userscript](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.5.15/builds/4chan-X-noupdate.user.js)] [[Chrome extension](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.5.15/builds/4chan-X-noupdate.crx)]
|
||||
- Show info relating to April 2019 event. #2266
|
||||
- Override 4chan CSS causing sauce links to get cut off. #2193
|
||||
- Unbreak warning in Chrome extension to reload the page after an update.
|
||||
|
||||
**v1.14.5.14** *(2019-03-22)* - [[Userscript](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.5.14/builds/4chan-X-noupdate.user.js)] [[Chrome extension](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.5.14/builds/4chan-X-noupdate.crx)]
|
||||
- Add message alerting Chrome extension users to disable chrome://flags/#network-service
|
||||
- Minor bugfix in catalog/index loading.
|
||||
|
||||
**v1.14.5.13** *(2019-03-08)* - [[Userscript](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.5.13/builds/4chan-X-noupdate.user.js)] [[Chrome extension](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.5.13/builds/4chan-X-noupdate.crx)]
|
||||
- Fix bugs related to additional permissions requests. #2230
|
||||
- Revert changes in thread watcher that caused performance decrease.
|
||||
|
||||
@ -30,7 +30,8 @@ If you're reporting a bug, the more detail you can give, the better. If I can't
|
||||
|
||||
- 4chan X is mostly written in [CoffeeScript](http://coffeescript.org/). If you're already familiar with Javascript, it doesn't take long to pick up.
|
||||
- Edit the sources in the src/ directory (not the compiled scripts in builds/).
|
||||
- Compile the script with: `make` (this should fetch needed dependencies automatically; if not, do an `npm install` first)
|
||||
- Fetch needed dependencies with: `npm install`
|
||||
- Compile the script with: `make`
|
||||
- Install the compiled script (found in the testbuilds/ directory), and test your changes.
|
||||
- Make sure you have set your name and email as you want them, as they will be published in your commit message:<br>`git config user.name yourname`<br>`git config user.email youremail`
|
||||
- Commit your changes: `git commit -a`
|
||||
|
||||
65
Makefile
65
Makefile
@ -15,17 +15,16 @@ else
|
||||
endif
|
||||
CP = $(call CAT,$<,$@)
|
||||
|
||||
npgoals := clean cleanrel cleanweb cleanfull withtests wrapped archives $(foreach i,1 2 3 4,bump$(i)) tag tagcommit beta stable web update updatehard
|
||||
npgoals := clean cleanrel cleanweb cleanfull withtests archives $(foreach i,1 2 3 4,bump$(i)) tag tagcommit beta stable web update updatehard
|
||||
ifneq "$(filter $(npgoals),$(MAKECMDGOALS))" ""
|
||||
.NOTPARALLEL :
|
||||
endif
|
||||
|
||||
coffee := $(BIN)coffee -c --no-header
|
||||
coffee_deps := node_modules/coffee-script/package.json
|
||||
template := node tools/template.js
|
||||
template_deps := package.json tools/template.js node_modules/lodash.template/package.json node_modules/esprima/package.json
|
||||
template_deps := package.json tools/template.js
|
||||
|
||||
# read name meta_name meta_distBranch meta_uploadPath
|
||||
# read name meta_name meta_distBranch
|
||||
$(eval $(shell node tools/pkgvars.js))
|
||||
|
||||
# must be read in when needed to prevent out-of-date version
|
||||
@ -55,7 +54,7 @@ uses_tests_enabled := \
|
||||
imports_src/globals/globals.js := \
|
||||
version.json
|
||||
imports_src/css/CSS.js := \
|
||||
node_modules/font-awesome/package.json
|
||||
node_modules/font-awesome/fonts/fontawesome-webfont.woff
|
||||
imports_src/Monitoring/Favicon.coffee := \
|
||||
src/meta/icon128.png
|
||||
|
||||
@ -104,22 +103,6 @@ all : default release
|
||||
.events .events2 tmp testbuilds builds :
|
||||
$(MKDIR)
|
||||
|
||||
ifneq "$(wildcard npm-shrinkwrap.json)" ""
|
||||
|
||||
.events/npm : npm-shrinkwrap.json | .events
|
||||
npm install
|
||||
echo -> $@
|
||||
|
||||
node_modules/%/package.json : .events/npm
|
||||
$(if $(wildcard $@),,npm install && echo -> $<)
|
||||
|
||||
else
|
||||
|
||||
node_modules/%/package.json : package.json
|
||||
npm install $(call QUOTE,$*@$(version_$*))
|
||||
|
||||
endif
|
||||
|
||||
.tests_enabled :
|
||||
echo false> .tests_enabled
|
||||
|
||||
@ -137,7 +120,7 @@ endef
|
||||
|
||||
$(foreach s,$(sources),$(eval $(call check_source,$(subst $$,$$$$,$(s)))))
|
||||
|
||||
.events/compile : $(updates) $(template_deps) $(coffee_deps) tools/chain.js
|
||||
.events/compile : $(updates) $(template_deps) tools/chain.js
|
||||
node tools/chain.js $(call QUOTE, \
|
||||
$(subst .events/,tmp/, \
|
||||
$(if $(filter-out $(updates),$?), \
|
||||
@ -154,7 +137,7 @@ $(dests) : .events/compile
|
||||
&& echo -> $< \
|
||||
)
|
||||
|
||||
tmp/eventPage.js : src/meta/eventPage.coffee $(coffee_deps) | tmp
|
||||
tmp/eventPage.js : src/meta/eventPage.coffee | tmp
|
||||
$(coffee) -o tmp src/meta/eventPage.coffee
|
||||
|
||||
tmp/LICENSE : LICENSE tools/newlinefix.js | tmp
|
||||
@ -189,11 +172,11 @@ testbuilds/updates$1.json : src/meta/updates.json version.json $(template_deps)
|
||||
|
||||
testbuilds/$(name)$1.crx.zip : \
|
||||
$(foreach f,$(crx_contents),testbuilds/crx$1/$(f)) \
|
||||
package.json version.json tools/zip-crx.js node_modules/jszip/package.json
|
||||
package.json version.json tools/zip-crx.js
|
||||
node tools/zip-crx.js $1
|
||||
|
||||
testbuilds/$(name)$1.crx : testbuilds/$(name)$1.crx.zip package.json tools/sign.js node_modules/node-rsa/package.json
|
||||
node tools/sign.js $1
|
||||
testbuilds/$(name)$1.crx : $(foreach f,$(crx_contents),testbuilds/crx$1/$(f)) version.json tools/sign.sh | tmp
|
||||
tools/sign.sh $1
|
||||
|
||||
testbuilds/$(name)$1.meta.js : src/meta/metadata.js src/meta/icon48.png version.json src/Archive/archives.json $(template_deps) | testbuilds
|
||||
$(template) $$< $$@ type=userscript channel=$1
|
||||
@ -214,7 +197,7 @@ testbuilds/$(name).zip : testbuilds/$(name)-noupdate.crx.zip
|
||||
builds/% : testbuilds/% | builds
|
||||
$(CP)
|
||||
|
||||
test.html : README.md template.jst tools/markdown.js node_modules/markdown-it/package.json node_modules/markdown-it-anchor/package.json node_modules/lodash.template/package.json
|
||||
test.html : README.md template.jst tools/markdown.js
|
||||
node tools/markdown.js
|
||||
|
||||
index.html : test.html
|
||||
@ -223,7 +206,7 @@ index.html : test.html
|
||||
tmp/.jshintrc : src/meta/jshint.json tmp/declaration.js src/globals/globals.js $(template_deps) | tmp
|
||||
$(template) $< $@
|
||||
|
||||
.events/jshint : $(dests) tmp/.jshintrc node_modules/jshint/package.json
|
||||
.events/jshint : $(dests) tmp/.jshintrc
|
||||
$(BIN)jshint $(call QUOTE, \
|
||||
$(if $(filter-out $(dests),$?), \
|
||||
$(dests), \
|
||||
@ -263,13 +246,13 @@ distready : dist $(wildcard dist/* dist/*/*)
|
||||
git push web $(meta_distBranch)
|
||||
echo -> $@
|
||||
|
||||
.events2/push-store : .git/refs/tags/stable | .events2 distready node_modules/webstore-upload/package.json node_modules/request/package.json
|
||||
.events2/push-store : .git/refs/tags/stable | .events2 distready
|
||||
node tools/webstore.js
|
||||
echo -> $@
|
||||
|
||||
.SECONDARY :
|
||||
|
||||
.PHONY: default all distready script crx release jshint install push captchas $(npgoals)
|
||||
.PHONY: default all distready script crx release jshint install push $(npgoals)
|
||||
|
||||
script : $(script)
|
||||
|
||||
@ -283,23 +266,18 @@ install : .events/install
|
||||
|
||||
push : .events2/push-git .events2/push-web .events2/push-store
|
||||
|
||||
captchas : redirect.html $(template_deps)
|
||||
$(template) redirect.html captchas.html url="$(url)"
|
||||
scp captchas.html $(meta_uploadPath)
|
||||
|
||||
clean :
|
||||
$(RMDIR) tmp testbuilds .events
|
||||
$(RMDIR) tmp tmp-crx testbuilds .events
|
||||
$(RM) .tests_enabled
|
||||
|
||||
cleanrel : clean
|
||||
$(RMDIR) builds
|
||||
|
||||
cleanweb :
|
||||
$(RM) test.html captchas.html
|
||||
$(RM) test.html
|
||||
|
||||
cleanfull : clean cleanweb
|
||||
$(RMDIR) .events2 dist node_modules
|
||||
$(RM) npm-shrinkwrap.json
|
||||
git worktree prune
|
||||
|
||||
withtests :
|
||||
@ -307,10 +285,6 @@ withtests :
|
||||
-$(MAKE)
|
||||
echo false> .tests_enabled
|
||||
|
||||
wrapped : src/meta/npm-shrinkwrap.json
|
||||
$(call CAT,$<,npm-shrinkwrap.json)
|
||||
npm install
|
||||
|
||||
archives :
|
||||
git fetch -n archives
|
||||
git merge --no-commit -s ours archives/gh-pages
|
||||
@ -326,7 +300,6 @@ $(foreach i,1 2 3 4,bump$(i)) :
|
||||
tag :
|
||||
git add builds
|
||||
$(MAKE) cleanrel
|
||||
$(MAKE) wrapped
|
||||
$(MAKE) all
|
||||
git diff --quiet -- builds
|
||||
$(MAKE) tagcommit
|
||||
@ -355,15 +328,11 @@ web : index.html distready
|
||||
cd dist && git commit -am "Update web page."
|
||||
|
||||
update :
|
||||
$(RM) npm-shrinkwrap.json
|
||||
$(RM) package-lock.json
|
||||
npm install --save-dev $(shell node tools/unpinned.js)
|
||||
npm install
|
||||
npm shrinkwrap --dev
|
||||
$(call CAT,npm-shrinkwrap.json,src/meta/npm-shrinkwrap.json)
|
||||
|
||||
updatehard :
|
||||
$(RM) npm-shrinkwrap.json
|
||||
$(RM) package-lock.json
|
||||
npm install --save-dev $(shell node tools/unpinned.js latest)
|
||||
npm install
|
||||
npm shrinkwrap --dev
|
||||
$(call CAT,npm-shrinkwrap.json,src/meta/npm-shrinkwrap.json)
|
||||
|
||||
@ -15,7 +15,7 @@ https://github.com/KevinParnell/OneeChan.
|
||||
## Install
|
||||
|
||||
### Firefox
|
||||
Install [Greasemonkey](https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/), [Violentmonkey](https://addons.mozilla.org/en-US/firefox/addon/violentmonkey/) or [Tampermonkey](https://addons.mozilla.org/en-US/firefox/addon/tampermonkey/), then **[click here to install 4chan X](https://www.4chan-x.net/builds/4chan-X.user.js)**.
|
||||
Install [Violentmonkey](https://addons.mozilla.org/en-US/firefox/addon/violentmonkey/), [Tampermonkey](https://addons.mozilla.org/en-US/firefox/addon/tampermonkey/), or [Greasemonkey](https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/) (issues since v4: [#2526](https://github.com/greasemonkey/greasemonkey/issues/2526), [#2576](https://github.com/greasemonkey/greasemonkey/issues/2574)), then **[click here to install 4chan X](https://www.4chan-x.net/builds/4chan-X.user.js)**.
|
||||
|
||||
Ports of Greasemonkey are available for [SeaMonkey](https://sourceforge.net/projects/gmport/) and [Pale Moon](https://github.com/janekptacijarabaci/greasemonkey/releases/latest).
|
||||
|
||||
|
||||
Binary file not shown.
@ -1,6 +1,6 @@
|
||||
// ==UserScript==
|
||||
// @name 4chan X beta
|
||||
// @version 1.14.5.13
|
||||
// @version 1.14.7.2
|
||||
// @minGMVer 1.14
|
||||
// @minFFVer 26
|
||||
// @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
Binary file not shown.
@ -1,6 +1,6 @@
|
||||
// ==UserScript==
|
||||
// @name 4chan X
|
||||
// @version 1.14.5.13
|
||||
// @version 1.14.7.2
|
||||
// @minGMVer 1.14
|
||||
// @minFFVer 26
|
||||
// @namespace 4chan-X
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -3,7 +3,7 @@
|
||||
"4chan-x@4chan-x.net": {
|
||||
"updates": [
|
||||
{
|
||||
"version": "1.14.5.13",
|
||||
"version": "1.14.7.2",
|
||||
"update_link": "https://www.4chan-x.net/builds/4chan-X-beta.crx"
|
||||
}
|
||||
]
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<gupdate xmlns='http://www.google.com/update2/response' protocol='2.0'>
|
||||
<app appid='lacclbnghgdicfifcamcmcnilckjamag'>
|
||||
<updatecheck codebase='https://www.4chan-x.net/builds/4chan-X-beta.crx' version='1.14.5.13' />
|
||||
<updatecheck codebase='https://www.4chan-x.net/builds/4chan-X-beta.crx' version='1.14.7.2' />
|
||||
</app>
|
||||
</gupdate>
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
"4chan-x@4chan-x.net": {
|
||||
"updates": [
|
||||
{
|
||||
"version": "1.14.5.13",
|
||||
"version": "1.14.7.2",
|
||||
"update_link": "https://www.4chan-x.net/builds/4chan-X.crx"
|
||||
}
|
||||
]
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<gupdate xmlns='http://www.google.com/update2/response' protocol='2.0'>
|
||||
<app appid='lacclbnghgdicfifcamcmcnilckjamag'>
|
||||
<updatecheck codebase='https://www.4chan-x.net/builds/4chan-X.crx' version='1.14.5.13' />
|
||||
<updatecheck codebase='https://www.4chan-x.net/builds/4chan-X.crx' version='1.14.7.2' />
|
||||
</app>
|
||||
</gupdate>
|
||||
|
||||
|
||||
1
crx-chromium-version.txt
Normal file
1
crx-chromium-version.txt
Normal file
@ -0,0 +1 @@
|
||||
Chromium 73.0.3683.75 built on Debian buster/sid, running on Debian buster/sid
|
||||
@ -26,7 +26,7 @@
|
||||
<p><strong>Private browsing</strong>: By default, 4chan X remembers your last read post in a thread and which posts were made by you, even if you are in private browsing / incognito mode. If you want to turn this off, uncheck the <code>Remember Last Read Post</code> and <code>Remember Your Posts</code> options in the settings panel. You can clear all 4chan browsing history saved by 4chan X by resetting your settings.</p>
|
||||
<h2 id="install">Install</h2>
|
||||
<input hidden type="checkbox" id="firefox-hide"><div><h3 id="firefox"><label for="firefox-hide">Firefox</label></h3>
|
||||
<p>Install <a href="https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/">Greasemonkey</a>, <a href="https://addons.mozilla.org/en-US/firefox/addon/violentmonkey/">Violentmonkey</a> or <a href="https://addons.mozilla.org/en-US/firefox/addon/tampermonkey/">Tampermonkey</a>, then <strong><a href="https://www.4chan-x.net/builds/4chan-X.user.js">click here to install 4chan X</a></strong>.</p>
|
||||
<p>Install <a href="https://addons.mozilla.org/en-US/firefox/addon/violentmonkey/">Violentmonkey</a>, <a href="https://addons.mozilla.org/en-US/firefox/addon/tampermonkey/">Tampermonkey</a>, or <a href="https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/">Greasemonkey</a> (issues since v4: <a href="https://github.com/greasemonkey/greasemonkey/issues/2526">#2526</a>, <a href="https://github.com/greasemonkey/greasemonkey/issues/2574">#2576</a>), then <strong><a href="https://www.4chan-x.net/builds/4chan-X.user.js">click here to install 4chan X</a></strong>.</p>
|
||||
<p>Ports of Greasemonkey are available for <a href="https://sourceforge.net/projects/gmport/">SeaMonkey</a> and <a href="https://github.com/janekptacijarabaci/greasemonkey/releases/latest">Pale Moon</a>.</p>
|
||||
</div><input hidden type="checkbox" id="chromium-hide"><div><h3 id="chromium"><label for="chromium-hide">Chromium</label></h3>
|
||||
<p><strong>Userscript</strong>: Install <a href="https://chrome.google.com/webstore/detail/violent-monkey/jinjaccalgkegednnccohejagnlnfdag">Violentmonkey</a>) or <a href="https://tampermonkey.net/">Tampermonkey</a>, then <strong><a href="https://www.4chan-x.net/builds/4chan-X.user.js">click here to install 4chan X</a></strong>.</p>
|
||||
|
||||
528
src/meta/npm-shrinkwrap.json → package-lock.json
generated
528
src/meta/npm-shrinkwrap.json → package-lock.json
generated
@ -4,31 +4,34 @@
|
||||
"lockfileVersion": 1,
|
||||
"dependencies": {
|
||||
"ajv": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-5.2.3.tgz",
|
||||
"integrity": "sha1-wG9Zh3jETGsWGrr+NGa4GtGBTtI=",
|
||||
"version": "6.10.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz",
|
||||
"integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"co": "4.6.0",
|
||||
"fast-deep-equal": "1.0.0",
|
||||
"json-schema-traverse": "0.3.1",
|
||||
"json-stable-stringify": "1.0.1"
|
||||
"fast-deep-equal": "2.0.1",
|
||||
"fast-json-stable-stringify": "2.0.0",
|
||||
"json-schema-traverse": "0.4.1",
|
||||
"uri-js": "4.2.2"
|
||||
}
|
||||
},
|
||||
"argparse": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz",
|
||||
"integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=",
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"sprintf-js": "1.0.3"
|
||||
}
|
||||
},
|
||||
"asn1": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz",
|
||||
"integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=",
|
||||
"dev": true
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
|
||||
"integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"safer-buffer": "2.1.2"
|
||||
}
|
||||
},
|
||||
"assert-plus": {
|
||||
"version": "1.0.0",
|
||||
@ -49,9 +52,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"aws4": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz",
|
||||
"integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=",
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
|
||||
"integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==",
|
||||
"dev": true
|
||||
},
|
||||
"balanced-match": {
|
||||
@ -61,11 +64,10 @@
|
||||
"dev": true
|
||||
},
|
||||
"bcrypt-pbkdf": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz",
|
||||
"integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
|
||||
"integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"tweetnacl": "0.14.5"
|
||||
}
|
||||
@ -76,19 +78,10 @@
|
||||
"integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=",
|
||||
"dev": true
|
||||
},
|
||||
"boom": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz",
|
||||
"integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"hoek": "4.2.0"
|
||||
}
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz",
|
||||
"integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=",
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "1.0.0",
|
||||
@ -108,15 +101,9 @@
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"exit": "0.1.2",
|
||||
"glob": "7.1.2"
|
||||
"glob": "7.1.3"
|
||||
}
|
||||
},
|
||||
"co": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
||||
"integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=",
|
||||
"dev": true
|
||||
},
|
||||
"coffee-script": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.9.3.tgz",
|
||||
@ -124,9 +111,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"combined-stream": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz",
|
||||
"integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=",
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz",
|
||||
"integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"delayed-stream": "1.0.0"
|
||||
@ -147,38 +134,12 @@
|
||||
"date-now": "0.1.4"
|
||||
}
|
||||
},
|
||||
"core-js": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.3.0.tgz",
|
||||
"integrity": "sha1-+rg/uwstjchfpjbEudNMdUIMbWU=",
|
||||
"dev": true
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
|
||||
"dev": true
|
||||
},
|
||||
"cryptiles": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz",
|
||||
"integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"boom": "5.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"boom": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz",
|
||||
"integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"hoek": "4.2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dashdash": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
|
||||
@ -201,33 +162,27 @@
|
||||
"dev": true
|
||||
},
|
||||
"dom-serializer": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz",
|
||||
"integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=",
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz",
|
||||
"integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"domelementtype": "1.1.3",
|
||||
"entities": "1.1.1"
|
||||
"domelementtype": "1.3.1",
|
||||
"entities": "1.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"domelementtype": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz",
|
||||
"integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=",
|
||||
"dev": true
|
||||
},
|
||||
"entities": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz",
|
||||
"integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=",
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz",
|
||||
"integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"domelementtype": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz",
|
||||
"integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz",
|
||||
"integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==",
|
||||
"dev": true
|
||||
},
|
||||
"domhandler": {
|
||||
@ -236,7 +191,7 @@
|
||||
"integrity": "sha1-LeWaCCLVAn+r/28DLCsloqir5zg=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"domelementtype": "1.3.0"
|
||||
"domelementtype": "1.3.1"
|
||||
}
|
||||
},
|
||||
"domutils": {
|
||||
@ -245,18 +200,18 @@
|
||||
"integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"dom-serializer": "0.1.0",
|
||||
"domelementtype": "1.3.0"
|
||||
"dom-serializer": "0.1.1",
|
||||
"domelementtype": "1.3.1"
|
||||
}
|
||||
},
|
||||
"ecc-jsbn": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz",
|
||||
"integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=",
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
|
||||
"integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"jsbn": "0.1.1"
|
||||
"jsbn": "0.1.1",
|
||||
"safer-buffer": "2.1.2"
|
||||
}
|
||||
},
|
||||
"entities": {
|
||||
@ -265,16 +220,10 @@
|
||||
"integrity": "sha1-sph6o4ITR/zeZCsk/fyeT7cSvyY=",
|
||||
"dev": true
|
||||
},
|
||||
"es6-promise": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.0.2.tgz",
|
||||
"integrity": "sha1-AQ1YWEI6XxGJeWZfRkhqlcbuK7Y=",
|
||||
"dev": true
|
||||
},
|
||||
"esprima": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz",
|
||||
"integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==",
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
|
||||
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
|
||||
"dev": true
|
||||
},
|
||||
"exit": {
|
||||
@ -284,9 +233,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"extend": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz",
|
||||
"integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=",
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
||||
"dev": true
|
||||
},
|
||||
"extsprintf": {
|
||||
@ -296,9 +245,15 @@
|
||||
"dev": true
|
||||
},
|
||||
"fast-deep-equal": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz",
|
||||
"integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
|
||||
"integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
|
||||
"dev": true
|
||||
},
|
||||
"fast-json-stable-stringify": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
|
||||
"integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=",
|
||||
"dev": true
|
||||
},
|
||||
"font-awesome": {
|
||||
@ -314,14 +269,14 @@
|
||||
"dev": true
|
||||
},
|
||||
"form-data": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz",
|
||||
"integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=",
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
|
||||
"integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"asynckit": "0.4.0",
|
||||
"combined-stream": "1.0.5",
|
||||
"mime-types": "2.1.17"
|
||||
"combined-stream": "1.0.7",
|
||||
"mime-types": "2.1.22"
|
||||
}
|
||||
},
|
||||
"fs.realpath": {
|
||||
@ -340,9 +295,9 @@
|
||||
}
|
||||
},
|
||||
"glob": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
|
||||
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
|
||||
"integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fs.realpath": "1.0.0",
|
||||
@ -360,40 +315,22 @@
|
||||
"dev": true
|
||||
},
|
||||
"har-validator": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz",
|
||||
"integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=",
|
||||
"version": "5.1.3",
|
||||
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
|
||||
"integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ajv": "5.2.3",
|
||||
"ajv": "6.10.0",
|
||||
"har-schema": "2.0.0"
|
||||
}
|
||||
},
|
||||
"hawk": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz",
|
||||
"integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"boom": "4.3.1",
|
||||
"cryptiles": "3.1.2",
|
||||
"hoek": "4.2.0",
|
||||
"sntp": "2.0.2"
|
||||
}
|
||||
},
|
||||
"hoek": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz",
|
||||
"integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ==",
|
||||
"dev": true
|
||||
},
|
||||
"htmlparser2": {
|
||||
"version": "3.8.3",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz",
|
||||
"integrity": "sha1-mWwosZFRaovoZQGn15dX5ccMEGg=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"domelementtype": "1.3.0",
|
||||
"domelementtype": "1.3.1",
|
||||
"domhandler": "2.3.0",
|
||||
"domutils": "1.5.1",
|
||||
"entities": "1.0.0",
|
||||
@ -408,7 +345,7 @@
|
||||
"requires": {
|
||||
"assert-plus": "1.0.0",
|
||||
"jsprim": "1.4.1",
|
||||
"sshpk": "1.13.1"
|
||||
"sshpk": "1.16.1"
|
||||
}
|
||||
},
|
||||
"immediate": {
|
||||
@ -455,20 +392,19 @@
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
|
||||
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"jshint": {
|
||||
"version": "2.9.5",
|
||||
"resolved": "https://registry.npmjs.org/jshint/-/jshint-2.9.5.tgz",
|
||||
"integrity": "sha1-HnJSkVzmgbQIJ+4UJIxG006apiw=",
|
||||
"version": "2.10.2",
|
||||
"resolved": "https://registry.npmjs.org/jshint/-/jshint-2.10.2.tgz",
|
||||
"integrity": "sha512-e7KZgCSXMJxznE/4WULzybCMNXNAd/bf5TSrvVEq78Q/K8ZwFpmBqQeDtNiHc3l49nV4E/+YeHU/JZjSUIrLAA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"cli": "1.0.1",
|
||||
"console-browserify": "1.1.0",
|
||||
"exit": "0.1.2",
|
||||
"htmlparser2": "3.8.3",
|
||||
"lodash": "3.7.0",
|
||||
"lodash": "4.17.11",
|
||||
"minimatch": "3.0.4",
|
||||
"shelljs": "0.3.0",
|
||||
"strip-json-comments": "1.0.4"
|
||||
@ -481,32 +417,17 @@
|
||||
"dev": true
|
||||
},
|
||||
"json-schema-traverse": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz",
|
||||
"integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=",
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
||||
"dev": true
|
||||
},
|
||||
"json-stable-stringify": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz",
|
||||
"integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"jsonify": "0.0.0"
|
||||
}
|
||||
},
|
||||
"json-stringify-safe": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
||||
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=",
|
||||
"dev": true
|
||||
},
|
||||
"jsonify": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz",
|
||||
"integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=",
|
||||
"dev": true
|
||||
},
|
||||
"jsprim": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
|
||||
@ -520,16 +441,15 @@
|
||||
}
|
||||
},
|
||||
"jszip": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.1.4.tgz",
|
||||
"integrity": "sha512-z6w8iYIxZ/fcgul0j/OerkYnkomH8BZigvzbxVmr2h5HkZUrPtk2kjYtLkqR9wwQxEP6ecKNoKLsbhd18jfnGA==",
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.2.1.tgz",
|
||||
"integrity": "sha512-iCMBbo4eE5rb1VCpm5qXOAaUiRKRUKiItn8ah2YQQx9qymmSAY98eyQfioChEYcVQLh0zxJ3wS4A0mh90AVPvw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"core-js": "2.3.0",
|
||||
"es6-promise": "3.0.2",
|
||||
"lie": "3.1.1",
|
||||
"pako": "1.0.6",
|
||||
"readable-stream": "2.0.6"
|
||||
"lie": "3.3.0",
|
||||
"pako": "1.0.10",
|
||||
"readable-stream": "2.3.6",
|
||||
"set-immediate-shim": "1.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"isarray": {
|
||||
@ -539,43 +459,53 @@
|
||||
"dev": true
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz",
|
||||
"integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=",
|
||||
"version": "2.3.6",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
|
||||
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"core-util-is": "1.0.2",
|
||||
"inherits": "2.0.3",
|
||||
"isarray": "1.0.0",
|
||||
"process-nextick-args": "1.0.7",
|
||||
"string_decoder": "0.10.31",
|
||||
"process-nextick-args": "2.0.0",
|
||||
"safe-buffer": "5.1.2",
|
||||
"string_decoder": "1.1.1",
|
||||
"util-deprecate": "1.0.2"
|
||||
}
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"safe-buffer": "5.1.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"lie": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
|
||||
"integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=",
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"immediate": "3.0.6"
|
||||
}
|
||||
},
|
||||
"linkify-it": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.0.3.tgz",
|
||||
"integrity": "sha1-2UpGSPmxwXnWT6lykSaL22zpQ08=",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.1.0.tgz",
|
||||
"integrity": "sha512-4REs8/062kV2DSHxNfq5183zrqXMl7WP0WzABH9IeJI+NLm429FgE1PDecltYfnOoFDFlZGh2T8PfZn0r+GTRg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"uc.micro": "1.0.3"
|
||||
"uc.micro": "1.0.6"
|
||||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "3.7.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-3.7.0.tgz",
|
||||
"integrity": "sha1-Nni9irmVBXwHreg27S7wh9qBHUU=",
|
||||
"version": "4.17.11",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
|
||||
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
|
||||
"dev": true
|
||||
},
|
||||
"lodash._reinterpolate": {
|
||||
@ -604,22 +534,22 @@
|
||||
}
|
||||
},
|
||||
"markdown-it": {
|
||||
"version": "8.4.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.0.tgz",
|
||||
"integrity": "sha512-tNuOCCfunY5v5uhcO2AUMArvKAyKMygX8tfup/JrgnsDqcCATQsAExBq7o5Ml9iMmO82bk6jYNLj6khcrl0JGA==",
|
||||
"version": "8.4.2",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.2.tgz",
|
||||
"integrity": "sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"argparse": "1.0.9",
|
||||
"entities": "1.1.1",
|
||||
"linkify-it": "2.0.3",
|
||||
"argparse": "1.0.10",
|
||||
"entities": "1.1.2",
|
||||
"linkify-it": "2.1.0",
|
||||
"mdurl": "1.0.1",
|
||||
"uc.micro": "1.0.3"
|
||||
"uc.micro": "1.0.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"entities": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz",
|
||||
"integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=",
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz",
|
||||
"integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
@ -640,18 +570,18 @@
|
||||
"dev": true
|
||||
},
|
||||
"mime-db": {
|
||||
"version": "1.30.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz",
|
||||
"integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=",
|
||||
"version": "1.38.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz",
|
||||
"integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==",
|
||||
"dev": true
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.17",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz",
|
||||
"integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=",
|
||||
"version": "2.1.22",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz",
|
||||
"integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"mime-db": "1.30.0"
|
||||
"mime-db": "1.38.0"
|
||||
}
|
||||
},
|
||||
"minimatch": {
|
||||
@ -660,22 +590,13 @@
|
||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"brace-expansion": "1.1.8"
|
||||
}
|
||||
},
|
||||
"node-rsa": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/node-rsa/-/node-rsa-0.4.2.tgz",
|
||||
"integrity": "sha1-1jkXKewWqDDtWjgEKzFX0tXXJTA=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"asn1": "0.2.3"
|
||||
"brace-expansion": "1.1.11"
|
||||
}
|
||||
},
|
||||
"oauth-sign": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz",
|
||||
"integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=",
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
|
||||
"integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==",
|
||||
"dev": true
|
||||
},
|
||||
"once": {
|
||||
@ -694,9 +615,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"pako": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz",
|
||||
"integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==",
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz",
|
||||
"integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==",
|
||||
"dev": true
|
||||
},
|
||||
"path-is-absolute": {
|
||||
@ -712,27 +633,33 @@
|
||||
"dev": true
|
||||
},
|
||||
"process-nextick-args": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz",
|
||||
"integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
|
||||
"integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
|
||||
"dev": true
|
||||
},
|
||||
"psl": {
|
||||
"version": "1.1.31",
|
||||
"resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz",
|
||||
"integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==",
|
||||
"dev": true
|
||||
},
|
||||
"punycode": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
|
||||
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
|
||||
"dev": true
|
||||
},
|
||||
"q": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/q/-/q-1.5.0.tgz",
|
||||
"integrity": "sha1-3QG6ydBtMObyGa7LglPunr3DCPE=",
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
|
||||
"integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=",
|
||||
"dev": true
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.5.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz",
|
||||
"integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==",
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
|
||||
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
|
||||
"dev": true
|
||||
},
|
||||
"readable-stream": {
|
||||
@ -748,33 +675,31 @@
|
||||
}
|
||||
},
|
||||
"request": {
|
||||
"version": "2.83.0",
|
||||
"resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz",
|
||||
"integrity": "sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw==",
|
||||
"version": "2.88.0",
|
||||
"resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
|
||||
"integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"aws-sign2": "0.7.0",
|
||||
"aws4": "1.6.0",
|
||||
"aws4": "1.8.0",
|
||||
"caseless": "0.12.0",
|
||||
"combined-stream": "1.0.5",
|
||||
"extend": "3.0.1",
|
||||
"combined-stream": "1.0.7",
|
||||
"extend": "3.0.2",
|
||||
"forever-agent": "0.6.1",
|
||||
"form-data": "2.3.1",
|
||||
"har-validator": "5.0.3",
|
||||
"hawk": "6.0.2",
|
||||
"form-data": "2.3.3",
|
||||
"har-validator": "5.1.3",
|
||||
"http-signature": "1.2.0",
|
||||
"is-typedarray": "1.0.0",
|
||||
"isstream": "0.1.2",
|
||||
"json-stringify-safe": "5.0.1",
|
||||
"mime-types": "2.1.17",
|
||||
"oauth-sign": "0.8.2",
|
||||
"mime-types": "2.1.22",
|
||||
"oauth-sign": "0.9.0",
|
||||
"performance-now": "2.1.0",
|
||||
"qs": "6.5.1",
|
||||
"safe-buffer": "5.1.1",
|
||||
"stringstream": "0.0.5",
|
||||
"tough-cookie": "2.3.3",
|
||||
"qs": "6.5.2",
|
||||
"safe-buffer": "5.1.2",
|
||||
"tough-cookie": "2.4.3",
|
||||
"tunnel-agent": "0.6.0",
|
||||
"uuid": "3.1.0"
|
||||
"uuid": "3.3.2"
|
||||
}
|
||||
},
|
||||
"request-promise": {
|
||||
@ -784,22 +709,26 @@
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"bluebird": "2.11.0",
|
||||
"lodash": "4.17.4",
|
||||
"request": "2.83.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": {
|
||||
"version": "4.17.4",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz",
|
||||
"integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=",
|
||||
"dev": true
|
||||
}
|
||||
"lodash": "4.17.11",
|
||||
"request": "2.88.0"
|
||||
}
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
|
||||
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==",
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"dev": true
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"dev": true
|
||||
},
|
||||
"set-immediate-shim": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz",
|
||||
"integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=",
|
||||
"dev": true
|
||||
},
|
||||
"shelljs": {
|
||||
@ -808,15 +737,6 @@
|
||||
"integrity": "sha1-NZbmMHp4FUT1kfN9phg2DzHbV7E=",
|
||||
"dev": true
|
||||
},
|
||||
"sntp": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/sntp/-/sntp-2.0.2.tgz",
|
||||
"integrity": "sha1-UGQRDwr4X3z9t9a2ekACjOUrSys=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"hoek": "4.2.0"
|
||||
}
|
||||
},
|
||||
"sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
@ -824,18 +744,19 @@
|
||||
"dev": true
|
||||
},
|
||||
"sshpk": {
|
||||
"version": "1.13.1",
|
||||
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz",
|
||||
"integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=",
|
||||
"version": "1.16.1",
|
||||
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz",
|
||||
"integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"asn1": "0.2.3",
|
||||
"asn1": "0.2.4",
|
||||
"assert-plus": "1.0.0",
|
||||
"bcrypt-pbkdf": "1.0.1",
|
||||
"bcrypt-pbkdf": "1.0.2",
|
||||
"dashdash": "1.14.1",
|
||||
"ecc-jsbn": "0.1.1",
|
||||
"ecc-jsbn": "0.1.2",
|
||||
"getpass": "0.1.7",
|
||||
"jsbn": "0.1.1",
|
||||
"safer-buffer": "2.1.2",
|
||||
"tweetnacl": "0.14.5"
|
||||
}
|
||||
},
|
||||
@ -851,12 +772,6 @@
|
||||
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
|
||||
"dev": true
|
||||
},
|
||||
"stringstream": {
|
||||
"version": "0.0.5",
|
||||
"resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz",
|
||||
"integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=",
|
||||
"dev": true
|
||||
},
|
||||
"strip-json-comments": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz",
|
||||
@ -864,12 +779,21 @@
|
||||
"dev": true
|
||||
},
|
||||
"tough-cookie": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz",
|
||||
"integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=",
|
||||
"version": "2.4.3",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
|
||||
"integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"psl": "1.1.31",
|
||||
"punycode": "1.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"punycode": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
|
||||
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"tunnel-agent": {
|
||||
@ -878,22 +802,30 @@
|
||||
"integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"safe-buffer": "5.1.1"
|
||||
"safe-buffer": "5.1.2"
|
||||
}
|
||||
},
|
||||
"tweetnacl": {
|
||||
"version": "0.14.5",
|
||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
||||
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"uc.micro": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.3.tgz",
|
||||
"integrity": "sha1-ftUNXg+an7ClczeSWfKndFjVAZI=",
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
|
||||
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==",
|
||||
"dev": true
|
||||
},
|
||||
"uri-js": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
|
||||
"integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"punycode": "2.1.1"
|
||||
}
|
||||
},
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
@ -901,9 +833,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"uuid": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz",
|
||||
"integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==",
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
|
||||
"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==",
|
||||
"dev": true
|
||||
},
|
||||
"verror": {
|
||||
@ -923,11 +855,11 @@
|
||||
"integrity": "sha1-aVfXgSzXlgZDAU0Fea+Y3HakPow=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"glob": "7.1.2",
|
||||
"glob": "7.1.3",
|
||||
"lodash": "2.4.2",
|
||||
"open": "0.0.5",
|
||||
"q": "1.5.0",
|
||||
"request": "2.83.0",
|
||||
"q": "1.5.1",
|
||||
"request": "2.88.0",
|
||||
"request-promise": "2.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
12
package.json
12
package.json
@ -22,7 +22,6 @@
|
||||
"recaptchaKey": "6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc",
|
||||
"youtubeAPIKey": "AIzaSyB5_zaen_-46Uhz1xGR-lz1YoUMHqCD6CE",
|
||||
"distBranch": "gh-pages",
|
||||
"uploadPath": "www.4chan-x.net:/var/www/html/",
|
||||
"includes_only": [
|
||||
"*://boards.4chan.org/*",
|
||||
"*://sys.4chan.org/*",
|
||||
@ -85,15 +84,14 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"coffee-script": "=1.9.3",
|
||||
"esprima": "^4.0.0",
|
||||
"esprima": "^4.0.1",
|
||||
"font-awesome": "=4.6.3",
|
||||
"jshint": "^2.9.5",
|
||||
"jszip": "^3.1.4",
|
||||
"jshint": "^2.10.2",
|
||||
"jszip": "^3.2.1",
|
||||
"lodash.template": "^4.4.0",
|
||||
"markdown-it": "^8.4.0",
|
||||
"markdown-it": "^8.4.2",
|
||||
"markdown-it-anchor": "^4.0.0",
|
||||
"node-rsa": "^0.4.2",
|
||||
"request": "^2.83.0",
|
||||
"request": "^2.88.0",
|
||||
"webstore-upload": "0.0.7"
|
||||
},
|
||||
"repository": {
|
||||
|
||||
@ -1,10 +0,0 @@
|
||||
<% url = (url || 'https://www.4chan.org/feedback'); %><!doctype html>
|
||||
<html><head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="refresh" content="0; url=<%= url %>">
|
||||
<title>Redirect</title>
|
||||
</head>
|
||||
<body>
|
||||
Redirecting to <a href="<%= url %>"><%= url %></a>...
|
||||
</body>
|
||||
</html>
|
||||
@ -68,7 +68,8 @@ Redirect =
|
||||
continue
|
||||
load(i).call {status: 200, response}
|
||||
else
|
||||
CrossOrigin.json url, load(i), true
|
||||
CrossOrigin.ajax url,
|
||||
onloadend: load(i)
|
||||
else
|
||||
Redirect.parse [], cb
|
||||
return
|
||||
|
||||
@ -7,32 +7,23 @@ Filter =
|
||||
unless Conf['Filtered Backlinks']
|
||||
$.addClass doc, 'hide-backlinks'
|
||||
|
||||
nsfwBoards = BoardConfig.sfwBoards(false).join(',')
|
||||
sfwBoards = BoardConfig.sfwBoards(true).join(',')
|
||||
|
||||
for key of Config.filter
|
||||
for line in Conf[key].split '\n'
|
||||
continue if line[0] is '#'
|
||||
|
||||
if not (regexp = line.match /\/(.+)\/(\w*)/)
|
||||
if not (regexp = line.match /\/(.*)\/(\w*)/)
|
||||
continue
|
||||
|
||||
# Don't mix up filter flags with the regular expression.
|
||||
filter = line.replace regexp[0], ''
|
||||
|
||||
# Comma-separated list of the boards this filter applies to.
|
||||
# Defaults to global.
|
||||
boards = filter.match(/boards:([^;]+)/)?[1].toLowerCase() or 'global'
|
||||
boards = boards.replace('nsfw', nsfwBoards).replace('sfw', sfwBoards)
|
||||
boards = if boards is 'global' then null else boards.split(',')
|
||||
# List of the boards this filter applies to.
|
||||
boards = @parseBoards filter.match(/(?:^|;)\s*boards:([^;]+)/)?[1]
|
||||
|
||||
# boards to exclude from an otherwise global rule
|
||||
# due to the sfw and nsfw keywords, also works on all filters
|
||||
# replaces 'nsfw' and 'sfw' for consistency
|
||||
excludes = filter.match(/exclude:([^;]+)/)?[1].toLowerCase() or null
|
||||
excludes = if excludes is null then null else excludes.replace('nsfw', nsfwBoards).replace('sfw', sfwBoards).split(',')
|
||||
# Boards to exclude from an otherwise global rule.
|
||||
excludes = @parseBoards filter.match(/(?:^|;)\s*exclude:([^;]+)/)?[1]
|
||||
|
||||
if key in ['uniqueID', 'MD5']
|
||||
if (isstring = (key in ['uniqueID', 'MD5']))
|
||||
# MD5 filter will use strings instead of regular expressions.
|
||||
regexp = regexp[1]
|
||||
else
|
||||
@ -50,13 +41,17 @@ Filter =
|
||||
], 60
|
||||
continue
|
||||
|
||||
# Filter OPs along with their threads, replies only, or both.
|
||||
# Defaults to both.
|
||||
op = filter.match(/[^t]op:(yes|no|only)/)?[1] or 'yes'
|
||||
# Filter OPs along with their threads or replies only.
|
||||
op = filter.match(/(?:^|;)\s*op:(no|only)/)?[1] or ''
|
||||
mask = {'no': 1, 'only': 2}[op] or 0
|
||||
|
||||
# Filter only posts with/without files.
|
||||
file = filter.match(/(?:^|;)\s*file:(no|only)/)?[1] or ''
|
||||
mask = mask | ({'no': 4, 'only': 8}[file] or 0)
|
||||
|
||||
# Overrule the `Show Stubs` setting.
|
||||
# Defaults to stub showing.
|
||||
stub = switch filter.match(/stub:(yes|no)/)?[1]
|
||||
stub = switch filter.match(/(?:^|;)\s*stub:(yes|no)/)?[1]
|
||||
when 'yes'
|
||||
true
|
||||
when 'no'
|
||||
@ -64,25 +59,29 @@ Filter =
|
||||
else
|
||||
Conf['Stubs']
|
||||
|
||||
# Highlight the post, or hide it.
|
||||
# Desktop notification
|
||||
noti = /(?:^|;)\s*notify/.test filter
|
||||
|
||||
# Highlight the post.
|
||||
# If not specified, the highlight class will be filter-highlight.
|
||||
# Defaults to post hiding.
|
||||
if hl = /highlight/.test filter
|
||||
hl = filter.match(/highlight:([\w-]+)/)?[1] or 'filter-highlight'
|
||||
if (hl = /(?:^|;)\s*highlight/.test filter)
|
||||
hl = filter.match(/(?:^|;)\s*highlight:([\w-]+)/)?[1] or 'filter-highlight'
|
||||
# Put highlighted OP's thread on top of the board page or not.
|
||||
# Defaults to on top.
|
||||
top = filter.match(/top:(yes|no)/)?[1] or 'yes'
|
||||
top = filter.match(/(?:^|;)\s*top:(yes|no)/)?[1] or 'yes'
|
||||
top = top is 'yes' # Turn it into a boolean
|
||||
|
||||
# Fields that this filter applies to (for 'general' filters)
|
||||
if key is 'general'
|
||||
if (types = filter.match /(?:^|;)\s*type:([^;]*)/)
|
||||
types = types[1].split(',').filter (x) ->
|
||||
x of Config.filter and x isnt 'general'
|
||||
types = types[1].split(',')
|
||||
else
|
||||
types = ['subject', 'name', 'filename', 'comment']
|
||||
|
||||
filter = @createFilter regexp, boards, excludes, op, stub, hl, top
|
||||
# Hide the post (default case).
|
||||
hide = !(hl or noti)
|
||||
|
||||
filter = {isstring, regexp, boards, excludes, mask, hide, stub, hl, top, noti}
|
||||
if key is 'general'
|
||||
for type in types
|
||||
(@filters[type] or= []).push filter
|
||||
@ -94,30 +93,27 @@ Filter =
|
||||
name: 'Filter'
|
||||
cb: @node
|
||||
|
||||
createFilter: (regexp, boards, excludes, op, stub, hl, top) ->
|
||||
test =
|
||||
if typeof regexp is 'string'
|
||||
# MD5 checking
|
||||
(value) -> regexp is value
|
||||
else
|
||||
(value) -> regexp.test value
|
||||
# Parse comma-separated list of boards.
|
||||
# Sites can be specified by a beginning part of the site domain followed by a colon.
|
||||
parseBoards: (boardsRaw) ->
|
||||
return false unless boardsRaw
|
||||
return boards if (boards = Filter.parseBoardsMemo[boardsRaw])
|
||||
boards = {}
|
||||
siteFilter = ''
|
||||
for boardID in boardsRaw.split(',')
|
||||
if ':' in boardID
|
||||
[siteFilter, boardID] = boardID.split(':')[-2..]
|
||||
for siteID, siteProperties of Conf['siteProperties']
|
||||
continue if siteProperties.canonical or siteID[...siteFilter.length] isnt siteFilter
|
||||
if boardID in ['nsfw', 'sfw']
|
||||
for boardID2 in SW[siteProperties.software]?.sfwBoards?(boardID is 'sfw') or []
|
||||
boards["#{siteID}/#{boardID2}"] = true
|
||||
else
|
||||
boards["#{siteID}/#{encodeURIComponent boardID}"] = true
|
||||
Filter.parseBoardsMemo[boardsRaw] = boards
|
||||
boards
|
||||
|
||||
settings =
|
||||
hide: !hl
|
||||
stub: stub
|
||||
class: hl
|
||||
top: top
|
||||
|
||||
(value, boardID, isReply) ->
|
||||
if boards and boardID not in boards
|
||||
return false
|
||||
if excludes and boardID in excludes
|
||||
return false
|
||||
if isReply and op is 'only' or !isReply and op is 'no'
|
||||
return false
|
||||
unless test value
|
||||
return false
|
||||
settings
|
||||
parseBoardsMemo: {}
|
||||
|
||||
test: (post, hideable=true) ->
|
||||
return post.filterResults if post.filterResults
|
||||
@ -125,27 +121,40 @@ Filter =
|
||||
stub = true
|
||||
hl = undefined
|
||||
top = false
|
||||
noti = false
|
||||
if QuoteYou.isYou(post)
|
||||
hideable = false
|
||||
for key of Filter.filters when ((value = Filter[key] post)?)
|
||||
mask = (if post.isReply then 2 else 1)
|
||||
mask = (mask | (if post.file then 4 else 8))
|
||||
board = "#{post.siteID}/#{post.boardID}"
|
||||
site = "#{post.siteID}/*"
|
||||
for key of Filter.filters when ((value = Filter.value key, post)?)
|
||||
# Continue if there's nothing to filter (no tripcode for example).
|
||||
for filter in Filter.filters[key] when (result = filter value, post.boardID, post.isReply)
|
||||
if result.hide
|
||||
for filter in Filter.filters[key]
|
||||
continue if (
|
||||
(filter.boards and !(filter.boards[board] or filter.boards[site] )) or
|
||||
(filter.excludes and (filter.excludes[board] or filter.excludes[site])) or
|
||||
(filter.mask & mask) or
|
||||
(if filter.isstring then (filter.regexp isnt value) else !filter.regexp.test(value))
|
||||
)
|
||||
if filter.hide
|
||||
if hideable
|
||||
hide = true
|
||||
stub and= result.stub
|
||||
stub and= filter.stub
|
||||
else
|
||||
unless hl and result.class in hl
|
||||
(hl or= []).push result.class
|
||||
top or= result.top
|
||||
unless hl and filter.hl in hl
|
||||
(hl or= []).push filter.hl
|
||||
top or= filter.top
|
||||
if filter.noti
|
||||
noti = true
|
||||
if hide
|
||||
{hide, stub}
|
||||
else
|
||||
{hl, top}
|
||||
{hl, top, noti}
|
||||
|
||||
node: ->
|
||||
return if @isClone
|
||||
{hide, stub, hl, top} = Filter.test @, (!@isFetchedQuote and (@isReply or g.VIEW is 'index'))
|
||||
{hide, stub, hl, top, noti} = Filter.test @, (!@isFetchedQuote and (@isReply or g.VIEW is 'index'))
|
||||
if hide
|
||||
if @isReply
|
||||
PostHiding.hide @, stub
|
||||
@ -155,24 +164,33 @@ Filter =
|
||||
if hl
|
||||
@highlights = hl
|
||||
$.addClass @nodes.root, hl...
|
||||
return
|
||||
if noti and Unread.posts and (@ID > Unread.lastReadPost) and not QuoteYou.isYou(@)
|
||||
Unread.openNotification @, ' triggered a notification filter'
|
||||
|
||||
isHidden: (post) ->
|
||||
!!Filter.test(post).hide
|
||||
|
||||
postID: (post) -> "#{post.ID}"
|
||||
name: (post) -> post.info.name
|
||||
uniqueID: (post) -> post.info.uniqueID
|
||||
tripcode: (post) -> post.info.tripcode
|
||||
capcode: (post) -> post.info.capcode
|
||||
pass: (post) -> post.info.pass
|
||||
subject: (post) -> post.info.subject or (if post.isReply then undefined else '')
|
||||
comment: (post) -> (post.info.comment ?= Build.parseComment post.info.commentHTML.innerHTML)
|
||||
flag: (post) -> post.info.flag
|
||||
filename: (post) -> post.file?.name
|
||||
dimensions: (post) -> post.file?.dimensions
|
||||
filesize: (post) -> post.file?.size
|
||||
MD5: (post) -> post.file?.MD5
|
||||
valueF:
|
||||
postID: (post) -> "#{post.ID}"
|
||||
name: (post) -> post.info.name
|
||||
uniqueID: (post) -> post.info.uniqueID or ''
|
||||
tripcode: (post) -> post.info.tripcode
|
||||
capcode: (post) -> post.info.capcode
|
||||
pass: (post) -> post.info.pass
|
||||
email: (post) -> post.info.email
|
||||
subject: (post) -> post.info.subject or (if post.isReply then undefined else '')
|
||||
comment: (post) -> (post.info.comment ?= Build.parseComment post.info.commentHTML.innerHTML)
|
||||
flag: (post) -> post.info.flag
|
||||
filename: (post) -> post.file?.name
|
||||
dimensions: (post) -> post.file?.dimensions
|
||||
filesize: (post) -> post.file?.size
|
||||
MD5: (post) -> post.file?.MD5
|
||||
|
||||
value: (key, post) ->
|
||||
if key of Filter.valueF
|
||||
Filter.valueF[key](post)
|
||||
else
|
||||
key.split('+').map((k) -> Filter.valueF[k]?(post) or '').join('\n')
|
||||
|
||||
addFilter: (type, re, cb) ->
|
||||
$.get type, Conf[type], (item) ->
|
||||
@ -245,6 +263,7 @@ Filter =
|
||||
['Tripcode', 'tripcode']
|
||||
['Capcode', 'capcode']
|
||||
['Pass Date', 'pass']
|
||||
['Email', 'email']
|
||||
['Subject', 'subject']
|
||||
['Comment', 'comment']
|
||||
['Flag', 'flag']
|
||||
@ -268,14 +287,14 @@ Filter =
|
||||
return {
|
||||
el: el
|
||||
open: (post) ->
|
||||
value = Filter[type] post
|
||||
value = Filter.value type, post
|
||||
value?
|
||||
}
|
||||
|
||||
makeFilter: ->
|
||||
{type} = @dataset
|
||||
# Convert value -> regexp, unless type is MD5
|
||||
value = Filter[type] Filter.menu.post
|
||||
value = Filter.value type, Filter.menu.post
|
||||
re = if type in ['uniqueID', 'MD5'] then value else Filter.escape(value)
|
||||
re = if type in ['uniqueID', 'MD5']
|
||||
"/#{re}/"
|
||||
|
||||
@ -48,6 +48,11 @@ BoardConfig =
|
||||
domain: (board) ->
|
||||
"boards.#{if BoardConfig.isSFW(board) then '4channel' else '4chan'}.org"
|
||||
|
||||
isArchived: (board) ->
|
||||
# assume archive exists if no data available to prevent cleaning of archived threads
|
||||
data = (@boards or Conf['boardConfig'].boards)[board]
|
||||
!data or data.is_archived
|
||||
|
||||
noAudio: (boardID) ->
|
||||
return false unless Site.software is 'yotsuba'
|
||||
boards = @boards or Conf['boardConfig'].boards
|
||||
|
||||
@ -65,7 +65,8 @@ Build.Test =
|
||||
|
||||
testOne: (post) ->
|
||||
Build.Test.postsRemaining++
|
||||
$.cache "#{location.protocol}//a.4cdn.org/#{post.board.ID}/thread/#{post.thread.ID}.json", ->
|
||||
$.cache Site.urls.threadJSON({boardID: post.boardID, threadID: post.threadID}), ->
|
||||
return unless @response
|
||||
{posts} = @response
|
||||
Build.spoilerRange[post.board.ID] = posts[0].custom_spoiler
|
||||
for postData in posts
|
||||
@ -90,8 +91,8 @@ Build.Test =
|
||||
c.log y.outerHTML
|
||||
|
||||
for key of Config.filter when not key is 'General' and not (key is 'MD5' and post.board.ID is 'f')
|
||||
val1 = Filter[key] obj
|
||||
val2 = Filter[key] post2
|
||||
val1 = Filter.value key, obj
|
||||
val2 = Filter.value key, post2
|
||||
if val1 isnt val2
|
||||
fail = true
|
||||
c.log "#{post.fullID} has filter bug in #{key}"
|
||||
|
||||
@ -25,18 +25,24 @@ Build =
|
||||
sameThread: (boardID, threadID) ->
|
||||
g.VIEW is 'thread' and g.BOARD.ID is boardID and g.THREADID is +threadID
|
||||
|
||||
postURL: (boardID, threadID, postID) ->
|
||||
if Build.sameThread boardID, threadID
|
||||
"#p#{postID}"
|
||||
threadURL: (boardID, threadID) ->
|
||||
if boardID isnt g.BOARD.ID
|
||||
"//#{BoardConfig.domain(boardID)}/#{boardID}/thread/#{threadID}"
|
||||
else if g.VIEW isnt 'thread' or +threadID isnt g.THREADID
|
||||
"/#{boardID}/thread/#{threadID}"
|
||||
else
|
||||
"/#{boardID}/thread/#{threadID}#p#{postID}"
|
||||
''
|
||||
|
||||
parseJSON: (data, boardID) ->
|
||||
postURL: (boardID, threadID, postID) ->
|
||||
"#{Build.threadURL(boardID, threadID)}#p#{postID}"
|
||||
|
||||
parseJSON: (data, boardID, siteID) ->
|
||||
o =
|
||||
# id
|
||||
ID: data.no
|
||||
threadID: data.resto or data.no
|
||||
boardID: boardID
|
||||
siteID: siteID or Site.hostname
|
||||
isReply: !!data.resto
|
||||
# thread status
|
||||
isSticky: !!data.sticky
|
||||
@ -44,7 +50,6 @@ Build =
|
||||
isArchived: !!data.archived
|
||||
# file status
|
||||
fileDeleted: !!data.filedeleted
|
||||
xa18: data.xa18
|
||||
o.info =
|
||||
subject: Build.unescape data.sub
|
||||
email: Build.unescape data.email
|
||||
@ -80,6 +85,9 @@ Build =
|
||||
tag: data.tag
|
||||
hasDownscale: !!data.m_img
|
||||
o.file.dimensions = "#{o.file.width}x#{o.file.height}" unless /\.pdf$/.test o.file.url
|
||||
# Temporary JSON properties for events such as April 1 / Halloween
|
||||
for key of data when key[0] is 'x'
|
||||
o[key] = data[key]
|
||||
o
|
||||
|
||||
parseComment: (html) ->
|
||||
@ -124,11 +132,12 @@ Build =
|
||||
capcodePlural = "#{capcodeLong}s"
|
||||
capcodeDescription = "a 4chan #{capcodeLong}"
|
||||
|
||||
postLink = Build.postURL boardID, threadID, ID
|
||||
url = Build.threadURL boardID, threadID
|
||||
postLink = "#{url}#p#{ID}"
|
||||
quoteLink = if Build.sameThread boardID, threadID
|
||||
"javascript:quote('#{+ID}');"
|
||||
else
|
||||
"/#{boardID}/thread/#{threadID}#q#{ID}"
|
||||
"#{url}#q#{ID}"
|
||||
|
||||
postInfo = <%= readHTML('PostInfo.html') %>
|
||||
|
||||
@ -156,12 +165,12 @@ Build =
|
||||
# Fix quotelinks
|
||||
for quote in $$ '.quotelink', container
|
||||
href = quote.getAttribute 'href'
|
||||
if (href[0] is '#') and !(Build.sameThread boardID, threadID)
|
||||
quote.href = ("/#{boardID}/thread/#{threadID}") + href
|
||||
else if (match = href.match /^\/([^\/]+)\/thread\/(\d+)/) and (Build.sameThread match[1], match[2])
|
||||
quote.href = href.match(/(#[^#]*)?$/)[0] or '#'
|
||||
else if /^\d+(#|$)/.test(href) and not (g.VIEW is 'thread' and g.BOARD.ID is boardID) # used on /f/
|
||||
quote.href = "/#{boardID}/thread/#{href}"
|
||||
if (href[0] is '#')
|
||||
if !Build.sameThread(boardID, threadID)
|
||||
quote.href = Build.threadURL(boardID, threadID) + href
|
||||
else
|
||||
if (match = quote.href.match SW.yotsuba.regexp.quotelink) and (Build.sameThread match[1], match[2])
|
||||
quote.href = href.match(/(#[^#]*)?$/)[0] or '#'
|
||||
|
||||
container
|
||||
|
||||
|
||||
@ -5,9 +5,9 @@
|
||||
?{email}{<a href="mailto:${encodeURIComponent(email).replace(/%40/g, "@")}" class="useremail">}
|
||||
<span class="name?{capcode}{ capcode}">${name}</span>
|
||||
?{tripcode}{ <span class="postertrip">${tripcode}</span>}
|
||||
?{o.xa19s}{ <span class="like-score">${o.xa19s}</span>}
|
||||
?{pass}{ <span title="Pass user since ${pass}" class="n-pu"></span>}
|
||||
?{capcode}{ <strong class="capcode hand id_${capcodeLC}" title="Highlight posts by ${capcodePlural}">## ${capcode}</strong>}
|
||||
?{!capcode && typeof o.xa18 !== "undefined"}{ <strong class="capcode hand n-atb n-atb-${o.xa18} id_at${o.xa18}"></strong>}
|
||||
?{email}{</a>}
|
||||
?{boardID === "f" && !o.isReply || capcodeDescription}{}{ }
|
||||
?{capcodeDescription}{ <img src="${staticPath}${capcodeLC}icon${gifIcon}" alt="${capcode} Icon" title="This user is ${capcodeDescription}." class="identityIcon retina">}
|
||||
@ -19,6 +19,7 @@
|
||||
<span class="postNum?{!(boardID === "f" && !o.isReply)}{ desktop}">
|
||||
<a href="${postLink}" title="Link to this post">No.</a>
|
||||
<a href="${quoteLink}" title="Reply to this post">${ID}</a>
|
||||
?{o.xa19l && o.isReply}{ <a data-cmd="like-post" href="#" class="like-btn">Like! ×${o.xa19l}</a>}
|
||||
?{o.isSticky}{ <img src="${staticPath}sticky${gifIcon}" alt="Sticky" title="Sticky"?{boardID === "f"}{ style="height: 18px; width: 18px;"}{ class="stickyIcon retina"}>}
|
||||
?{o.isClosed && !o.isArchived}{ <img src="${staticPath}closed${gifIcon}" alt="Closed" title="Closed"?{boardID === "f"}{ style="height: 18px; width: 18px;"}{ class="closedIcon retina"}>}
|
||||
?{o.isArchived}{ <img src="${staticPath}archived${gifIcon}" alt="Archived" title="Archived" class="archivedIcon retina">}
|
||||
|
||||
@ -282,7 +282,7 @@ Header =
|
||||
return a.firstChild # Its text node.
|
||||
|
||||
if /-expired/.test t
|
||||
if boardID not in ['b', 'f', 'trash', 'bant']
|
||||
if BoardConfig.isArchived(boardID)
|
||||
a.href = "//#{BoardConfig.domain(boardID)}/#{boardID}/archive"
|
||||
else
|
||||
return a.firstChild # Its text node.
|
||||
|
||||
@ -84,7 +84,7 @@ Index =
|
||||
@navLinks = $.el 'div', className: 'navLinks json-index'
|
||||
$.extend @navLinks, <%= readHTML('NavLinks.html') %>
|
||||
$('.cataloglink a', @navLinks).href = CatalogLinks.catalog()
|
||||
$('.archlistlink', @navLinks).hidden = true if g.BOARD.ID in ['b', 'trash', 'bant']
|
||||
$('.archlistlink', @navLinks).hidden = true unless BoardConfig.isArchived(g.BOARD.ID)
|
||||
$.on $('#index-last-refresh a', @navLinks), 'click', @cb.refreshFront
|
||||
|
||||
# Search field
|
||||
@ -573,48 +573,43 @@ Index =
|
||||
"#{hiddenCount} hidden threads"
|
||||
|
||||
update: (firstTime) ->
|
||||
Index.req?.abort()
|
||||
Index.notice?.close()
|
||||
if (oldReq = Index.req)
|
||||
delete Index.req
|
||||
oldReq.abort()
|
||||
|
||||
if Conf['Index Refresh Notifications'] and d.readyState isnt 'loading'
|
||||
if Conf['Index Refresh Notifications']
|
||||
# Optional notification for manual refreshes
|
||||
Index.notice = new Notice 'info', 'Refreshing index...'
|
||||
Index.notice or= new Notice 'info', 'Refreshing index...'
|
||||
else
|
||||
# Also display notice if Index Refresh is taking too long
|
||||
now = Date.now()
|
||||
$.ready ->
|
||||
Index.nTimeout = setTimeout (->
|
||||
if Index.req and !Index.notice
|
||||
Index.notice = new Notice 'info', 'Refreshing index...'
|
||||
), 3 * $.SECOND - (Date.now() - now)
|
||||
Index.nTimeout or= setTimeout ->
|
||||
Index.notice or= new Notice 'info', 'Refreshing index...'
|
||||
, 3 * $.SECOND
|
||||
|
||||
# Hard refresh in case of incomplete page load.
|
||||
if not firstTime and d.readyState isnt 'loading' and not $('.board + *')
|
||||
location.reload()
|
||||
return
|
||||
|
||||
Index.req = $.ajax "#{location.protocol}//a.4cdn.org/#{g.BOARD}/catalog.json",
|
||||
onabort: Index.load
|
||||
onloadend: Index.load
|
||||
,
|
||||
whenModified: 'Index'
|
||||
Index.req = $.whenModified(
|
||||
Site.urls.catalogJSON({boardID: g.BOARD.ID}),
|
||||
'Index',
|
||||
Index.load
|
||||
)
|
||||
$.addClass Index.button, 'fa-spin'
|
||||
|
||||
load: (e) ->
|
||||
load: ->
|
||||
return if @ isnt Index.req # aborted
|
||||
|
||||
$.rmClass Index.button, 'fa-spin'
|
||||
{req, notice, nTimeout} = Index
|
||||
{notice, nTimeout} = Index
|
||||
clearTimeout nTimeout if nTimeout
|
||||
delete Index.nTimeout
|
||||
delete Index.req
|
||||
delete Index.notice
|
||||
|
||||
if e.type is 'abort'
|
||||
req.onloadend = null
|
||||
notice?.close()
|
||||
return
|
||||
|
||||
if req.status not in [200, 304]
|
||||
err = "Index refresh failed. #{if req.status then "Error #{req.statusText} (#{req.status})" else 'Connection Error'}"
|
||||
if @status not in [200, 304]
|
||||
err = "Index refresh failed. #{if @status then "Error #{@statusText} (#{@status})" else 'Connection Error'}"
|
||||
if notice
|
||||
notice.setType 'warning'
|
||||
notice.el.lastElementChild.textContent = err
|
||||
@ -624,13 +619,12 @@ Index =
|
||||
return
|
||||
|
||||
try
|
||||
if req.status is 200
|
||||
Index.parse req.response
|
||||
else if req.status is 304
|
||||
if @status is 200
|
||||
Index.parse @response
|
||||
else if @status is 304
|
||||
Index.pageLoad()
|
||||
catch err
|
||||
c.error "Index failure: #{err.message}", err.stack
|
||||
# network error or non-JSON content for example.
|
||||
if notice
|
||||
notice.setType 'error'
|
||||
notice.el.lastElementChild.textContent = 'Index refresh failed.'
|
||||
@ -648,7 +642,7 @@ Index =
|
||||
notice.close()
|
||||
|
||||
timeEl = $ '#index-last-refresh time', Index.navLinks
|
||||
timeEl.dataset.utc = Date.parse req.getResponseHeader 'Last-Modified'
|
||||
timeEl.dataset.utc = Date.parse @getResponseHeader 'Last-Modified'
|
||||
RelativeDates.update timeEl
|
||||
|
||||
parse: (pages) ->
|
||||
|
||||
@ -206,16 +206,20 @@ Settings =
|
||||
$.after $('input[name="Stubs"]', section).parentNode.parentNode, div
|
||||
|
||||
export: ->
|
||||
# Make sure to export the most recent data.
|
||||
$.get Conf, (Conf) ->
|
||||
# Make sure to export the most recent data, but don't overwrite existing `Conf` object.
|
||||
Conf2 = {}
|
||||
$.extend Conf2, Conf
|
||||
$.get Conf2, (Conf2) ->
|
||||
# Don't export cached JSON data.
|
||||
delete Conf['boardConfig']
|
||||
(Settings.downloadExport {version: g.VERSION, date: Date.now(), Conf})
|
||||
delete Conf2['boardConfig']
|
||||
(Settings.downloadExport {version: g.VERSION, date: Date.now(), Conf: Conf2})
|
||||
|
||||
downloadExport: (data) ->
|
||||
blob = new Blob [JSON.stringify(data, null, 2)], {type: 'application/json'}
|
||||
url = URL.createObjectURL blob
|
||||
a = $.el 'a',
|
||||
download: "<%= meta.name %> v#{g.VERSION}-#{data.date}.json"
|
||||
href: "data:application/json;base64,#{btoa unescape encodeURIComponent JSON.stringify data, null, 2}"
|
||||
href: url
|
||||
p = $ '.imp-exp-result', Settings.dialog
|
||||
$.rmAll p
|
||||
$.add p, a
|
||||
@ -473,6 +477,12 @@ Settings =
|
||||
[hostname, software] = line.split(' ')
|
||||
siteProperties[hostname] = {software}
|
||||
set 'siteProperties', siteProperties
|
||||
if compareString < '00001.00014.00006.00006'
|
||||
if data['sauces']?
|
||||
set 'sauces', data['sauces'].replace(
|
||||
/\/\/%\$1\.deviantart\.com\/gallery\/#\/d%\$2;regexp:\/\^\\w\+_by_\(\\w\+\)-d\(\[\\da-z\]\+\)\//g,
|
||||
'//www.deviantart.com/gallery/#/d%$1%$2;regexp:/^\\w+_by_\\w+[_-]d([\\da-z]{6})\\b|^d([\\da-z]{6})-[\\da-z]{8}-/'
|
||||
)
|
||||
changes
|
||||
|
||||
loadSettings: (data, cb) ->
|
||||
|
||||
@ -3,20 +3,29 @@
|
||||
Use <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions" target="_blank">regular expressions</a>, one per line.<br>
|
||||
Lines starting with a <code>#</code> will be ignored.<br>
|
||||
For example, <code>/weeaboo/i</code> will filter posts containing the string `<code>weeaboo</code>`, case-insensitive.<br>
|
||||
MD5 filtering uses exact string matching, not regular expressions.
|
||||
MD5 and Unique ID filtering use exact string matching, not regular expressions.
|
||||
</p>
|
||||
<ul>You can use these settings with each regular expression, separate them with semicolons:
|
||||
<li>
|
||||
Per boards, separate them with commas. It is global if not specified. Use <code>sfw</code> and <code>nsfw</code> to reference all worksafe or not-worksafe boards.<br>
|
||||
For example: <code>boards:a,jp;</code>.<br>
|
||||
To specify boards on a particular site, put the beginning of the domain and a slash character before the list.<br>
|
||||
Any initial <code>www.</code> should not be included, and all 4chan domains are considered <code>4chan.org</code>.<br>
|
||||
For example: <code>boards:4:a,jp,sama:a,z;</code>.<br>
|
||||
An asterisk can be used to specify all boards on a site.<br>
|
||||
For example: <code>boards:4:*;</code>.<br>
|
||||
</li>
|
||||
<li>
|
||||
In case of a global rule or one that uses <code>sfw</code>/<code>nsfw</code>, select boards to be excluded from the filter.<br>
|
||||
Select boards to be excluded from the filter. The syntax is the same as for the <code>boards:</code> option above.<br>
|
||||
For example: <code>exclude:vg,v;</code>.
|
||||
</li>
|
||||
<li>
|
||||
Filter OPs only along with their threads (`only`), replies only (`no`), or both (`yes`, this is default).<br>
|
||||
For example: <code>op:only;</code>, <code>op:no;</code> or <code>op:yes;</code>.
|
||||
Filter OPs only along with their threads (`only`) or replies only (`no`).<br>
|
||||
For example: <code>op:only;</code> or <code>op:no;</code>.
|
||||
</li>
|
||||
<li>
|
||||
Filter only posts with files (`only`) or only posts without files (`no`).<br>
|
||||
For example: <code>file:only;</code> or <code>file:no;</code>.
|
||||
</li>
|
||||
<li>
|
||||
Overrule the `Show Stubs` setting if specified: create a stub (`yes`) or not (`no`).<br>
|
||||
@ -30,10 +39,16 @@
|
||||
Highlighted OPs will have their threads put on top of the board index by default.<br>
|
||||
For example: <code>top:yes;</code> or <code>top:no;</code>.
|
||||
</li>
|
||||
<li>
|
||||
Show a desktop notification instead of hiding.<br>
|
||||
For example: <code>notify;</code>.
|
||||
</li>
|
||||
<li>
|
||||
Filters in the "General" section apply to multiple fields, by default <code>subject,name,filename,comment</code>.<br>
|
||||
The fields can be specified with the <code>type</code> option, separated by commas.<br>
|
||||
For example: <code>type:@{filterTypes};</code>.
|
||||
For example: <code>type:@{filterTypes};</code>.<br>
|
||||
Types can also be combined with a <code>+</code> sign; this indicates the filter applies to the given fields joined by newlines.<br>
|
||||
For example: <code>type:filename+filesize+dimensions;</code>.<br>
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
<option value="tripcode">Tripcode</option>
|
||||
<option value="capcode">Capcode</option>
|
||||
<option value="pass">Pass Date</option>
|
||||
<option value="email">Email</option>
|
||||
<option value="subject">Subject</option>
|
||||
<option value="comment">Comment</option>
|
||||
<option value="flag">Flag</option>
|
||||
|
||||
@ -308,7 +308,8 @@ dragend = ->
|
||||
$.off d, 'mouseup', @up
|
||||
$.set "#{@id}.position", @style.cssText
|
||||
|
||||
hoverstart = ({root, el, latestEvent, endEvents, height, cb, noRemove}) ->
|
||||
hoverstart = ({root, el, latestEvent, endEvents, height, width, cb, noRemove}) ->
|
||||
rect = root.getBoundingClientRect()
|
||||
o = {
|
||||
root
|
||||
el
|
||||
@ -320,7 +321,10 @@ hoverstart = ({root, el, latestEvent, endEvents, height, cb, noRemove}) ->
|
||||
clientHeight: doc.clientHeight
|
||||
clientWidth: doc.clientWidth
|
||||
height
|
||||
width
|
||||
noRemove
|
||||
clientX: (rect.left + rect.right) / 2
|
||||
clientY: (rect.top + rect.bottom) / 2
|
||||
}
|
||||
o.hover = hover.bind o
|
||||
o.hoverend = hoverend.bind o
|
||||
@ -344,7 +348,8 @@ hoverstart.padding = 25
|
||||
hover = (e) ->
|
||||
@latestEvent = e
|
||||
height = (@height or @el.offsetHeight) + hoverstart.padding
|
||||
{clientX, clientY} = e
|
||||
width = (@width or @el.offsetWidth)
|
||||
{clientX, clientY} = if Conf['Follow Cursor'] then e else @
|
||||
|
||||
top = if @isImage
|
||||
Math.max 0, clientY * (@clientHeight - height) / @clientHeight
|
||||
@ -353,10 +358,10 @@ hover = (e) ->
|
||||
|
||||
threshold = @clientWidth / 2
|
||||
threshold = Math.max threshold, @clientWidth - 400 unless @isImage
|
||||
[left, right] = if clientX <= threshold
|
||||
[clientX + 45 + 'px', '']
|
||||
else
|
||||
['', @clientWidth - clientX + 45 + 'px']
|
||||
marginX = (if clientX <= threshold then clientX else @clientWidth - clientX) + 45
|
||||
marginX = Math.min(marginX, @clientWidth - width) if @isImage
|
||||
marginX += 'px'
|
||||
[left, right] = if clientX <= threshold then [marginX, ''] else ['', marginX]
|
||||
|
||||
{style} = @
|
||||
style.top = top + 'px'
|
||||
|
||||
@ -54,7 +54,7 @@ ImageCommon =
|
||||
clearTimeout timeoutID if delay?
|
||||
cb URL
|
||||
|
||||
$.ajax "#{location.protocol}//a.4cdn.org/#{post.board}/thread/#{post.thread}.json", onload: ->
|
||||
$.ajax Site.urls.threadJSON({boardID: post.boardID, threadID: post.threadID}), onloadend: ->
|
||||
post.kill !post.isClone if @status is 404
|
||||
return redirect() if @status isnt 200
|
||||
for postObj in @response.posts
|
||||
|
||||
@ -250,7 +250,7 @@ ImageExpand =
|
||||
mouseover: -> mousedown = false
|
||||
mousedown: (e) -> mousedown = true if e.button is 0
|
||||
mouseup: (e) -> mousedown = false if e.button is 0
|
||||
mouseout: (e) -> ImageExpand.toggle(Get.postFromNode @) if mousedown and e.clientX <= @getBoundingClientRect().left
|
||||
mouseout: (e) -> ImageExpand.toggle(Get.postFromNode @) if ((e.buttons & 1) or mousedown) and e.clientX <= @getBoundingClientRect().left
|
||||
|
||||
setupVideoCB: (post) ->
|
||||
for eventName, cb of ImageExpand.videoCB
|
||||
|
||||
@ -46,19 +46,22 @@ ImageHover =
|
||||
if Conf['Autoplay']
|
||||
el.play()
|
||||
@currentTime = el.currentTime if @nodeName is 'VIDEO'
|
||||
[width, height] = (+x for x in file.dimensions.split 'x')
|
||||
{left, right} = @getBoundingClientRect()
|
||||
maxWidth = Math.max left, doc.clientWidth - right
|
||||
maxHeight = doc.clientHeight - UI.hover.padding
|
||||
scale = Math.min 1, maxWidth / width, maxHeight / height
|
||||
el.style.maxWidth = "#{scale * width}px"
|
||||
el.style.maxHeight = "#{scale * height}px"
|
||||
if file.dimensions
|
||||
[width, height] = (+x for x in file.dimensions.split 'x')
|
||||
maxWidth = doc.clientWidth
|
||||
maxHeight = doc.clientHeight - UI.hover.padding
|
||||
scale = Math.min 1, maxWidth / width, maxHeight / height
|
||||
width *= scale
|
||||
height *= scale
|
||||
el.style.maxWidth = "#{width}px"
|
||||
el.style.maxHeight = "#{height}px"
|
||||
UI.hover
|
||||
root: @
|
||||
el: el
|
||||
latestEvent: e
|
||||
endEvents: 'mouseout click'
|
||||
height: scale * height
|
||||
height: height
|
||||
width: width
|
||||
noRemove: true
|
||||
cb: ->
|
||||
$.off el, 'error', error
|
||||
|
||||
@ -57,7 +57,7 @@ Sauce =
|
||||
parts[key] = parts[key].replace /%(T?URL|IMG|[sh]?MD5|board|name|%|semi|\$\d+)/g, (orig, parameter) ->
|
||||
if parameter[0] is '$'
|
||||
return orig unless matches
|
||||
type = matches[parameter[1..]]
|
||||
type = matches[parameter[1..]] or ''
|
||||
else
|
||||
type = Sauce.formatters[parameter] post, ext
|
||||
if not type?
|
||||
|
||||
@ -111,7 +111,7 @@ Embedding =
|
||||
if service.queue.length >= service.batchSize
|
||||
Embedding.flushTitles service
|
||||
else
|
||||
CrossOrigin.json service.api(uid), (-> Embedding.cb.title @, data)
|
||||
CrossOrigin.cache service.api(uid), (-> Embedding.cb.title @, data)
|
||||
|
||||
flushTitles: (service) ->
|
||||
{queue} = service
|
||||
@ -120,7 +120,7 @@ Embedding =
|
||||
cb = ->
|
||||
Embedding.cb.title @, data for data in queue
|
||||
return
|
||||
CrossOrigin.json service.api(data.uid for data in queue), cb
|
||||
CrossOrigin.cache service.api(data.uid for data in queue), cb
|
||||
|
||||
preview: (data) ->
|
||||
{key, uid, link} = data
|
||||
@ -275,7 +275,7 @@ Embedding =
|
||||
el = $.el 'pre',
|
||||
hidden: true
|
||||
id: "gist-embed-#{counter++}"
|
||||
CrossOrigin.json "https://api.github.com/gists/#{a.dataset.uid}", ->
|
||||
CrossOrigin.cache "https://api.github.com/gists/#{a.dataset.uid}", ->
|
||||
el.textContent = Object.values(@response.files)[0].content
|
||||
el.className = 'prettyprint'
|
||||
$.global ->
|
||||
|
||||
@ -45,7 +45,7 @@ ArchiveLink =
|
||||
value = if type is 'country'
|
||||
post.info.flagCode or post.info.flagCodeTroll
|
||||
else
|
||||
Filter[type] post
|
||||
Filter.value type, post
|
||||
# We want to parse the exact same stuff as the filter does already.
|
||||
return false unless value
|
||||
el.href = Redirect.to 'search',
|
||||
|
||||
@ -82,12 +82,15 @@ DeleteLink =
|
||||
$.ajax $.id('delform').action.replace("/#{g.BOARD}/", "/#{post.board}/"),
|
||||
responseType: 'document'
|
||||
withCredentials: true
|
||||
onload: -> DeleteLink.load link, post, fileOnly, @response
|
||||
onerror: -> DeleteLink.error link, post
|
||||
,
|
||||
onloadend: -> DeleteLink.load link, post, fileOnly, @response
|
||||
form: $.formData form
|
||||
|
||||
load: (link, post, fileOnly, resDoc) ->
|
||||
unless resDoc
|
||||
new Notice 'warning', 'Connection error, please retry.', 20
|
||||
$.on link, 'click', DeleteLink.toggle if post.fullID is DeleteLink.post.fullID
|
||||
return
|
||||
|
||||
link.textContent = DeleteLink.linkText fileOnly
|
||||
if resDoc.title is '4chan - Banned' # Ban/warn check
|
||||
el = $.el 'span', <%= html('You can't delete posts because you are <a href="//www.4chan.org/banned" target="_blank">banned</a>.') %>
|
||||
@ -106,10 +109,6 @@ DeleteLink =
|
||||
(post.origin or post).kill fileOnly
|
||||
link.textContent = 'Deleted' if post.fullID is DeleteLink.post.fullID
|
||||
|
||||
error: (link, post) ->
|
||||
new Notice 'warning', 'Connection error, please retry.', 20
|
||||
$.on link, 'click', DeleteLink.toggle if post.fullID is DeleteLink.post.fullID
|
||||
|
||||
cooldown:
|
||||
seconds: {}
|
||||
|
||||
|
||||
@ -75,7 +75,7 @@ CatalogLinks =
|
||||
return
|
||||
|
||||
catalog: (board=g.BOARD.ID) ->
|
||||
if Conf['External Catalog'] and board in ['a', 'c', 'g', 'biz', 'k', 'm', 'o', 'p', 'v', 'vg', 'vr', 'w', 'wg', 'cm', '3', 'adv', 'an', 'asp', 'cgl', 'ck', 'co', 'diy', 'fa', 'fit', 'gd', 'int', 'jp', 'lit', 'mlp', 'mu', 'n', 'out', 'po', 'sci', 'sp', 'tg', 'toy', 'trv', 'tv', 'vp', 'wsg', 'x', 'f', 'pol', 's4s', 'lgbt']
|
||||
if Conf['External Catalog'] and board in ['3', 'a', 'adv', 'an', 'asp', 'biz', 'c', 'cgl', 'ck', 'cm', 'co', 'diy', 'f', 'fa', 'fit', 'g', 'gd', 'his', 'i', 'int', 'jp', 'k', 'lgbt', 'lit', 'm', 'mlp', 'mu', 'n', 'news', 'o', 'out', 'p', 'po', 'pol', 's4s', 'sci', 'sp', 'tg', 'toy', 'trv', 'tv', 'v', 'vg', 'vip', 'vp', 'vr', 'w', 'wg', 'wsg', 'wsr', 'x']
|
||||
"//catalog.neet.tv/#{board}/"
|
||||
else if Conf['JSON Index'] and Conf['Use <%= meta.name %> Catalog']
|
||||
if location.hostname in ['boards.4chan.org', 'boards.4channel.org'] and g.BOARD.ID is board and g.VIEW is 'index' then '#catalog' else "//#{BoardConfig.domain(board)}/#{board}/#catalog"
|
||||
|
||||
@ -2,9 +2,6 @@ ExpandComment =
|
||||
init: ->
|
||||
return if g.VIEW isnt 'index' or !Conf['Comment Expansion'] or Conf['JSON Index']
|
||||
|
||||
@callbacks.push Fourchan.code if g.BOARD.ID is 'g'
|
||||
@callbacks.push Fourchan.math if g.BOARD.ID is 'sci'
|
||||
|
||||
Callbacks.Post.push
|
||||
name: 'Comment Expansion'
|
||||
cb: @node
|
||||
@ -26,7 +23,7 @@ ExpandComment =
|
||||
return
|
||||
return if not (a = $ '.abbr > a', post.nodes.comment)
|
||||
a.textContent = "Post No.#{post} Loading..."
|
||||
$.cache "#{location.protocol}//a.4cdn.org#{a.pathname.split(/\/+/).splice(0,4).join('/')}.json", -> ExpandComment.parse @, a, post
|
||||
$.cache Site.urls.threadJSON({boardID: post.boardID, threadID: post.threadID}), -> ExpandComment.parse @, a, post
|
||||
|
||||
contract: (post) ->
|
||||
return unless post.nodes.shortComment
|
||||
@ -38,7 +35,7 @@ ExpandComment =
|
||||
parse: (req, a, post) ->
|
||||
{status} = req
|
||||
unless status in [200, 304]
|
||||
a.textContent = "Error #{req.statusText} (#{status})"
|
||||
a.textContent = if status then "Error #{req.statusText} (#{status})" else 'Connection Error'
|
||||
return
|
||||
|
||||
posts = req.response.posts
|
||||
|
||||
@ -18,7 +18,9 @@ ExpandThread =
|
||||
disconnect: (refresh) ->
|
||||
return if g.VIEW is 'thread' or !Conf['Thread Expansion']
|
||||
for threadID, status of ExpandThread.statuses
|
||||
status.req?.abort()
|
||||
if (oldReq = status.req)
|
||||
delete status.req
|
||||
oldReq.abort()
|
||||
delete ExpandThread.statuses[threadID]
|
||||
|
||||
$.off d, 'IndexRefreshInternal', @onIndexRefresh unless refresh
|
||||
@ -52,15 +54,17 @@ ExpandThread =
|
||||
expand: (thread, a) ->
|
||||
ExpandThread.statuses[thread] = status = {}
|
||||
a.textContent = Build.summaryText '...', a.textContent.match(/\d+/g)...
|
||||
status.req = $.cache "#{location.protocol}//a.4cdn.org/#{thread.board}/thread/#{thread}.json", ->
|
||||
status.req = $.cache Site.urls.threadJSON({boardID: thread.board.ID, threadID: thread.ID}), ->
|
||||
return if @ isnt status.req # aborted
|
||||
delete status.req
|
||||
ExpandThread.parse @, thread, a
|
||||
|
||||
contract: (thread, a, threadRoot) ->
|
||||
status = ExpandThread.statuses[thread]
|
||||
delete ExpandThread.statuses[thread]
|
||||
if status.req
|
||||
status.req.abort()
|
||||
if (oldReq = status.req)
|
||||
delete status.req
|
||||
oldReq.abort()
|
||||
a.textContent = Build.summaryText '+', a.textContent.match(/\d+/g)... if a
|
||||
return
|
||||
|
||||
@ -89,7 +93,7 @@ ExpandThread =
|
||||
|
||||
parse: (req, thread, a) ->
|
||||
if req.status not in [200, 304]
|
||||
a.textContent = "Error #{req.statusText} (#{req.status})"
|
||||
a.textContent = if req.status then "Error #{req.statusText} (#{req.status})" else 'Connection Error'
|
||||
return
|
||||
|
||||
Build.spoilerRange[thread.board] = req.response.posts[0].custom_spoiler
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
Fourchan =
|
||||
init: ->
|
||||
return unless g.VIEW in ['index', 'thread', 'archive']
|
||||
return unless Site.software is 'yotsuba' and g.VIEW in ['index', 'thread', 'archive']
|
||||
BoardConfig.ready @initBoard
|
||||
Main.ready @initReady
|
||||
|
||||
if g.BOARD.ID is 'g'
|
||||
initBoard: ->
|
||||
if g.BOARD.config.code_tags
|
||||
$.on window, 'prettyprint:cb', (e) ->
|
||||
return if not (post = g.posts[e.detail.ID])
|
||||
return if not (pre = $$('.prettyprint', post.nodes.comment)[e.detail.i])
|
||||
@ -21,10 +24,12 @@ Fourchan =
|
||||
}, false);
|
||||
'''
|
||||
Callbacks.Post.push
|
||||
name: 'Parse /g/ code'
|
||||
cb: @code
|
||||
name: 'Parse [code] tags'
|
||||
cb: Fourchan.code
|
||||
g.posts.forEach (post) -> Callbacks.Post.execute post, ['Parse [code] tags'], true
|
||||
ExpandComment.callbacks.push Fourchan.code
|
||||
|
||||
if g.BOARD.ID is 'sci'
|
||||
if g.BOARD.config.math_tags
|
||||
$.global ->
|
||||
window.addEventListener 'mathjax', (e) ->
|
||||
if window.MathJax
|
||||
@ -40,16 +45,18 @@ Fourchan =
|
||||
, false
|
||||
, false
|
||||
Callbacks.Post.push
|
||||
name: 'Parse /sci/ math'
|
||||
cb: @math
|
||||
name: 'Parse [math] tags'
|
||||
cb: Fourchan.math
|
||||
g.posts.forEach (post) -> Callbacks.Post.execute post, ['Parse [math] tags'], true
|
||||
ExpandComment.callbacks.push Fourchan.math
|
||||
|
||||
# Disable 4chan's ID highlighting (replaced by IDHighlight) and reported post hiding.
|
||||
Main.ready ->
|
||||
$.global ->
|
||||
window.clickable_ids = false
|
||||
for node in document.querySelectorAll '.posteruid, .capcode'
|
||||
node.removeEventListener 'click', window.idClick, false
|
||||
return
|
||||
# Disable 4chan's ID highlighting (replaced by IDHighlight) and reported post hiding.
|
||||
initReady: ->
|
||||
$.global ->
|
||||
window.clickable_ids = false
|
||||
for node in document.querySelectorAll '.posteruid, .capcode'
|
||||
node.removeEventListener 'click', window.idClick, false
|
||||
return
|
||||
|
||||
code: ->
|
||||
return if @isClone
|
||||
|
||||
@ -109,6 +109,9 @@ Keybinds =
|
||||
when Conf['Toggle thread watcher']
|
||||
return unless ThreadWatcher.enabled
|
||||
ThreadWatcher.toggleWatcher()
|
||||
when Conf['Toggle threading']
|
||||
return unless QuoteThreading.ready
|
||||
QuoteThreading.toggleThreading()
|
||||
when Conf['Mark thread read']
|
||||
return unless g.VIEW is 'index' and thread and UnreadIndex.enabled
|
||||
UnreadIndex.markRead.call threadRoot
|
||||
|
||||
@ -76,14 +76,13 @@ Report =
|
||||
results = []
|
||||
for [name, url] in urls
|
||||
do (name, url) ->
|
||||
$.ajax url,
|
||||
responseType: 'json'
|
||||
$.ajax url, {
|
||||
onloadend: ->
|
||||
results.push [name, @response or {error: ''}]
|
||||
if results.length is urls.length
|
||||
cb results
|
||||
,
|
||||
{form}
|
||||
form
|
||||
}
|
||||
return
|
||||
|
||||
archiveResults: (results) ->
|
||||
|
||||
@ -75,9 +75,11 @@ ThreadStats =
|
||||
$.addClass ThreadStats.pageCountEl, 'warning'
|
||||
return
|
||||
ThreadStats.timeout = setTimeout ThreadStats.fetchPage, 2 * $.MINUTE
|
||||
$.ajax "#{location.protocol}//a.4cdn.org/#{ThreadStats.thread.board}/threads.json", onload: ThreadStats.onThreadsLoad,
|
||||
whenModified: 'ThreadStats'
|
||||
bypassCache: true
|
||||
$.whenModified(
|
||||
Site.urls.threadsListJSON({boardID: ThreadStats.thread.board}),
|
||||
'ThreadStats',
|
||||
ThreadStats.onThreadsLoad
|
||||
)
|
||||
|
||||
onThreadsLoad: ->
|
||||
if @status is 200
|
||||
|
||||
@ -128,17 +128,17 @@ ThreadUpdater =
|
||||
$.cb.value.call @ if e
|
||||
|
||||
load: ->
|
||||
{req} = ThreadUpdater
|
||||
switch req.status
|
||||
return if @ isnt ThreadUpdater.req # aborted
|
||||
switch @status
|
||||
when 200
|
||||
ThreadUpdater.parse req
|
||||
ThreadUpdater.parse @
|
||||
if ThreadUpdater.thread.isArchived
|
||||
ThreadUpdater.kill()
|
||||
else
|
||||
ThreadUpdater.setInterval()
|
||||
when 404
|
||||
# XXX workaround for 4chan sending false 404s
|
||||
$.ajax "#{location.protocol}//a.4cdn.org/#{ThreadUpdater.thread.board}/catalog.json", onloadend: ->
|
||||
$.ajax Site.urls.catalogJSON({boardID: ThreadUpdater.thread.board.ID}), onloadend: ->
|
||||
if @status is 200
|
||||
confirmed = true
|
||||
for page in @response
|
||||
@ -151,9 +151,9 @@ ThreadUpdater =
|
||||
if confirmed
|
||||
ThreadUpdater.kill()
|
||||
else
|
||||
ThreadUpdater.error req
|
||||
ThreadUpdater.error @
|
||||
else
|
||||
ThreadUpdater.error req
|
||||
ThreadUpdater.error @
|
||||
|
||||
kill: ->
|
||||
ThreadUpdater.thread.kill()
|
||||
@ -230,13 +230,15 @@ ThreadUpdater =
|
||||
update: ->
|
||||
clearTimeout ThreadUpdater.timeoutID
|
||||
ThreadUpdater.set 'timer', '...', 'loading'
|
||||
ThreadUpdater.req?.abort()
|
||||
ThreadUpdater.req = $.ajax "#{location.protocol}//a.4cdn.org/#{ThreadUpdater.thread.board}/thread/#{ThreadUpdater.thread}.json",
|
||||
onloadend: ThreadUpdater.cb.load
|
||||
timeout: $.MINUTE
|
||||
,
|
||||
whenModified: 'ThreadUpdater'
|
||||
bypassCache: true
|
||||
if (oldReq = ThreadUpdater.req)
|
||||
delete ThreadUpdater.req
|
||||
oldReq.abort()
|
||||
ThreadUpdater.req = $.whenModified(
|
||||
Site.urls.threadJSON({boardID: ThreadUpdater.thread.board.ID, threadID: ThreadUpdater.thread.ID}),
|
||||
'ThreadUpdater',
|
||||
ThreadUpdater.cb.load,
|
||||
{timeout: $.MINUTE}
|
||||
)
|
||||
|
||||
updateThreadStatus: (type, status) ->
|
||||
return if not (hasChanged = ThreadUpdater.thread["is#{type}"] isnt status)
|
||||
|
||||
@ -10,8 +10,8 @@ ThreadWatcher =
|
||||
className: 'fa fa-eye'
|
||||
|
||||
@db = new DataBoard 'watchedThreads', @refresh, true
|
||||
@dbLM = new DataBoard 'watcherLastModified', null, true
|
||||
@dialog = UI.dialog 'thread-watcher', <%= readHTML('ThreadWatcher.html') %>
|
||||
|
||||
@status = $ '#watcher-status', @dialog
|
||||
@list = @dialog.lastElementChild
|
||||
@refreshButton = $ '.refresh', @dialog
|
||||
@ -41,6 +41,7 @@ ThreadWatcher =
|
||||
|
||||
Header.addShortcut 'watcher', sc, 510
|
||||
|
||||
ThreadWatcher.initLastModified()
|
||||
ThreadWatcher.fetchAuto()
|
||||
$.on window, 'visibilitychange focus', -> $.queueTask ThreadWatcher.fetchAuto
|
||||
|
||||
@ -92,16 +93,16 @@ ThreadWatcher =
|
||||
href: 'javascript:;'
|
||||
className: 'watch-thread-link'
|
||||
$.before $('input', @nodes.info), toggler
|
||||
siteID = Site.hostname
|
||||
boardID = @board.ID
|
||||
threadID = @thread.ID
|
||||
data = ThreadWatcher.db.get {boardID, threadID}
|
||||
data = ThreadWatcher.db.get {siteID, boardID, threadID}
|
||||
ThreadWatcher.setToggler toggler, !!data
|
||||
$.on toggler, 'click', ThreadWatcher.cb.toggle
|
||||
# Add missing excerpt for threads added by Auto Watch
|
||||
if data and not data.excerpt?
|
||||
$.queueTask =>
|
||||
ThreadWatcher.db.extend {boardID, threadID, val: {excerpt: Get.threadExcerpt @thread}}
|
||||
ThreadWatcher.refresh()
|
||||
ThreadWatcher.update siteID, boardID, threadID, val: {excerpt: Get.threadExcerpt @thread}
|
||||
|
||||
catalogNode: ->
|
||||
$.addClass @nodes.root, 'watched' if ThreadWatcher.isWatched @thread
|
||||
@ -153,13 +154,14 @@ ThreadWatcher =
|
||||
for threadID, data of db.data[siteID].boards[boardID] when not data?.isDead and "#{boardID}.#{threadID}" not in e.detail.threads
|
||||
# Don't prune threads that have yet to appear in index.
|
||||
continue unless e.detail.threads.some (fullID) -> +fullID.split('.')[1] > threadID
|
||||
nKilled++
|
||||
if Conf['Auto Prune'] or not (data and typeof data is 'object') # corrupt data
|
||||
db.delete {boardID, threadID}
|
||||
nKilled++
|
||||
else if ThreadWatcher.unreadEnabled and Conf['Show Unread Count']
|
||||
ThreadWatcher.fetchStatus {siteID, boardID, threadID, data}
|
||||
else
|
||||
db.extend {boardID, threadID, val: {isDead: true}}
|
||||
if ThreadWatcher.unreadEnabled and Conf['Show Unread Count']
|
||||
ThreadWatcher.fetchStatus {siteID, boardID, threadID, data}
|
||||
db.extend {boardID, threadID, val: {isDead: true, page: undefined, lastPage: undefined, unread: undefined, quotingYou: undefined}}
|
||||
nKilled++
|
||||
ThreadWatcher.refresh() if nKilled
|
||||
onThreadRefresh: (e) ->
|
||||
thread = g.threads[e.detail.threadID]
|
||||
@ -170,6 +172,30 @@ ThreadWatcher =
|
||||
requests: []
|
||||
fetched: 0
|
||||
|
||||
fetch: (url, {siteID, force}, args, cb) ->
|
||||
if ThreadWatcher.requests.length is 0
|
||||
ThreadWatcher.status.textContent = '...'
|
||||
$.addClass ThreadWatcher.refreshButton, 'fa-spin'
|
||||
onloadend = ->
|
||||
return if @finished
|
||||
@finished = true
|
||||
ThreadWatcher.fetched++
|
||||
if ThreadWatcher.fetched is ThreadWatcher.requests.length
|
||||
ThreadWatcher.clearRequests()
|
||||
else
|
||||
ThreadWatcher.status.textContent = "#{Math.round(ThreadWatcher.fetched / ThreadWatcher.requests.length * 100)}%"
|
||||
cb.apply @, args
|
||||
ajax = if siteID is Site.hostname then $.ajax else CrossOrigin.ajax
|
||||
if force
|
||||
delete $.lastModified.ThreadWatcher?[url]
|
||||
req = $.whenModified(
|
||||
url,
|
||||
'ThreadWatcher',
|
||||
onloadend,
|
||||
{timeout: $.MINUTE, ajax}
|
||||
)
|
||||
ThreadWatcher.requests.push req
|
||||
|
||||
clearRequests: ->
|
||||
ThreadWatcher.requests = []
|
||||
ThreadWatcher.fetched = 0
|
||||
@ -177,78 +203,137 @@ ThreadWatcher =
|
||||
$.rmClass ThreadWatcher.refreshButton, 'fa-spin'
|
||||
|
||||
abort: ->
|
||||
for req in ThreadWatcher.requests when req.readyState isnt 4 # DONE
|
||||
delete ThreadWatcher.syncing
|
||||
for req in ThreadWatcher.requests when !req.finished
|
||||
req.finished = true
|
||||
req.abort()
|
||||
ThreadWatcher.clearRequests()
|
||||
|
||||
initLastModified: ->
|
||||
lm = ($.lastModified['ThreadWatcher'] or= {})
|
||||
for siteID, boards of ThreadWatcher.dbLM.data
|
||||
for boardID, data of boards.boards
|
||||
if ThreadWatcher.db.get {siteID, boardID}
|
||||
for url, date of data
|
||||
lm[url] = date
|
||||
else
|
||||
ThreadWatcher.dbLM.delete {siteID, boardID}
|
||||
return
|
||||
|
||||
fetchAuto: ->
|
||||
clearTimeout ThreadWatcher.timeout
|
||||
return unless Conf['Auto Update Thread Watcher']
|
||||
{db} = ThreadWatcher
|
||||
interval = if ThreadWatcher.unreadEnabled and Conf['Show Unread Count'] then 5 * $.MINUTE else 2 * $.HOUR
|
||||
interval = if Conf['Show Page'] or (ThreadWatcher.unreadEnabled and Conf['Show Unread Count']) then 5 * $.MINUTE else 2 * $.HOUR
|
||||
now = Date.now()
|
||||
unless now - interval < (db.data.lastChecked or 0) <= now or d.hidden or not d.hasFocus()
|
||||
ThreadWatcher.fetchAllStatus()
|
||||
db.setLastChecked()
|
||||
ThreadWatcher.timeout = setTimeout ThreadWatcher.fetchAuto, interval
|
||||
|
||||
buttonFetchAll: ->
|
||||
if ThreadWatcher.requests.length
|
||||
if ThreadWatcher.syncing or ThreadWatcher.requests.length
|
||||
ThreadWatcher.abort()
|
||||
else
|
||||
ThreadWatcher.fetchAllStatus()
|
||||
|
||||
fetchAllStatus: ->
|
||||
ThreadWatcher.status.textContent = '...'
|
||||
$.addClass ThreadWatcher.refreshButton, 'fa-spin'
|
||||
ThreadWatcher.syncing = true
|
||||
dbs = [ThreadWatcher.db, ThreadWatcher.unreaddb, QuoteYou.db].filter((x) -> x)
|
||||
n = 0
|
||||
for db in dbs
|
||||
db.forceSync ->
|
||||
for dbi in dbs
|
||||
dbi.forceSync ->
|
||||
if (++n) is dbs.length
|
||||
threads = ThreadWatcher.getAll()
|
||||
for thread in threads
|
||||
ThreadWatcher.fetchStatus thread
|
||||
return
|
||||
return if !ThreadWatcher.syncing # aborted
|
||||
delete ThreadWatcher.syncing
|
||||
# XXX On vichan boards, last_modified field of threads.json does not account for sage posts.
|
||||
# Occasionally check replies field of catalog.json to find these posts.
|
||||
{db} = ThreadWatcher
|
||||
now = Date.now()
|
||||
deep = !(now - 2 * $.HOUR < (db.data.lastChecked2 or 0) <= now)
|
||||
boards = ThreadWatcher.getAll(true)
|
||||
for board in boards
|
||||
ThreadWatcher.fetchBoard board, deep
|
||||
db.setLastChecked()
|
||||
db.setLastChecked('lastChecked2') if deep
|
||||
if ThreadWatcher.fetched is ThreadWatcher.requests.length
|
||||
ThreadWatcher.clearRequests()
|
||||
|
||||
fetchStatus: (thread, force) ->
|
||||
{siteID, boardID, threadID, data} = thread
|
||||
fetchBoard: (board, deep) ->
|
||||
return unless board.some (thread) -> !thread.data.isDead
|
||||
force = Conf['Show Page'] and board.some((thread) -> !thread.data.page? and !thread.data.isDead and thread.data.last isnt -1)
|
||||
{siteID, boardID} = board[0]
|
||||
software = Conf['siteProperties'][siteID]?.software
|
||||
urlF = if deep and software is 'tinyboard' then 'catalogJSON' else 'threadsListJSON'
|
||||
url = SW[software]?.urls[urlF]?({siteID, boardID})
|
||||
return unless url
|
||||
ThreadWatcher.fetch url, {siteID, force}, [board, url], ThreadWatcher.parseBoard
|
||||
|
||||
parseBoard: (board, url) ->
|
||||
return unless @status is 200
|
||||
{siteID, boardID} = board[0]
|
||||
software = Conf['siteProperties'][siteID]?.software
|
||||
lmDate = @getResponseHeader('Last-Modified')
|
||||
ThreadWatcher.dbLM.extend {siteID, boardID, val: $.item(url, lmDate)}
|
||||
threads = {}
|
||||
pageLength = 0
|
||||
nThreads = 0
|
||||
oldest = null
|
||||
try
|
||||
pageLength = @response[0]?.threads.length or 0
|
||||
for page, i in @response
|
||||
for item in page.threads
|
||||
threads[item.no] =
|
||||
page: i + 1
|
||||
index: nThreads
|
||||
modified: item.last_modified
|
||||
replies: item.replies
|
||||
nThreads++
|
||||
if !oldest? or item.no < oldest
|
||||
oldest = item.no
|
||||
catch
|
||||
for thread in board
|
||||
ThreadWatcher.fetchStatus thread
|
||||
for thread in board
|
||||
{threadID, data} = thread
|
||||
if threads[threadID]
|
||||
{page, index, modified, replies} = threads[threadID]
|
||||
if Conf['Show Page']
|
||||
lastPage = if SW[software]?.isPrunedByAge?({siteID, boardID})
|
||||
threadID is oldest
|
||||
else
|
||||
index >= nThreads - pageLength
|
||||
ThreadWatcher.update siteID, boardID, threadID, {page, lastPage}
|
||||
if ThreadWatcher.unreadEnabled and Conf['Show Unread Count']
|
||||
if modified isnt data.modified or (replies? and replies isnt data.replies)
|
||||
ThreadWatcher.db.extend {siteID, boardID, threadID, val: {modified}}
|
||||
ThreadWatcher.fetchStatus thread
|
||||
else
|
||||
if ThreadWatcher.unreadEnabled and Conf['Show Unread Count']
|
||||
ThreadWatcher.fetchStatus thread
|
||||
else
|
||||
ThreadWatcher.update siteID, boardID, threadID, {isDead: true}
|
||||
return
|
||||
|
||||
fetchStatus: (thread) ->
|
||||
{siteID, boardID, threadID, data, force} = thread
|
||||
software = Conf['siteProperties'][siteID]?.software
|
||||
url = SW[software]?.urls.threadJSON?({siteID, boardID, threadID})
|
||||
return unless url
|
||||
return if data.isDead and not force
|
||||
return if data.last is -1 # 404 or no JSON API
|
||||
if ThreadWatcher.requests.length is 0
|
||||
ThreadWatcher.status.textContent = '...'
|
||||
$.addClass ThreadWatcher.refreshButton, 'fa-spin'
|
||||
if Site.hasCORS?(url) or url.split('/')[...3].join('/') is location.origin
|
||||
req = $.ajax url,
|
||||
onloadend: ->
|
||||
ThreadWatcher.parseStatus.call @, thread
|
||||
timeout: $.MINUTE
|
||||
,
|
||||
whenModified: if force then false else 'ThreadWatcher'
|
||||
else
|
||||
req = {abort: () -> req.aborted = true}
|
||||
CrossOrigin.json url, ->
|
||||
return if req.aborted
|
||||
ThreadWatcher.parseStatus.call @, thread
|
||||
, true, $.MINUTE
|
||||
ThreadWatcher.requests.push req
|
||||
ThreadWatcher.fetch url, {siteID, force}, [thread], ThreadWatcher.parseStatus
|
||||
|
||||
parseStatus: ({siteID, boardID, threadID, data}) ->
|
||||
ThreadWatcher.fetched++
|
||||
if ThreadWatcher.fetched is ThreadWatcher.requests.length
|
||||
ThreadWatcher.clearRequests()
|
||||
else
|
||||
ThreadWatcher.status.textContent = "#{Math.round(ThreadWatcher.fetched / ThreadWatcher.requests.length * 100)}%"
|
||||
|
||||
software = Conf['siteProperties'][siteID]?.software
|
||||
|
||||
if @status is 200 and @response
|
||||
last = @response.posts[@response.posts.length-1].no
|
||||
replies = @response.posts.length-1
|
||||
isDead = !!@response.posts[0].archived
|
||||
if isDead and Conf['Auto Prune']
|
||||
ThreadWatcher.db.delete {siteID, boardID, threadID}
|
||||
ThreadWatcher.refresh()
|
||||
ThreadWatcher.rm siteID, boardID, threadID
|
||||
return
|
||||
|
||||
return if last is data.last and isDead is data.isDead
|
||||
@ -264,7 +349,7 @@ ThreadWatcher =
|
||||
|
||||
unread++
|
||||
|
||||
if !quotingYou and !Conf['Require OP Quote Link'] and youOP and not Filter.isHidden(Build.parseJSON postObj, boardID)
|
||||
if !quotingYou and !Conf['Require OP Quote Link'] and youOP and not Filter.isHidden(Build.parseJSON postObj, boardID, siteID)
|
||||
quotingYou = true
|
||||
continue
|
||||
|
||||
@ -282,31 +367,27 @@ ThreadWatcher =
|
||||
}
|
||||
quotesYou = true
|
||||
break
|
||||
if quotesYou and not Filter.isHidden(Build.parseJSON postObj, boardID)
|
||||
if quotesYou and not Filter.isHidden(Build.parseJSON postObj, boardID, siteID)
|
||||
quotingYou = true
|
||||
|
||||
updated = (isDead isnt data.isDead or unread isnt data.unread or quotingYou isnt data.quotingYou)
|
||||
ThreadWatcher.db.extend {siteID, boardID, threadID, val: {last, isDead, unread, quotingYou}}
|
||||
ThreadWatcher.refresh() if updated
|
||||
ThreadWatcher.update siteID, boardID, threadID, {last, replies, isDead, unread, quotingYou}
|
||||
|
||||
else if @status is 404
|
||||
if SW[software].mayLackJSON and !data.last?
|
||||
ThreadWatcher.db.extend {siteID, boardID, threadID, val: {last: -1}, rm: ['unread', 'quotingYou']}
|
||||
else if Conf['Auto Prune']
|
||||
ThreadWatcher.db.delete {siteID, boardID, threadID}
|
||||
ThreadWatcher.update siteID, boardID, threadID, {last: -1}
|
||||
else
|
||||
ThreadWatcher.db.extend {siteID, boardID, threadID, val: {isDead: true}, rm: ['unread', 'quotingYou']}
|
||||
ThreadWatcher.update siteID, boardID, threadID, {isDead: true}
|
||||
|
||||
ThreadWatcher.refresh()
|
||||
|
||||
getAll: ->
|
||||
getAll: (groupByBoard) ->
|
||||
all = []
|
||||
for siteID, boards of ThreadWatcher.db.data
|
||||
for boardID, threads of boards.boards
|
||||
if Conf['Current Board'] and (siteID isnt Site.hostname or boardID isnt g.BOARD.ID)
|
||||
continue
|
||||
if groupByBoard
|
||||
all.push (cont = [])
|
||||
for threadID, data of threads when data and typeof data is 'object'
|
||||
all.push {siteID, boardID, threadID, data}
|
||||
(if groupByBoard then cont else all).push {siteID, boardID, threadID, data}
|
||||
all
|
||||
|
||||
makeLine: (siteID, boardID, threadID, data) ->
|
||||
@ -326,6 +407,12 @@ ThreadWatcher =
|
||||
title: excerpt
|
||||
className: 'watcher-link'
|
||||
|
||||
if Conf['Show Page'] and data.page?
|
||||
page = $.el 'span',
|
||||
textContent: "[#{data.page}]"
|
||||
className: 'watcher-page'
|
||||
$.add link, page
|
||||
|
||||
if ThreadWatcher.unreadEnabled and Conf['Show Unread Count'] and data.unread?
|
||||
count = $.el 'span',
|
||||
textContent: "(#{data.unread})"
|
||||
@ -343,6 +430,9 @@ ThreadWatcher =
|
||||
div.dataset.siteID = siteID
|
||||
$.addClass div, 'current' if g.VIEW is 'thread' and fullID is "#{g.BOARD}.#{g.THREADID}"
|
||||
$.addClass div, 'dead-thread' if data.isDead
|
||||
if Conf['Show Page']
|
||||
$.addClass div, 'last-page' if data.lastPage
|
||||
div.dataset.page = data.page if data.page?
|
||||
if ThreadWatcher.unreadEnabled and Conf['Show Unread Count']
|
||||
$.addClass div, 'replies-read' if data.unread is 0
|
||||
$.addClass div, 'replies-unread' if data.unread
|
||||
@ -384,9 +474,6 @@ ThreadWatcher =
|
||||
$.add list, nodes
|
||||
|
||||
ThreadWatcher.refreshIcon()
|
||||
for refresher in ThreadWatcher.menu.refreshers
|
||||
refresher()
|
||||
return
|
||||
|
||||
refresh: ->
|
||||
ThreadWatcher.build()
|
||||
@ -407,19 +494,19 @@ ThreadWatcher =
|
||||
ThreadWatcher.shortcut.classList.toggle className, !!$(".#{className}", ThreadWatcher.dialog)
|
||||
return
|
||||
|
||||
update: (boardID, threadID, newData) ->
|
||||
siteID = Site.hostname
|
||||
return if not (data = ThreadWatcher.db?.get {boardID, threadID})
|
||||
update: (siteID, boardID, threadID, newData) ->
|
||||
return if not (data = ThreadWatcher.db?.get {siteID, boardID, threadID})
|
||||
if newData.isDead and Conf['Auto Prune']
|
||||
ThreadWatcher.db.delete {boardID, threadID}
|
||||
ThreadWatcher.refresh()
|
||||
ThreadWatcher.rm siteID, boardID, threadID
|
||||
return
|
||||
if newData.isDead or newData.last is -1
|
||||
for key in ['page', 'lastPage', 'unread', 'quotingyou'] when key not of newData
|
||||
newData[key] = undefined
|
||||
n = 0
|
||||
n++ for key, val of newData when data[key] isnt val
|
||||
return unless n
|
||||
return if not (data = ThreadWatcher.db.get {boardID, threadID})
|
||||
ThreadWatcher.db.extend {boardID, threadID, val: newData}
|
||||
if line = $ "#watched-threads > [data-site-i-d='#{siteID}'][data-full-i-d='#{boardID}.#{threadID}']", ThreadWatcher.dialog
|
||||
ThreadWatcher.db.extend {siteID, boardID, threadID, val: newData}
|
||||
if (line = $ "#watched-threads > [data-site-i-d='#{siteID}'][data-full-i-d='#{boardID}.#{threadID}']", ThreadWatcher.dialog)
|
||||
newLine = ThreadWatcher.makeLine siteID, boardID, threadID, data
|
||||
$.replace line, newLine
|
||||
ThreadWatcher.refreshIcon()
|
||||
@ -431,8 +518,8 @@ ThreadWatcher =
|
||||
if Conf['Auto Prune']
|
||||
ThreadWatcher.db.delete {boardID, threadID}
|
||||
return cb()
|
||||
return cb() if data.isDead and not (data.unread? or data.quotingYou?)
|
||||
ThreadWatcher.db.extend {boardID, threadID, val: {isDead: true}, rm: ['unread', 'quotingYou']}, cb
|
||||
return cb() if data.isDead and not (data.page? or data.lastPage? or data.unread? or data.quotingYou?)
|
||||
ThreadWatcher.db.extend {boardID, threadID, val: {isDead: true, page: undefined, lastPage: undefined, unread: undefined, quotingYou: undefined}}, cb
|
||||
|
||||
toggle: (thread) ->
|
||||
siteID = Site.hostname
|
||||
@ -459,15 +546,17 @@ ThreadWatcher =
|
||||
addRaw: (boardID, threadID, data) ->
|
||||
ThreadWatcher.db.set {boardID, threadID, val: data}
|
||||
ThreadWatcher.refresh()
|
||||
if ThreadWatcher.unreadEnabled and Conf['Show Unread Count']
|
||||
ThreadWatcher.fetchStatus {siteID: Site.hostname, boardID, threadID, data}, true
|
||||
thread = {siteID: Site.hostname, boardID, threadID, data, force: true}
|
||||
if Conf['Show Page'] and !data.isDead
|
||||
ThreadWatcher.fetchBoard [thread]
|
||||
else if ThreadWatcher.unreadEnabled and Conf['Show Unread Count']
|
||||
ThreadWatcher.fetchStatus thread
|
||||
|
||||
rm: (siteID, boardID, threadID) ->
|
||||
ThreadWatcher.db.delete {siteID, boardID, threadID}
|
||||
ThreadWatcher.refresh()
|
||||
|
||||
menu:
|
||||
refreshers: []
|
||||
init: ->
|
||||
return if !Conf['Thread Watcher']
|
||||
menu = @menu = new UI.Menu 'thread watcher'
|
||||
@ -482,53 +571,52 @@ ThreadWatcher =
|
||||
Header.menu.addEntry
|
||||
el: entryEl
|
||||
order: 60
|
||||
open: ->
|
||||
[addClass, rmClass, text] = if !!ThreadWatcher.db.get {boardID: g.BOARD.ID, threadID: g.THREADID}
|
||||
['unwatch-thread', 'watch-thread', 'Unwatch thread']
|
||||
else
|
||||
['watch-thread', 'unwatch-thread', 'Watch thread']
|
||||
$.addClass entryEl, addClass
|
||||
$.rmClass entryEl, rmClass
|
||||
entryEl.textContent = text
|
||||
true
|
||||
$.on entryEl, 'click', -> ThreadWatcher.toggle g.threads["#{g.BOARD}.#{g.THREADID}"]
|
||||
@refreshers.push ->
|
||||
[addClass, rmClass, text] = if $ '.current', ThreadWatcher.list
|
||||
['unwatch-thread', 'watch-thread', 'Unwatch thread']
|
||||
else
|
||||
['watch-thread', 'unwatch-thread', 'Watch thread']
|
||||
$.addClass entryEl, addClass
|
||||
$.rmClass entryEl, rmClass
|
||||
entryEl.textContent = text
|
||||
|
||||
addMenuEntries: ->
|
||||
entries = []
|
||||
|
||||
# `Open all` entry
|
||||
entries.push
|
||||
text: 'Open all threads'
|
||||
cb: ThreadWatcher.cb.openAll
|
||||
entry:
|
||||
el: $.el 'a',
|
||||
textContent: 'Open all threads'
|
||||
refresh: -> (if ThreadWatcher.list.firstElementChild then $.rmClass else $.addClass) @el, 'disabled'
|
||||
open: ->
|
||||
@el.classList.toggle 'disabled', !ThreadWatcher.list.firstElementChild
|
||||
true
|
||||
|
||||
# `Prune dead threads` entry
|
||||
entries.push
|
||||
text: 'Prune dead threads'
|
||||
cb: ThreadWatcher.cb.pruneDeads
|
||||
entry:
|
||||
open: ->
|
||||
@el.classList.toggle 'disabled', !$('.dead-thread', ThreadWatcher.list)
|
||||
true
|
||||
|
||||
for {text, cb, open} in entries
|
||||
entry =
|
||||
el: $.el 'a',
|
||||
textContent: 'Prune dead threads'
|
||||
refresh: -> (if $('.dead-thread', ThreadWatcher.list) then $.rmClass else $.addClass) @el, 'disabled'
|
||||
|
||||
# `Settings` entries:
|
||||
subEntries = []
|
||||
for name, conf of Config.threadWatcher
|
||||
subEntries.push @createSubEntry name, conf[1]
|
||||
entries.push
|
||||
entry:
|
||||
el: $.el 'span',
|
||||
textContent: 'Settings'
|
||||
subEntries: subEntries
|
||||
|
||||
for {entry, cb, refresh} in entries
|
||||
entry.el.href = 'javascript:;' if entry.el.nodeName is 'A'
|
||||
$.on entry.el, 'click', cb if cb
|
||||
@refreshers.push refresh.bind entry if refresh
|
||||
textContent: text
|
||||
href: 'javascript:;'
|
||||
$.on entry.el, 'click', cb
|
||||
entry.open = open.bind(entry)
|
||||
@menu.addEntry entry
|
||||
|
||||
# Settings checkbox entries:
|
||||
for name, conf of Config.threadWatcher
|
||||
@addCheckbox name, conf[1]
|
||||
|
||||
return
|
||||
|
||||
createSubEntry: (name, desc) ->
|
||||
addCheckbox: (name, desc) ->
|
||||
entry =
|
||||
type: 'thread watcher'
|
||||
el: UI.checkbox name, name.replace(' Thread Watcher', '')
|
||||
@ -539,6 +627,6 @@ ThreadWatcher =
|
||||
$.addClass entry.el, 'disabled'
|
||||
entry.el.title += '\n[Remember Last Read Post is disabled.]'
|
||||
$.on input, 'change', $.cb.checked
|
||||
$.on input, 'change', ThreadWatcher.refresh if name in ['Current Board', 'Show Unread Count', 'Show Site Prefix']
|
||||
$.on input, 'change', ThreadWatcher.fetchAuto if name in ['Show Unread Count', 'Auto Update Thread Watcher']
|
||||
entry
|
||||
$.on input, 'change', ThreadWatcher.refresh if name in ['Current Board', 'Show Page', 'Show Unread Count', 'Show Site Prefix']
|
||||
$.on input, 'change', ThreadWatcher.fetchAuto if name in ['Show Page', 'Show Unread Count', 'Auto Update Thread Watcher']
|
||||
@menu.addEntry entry
|
||||
|
||||
@ -125,9 +125,9 @@ Unread =
|
||||
Unread.openNotification post
|
||||
return
|
||||
|
||||
openNotification: (post) ->
|
||||
openNotification: (post, predicate=' replied to you') ->
|
||||
return unless Header.areNotificationsEnabled
|
||||
notif = new Notification "#{post.info.nameBlock} replied to you",
|
||||
notif = new Notification "#{post.info.nameBlock}#{predicate}",
|
||||
body: post.commentDisplay()
|
||||
icon: Favicon.logo
|
||||
notif.onclick = ->
|
||||
@ -238,7 +238,7 @@ Unread =
|
||||
saveThreadWatcherCount: $.debounce 2 * $.SECOND, ->
|
||||
$.forceSync 'Remember Last Read Post'
|
||||
if Conf['Remember Last Read Post'] and (!Unread.thread.isDead or Unread.thread.isArchived)
|
||||
ThreadWatcher.update Unread.thread.board.ID, Unread.thread.ID,
|
||||
ThreadWatcher.update Site.hostname, Unread.thread.board.ID, Unread.thread.ID,
|
||||
isDead: Unread.thread.isDead
|
||||
unread: Unread.posts.size
|
||||
quotingYou: !!(if !Conf['Require OP Quote Link'] and QuoteYou.isYou(Unread.thread.OP) then Unread.posts.size else Unread.postsQuotingYou.size)
|
||||
|
||||
@ -94,13 +94,10 @@ UnreadIndex =
|
||||
|
||||
markRead: ->
|
||||
thread = Get.threadFromNode @
|
||||
if Index.enabled
|
||||
lastPost = Index.lastPost(thread.ID)
|
||||
else
|
||||
lastPost = 0
|
||||
thread.posts.forEach (post) ->
|
||||
if post.ID > lastPost and !post.isFetchedQuote
|
||||
lastPost = post.ID
|
||||
lastPost = if Index.enabled then Index.lastPost(thread.ID) else 0
|
||||
thread.posts.forEach (post) ->
|
||||
if post.ID > lastPost and !post.isFetchedQuote
|
||||
lastPost = post.ID
|
||||
UnreadIndex.lastReadPost[thread.fullID] = lastPost
|
||||
UnreadIndex.db.set
|
||||
boardID: thread.board.ID
|
||||
@ -108,6 +105,6 @@ UnreadIndex =
|
||||
val: lastPost
|
||||
$.rm UnreadIndex.hr[thread.fullID]
|
||||
thread.nodes.root.classList.remove 'unread-thread'
|
||||
ThreadWatcher.update thread.board.ID, thread.ID,
|
||||
ThreadWatcher.update Site.hostname, thread.board.ID, thread.ID,
|
||||
unread: 0
|
||||
quotingYou: false
|
||||
|
||||
@ -707,41 +707,29 @@ QR =
|
||||
options =
|
||||
responseType: 'document'
|
||||
withCredentials: true
|
||||
onload: QR.response
|
||||
onerror: ->
|
||||
# On connection error, the post most likely didn't go through.
|
||||
# If the post did go through, it should be stopped by the duplicate reply cooldown.
|
||||
delete QR.req
|
||||
Captcha.cache.save QR.currentCaptcha if QR.currentCaptcha
|
||||
delete QR.currentCaptcha
|
||||
post.unlock()
|
||||
QR.cooldown.auto = true
|
||||
QR.cooldown.addDelay post, 2
|
||||
QR.status()
|
||||
QR.error QR.connectionError()
|
||||
extra =
|
||||
onloadend: QR.response
|
||||
form: $.formData formData
|
||||
if Conf['Show Upload Progress']
|
||||
extra.upCallbacks =
|
||||
onload: ->
|
||||
options.onprogress = (e) ->
|
||||
return if @ isnt QR.req?.upload # aborted
|
||||
if e.loaded < e.total
|
||||
# Uploading...
|
||||
QR.req.progress = "#{Math.round e.loaded / e.total * 100}%"
|
||||
else
|
||||
# Upload done, waiting for server response.
|
||||
QR.req.isUploadFinished = true
|
||||
QR.req.progress = '...'
|
||||
QR.status()
|
||||
onprogress: (e) ->
|
||||
# Uploading...
|
||||
QR.req.progress = "#{Math.round e.loaded / e.total * 100}%"
|
||||
QR.status()
|
||||
QR.status()
|
||||
|
||||
cb = (response) ->
|
||||
if response?
|
||||
QR.currentCaptcha = response
|
||||
if response.challenge?
|
||||
extra.form.append 'recaptcha_challenge_field', response.challenge
|
||||
extra.form.append 'recaptcha_response_field', response.response
|
||||
options.form.append 'recaptcha_challenge_field', response.challenge
|
||||
options.form.append 'recaptcha_response_field', response.response
|
||||
else
|
||||
extra.form.append 'g-recaptcha-response', response.response
|
||||
QR.req = $.ajax "https://sys.#{location.hostname.split('.')[1]}.org/#{g.BOARD}/post", options, extra
|
||||
options.form.append 'g-recaptcha-response', response.response
|
||||
QR.req = $.ajax "https://sys.#{location.hostname.split('.')[1]}.org/#{g.BOARD}/post", options
|
||||
QR.req.progress = '...'
|
||||
|
||||
if typeof captcha is 'function'
|
||||
@ -765,20 +753,19 @@ QR =
|
||||
QR.status()
|
||||
|
||||
response: ->
|
||||
{req} = QR
|
||||
return if @ isnt QR.req # aborted
|
||||
delete QR.req
|
||||
|
||||
post = QR.posts[0]
|
||||
post.unlock()
|
||||
|
||||
resDoc = req.response
|
||||
if (err = resDoc.getElementById 'errmsg') # error!
|
||||
if (err = @response?.getElementById 'errmsg') # error!
|
||||
$('a', err)?.target = '_blank' # duplicate image link
|
||||
else if (connErr = resDoc.title isnt 'Post successful!')
|
||||
else if (connErr = (!@response or @response.title isnt 'Post successful!'))
|
||||
err = QR.connectionError()
|
||||
Captcha.cache.save QR.currentCaptcha if QR.currentCaptcha
|
||||
else if req.status isnt 200
|
||||
err = "Error #{req.statusText} (#{req.status})"
|
||||
else if @status isnt 200
|
||||
err = "Error #{@statusText} (#{@status})"
|
||||
|
||||
delete QR.currentCaptcha
|
||||
|
||||
@ -810,7 +797,7 @@ QR =
|
||||
QR.error err
|
||||
return
|
||||
|
||||
h1 = $ 'h1', resDoc
|
||||
h1 = $ 'h1', @response
|
||||
|
||||
[_, threadID, postID] = h1.nextSibling.textContent.match /thread:(\d+),no:(\d+)/
|
||||
postID = +postID
|
||||
@ -880,14 +867,14 @@ QR =
|
||||
cb()
|
||||
else
|
||||
setTimeout check, attempts * $.SECOND
|
||||
,
|
||||
responseType: 'text'
|
||||
type: 'HEAD'
|
||||
check()
|
||||
|
||||
abort: ->
|
||||
if QR.req and !QR.req.isUploadFinished
|
||||
QR.req.abort()
|
||||
if (oldReq = QR.req) and !QR.req.isUploadFinished
|
||||
delete QR.req
|
||||
oldReq.abort()
|
||||
Captcha.cache.save QR.currentCaptcha if QR.currentCaptcha
|
||||
delete QR.currentCaptcha
|
||||
QR.posts[0].unlock()
|
||||
|
||||
@ -38,6 +38,14 @@ QuoteThreading =
|
||||
children: {}
|
||||
inserted: {}
|
||||
|
||||
toggleThreading: ->
|
||||
@setThreadingState !Conf['Thread Quotes']
|
||||
|
||||
setThreadingState: (enabled) ->
|
||||
@input.checked = enabled
|
||||
@setEnabled.call @input
|
||||
@rethread.call @input
|
||||
|
||||
setEnabled: ->
|
||||
if @checked
|
||||
$.set 'Prune All Threads', false
|
||||
|
||||
@ -10,8 +10,8 @@ class Callbacks
|
||||
@keys.push name unless @[name]
|
||||
@[name] = cb
|
||||
|
||||
execute: (node, keys=@keys) ->
|
||||
return if node.callbacksExecuted
|
||||
execute: (node, keys=@keys, force) ->
|
||||
return if node.callbacksExecuted and !force
|
||||
node.callbacksExecuted = true
|
||||
for name in keys
|
||||
try
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
class DataBoard
|
||||
@keys = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads', 'customTitles']
|
||||
@keys = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads', 'watcherLastModified', 'customTitles']
|
||||
|
||||
constructor: (@key, sync, dontClean) ->
|
||||
@initData Conf[@key]
|
||||
@ -85,17 +85,20 @@ class DataBoard
|
||||
else
|
||||
@data[siteID].boards[boardID] = val
|
||||
|
||||
extend: ({siteID, boardID, threadID, postID, val, rm}, cb) ->
|
||||
extend: ({siteID, boardID, threadID, postID, val}, cb) ->
|
||||
@save =>
|
||||
oldVal = @get {siteID, boardID, threadID, postID, val: {}}
|
||||
delete oldVal[key] for key in rm or []
|
||||
$.extend oldVal, val
|
||||
oldVal = @get {siteID, boardID, threadID, postID, defaultValue: {}}
|
||||
for key, subVal of val
|
||||
if typeof subVal is 'undefined'
|
||||
delete oldVal[key]
|
||||
else
|
||||
oldVal[key] = subVal
|
||||
@setUnsafe {siteID, boardID, threadID, postID, val: oldVal}
|
||||
, cb
|
||||
|
||||
setLastChecked: ->
|
||||
setLastChecked: (key='lastChecked') ->
|
||||
@save =>
|
||||
@data.lastChecked = Date.now()
|
||||
@data[key] = Date.now()
|
||||
|
||||
get: ({siteID, boardID, threadID, postID, defaultValue}) ->
|
||||
siteID or= Site.hostname
|
||||
@ -116,13 +119,9 @@ class DataBoard
|
||||
val or defaultValue
|
||||
|
||||
clean: ->
|
||||
# XXX not yet multisite ready
|
||||
return unless Site.software is 'yotsuba'
|
||||
siteID = Site.hostname
|
||||
|
||||
for boardID, val of @data[siteID].boards
|
||||
@deleteIfEmpty {siteID, boardID}
|
||||
|
||||
now = Date.now()
|
||||
unless now - 2 * $.HOUR < (@data[siteID].lastChecked or 0) <= now
|
||||
@data[siteID].lastChecked = now
|
||||
@ -131,12 +130,18 @@ class DataBoard
|
||||
return
|
||||
|
||||
ajaxClean: (boardID) ->
|
||||
$.cache "#{location.protocol}//a.4cdn.org/#{boardID}/threads.json", (e1) =>
|
||||
return unless e1.target.status is 200
|
||||
response1 = e1.target.response
|
||||
$.cache "#{location.protocol}//a.4cdn.org/#{boardID}/archive.json", (e2) =>
|
||||
return unless e2.target.status is 200 or boardID in ['b', 'f', 'trash', 'bant']
|
||||
@ajaxCleanParse boardID, response1, e2.target.response
|
||||
that = @
|
||||
siteID = Site.hostname
|
||||
threadsList = Site.urls.threadsListJSON?({siteID, boardID})
|
||||
return unless threadsList
|
||||
$.cache threadsList, ->
|
||||
return unless @status is 200
|
||||
archiveList = Site.urls.archiveListJSON?({siteID, boardID})
|
||||
return that.ajaxCleanParse(boardID, @response) unless archiveList
|
||||
response1 = @response
|
||||
$.cache archiveList, ->
|
||||
return unless @status is 200
|
||||
that.ajaxCleanParse(boardID, response1, @response)
|
||||
|
||||
ajaxCleanParse: (boardID, response1, response2) ->
|
||||
siteID = Site.hostname
|
||||
|
||||
@ -15,8 +15,9 @@ class Fetcher
|
||||
|
||||
@root.textContent = "Loading post No.#{@postID}..."
|
||||
if @threadID
|
||||
$.cache "#{location.protocol}//a.4cdn.org/#{@boardID}/thread/#{@threadID}.json", (e, isCached) =>
|
||||
@fetchedPost e.target, isCached
|
||||
that = @
|
||||
$.cache Site.urls.threadJSON({boardID: @boardID, threadID: @threadID}), ({isCached}) ->
|
||||
that.fetchedPost @, isCached
|
||||
else
|
||||
@archivedPost()
|
||||
|
||||
@ -60,12 +61,14 @@ class Fetcher
|
||||
{status} = req
|
||||
unless status is 200
|
||||
# The thread can die by the time we check a quote.
|
||||
return if @archivedPost()
|
||||
return if status and @archivedPost()
|
||||
|
||||
$.addClass @root, 'warning'
|
||||
@root.textContent =
|
||||
if status is 404
|
||||
"Thread No.#{@threadID} 404'd."
|
||||
else if !status
|
||||
'Connection Error'
|
||||
else
|
||||
"Error #{req.statusText} (#{req.status})."
|
||||
return
|
||||
@ -78,10 +81,11 @@ class Fetcher
|
||||
if post.no isnt @postID
|
||||
# Cached requests can be stale and must be rechecked.
|
||||
if isCached
|
||||
api = "#{location.protocol}//a.4cdn.org/#{@boardID}/thread/#{@threadID}.json"
|
||||
api = Site.urls.threadJSON({boardID: @boardID, threadID: @threadID})
|
||||
$.cleanCache (url) -> url is api
|
||||
$.cache api, (e) =>
|
||||
@fetchedPost e.target, false
|
||||
that = @
|
||||
$.cache api, ->
|
||||
that.fetchedPost @, false
|
||||
return
|
||||
|
||||
# The post can be deleted by the time we check a quote.
|
||||
@ -107,7 +111,7 @@ class Fetcher
|
||||
encryptionOK = /^https:\/\//.test(url) or location.protocol is 'http:'
|
||||
if encryptionOK or Conf['Exempt Archives from Encryption']
|
||||
that = @
|
||||
CrossOrigin.json url, ->
|
||||
CrossOrigin.cache url, ->
|
||||
if !encryptionOK and @response?.media
|
||||
{media} = @response
|
||||
for key of media when /_link$/.test key
|
||||
|
||||
@ -9,6 +9,7 @@ class Post
|
||||
@ID = +root.id.match(/\d*$/)[0]
|
||||
@threadID = @thread.ID
|
||||
@boardID = @board.ID
|
||||
@siteID = Site.hostname
|
||||
@fullID = "#{@board}.#{@ID}"
|
||||
@context = @
|
||||
@isReply = (@ID isnt @threadID)
|
||||
@ -28,6 +29,7 @@ class Post
|
||||
@info =
|
||||
subject: @nodes.subject?.textContent or undefined
|
||||
name: @nodes.name?.textContent
|
||||
email: if @nodes.email then decodeURIComponent(@nodes.email.href.replace(/^mailto:/, ''))
|
||||
tripcode: @nodes.tripcode?.textContent
|
||||
uniqueID: @nodes.uniqueID?.textContent
|
||||
capcode: @nodes.capcode?.textContent.replace '## ', ''
|
||||
|
||||
@ -19,6 +19,10 @@ Config =
|
||||
'Show a notice at the top of the page when the index is refreshed.'
|
||||
1
|
||||
]
|
||||
'Follow Cursor': [
|
||||
true
|
||||
'Image Hover and Quote Preview move with the mouse cursor.'
|
||||
]
|
||||
'Open Threads in New Tab': [
|
||||
false
|
||||
'Make links to threads in the index / <%= meta.name %> catalog open in a new tab.'
|
||||
@ -628,7 +632,7 @@ Config =
|
||||
false
|
||||
'Advance to next post when contracting an expanded image.'
|
||||
]
|
||||
|
||||
|
||||
gallery:
|
||||
'Hide Thumbnails': [
|
||||
false
|
||||
@ -672,6 +676,10 @@ Config =
|
||||
false
|
||||
'Automatically remove dead threads.'
|
||||
]
|
||||
'Show Page': [
|
||||
true
|
||||
'Show what page watched threads are on.'
|
||||
]
|
||||
'Show Unread Count': [
|
||||
true
|
||||
'Show number of unread posts in watched threads.'
|
||||
@ -720,6 +728,8 @@ Config =
|
||||
#/./
|
||||
"""
|
||||
|
||||
email: ''
|
||||
|
||||
subject: """
|
||||
# Filter Generals on /v/:
|
||||
#/general/i;boards:v;op:only
|
||||
@ -748,7 +758,7 @@ Config =
|
||||
sauces: """
|
||||
# Known filename formats:
|
||||
http://www.pixiv.net/member_illust.php?mode=medium&illust_id=%$1;regexp:/^(\\d+)_p\\d+/
|
||||
//%$1.deviantart.com/gallery/#/d%$2;regexp:/^\\w+_by_(\\w+)-d([\\da-z]+)/
|
||||
//www.deviantart.com/gallery/#/d%$1%$2;regexp:/^\\w+_by_\\w+[_-]d([\\da-z]{6})\\b|^d([\\da-z]{6})-[\\da-z]{8}-/
|
||||
//imgur.com/%$1;regexp:/^(?![a-zA-Z][a-z]{6})(?![A-Z]{7})(?!\\d{7})([\\da-zA-Z]{7})(?: \\(\\d+\\))?\\.\\w+$/
|
||||
http://flickr.com/photo.gne?id=%$1;regexp:/^(\\d+)_[\\da-f]{10}(?:_\\w)*\\b/
|
||||
https://www.facebook.com/photo.php?fbid=%$1;regexp:/^\\d+_(\\d+)_\\d+_[no]\\b/
|
||||
@ -955,6 +965,10 @@ Config =
|
||||
't'
|
||||
'Toggle visibility of thread watcher.'
|
||||
]
|
||||
'Toggle threading': [
|
||||
'Shift+t'
|
||||
'Toggle threading.'
|
||||
]
|
||||
'Mark thread read': [
|
||||
'Ctrl+0'
|
||||
'Mark thread read from index (requires "Unread Line in Index").'
|
||||
|
||||
@ -93,7 +93,7 @@
|
||||
}
|
||||
|
||||
/* Thread Watcher */
|
||||
:root.burichan .replies-quoting-you > a, :root.burichan #watcher-link.replies-quoting-you {
|
||||
:root.burichan .replies-quoting-you > a, :root.burichan #watcher-link.replies-quoting-you, :root.burichan .last-page > a > .watcher-page {
|
||||
color: #F00;
|
||||
}
|
||||
|
||||
|
||||
@ -93,7 +93,7 @@
|
||||
}
|
||||
|
||||
/* Thread Watcher */
|
||||
:root.futaba .replies-quoting-you > a, :root.futaba #watcher-link.replies-quoting-you {
|
||||
:root.futaba .replies-quoting-you > a, :root.futaba #watcher-link.replies-quoting-you, :root.futaba .last-page > a > .watcher-page {
|
||||
color: #F00;
|
||||
}
|
||||
|
||||
|
||||
@ -91,7 +91,7 @@
|
||||
}
|
||||
|
||||
/* Thread Watcher */
|
||||
:root.photon .replies-quoting-you > a, :root.photon #watcher-link.replies-quoting-you {
|
||||
:root.photon .replies-quoting-you > a, :root.photon #watcher-link.replies-quoting-you, :root.photon .last-page > a > .watcher-page {
|
||||
color: #00F !important;
|
||||
}
|
||||
|
||||
|
||||
@ -157,7 +157,7 @@
|
||||
}
|
||||
|
||||
/* Thread Watcher */
|
||||
:root.spooky .replies-quoting-you > a, :root.spooky #watcher-link.replies-quoting-you {
|
||||
:root.spooky .replies-quoting-you > a, :root.spooky #watcher-link.replies-quoting-you, :root.spooky .last-page > a > .watcher-page {
|
||||
color: #F00 !important;
|
||||
}
|
||||
|
||||
|
||||
@ -129,6 +129,10 @@ body.is_catalog .thread > a > img {
|
||||
.nwsb {
|
||||
display: inline;
|
||||
}
|
||||
.fileText {
|
||||
max-width: auto;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
/* Ads */
|
||||
.ad-cnt > *, .adg-rects > *, .bsa-cnt {
|
||||
@ -1164,12 +1168,11 @@ span.hide-announcement {
|
||||
-webkit-flex-direction: row;
|
||||
flex-direction: row;
|
||||
}
|
||||
#watched-threads .watcher-page,
|
||||
#watched-threads .watcher-unread {
|
||||
-webkit-flex: 0 0 auto;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
#watched-threads .watcher-unread::after {
|
||||
content: "\00a0";
|
||||
margin-right: 2px;
|
||||
}
|
||||
#watched-threads .watcher-title {
|
||||
overflow: hidden;
|
||||
@ -1177,7 +1180,10 @@ span.hide-announcement {
|
||||
-webkit-flex: 0 1 auto;
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
.replies-quoting-you > a, #watcher-link.replies-quoting-you {
|
||||
#watched-threads .watcher-title:not(:first-child) {
|
||||
margin-left: 2px;
|
||||
}
|
||||
.replies-quoting-you > a, #watcher-link.replies-quoting-you, .last-page > a > .watcher-page {
|
||||
color: #F00;
|
||||
}
|
||||
#thread-watcher a {
|
||||
@ -1344,6 +1350,13 @@ span.hide-announcement {
|
||||
.fileThumb > .warning {
|
||||
clear: both;
|
||||
}
|
||||
#ihover {
|
||||
pointer-events: none;
|
||||
/* XXX https://code.google.com/p/chromium/issues/detail?id=168840, https://bugs.webkit.org/show_bug.cgi?id=94158 */
|
||||
max-height: 95vh;
|
||||
max-height: calc(100vh - 25px);
|
||||
max-width: 100vw;
|
||||
}
|
||||
/* WEBM Metadata */
|
||||
.webm-title > a::before {
|
||||
content: "title";
|
||||
|
||||
@ -162,7 +162,7 @@
|
||||
}
|
||||
|
||||
/* Thread Watcher */
|
||||
:root.tomorrow .replies-quoting-you > a, :root.tomorrow #watcher-link.replies-quoting-you {
|
||||
:root.tomorrow .replies-quoting-you > a, :root.tomorrow #watcher-link.replies-quoting-you, :root.tomorrow .last-page > a > .watcher-page {
|
||||
color: #F00 !important;
|
||||
}
|
||||
|
||||
|
||||
@ -88,7 +88,7 @@
|
||||
}
|
||||
|
||||
/* Thread Watcher */
|
||||
:root.yotsuba .replies-quoting-you > a, :root.yotsuba #watcher-link.replies-quoting-you {
|
||||
:root.yotsuba .replies-quoting-you > a, :root.yotsuba #watcher-link.replies-quoting-you, :root.yotsuba .last-page > a > .watcher-page {
|
||||
color: #F00;
|
||||
}
|
||||
|
||||
|
||||
@ -1,8 +1,5 @@
|
||||
Main =
|
||||
init: ->
|
||||
# XXX Work around Pale Moon / old Firefox + GM 1.15 bug where script runs in iframe with wrong window.location.
|
||||
return if d.body and not $ 'title', d.head
|
||||
|
||||
# XXX dwb userscripts extension reloads scripts run at document-start when replaceState/pushState is called.
|
||||
# XXX Firefox reinjects WebExtension content scripts when extension is updated / reloaded.
|
||||
try
|
||||
|
||||
@ -25,16 +25,14 @@ handlers =
|
||||
xhr.open 'GET', request.url, true
|
||||
xhr.responseType = request.responseType
|
||||
xhr.timeout = request.timeout
|
||||
for key, value of (request.headers or {})
|
||||
xhr.setRequestHeader key, value
|
||||
xhr.addEventListener 'load', ->
|
||||
{status, statusText, response} = @
|
||||
if @readyState is @DONE && xhr.status is 200
|
||||
if request.responseType is 'arraybuffer'
|
||||
response = [new Uint8Array(response)...]
|
||||
contentType = @getResponseHeader 'Content-Type'
|
||||
contentDisposition = @getResponseHeader 'Content-Disposition'
|
||||
cb {status, statusText, response, contentType, contentDisposition}
|
||||
else
|
||||
cb {status, statusText, response, error: true}
|
||||
responseHeaderString = @getAllResponseHeaders()
|
||||
if response and request.responseType is 'arraybuffer'
|
||||
response = [new Uint8Array(response)...]
|
||||
cb {status, statusText, response, responseHeaderString}
|
||||
, false
|
||||
xhr.addEventListener 'error', ->
|
||||
cb {error: true}
|
||||
|
||||
@ -41,67 +41,83 @@ $.extend = (object, properties) ->
|
||||
return
|
||||
|
||||
$.ajax = do ->
|
||||
# Status Code 304: Not modified
|
||||
# With the `If-Modified-Since` header we only receive the HTTP headers and no body for 304 responses.
|
||||
# This saves a lot of bandwidth and CPU time for both the users and the servers.
|
||||
lastModified = {}
|
||||
if window.wrappedJSObject and not XMLHttpRequest.wrappedJSObject
|
||||
pageXHR = XPCNativeWrapper window.wrappedJSObject.XMLHttpRequest
|
||||
else
|
||||
pageXHR = XMLHttpRequest
|
||||
|
||||
(url, options={}, extra={}) ->
|
||||
{type, whenModified, bypassCache, upCallbacks, form} = extra
|
||||
options.responseType ?= 'json' if /\.json$/.test url
|
||||
(url, options={}) ->
|
||||
{onloadend, timeout, responseType, withCredentials, type, onprogress, form, headers} = options
|
||||
responseType ?= 'json'
|
||||
# XXX https://forums.lanik.us/viewtopic.php?f=64&t=24173&p=78310
|
||||
url = url.replace /^((?:https?:)?\/\/(?:\w+\.)?4c(?:ha|d)n\.org)\/adv\//, '$1//adv/'
|
||||
if whenModified
|
||||
params = []
|
||||
# XXX https://bugs.chromium.org/p/chromium/issues/detail?id=643659
|
||||
params.push "s=#{whenModified}" if $.engine is 'blink'
|
||||
params.push "t=#{Date.now()}" if Site.software is 'yotsuba' and bypassCache
|
||||
url0 = url
|
||||
url += '?' + params.join('&') if params.length
|
||||
r = new pageXHR()
|
||||
type or= form and 'post' or 'get'
|
||||
try
|
||||
r.open type, url, true
|
||||
if whenModified
|
||||
r.setRequestHeader 'If-Modified-Since', lastModified[whenModified][url0] if lastModified[whenModified]?[url0]?
|
||||
$.on r, 'load', -> (lastModified[whenModified] or= {})[url0] = r.getResponseHeader 'Last-Modified'
|
||||
$.extend r, options
|
||||
$.extend r.upload, upCallbacks
|
||||
for key, value of (headers or {})
|
||||
r.setRequestHeader key, value
|
||||
$.extend r, {onloadend, timeout, responseType, withCredentials}
|
||||
$.extend r.upload, {onprogress}
|
||||
# connection error or content blocker
|
||||
$.on r, 'error', -> (c.error "4chan X failed to load: #{url}" unless r.status)
|
||||
$.on r, 'error', -> (c.warn "4chan X failed to load: #{url}" unless r.status)
|
||||
<% if (type === 'crx') { %>
|
||||
# XXX https://bugs.chromium.org/p/chromium/issues/detail?id=920638
|
||||
$.on r, 'load', ->
|
||||
return unless r.readyState is 4 and r.status is 200 and r.statusText is '' and r.response is null and !$.ajaxWarningShown
|
||||
new Notice 'warning', "Error loading #{url}; try going to chrome://flags/#network-service and disabling the network service flag."
|
||||
$.ajaxWarningShown = true
|
||||
<% } %>
|
||||
r.send form
|
||||
catch err
|
||||
# XXX Some content blockers in Firefox (e.g. Adblock Plus and NoScript) throw an exception instead of simulating a connection error.
|
||||
throw err unless err.result is 0x805e0006
|
||||
for event in ['error', 'loadend']
|
||||
r["on#{event}"] = options["on#{event}"]
|
||||
$.queueTask $.event, event, null, r
|
||||
r.onloadend = onloadend
|
||||
$.queueTask $.event, 'error', null, r
|
||||
$.queueTask $.event, 'loadend', null, r
|
||||
r
|
||||
|
||||
# Status Code 304: Not modified
|
||||
# With the `If-Modified-Since` header we only receive the HTTP headers and no body for 304 responses.
|
||||
# This saves a lot of bandwidth and CPU time for both the users and the servers.
|
||||
$.lastModified = {}
|
||||
$.whenModified = (url, bucket, cb, options={}) ->
|
||||
{timeout, ajax} = options
|
||||
params = []
|
||||
# XXX https://bugs.chromium.org/p/chromium/issues/detail?id=643659
|
||||
params.push "s=#{bucket}" if $.engine is 'blink'
|
||||
params.push "t=#{Date.now()}" if url.split('/')[2] is 'a.4cdn.org'
|
||||
url0 = url
|
||||
url += '?' + params.join('&') if params.length
|
||||
headers = {}
|
||||
if (t = $.lastModified[bucket]?[url0])?
|
||||
headers['If-Modified-Since'] = t
|
||||
r = (ajax or $.ajax) url, {
|
||||
onloadend: ->
|
||||
($.lastModified[bucket] or= {})[url0] = @getResponseHeader('Last-Modified')
|
||||
cb.call @
|
||||
timeout
|
||||
headers
|
||||
}
|
||||
r
|
||||
|
||||
do ->
|
||||
reqs = {}
|
||||
$.cache = (url, cb, options) ->
|
||||
if req = reqs[url]
|
||||
if req.readyState is 4
|
||||
$.queueTask -> cb.call req, req.evt, true
|
||||
else
|
||||
$.cache = (url, cb, options={}) ->
|
||||
{ajax} = options
|
||||
if (req = reqs[url])
|
||||
if req.callbacks
|
||||
req.callbacks.push cb
|
||||
else
|
||||
$.queueTask -> cb.call req, {isCached: true}
|
||||
return req
|
||||
rm = -> delete reqs[url]
|
||||
try
|
||||
return if not (req = $.ajax url, options)
|
||||
catch err
|
||||
return
|
||||
$.on req, 'load', (e) ->
|
||||
@evt = e
|
||||
onloadend = ->
|
||||
unless @status
|
||||
delete reqs[url]
|
||||
for cb in @callbacks
|
||||
do (cb) => $.queueTask => cb.call @, e, false
|
||||
do (cb) => $.queueTask => cb.call @, {isCached: false}
|
||||
delete @callbacks
|
||||
$.on req, 'abort error', rm
|
||||
req = (ajax or $.ajax) url, {onloadend}
|
||||
req.callbacks = [cb]
|
||||
reqs[url] = req
|
||||
$.cleanCache = (testf) ->
|
||||
@ -413,16 +429,16 @@ $.sync = (key, cb) ->
|
||||
$.forceSync = -> return
|
||||
|
||||
$.crxWorking = ->
|
||||
if chrome.runtime.getManifest()
|
||||
true
|
||||
else
|
||||
unless $.crxWarningShown
|
||||
msg = $.el 'div',
|
||||
<%= html('4chan X seems to have been updated. You will need to <a href="javascript:;">reload</a> the page.') %>
|
||||
$.on $('a', msg), 'click', -> location.reload()
|
||||
new Notice 'warning', msg
|
||||
$.crxWarningShown = true
|
||||
false
|
||||
try
|
||||
if chrome.runtime.getManifest()
|
||||
return true
|
||||
unless $.crxWarningShown
|
||||
msg = $.el 'div',
|
||||
<%= html('4chan X seems to have been updated. You will need to <a href="javascript:;">reload</a> the page.') %>
|
||||
$.on $('a', msg), 'click', -> location.reload()
|
||||
new Notice 'warning', msg
|
||||
$.crxWarningShown = true
|
||||
false
|
||||
|
||||
$.get = $.oneItemSugar (data, cb) ->
|
||||
return unless $.crxWorking()
|
||||
|
||||
@ -14,123 +14,131 @@ CrossOrigin =
|
||||
# XXX https://forums.lanik.us/viewtopic.php?f=64&t=24173&p=78310
|
||||
url = url.replace /^((?:https?:)?\/\/(?:\w+\.)?4c(?:ha|d)n\.org)\/adv\//, '$1//adv/'
|
||||
<% if (type === 'crx') { %>
|
||||
eventPageRequest {type: 'ajax', url, responseType: 'arraybuffer'}, ({response, contentType, contentDisposition, error}) ->
|
||||
return cb null if error
|
||||
cb new Uint8Array(response), contentType, contentDisposition
|
||||
eventPageRequest {type: 'ajax', url, headers, responseType: 'arraybuffer'}, ({response, responseHeaderString}) ->
|
||||
response = new Uint8Array(response) if response
|
||||
cb response, responseHeaderString
|
||||
<% } %>
|
||||
<% if (type === 'userscript') { %>
|
||||
# Use workaround for binary data in Greasemonkey versions < 3.2, in Pale Moon for all GM versions, and in JS Blocker (Safari).
|
||||
workaround = $.engine is 'gecko' and GM_info? and /^[0-2]\.|^3\.[01](?!\d)/.test(GM_info.version)
|
||||
workaround or= /PaleMoon\//.test(navigator.userAgent)
|
||||
workaround or= GM_info?.script?.includeJSB?
|
||||
options =
|
||||
(GM?.xmlHttpRequest or GM_xmlhttpRequest)
|
||||
method: "GET"
|
||||
url: url
|
||||
headers: headers
|
||||
responseType: 'arraybuffer'
|
||||
overrideMimeType: 'text/plain; charset=x-user-defined'
|
||||
onload: (xhr) ->
|
||||
if workaround
|
||||
if xhr.response instanceof ArrayBuffer
|
||||
data = new Uint8Array xhr.response
|
||||
else
|
||||
r = xhr.responseText
|
||||
data = new Uint8Array r.length
|
||||
i = 0
|
||||
while i < r.length
|
||||
data[i] = r.charCodeAt i
|
||||
i++
|
||||
else
|
||||
data = new Uint8Array xhr.response
|
||||
contentType = xhr.responseHeaders.match(/Content-Type:\s*(.*)/i)?[1]
|
||||
contentDisposition = xhr.responseHeaders.match(/Content-Disposition:\s*(.*)/i)?[1]
|
||||
cb data, contentType, contentDisposition
|
||||
cb data, xhr.responseHeaders
|
||||
onerror: ->
|
||||
cb null
|
||||
onabort: ->
|
||||
cb null
|
||||
if workaround
|
||||
options.overrideMimeType = 'text/plain; charset=x-user-defined'
|
||||
else
|
||||
options.responseType = 'arraybuffer'
|
||||
(GM?.xmlHttpRequest or GM_xmlhttpRequest) options
|
||||
<% } %>
|
||||
|
||||
file: (url, cb) ->
|
||||
CrossOrigin.binary url, (data, contentType, contentDisposition) ->
|
||||
CrossOrigin.binary url, (data, headers) ->
|
||||
return cb null unless data?
|
||||
name = url.match(/([^\/]+)\/*$/)?[1]
|
||||
name = url.match(/([^\/?#]+)\/*(?:$|[?#])/)?[1]
|
||||
contentType = headers.match(/Content-Type:\s*(.*)/i)?[1]
|
||||
contentDisposition = headers.match(/Content-Disposition:\s*(.*)/i)?[1]
|
||||
mime = contentType?.match(/[^;]*/)[0] or 'application/octet-stream'
|
||||
match =
|
||||
contentDisposition?.match(/\bfilename\s*=\s*"((\\"|[^"])+)"/i)?[1] or
|
||||
contentType?.match(/\bname\s*=\s*"((\\"|[^"])+)"/i)?[1]
|
||||
if match
|
||||
name = match.replace /\\"/g, '"'
|
||||
if GM_info?.script?.includeJSB?
|
||||
# Content type comes back as 'text/plain; charset=x-user-defined'; guess from filename instead.
|
||||
if /^text\/plain;\s*charset=x-user-defined$/i.test(mime)
|
||||
# In JS Blocker (Safari) content type comes back as 'text/plain; charset=x-user-defined'; guess from filename instead.
|
||||
mime = QR.typeFromExtension[name.match(/[^.]*$/)[0].toLowerCase()] or 'application/octet-stream'
|
||||
blob = new Blob([data], {type: mime})
|
||||
blob.name = name
|
||||
cb blob
|
||||
|
||||
Request: class Request
|
||||
status: 0
|
||||
statusText: ''
|
||||
response: null
|
||||
responseHeaderString: null
|
||||
getResponseHeader: (headerName) ->
|
||||
if !@responseHeaders? and @responseHeaderString?
|
||||
@responseHeaders = {}
|
||||
for header in @responseHeaderString.split('\r\n')
|
||||
if (i = header.indexOf(':')) >= 0
|
||||
key = header[...i].trim().toLowerCase()
|
||||
val = header[i+1..].trim()
|
||||
@responseHeaders[key] = val
|
||||
(@responseHeaders or {})[headerName.toLowerCase()] ? null
|
||||
abort: ->
|
||||
onloadend: ->
|
||||
|
||||
# Attempts to fetch `url` in JSON format using cross-origin privileges, if available.
|
||||
# On success, calls `cb` with a `this` containing properties `status`, `statusText`, `response` and caches result.
|
||||
# On error/abort, calls `cb` with a `this` of `{}`.
|
||||
# If `bypassCache` is true, ignores previously cached results.
|
||||
json: do ->
|
||||
callbacks = {}
|
||||
results = {}
|
||||
success = (url, result) ->
|
||||
for cb in callbacks[url]
|
||||
$.queueTask -> cb.call result
|
||||
delete callbacks[url]
|
||||
results[url] = result
|
||||
failure = (url) ->
|
||||
for cb in callbacks[url]
|
||||
$.queueTask -> cb.call {}
|
||||
delete callbacks[url]
|
||||
# Interface is a subset of that of $.ajax.
|
||||
# Options:
|
||||
# `onloadend` - called with the returned object as `this` on success or error/abort/timeout.
|
||||
# `timeout` - time limit for request
|
||||
# `headers` - request headers
|
||||
# Returned object properties:
|
||||
# `status` - HTTP status (0 if connection not successful)
|
||||
# `statusText` - HTTP status text
|
||||
# `response` - decoded response body
|
||||
# `abort` - function for aborting the request (silently fails on some platforms)
|
||||
# `getResponseHeader` - function for reading response headers
|
||||
ajax: (url, options={}) ->
|
||||
{onloadend, timeout, headers} = options
|
||||
|
||||
(url, cb, bypassCache, timeout) ->
|
||||
<% if (type === 'userscript') { %>
|
||||
unless GM?.xmlHttpRequest? or GM_xmlhttpRequest?
|
||||
if bypassCache
|
||||
$.cleanCache (url2) -> url2 is url
|
||||
if (req = $.cache url, cb, responseType: 'json')
|
||||
$.on req, 'abort error', -> cb.call({})
|
||||
else
|
||||
cb.call {}
|
||||
return
|
||||
<% } %>
|
||||
<% if (type === 'userscript') { %>
|
||||
unless GM?.xmlHttpRequest? or GM_xmlhttpRequest?
|
||||
return $.ajax url, options
|
||||
<% } %>
|
||||
|
||||
if bypassCache
|
||||
delete results[url]
|
||||
else
|
||||
if results[url]
|
||||
cb.call results[url]
|
||||
return
|
||||
if callbacks[url]
|
||||
callbacks[url].push cb
|
||||
return
|
||||
callbacks[url] = [cb]
|
||||
req = new CrossOrigin.Request()
|
||||
req.onloadend = onloadend
|
||||
|
||||
<% if (type === 'userscript') { %>
|
||||
(GM?.xmlHttpRequest or GM_xmlhttpRequest)
|
||||
method: "GET"
|
||||
url: url+''
|
||||
timeout: timeout
|
||||
onload: (xhr) ->
|
||||
{status, statusText} = xhr
|
||||
try
|
||||
response = JSON.parse(xhr.responseText)
|
||||
success url, {status, statusText, response}
|
||||
catch
|
||||
failure url
|
||||
onerror: -> failure(url)
|
||||
onabort: -> failure(url)
|
||||
ontimeout: -> failure(url)
|
||||
<% } %>
|
||||
<% if (type === 'crx') { %>
|
||||
eventPageRequest {type: 'ajax', url, responseType: 'json', timeout}, (result) ->
|
||||
if result.status
|
||||
success url, result
|
||||
else
|
||||
failure url
|
||||
<% } %>
|
||||
<% if (type === 'userscript') { %>
|
||||
gmReq = (GM?.xmlHttpRequest or GM_xmlhttpRequest) {
|
||||
method: 'GET'
|
||||
url
|
||||
headers
|
||||
timeout
|
||||
onload: (xhr) ->
|
||||
try
|
||||
response = if xhr.responseText then JSON.parse(xhr.responseText) else null
|
||||
$.extend req, {
|
||||
response
|
||||
status: xhr.status
|
||||
statusText: xhr.statusText
|
||||
responseHeaderString: xhr.responseHeaders
|
||||
}
|
||||
req.onloadend()
|
||||
onerror: -> req.onloadend()
|
||||
onabort: -> req.onloadend()
|
||||
ontimeout: -> req.onloadend()
|
||||
}
|
||||
if gmReq and typeof gmReq.abort is 'function'
|
||||
req.abort = ->
|
||||
try
|
||||
gmReq.abort()
|
||||
<% } %>
|
||||
|
||||
<% if (type === 'crx') { %>
|
||||
eventPageRequest {type: 'ajax', url, responseType: 'json', headers, timeout}, (result) ->
|
||||
if result.status
|
||||
$.extend req, result
|
||||
req.onloadend()
|
||||
<% } %>
|
||||
|
||||
req
|
||||
|
||||
cache: (url, cb) ->
|
||||
$.cache url, cb,
|
||||
ajax: CrossOrigin.ajax
|
||||
|
||||
permission: (cb) ->
|
||||
<% if (type === 'crx') { %>
|
||||
|
||||
@ -9,7 +9,6 @@ SW.tinyboard =
|
||||
'Image Host Rewriting'
|
||||
'Index Generator'
|
||||
'Announcement Hiding'
|
||||
'Fourchan thingies'
|
||||
'Resurrect Quotes'
|
||||
'Quick Reply Personas'
|
||||
'Quick Reply'
|
||||
@ -60,6 +59,12 @@ SW.tinyboard =
|
||||
threadJSON: ({siteID, boardID, threadID}) ->
|
||||
root = Conf['siteProperties'][siteID]?.root
|
||||
if root then "#{root}#{boardID}/res/#{threadID}.json" else ''
|
||||
threadsListJSON: ({siteID, boardID}) ->
|
||||
root = Conf['siteProperties'][siteID]?.root
|
||||
if root then "#{root}#{boardID}/threads.json" else ''
|
||||
catalogJSON: ({siteID, boardID}) ->
|
||||
root = Conf['siteProperties'][siteID]?.root
|
||||
if root then "#{root}#{boardID}/catalog.json" else ''
|
||||
|
||||
selectors:
|
||||
board: 'form[name="postcontrols"]'
|
||||
|
||||
@ -4,6 +4,11 @@ SW.yotsuba =
|
||||
urls:
|
||||
thread: ({boardID, threadID}) -> "#{location.protocol}//#{BoardConfig.domain(boardID)}/#{boardID}/thread/#{threadID}"
|
||||
threadJSON: ({boardID, threadID}) -> "#{location.protocol}//a.4cdn.org/#{boardID}/thread/#{threadID}.json"
|
||||
threadsListJSON: ({boardID}) -> "#{location.protocol}//a.4cdn.org/#{boardID}/threads.json"
|
||||
archiveListJSON: ({boardID}) -> if BoardConfig.isArchived(boardID) then "#{location.protocol}//a.4cdn.org/#{boardID}/archive.json" else ''
|
||||
catalogJSON: ({boardID}) -> "#{location.protocol}//a.4cdn.org/#{boardID}/catalog.json"
|
||||
|
||||
isPrunedByAge: ({boardID}) -> boardID is 'f'
|
||||
|
||||
selectors:
|
||||
board: '.board'
|
||||
@ -101,7 +106,7 @@ SW.yotsuba =
|
||||
|
||||
if g.BOARD.ID is 'f' and thread.OP.file
|
||||
{file} = thread.OP
|
||||
$.ajax "#{location.protocol}//a.4cdn.org/f/thread/#{thread}.json",
|
||||
$.ajax Site.urls.threadJSON({boardID: 'f', threadID: thread.ID}),
|
||||
timeout: $.MINUTE
|
||||
onloadend: ->
|
||||
if @response
|
||||
@ -152,3 +157,6 @@ SW.yotsuba =
|
||||
|
||||
hasCORS: (url) ->
|
||||
url.split('/')[...3].join('/') is location.protocol + '//a.4cdn.org'
|
||||
|
||||
sfwBoards: (sfw) ->
|
||||
BoardConfig.sfwBoards(sfw)
|
||||
|
||||
@ -1,17 +1,19 @@
|
||||
Site =
|
||||
defaultProperties:
|
||||
'4chan.org': {software: 'yotsuba'}
|
||||
'4channel.org': {software: 'yotsuba'}
|
||||
'4cdn.org': {software: 'yotsuba'}
|
||||
'4channel.org': {canonical: '4chan.org'}
|
||||
'4cdn.org': {canonical: '4chan.org'}
|
||||
|
||||
init: (cb) ->
|
||||
$.extend Conf['siteProperties'], Site.defaultProperties
|
||||
{hostname} = location
|
||||
while hostname and hostname not of Conf['siteProperties']
|
||||
hostname = hostname.replace(/^[^.]*\.?/, '')
|
||||
if hostname and Conf['siteProperties'][hostname].software of SW
|
||||
@set hostname
|
||||
cb()
|
||||
if hostname
|
||||
hostname = canonical if (canonical = Conf['siteProperties'][hostname].canonical)
|
||||
if Conf['siteProperties'][hostname].software of SW
|
||||
@set hostname
|
||||
cb()
|
||||
$.onExists doc, 'body', =>
|
||||
for software of SW when (changes = SW[software].detect?())
|
||||
changes.software = software
|
||||
@ -32,5 +34,4 @@ Site =
|
||||
set: (@hostname) ->
|
||||
@properties = Conf['siteProperties'][@hostname]
|
||||
@software = @properties.software
|
||||
@hostname = '4chan.org' if @software is 'yotsuba'
|
||||
$.extend @, SW[@software]
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
var fs = require('fs');
|
||||
var crypto = require('crypto');
|
||||
var RSA = require('node-rsa');
|
||||
|
||||
var pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
||||
var channel = process.argv[2] || '';
|
||||
|
||||
var privateKey = fs.readFileSync(`../${pkg.meta.path}.keys/${pkg.name}.pem`);
|
||||
var archive = fs.readFileSync(`testbuilds/${pkg.name}${channel}.crx.zip`);
|
||||
|
||||
// https://developer.chrome.com/extensions/crx
|
||||
|
||||
var publicKey = new RSA(privateKey).exportKey('pkcs8-public-der');
|
||||
var signature = crypto.createSign('sha1').update(archive).sign(privateKey);
|
||||
|
||||
var header = Buffer.alloc(16);
|
||||
header.write('Cr24');
|
||||
header.writeInt32LE(2, 4);
|
||||
header.writeInt32LE(publicKey.length, 8);
|
||||
header.writeInt32LE(signature.length, 12);
|
||||
|
||||
var crx = Buffer.concat([header, publicKey, signature, archive]);
|
||||
|
||||
fs.writeFileSync(`testbuilds/${pkg.name}${channel}.crx`, crx);
|
||||
8
tools/sign.sh
Executable file
8
tools/sign.sh
Executable file
@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
channel=$1
|
||||
mkdir -p tmp-crx
|
||||
cp -r "testbuilds/crx$channel" "tmp-crx/crx$channel"
|
||||
touch -d "$(jq -r '.date' version.json)" "tmp-crx/crx$channel"/*
|
||||
chromium --pack-extension="tmp-crx/crx$channel" --pack-extension-key="$(dirname "$PWD")/4chan-x.keys/4chan-X.pem"
|
||||
mv "tmp-crx/crx$channel.crx" "testbuilds/4chan-X$channel.crx"
|
||||
rm -r 'tmp-crx/'
|
||||
@ -1,4 +1,4 @@
|
||||
{
|
||||
"version": "1.14.5.13",
|
||||
"date": "2019-03-08T23:32:11.908Z"
|
||||
"version": "1.14.7.2",
|
||||
"date": "2019-04-11T15:38:53.367Z"
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user