Merge branch 'v3' into cr32

This commit is contained in:
Mayhem 2014-02-19 00:55:33 +01:00
commit 756722e184
74 changed files with 1176 additions and 642 deletions

View File

@ -1,3 +1,72 @@
## 3.18.0 - *2014-02-15*
- Added `Image Size` setting for the catalog.
- Added `Open threads in a new tab` setting for the catalog.
- Added `board-mode:"type"` and `board-sort:"type"` parameters to custom board navigation.
- Added OP name/date tooltip in the catalog.
- Added a keybind to cycle through index sort types, `Ctrl+x` by default.
- Added keybindings for index modes, `Ctrl+{1,2,3}` by default.
### 3.17.1 - *2014-02-10*
- `Index Mode` and `Index Sort` have been moved out of the header's menu into the index page.
- Minor captcha fixes.
## 3.17.0 - *2014-02-09*
- Fixed captcha loading in the QR.
- New setting: `Quote Markers`, enabled by default
- This merges `Mark Quotes of You`, `Mark OP Quotes` and `Mark Cross-thread Quotes` into one feature.
- Backlinks now also get these markers.
- Multiple markers are now more compact, for example `>>123 (You/OP)` instead of `>>123 (You) (OP)`.
- New setting: `Image Hover in Catalog`
- Like `Image Hover`, but for the catalog only.
### 3.16.5 - *2014-02-07*
- Added `Archive link` to the Custom Board Navigation Rice
- Added a setting to configure the number of threads per page for the paged mode of the index.
- Dropped support for the official catalog.
### 3.16.4 - *2014-02-04*
- Firefox release only: fix catalog layout alignment.
### 3.16.3 - *2014-02-04*
- Firefox release only: fix catalog layout.
### 3.16.2 - *2014-02-04*
- More index navigation improvements:
- Threads in catalog mode have the usual menu.
- When in catalog mode, the menu now also allows to pin/unpin threads.
- Minor bug fixes.
### 3.16.1 - *2014-02-01*
- More index navigation improvements:
- The index will now display how many threads are hidden.
- When in catalog mode, you can toggle between hidden/non-hidden threads.
- Fixed a bug which prevented QR cooldowns from being pruned from storage.
- On Chrome, the storage could reach the quota and prevent 4chan X from saving data like QR name/mail or auto-watch for example.
## 3.16.0 - *2014-01-30*
- More index navigation improvements:
- New index mode: `catalog`<br>
![catalog mode](img/changelog/3.16.0/0.png)
- When in catalog mode, use `Shift+Click` to hide, and `Alt+Click` to pin threads.
- Existing features affect the catalog mode such as:
<ul>
<li> Filter (hiding, highlighting)
<li> Thread Hiding
<li> Linkify
<li> Auto-GIF
<li> Image Hover
</ul>
- Support for the official catalog will be removed in the future, once the catalog mode for the index is deemed satisfactory.
- Added `Original filename` variable to Sauce panel.
- Added a `Reset Settings` button in the settings.
### 3.15.2 - *2014-01-22*

View File

@ -4,14 +4,14 @@ Reporting bugs:
1. Make sure both your **browser** and **4chan X** are up to date.<br>
Only **Chrome**, **Firefox** and **Opera** are supported.<br>
**SRWare Iron**, **Firefox ESR**, **Pale Moon**, **Waterfox**, and other derivatives are not supported, use them at your own risk.
**SRWare Iron**, **Firefox ESR**, **Pale Moon**, **Waterfox**, and other derivatives are not supported; use them at your own risk.
2. Look at the list of [known problems and solutions](https://github.com/MayhemYDG/4chan-x/wiki/FAQ#known-problems).
3. Disable your other extensions & scripts to identify conflicts.
4. If your issue persists, open a [new issue](https://github.com/MayhemYDG/4chan-x/issues) with the following information:
1. Precise steps to reproduce the problem, with the expected and actual results.
2. [Console errors](https://github.com/MayhemYDG/4chan-x/wiki/FAQ#console-errors), if any.
3. 4chan X version, browser variant, browser version, and Greasemonkey version if you are using it.
4. Your exported settings. If your settings contains sensible information (e.g. personas), edit the text file manually.
4. Your exported settings. If your settings contain sensitive information (e.g. personas), edit the text file manually.
Respect these guidelines:
- Describe the issue clearly, put some effort into it. A one-liner isn't a good enough description.

View File

@ -40,6 +40,7 @@ module.exports = (grunt) ->
# <--|
'src/General/Board.coffee'
'src/General/Thread.coffee'
'src/General/CatalogThread.coffee'
'src/General/Post.coffee'
'src/General/Clone.coffee'
'src/General/DataBoard.coffee'

View File

@ -56,3 +56,6 @@
:root.burichan .focused.entry {
background: rgba(255, 255, 255, .33);
}
:root.burichan .thumb > .menu-button > i {
background: #EEF2FF;
}

View File

@ -56,3 +56,6 @@
:root.futaba .focused.entry {
background: rgba(255, 255, 255, .33);
}
:root.futaba .thumb > .menu-button > i {
background: #FFE;
}

View File

@ -56,3 +56,6 @@
:root.photon .focused.entry {
background: rgba(255, 255, 255, .33);
}
:root.photon .thumb > .menu-button > i {
background: #EEE;
}

View File

@ -288,16 +288,8 @@ a[href="javascript:;"] {
.tab-selected {
font-weight: 700;
}
.section-container {
#fourchanx-settings > section {
flex: 1;
position: relative;
}
.section-container > section {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
overflow: auto;
}
.section-sauce ul,
@ -375,9 +367,15 @@ a[href="javascript:;"] {
/* Index */
:root.index-loading .navLinks,
:root.index-loading .board,
:root.index-loading .pagelist {
:root.index-loading .pagelist,
:root:not(.catalog-mode) #hidden-toggle,
:root:not(.catalog-mode) #index-size {
display: none;
}
#nav-links {
display: flex;
align-items: baseline;
}
#index-search {
padding-right: 1.5em;
width: 100px;
@ -389,7 +387,9 @@ a[href="javascript:;"] {
}
#index-search-clear {
color: gray;
margin-left: -1.25em;
position: relative;
left: -1.25em;
width: 0;
}
<% if (type === 'crx') { %>
/* ``::-webkit-*'' selectors break selector lists on Firefox. */
@ -401,6 +401,110 @@ a[href="javascript:;"] {
.summary {
text-decoration: none;
}
.catalog-mode .board {
text-align: center;
}
.catalog-thread {
display: inline-flex;
text-align: left;
flex-direction: column;
align-items: center;
margin: 0 2px 5px;
word-break: break-word;
vertical-align: top;
}
.catalog-small .catalog-thread {
width: 165px;
max-height: 320px;
}
.catalog-large .catalog-thread {
width: 270px;
max-height: 410px;
}
.thumb {
flex-shrink: 0;
position: relative;
background-size: 100% 100%;
background-repeat: no-repeat;
background-position: center;
border-radius: 2px;
box-shadow: 0 0 5px rgba(0, 0, 0, .25);
}
.thumb:not(.deleted-file):not(.no-file) {
min-width: 30px;
min-height: 30px;
}
.thumb.spoiler-file {
background-size: 100px;
width: 100px;
height: 100px;
}
.thumb.deleted-file {
background-size: 127px 13px;
width: 127px;
height: 13px;
padding: 20px 11px;
}
.thumb.no-file {
background-size: 77px 13px;
width: 77px;
height: 13px;
padding: 20px 36px;
}
.thread-icons > img {
width: 1em;
height: 1em;
margin: 0;
vertical-align: text-top;
}
.thumb:not(:hover):not(:focus) > .menu-button:not(.open):not(:focus) > i {
display: none;
}
.thumb > .menu-button {
position: absolute;
top: 0;
right: 0;
}
.thumb > .menu-button > i {
width: 1em;
height: 1em;
padding: 1px;
border-radius: 0 2px 0 2px;
font-size: 14px;
text-align: center;
<% if (type === 'userscript') { %>
line-height: normal;
<% } %>
}
.thread-stats {
flex-shrink: 0;
cursor: help;
font-size: 10px;
font-weight: 700;
margin-top: 2px;
}
.catalog-thread > .subject {
flex-shrink: 0;
font-weight: 700;
line-height: 1;
text-align: center;
}
.catalog-thread > .comment {
flex-shrink: 1;
align-self: stretch;
overflow: hidden;
text-align: center;
}
.thread-info {
position: fixed;
background-color: inherit;
padding: 2px;
border-radius: 2px;
box-shadow: 0 0 5px rgba(0, 0, 0, .25);
}
.thread-info .post {
margin: 0;
}
/* Announcement Hiding */
:root.hide-announcement #globalMessage,
@ -428,7 +532,7 @@ a.hide-announcement {
#updater > div:last-child {
text-align: center;
}
#updater input[type=number] {
#updater input[type="number"] {
width: 4em;
}
#updater:not(:hover) > div:not(.move) {
@ -597,6 +701,10 @@ a.hide-announcement {
.filter-highlight > .reply {
box-shadow: -5px 0 rgba(255, 0, 0, .5);
}
.pinned .thumb,
.filter-highlight .thumb {
border: 2px solid rgba(255, 0, 0, .5);
}
/* Thread & Reply Hiding */
.hide-thread-button,
@ -655,6 +763,7 @@ a.hide-announcement {
}
.persona .field {
flex: 1;
width: 0;
}
.persona .field:hover,
.persona .field:focus {
@ -679,28 +788,24 @@ a.hide-announcement {
:root.gecko #dump-button {
padding: 0;
}
#qr:not(.dump) #dump-list-container {
#qr:not(.dump) #dump-list,
#qr:not(.dump) #add-post {
display: none;
}
#dump-list-container {
height: 100px;
#dump-list {
counter-reset: qrpreviews;
width: 0;
min-width: 100%;
overflow: hidden;
white-space: nowrap;
position: relative;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
#dump-list {
counter-reset: qrpreviews;
top: 0;
right: 0;
bottom: 0;
left: 0;
overflow: hidden;
position: absolute;
white-space: nowrap;
}
#dump-list:hover {
bottom: -12px;
padding-bottom: 12px;
margin-bottom: -12px;
overflow-x: auto;
z-index: 1;
}
@ -777,13 +882,11 @@ a.remove {
vertical-align: bottom;
}
#add-post {
align-self: flex-end;
font-size: 20px;
height: 20px;
width: 20px;
width: 1em;
margin-top: -1em;
text-align: center;
position: absolute;
right: 0;
bottom: 0;
z-index: 1;
}
#qr textarea {
@ -819,23 +922,11 @@ a.remove {
height: 57px;
width: 300px;
}
#file-n-submit-container {
position: relative;
}
#file-n-submit {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
align-items: center;
}
#file-n-submit-container input[type='file'] {
/* Keep it to set an appropriate height to the container. */
visibility: hidden;
}
#file-n-submit-container input {
#file-n-submit input {
margin: 0;
}
#file-n-submit input[type='submit'] {
@ -852,8 +943,9 @@ a.remove {
#qr-no-file,
#qr-filename,
#qr-filesize,
#qr-filerm,
#qr-file-spoiler {
margin: 0 2px !important;
margin: 0 1px !important;
}
#qr-no-file {
cursor: default;
@ -864,17 +956,19 @@ a.remove {
-moz-appearance: none;
appearance: none;
background: none;
border: none !important;
border: none;
color: inherit;
font: inherit;
flex: 1;
width: 0;
padding: 0;
text-overflow: ellipsis;
}
#qr-filesize {
font-size: .8em;
}
#qr-filesize::before {
content: " (";
content: "(";
}
#qr-filesize::after {
content: ")";
@ -884,14 +978,6 @@ a.remove {
.menu-button {
position: relative;
}
.menu-button i:not(.fa-bars) {
border-top: 6px solid;
border-right: 4px solid transparent;
border-left: 4px solid transparent;
display: inline-block;
margin: 2px;
vertical-align: middle;
}
@media screen and (resolution: 1dppx) {
.fa-bars {
font-size: 14px;
@ -970,8 +1056,10 @@ a.remove {
margin: 0;
}
/* colored uid */
/* Other */
.linkified {
word-break: break-all;
}
.posteruid.painted {
padding: 0 5px;
border-radius: 1em;

View File

@ -56,3 +56,6 @@
:root.tomorrow .focused.entry {
background: rgba(0, 0, 0, .33);
}
:root.tomorrow .thumb > .menu-button > i {
background: #1D1F21;
}

View File

@ -56,3 +56,6 @@
:root.yotsuba-b .focused.entry {
background: rgba(255, 255, 255, .33);
}
:root.yotsuba-b .thumb > .menu-button > i {
background: #EEF2FF;
}

View File

@ -56,3 +56,6 @@
:root.yotsuba .focused.entry {
background: rgba(255, 255, 255, .33);
}
:root.yotsuba .thumb > .menu-button > i {
background: #FFE;
}

View File

@ -1,4 +1,25 @@
[<a href="./catalog">Catalog</a>]&nbsp;
[<time id="index-last-refresh" title="Last index refresh">...</time>]&nbsp;
<input type="search" id="index-search" class="field" placeholder="Search">
<a id="index-search-clear" class="fa fa-times-circle" href="javascript:;"></a>
&nbsp;
<time id="index-last-refresh" title="Last index refresh">...</time>
<span id="hidden-label" hidden>&nbsp;&mdash; <span id="hidden-count"></span> <span id="hidden-toggle">[<a href="javascript:;">Show</a>]</span></span>
<span style="flex:1"></span>
<select id="index-mode" name="Index Mode">
<option disabled>Index Mode</option>
<option value="paged">Paged</option>
<option value="all pages">All threads</option>
<option value="catalog">Catalog</option>
</select>
<select id="index-sort" name="Index Sort">
<option disabled>Index Sort</option>
<option value="bump">Bump order</option>
<option value="lastreply">Last reply</option>
<option value="birth">Creation date</option>
<option value="replycount">Reply count</option>
<option value="filecount">File count</option>
</select>
<select id="index-size" name="Index Size">
<option disabled>Image Size</option>
<option value="small">Small</option>
<option value="large">Large</option>
</select>

View File

@ -10,5 +10,5 @@
</a>
</div>
<div class="pages cataloglink">
<a href="./catalog">Catalog</a>
<a href="./" data-index-mode="catalog">Catalog</a>
</div>

View File

@ -3,13 +3,14 @@
<div><input name="boardnav" class="field" spellcheck="false"></div>
<div>In the following, <code>board</code> can translate to a board ID (<code>a</code>, <code>b</code>, etc...), the current board (<code>current</code>), or the Twitter link (<code>@</code>).</div>
<div>Board link: <code>board</code></div>
<div>Archive link: <code>board-archive</code></div>
<div>Title link: <code>board-title</code></div>
<div>Board link (Replace with title when on that board): <code>board-replace</code></div>
<div>Full text link: <code>board-full</code></div>
<div>Custom text link: <code>board-text:"VIP Board"</code></div>
<div>Index-only link: <code>board-index</code></div>
<div>Catalog-only link: <code>board-catalog</code></div>
<div>Combinations are possible: <code>board-index-text:"VIP Index"</code></div>
<div>Index mode: <code>board-mode:"type"</code> where type is <code>paged</code>, <code>all threads</code> or <code>catalog</code></div>
<div>Index sort: <code>board-sort:"type"</code> where type is <code>bump order</code>, <code>last reply</code>, <code>creation date</code>, <code>reply count</code> or <code>file count</code></div>
<div>Combinations are possible: <code>board-text:"VIP Catalog"-mode:"catalog"-sort:"creation date"</code></div>
<div>Full board list toggle: <code>toggle-all</code></div>
</fieldset>
@ -32,7 +33,7 @@
<fieldset>
<legend>File Info Formatting <span class="warning" #{if Conf['File Info Formatting'] then 'hidden' else ''}>is disabled.</span></legend>
<div><input name="fileInfo" class="field" spellcheck="false">: <span class="fileText file-info-preview"></span></div>
<div><input name="fileInfo" class="field" spellcheck="false">: <span class="file-info file-info-preview"></span></div>
<div>Link: <code>%l</code> (truncated), <code>%L</code> (untruncated), <code>%T</code> (Unix timestamp)</div>
<div>Original file name: <code>%n</code> (truncated), <code>%N</code> (untruncated), <code>%t</code> (Unix timestamp)</div>
<div>Spoiler indicator: <code>%p</code></div>

View File

@ -5,6 +5,7 @@
<li><code>%TURL</code>: Thumbnail URL.</li>
<li><code>%URL</code>: Full image URL.</li>
<li><code>%MD5</code>: MD5 hash.</li>
<li><code>%name</code>: Original file name.</li>
<li><code>%board</code>: Current board.</li>
</ul>
<textarea name="sauces" class="field" spellcheck="false"></textarea>

View File

@ -12,7 +12,5 @@
</div>
</nav>
<hr>
<div class="section-container">
<section></section>
</div>
<section></section>
</div>

View File

@ -0,0 +1,7 @@
<a href="/#{thread.board}/res/#{thread.ID}" class="thumb"></a>
<div class="thread-stats" title="Post count / File count / Page count">
<span class="post-count">#{postCount}</span> / <span class="file-count">#{fileCount}</span> / <span class="page-count">#{pageCount}</span>
<span class="thread-icons"></span>
</div>
#{subject}
<div class="comment">#{comment}</div>

View File

@ -9,29 +9,25 @@
<form>
<div class="persona">
<input type="button" id="dump-button" title="Dump list" value="+">
<input data-name="name" list="list-name" placeholder="Name" class="field" size="1">
<input data-name="email" list="list-email" placeholder="E-mail" class="field" size="1">
<input data-name="sub" list="list-sub" placeholder="Subject" class="field" size="1">
</div>
<div id="dump-list-container">
<div id="dump-list"></div>
<a href="javascript:;" id="add-post" class="fa fa-plus" title="Add a post"></a>
<input data-name="name" name="name" list="list-name" placeholder="Name" class="field">
<input data-name="email" name="email" list="list-email" placeholder="E-mail" class="field">
<input data-name="sub" name="sub" list="list-sub" placeholder="Subject" class="field">
</div>
<div id="dump-list"></div>
<a href="javascript:;" id="add-post" class="fa fa-plus" title="Add a post"></a>
<div class="textarea">
<textarea data-name="com" placeholder="Comment" class="field"></textarea>
<span id="char-count"></span>
</div>
<div id="file-n-submit-container">
<input type="file" multiple>
<div id="file-n-submit">
<input type="submit">
<input type="button" id="qr-file-button" value="Choose files">
<span id="qr-no-file">No selected file</span>
<input id="qr-filename" data-name="filename" spellcheck="false">
<span id="qr-filesize"></span>
<a href="javascript:;" id="qr-filerm" class="fa fa-times-circle" title="Remove file"></a>
<input type="checkbox" id="qr-file-spoiler" title="Spoiler image">
</div>
<div id="file-n-submit">
<input type="file" hidden multiple>
<input type="submit">
<input type="button" id="qr-file-button" value="Choose files">
<span id="qr-no-file">No selected file</span>
<input id="qr-filename" data-name="filename" spellcheck="false">
<span id="qr-filesize"></span>
<a href="javascript:;" id="qr-filerm" class="fa fa-times-circle" title="Remove file"></a>
<input type="checkbox" id="qr-file-spoiler" title="Spoiler image">
</div>
</form>
<datalist id="list-name"></datalist>

BIN
img/changelog/3.16.0/0.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -5,8 +5,8 @@
"http": true,
"https": true,
"software": "foolfuuka",
"boards": ["a", "co", "gd", "jp", "m", "sp", "tg", "tv", "v", "vg", "vp", "vr", "wsg"],
"files": ["a", "gd", "jp", "m", "tg", "vg", "vp", "vr", "wsg"]
"boards": ["a", "biz", "co", "gd", "jp", "m", "sp", "tg", "tv", "v", "vg", "vp", "vr", "wsg"],
"files": ["a", "biz", "gd", "jp", "m", "tg", "vg", "vp", "vr", "wsg"]
}, {
"uid": 1,
"name": "NSFW Foolz",
@ -86,8 +86,8 @@
"http": false,
"https": true,
"software": "fuuka",
"boards": ["3", "cgl", "ck", "fa", "ic", "jp", "lit", "tg", "vr"],
"files": ["3", "cgl", "ck", "fa", "ic", "jp", "lit", "tg", "vr"]
"boards": ["3", "biz", "cgl", "ck", "fa", "ic", "jp", "lit", "tg", "vr"],
"files": ["3", "biz", "cgl", "ck", "fa", "ic", "jp", "lit", "tg", "vr"]
}, {
"uid": 15,
"name": "fgts",
@ -95,8 +95,8 @@
"http": true,
"https": true,
"software": "foolfuuka",
"boards": ["soc"],
"files": ["soc"]
"boards": ["r", "soc"],
"files": ["r", "soc"]
}, {
"uid": 16,
"name": "maware",
@ -123,6 +123,6 @@
"https": true,
"withCredentials": true,
"software": "foolfuuka",
"boards": ["a", "co", "gd", "jp", "m", "s4s", "sp", "tg", "tv", "u", "v", "vg", "vp", "vr", "wsg"],
"files": ["a", "gd", "jp", "m", "s4s", "tg", "u", "vg", "vp", "vr", "wsg"]
"boards": ["a", "biz", "co", "d", "gd", "jp", "m", "mlp", "s4s", "sp", "tg", "tv", "u", "v", "vg", "vp", "vr", "wsg"],
"files": ["a", "biz", "d", "gd", "jp", "m", "s4s", "tg", "u", "vg", "vp", "vr", "wsg"]
}]

View File

@ -99,17 +99,11 @@ $.rmClass = (el, className...) ->
el.classList.remove className...
$.hasClass = (el, className) ->
el.classList.contains className
$.rm = do ->
if 'remove' of Element.prototype
(el) -> el.remove()
else
(el) -> el.parentNode?.removeChild el
$.rm = (el) ->
el.remove()
$.rmAll = (root) ->
# jsperf.com/emptify-element
for node in [root.childNodes...]
# HTMLSelectElement.remove !== Element.remove
root.removeChild node
return
# https://gist.github.com/MayhemYDG/8646194
root.textContent = null
$.tn = (s) ->
d.createTextNode s
$.nodes = (nodes) ->
@ -234,81 +228,91 @@ $.localKeys = [
'usercss'
]
# https://developer.chrome.com/extensions/storage.html
$.delete = (keys) ->
chrome.storage.sync.remove keys
$.get = (key, val, cb) ->
if typeof cb is 'function'
items = $.item key, val
else
items = key
cb = val
localItems = null
syncItems = null
for key, val of items
if key in $.localKeys
(localItems or= {})[key] = val
else
(syncItems or= {})[key] = val
count = 0
done = (item) ->
if chrome.runtime.lastError
c.error chrome.runtime.lastError.message
$.extend items, item
cb items unless --count
if localItems
count++
chrome.storage.local.get localItems, done
if syncItems
count++
chrome.storage.sync.get syncItems, done
$.set = do ->
do ->
items =
sync: {}
local: {}
timeout = {}
sync: {}
$.delete = (keys) ->
if typeof keys is 'string'
keys = [keys]
for key in keys
delete items.local[key]
delete items.sync[key]
chrome.storage.sync.remove keys
$.get = (key, val, cb) ->
if typeof cb is 'function'
data = $.item key, val
else
data = key
cb = val
localItems = null
syncItems = null
for key, val of data
if key in $.localKeys
(localItems or= {})[key] = val
else
(syncItems or= {})[key] = val
count = 0
done = (result) ->
if chrome.runtime.lastError
c.error chrome.runtime.lastError.message
$.extend data, result
cb data unless --count
if localItems
count++
chrome.storage.local.get localItems, done
if syncItems
count++
chrome.storage.sync.get syncItems, done
timeout = {}
setArea = (area) ->
data = items[area]
return if !Object.keys(data).length or timeout[area]
items[area] = {}
return if !Object.keys(data).length or timeout[area] > Date.now()
chrome.storage[area].set data, ->
if chrome.runtime.lastError
c.error chrome.runtime.lastError.message
for key, val of data when key not of items[area]
if area is 'sync' and chrome.storage.sync.QUOTA_BYTES_PER_ITEM < JSON.stringify(val).length + key.length
c.error chrome.runtime.lastError.message, key, val
continue
items[area][key] = val
timeout[area] = setTimeout setArea, $.MINUTE, area
setTimeout setArea, $.MINUTE, area
timeout[area] = Date.now() + $.MINUTE
return
delete timeout[area]
items[area] = {}
setAll = $.debounce $.SECOND, ->
for key in $.localKeys
if key of items.sync
items.local[key] = items.sync[key]
delete items.sync[key]
try
setArea 'local'
setArea 'sync'
catch err
c.error err.stack
setSync = $.debounce $.SECOND, ->
setArea 'sync'
(key, val) ->
$.set = (key, val) ->
if typeof key is 'string'
items.sync[key] = val
else
$.extend items.sync, key
setAll()
$.clear = (cb) ->
count = 2
done = ->
if chrome.runtime.lastError
c.error chrome.runtime.lastError.message
return
cb?() unless --count
chrome.storage.local.clear done
chrome.storage.sync.clear done
for key in $.localKeys when key of items.sync
items.local[key] = items.sync[key]
delete items.sync[key]
setArea 'local'
setSync()
$.clear = (cb) ->
items.local = {}
items.sync = {}
count = 2
done = ->
if chrome.runtime.lastError
c.error chrome.runtime.lastError.message
return
cb?() unless --count
chrome.storage.local.clear done
chrome.storage.sync.clear done
<% } else { %>
# http://wiki.greasespot.net/Main_Page
$.sync = do ->

View File

@ -1,6 +1,6 @@
{
"name": "4chan-X",
"version": "3.15.2",
"version": "3.18.0",
"description": "Cross-browser extension for productive lurking on 4chan.",
"meta": {
"name": "4chan X",
@ -26,13 +26,13 @@
"grunt-bump": "~0.0.13",
"grunt-concurrent": "~0.4.3",
"grunt-contrib-clean": "~0.5.0",
"grunt-contrib-coffee": "~0.8.2",
"grunt-contrib-coffee": "~0.10.0",
"grunt-contrib-compress": "~0.6.0",
"grunt-contrib-concat": "~0.3.0",
"grunt-contrib-copy": "~0.5.0",
"grunt-contrib-watch": "~0.5.3",
"grunt-shell": "~0.6.4",
"load-grunt-tasks": "~0.2.1"
"load-grunt-tasks": "~0.3.0"
},
"repository": {
"type": "git",

View File

@ -45,7 +45,7 @@ Redirect =
cb?()
to: (dest, data) ->
archive = (if dest is 'search' then Redirect.data.thread else Redirect.data[dest])[data.boardID]
archive = (if dest in ['search', 'board'] then Redirect.data.thread else Redirect.data[dest])[data.boardID]
return '' unless archive
Redirect[dest] archive, data
@ -80,6 +80,9 @@ Redirect =
file: (archive, {boardID, filename}) ->
"#{Redirect.protocol archive}#{archive.domain}/#{boardID}/full_image/#{filename}"
board: (archive, {boardID}) ->
"#{Redirect.protocol archive}#{archive.domain}/#{boardID}/"
search: (archive, {boardID, type, value}) ->
type = if type is 'name'
'username'

View File

@ -1,7 +1,7 @@
Filter =
filters: {}
init: ->
return if g.VIEW is 'catalog' or !Conf['Filter']
return if !Conf['Filter']
for key of Config.filter
@filters[key] = []
@ -110,6 +110,8 @@ Filter =
# Highlight
$.addClass @nodes.root, result.class
unless @highlights and result.class in @highlights
(@highlights or= []).push result.class
if !@isReply and result.top
@thread.isOnTop = true
@ -164,7 +166,7 @@ Filter =
menu:
init: ->
return if g.VIEW is 'catalog' or !Conf['Menu'] or !Conf['Filter']
return if !Conf['Menu'] or !Conf['Filter']
div = $.el 'div',
textContent: 'Filter'

View File

@ -1,6 +1,6 @@
PostHiding =
init: ->
return if g.VIEW is 'catalog' or !Conf['Reply Hiding'] and !Conf['Reply Hiding Link']
return if !Conf['Reply Hiding'] and !Conf['Reply Hiding Link']
@db = new DataBoard 'hiddenPosts'
Post.callbacks.push
@ -20,7 +20,7 @@ PostHiding =
menu:
init: ->
return if g.VIEW is 'catalog' or !Conf['Menu'] or !Conf['Reply Hiding Link']
return if !Conf['Menu'] or !Conf['Reply Hiding Link']
# Hide
div = $.el 'div',

View File

@ -1,8 +1,6 @@
Recursive =
recursives: {}
init: ->
return if g.VIEW is 'catalog'
Post.callbacks.push
name: 'Recursive'
cb: @node

View File

@ -1,10 +1,9 @@
ThreadHiding =
init: ->
return if g.VIEW isnt 'index' or !Conf['Thread Hiding'] and !Conf['Thread Hiding Link']
return if g.VIEW isnt 'index'
@db = new DataBoard 'hiddenThreads'
@syncCatalog()
$.on d, 'IndexBuild', @onIndexBuild
$.on d, 'IndexRefresh', @onIndexRefresh
Thread.callbacks.push
name: 'Thread Hiding'
cb: @node
@ -15,61 +14,17 @@ ThreadHiding =
return unless Conf['Thread Hiding']
$.prepend @OP.nodes.root, ThreadHiding.makeButton @, 'hide'
onIndexBuild: ({detail: nodes}) ->
for root, i in nodes by 2
onIndexRefresh: ->
for root, i in Index.nodes by 2
thread = Get.threadFromRoot root
continue unless thread.isHidden
unless thread.stub
nodes[i + 1].hidden = true
Index.nodes[i + 1].hidden = true
else unless root.contains thread.stub
# When we come back to a page, the stub is already there.
ThreadHiding.makeStub thread, root
return
syncCatalog: ->
# Sync hidden threads from the catalog into the index.
hiddenThreads = ThreadHiding.db.get
boardID: g.BOARD.ID
defaultValue: {}
hiddenThreadsOnCatalog = JSON.parse(localStorage.getItem "4chan-hide-t-#{g.BOARD}") or {}
# Add threads that were hidden in the catalog.
for threadID of hiddenThreadsOnCatalog
unless threadID of hiddenThreads
hiddenThreads[threadID] = {}
# Remove threads that were un-hidden in the catalog.
for threadID of hiddenThreads
unless threadID of hiddenThreadsOnCatalog
delete hiddenThreads[threadID]
if (ThreadHiding.db.data.lastChecked or 0) > Date.now() - $.MINUTE
# Was cleaned just now.
ThreadHiding.cleanCatalog hiddenThreadsOnCatalog
unless Object.keys(hiddenThreads).length
ThreadHiding.db.delete boardID: g.BOARD.ID
return
ThreadHiding.db.set
boardID: g.BOARD.ID
val: hiddenThreads
cleanCatalog: (hiddenThreadsOnCatalog) ->
# We need to clean hidden threads on the catalog ourselves,
# otherwise if we don't visit the catalog regularly
# it will pollute the localStorage and our data.
$.cache "//a.4cdn.org/#{g.BOARD}/threads.json", ->
return unless @status is 200
threads = {}
for page in @response
for thread in page.threads
if thread.no of hiddenThreadsOnCatalog
threads[thread.no] = hiddenThreadsOnCatalog[thread.no]
if Object.keys(threads).length
localStorage.setItem "4chan-hide-t-#{g.BOARD}", JSON.stringify threads
else
localStorage.removeItem "4chan-hide-t-#{g.BOARD}"
menu:
init: ->
return if g.VIEW isnt 'index' or !Conf['Menu'] or !Conf['Thread Hiding Link']
@ -91,7 +46,7 @@ ThreadHiding =
el: div
order: 20
open: ({thread, isReply}) ->
if isReply or thread.isHidden
if isReply or thread.isHidden or Conf['Index Mode'] is 'catalog'
return false
ThreadHiding.menu.thread = thread
true
@ -130,19 +85,15 @@ ThreadHiding =
$.prepend root, thread.stub
saveHiddenState: (thread, makeStub) ->
hiddenThreadsOnCatalog = JSON.parse(localStorage.getItem "4chan-hide-t-#{g.BOARD}") or {}
if thread.isHidden
ThreadHiding.db.set
boardID: thread.board.ID
threadID: thread.ID
val: {makeStub}
hiddenThreadsOnCatalog[thread] = true
else
ThreadHiding.db.delete
boardID: thread.board.ID
threadID: thread.ID
delete hiddenThreadsOnCatalog[thread]
localStorage.setItem "4chan-hide-t-#{g.BOARD}", JSON.stringify hiddenThreadsOnCatalog
toggle: (thread) ->
unless thread instanceof Thread
@ -157,6 +108,7 @@ ThreadHiding =
return if thread.isHidden
threadRoot = thread.OP.nodes.root.parentNode
thread.isHidden = true
Index.updateHideLabel()
unless makeStub
threadRoot.hidden = threadRoot.nextElementSibling.hidden = true # <hr>
@ -171,3 +123,4 @@ ThreadHiding =
threadRoot = thread.OP.nodes.root.parentNode
threadRoot.nextElementSibling.hidden =
threadRoot.hidden = thread.isHidden = false
Index.updateHideLabel()

View File

@ -110,18 +110,18 @@ Build =
flag = unless flagCode
''
else if boardID is 'pol'
" <img src='#{staticPath}country/troll/#{flagCode.toLowerCase()}.gif' alt=#{flagCode} title='#{flagName}' class=countryFlag>"
" <img src='#{staticPath}country/troll/#{flagCode.toLowerCase()}.gif' title='#{flagName}' class=countryFlag>"
else
" <span title='#{flagName}' class='flag flag-#{flagCode.toLowerCase()}'></span>"
if file?.isDeleted
fileHTML = if isOP
"<div class=file id=f#{postID}><span class=fileThumb>" +
"<img src='#{staticPath}filedeleted#{gifIcon}' alt='File deleted.' class=fileDeleted>" +
"<img src='#{staticPath}filedeleted#{gifIcon}' class=fileDeleted>" +
"</span></div>"
else
"<div class=file id=f#{postID}><span class=fileThumb>" +
"<img src='#{staticPath}filedeleted-res#{gifIcon}' alt='File deleted.' class=fileDeletedRes>" +
"<img src='#{staticPath}filedeleted-res#{gifIcon}' class=fileDeletedRes>" +
"</span></div>"
else if file
fileSize = $.bytesToString file.size
@ -167,16 +167,16 @@ Build =
fileHTML = ''
sticky = if isSticky
" <img src=#{staticPath}sticky#{gifIcon} alt=Sticky title=Sticky class=stickyIcon>"
" <img src=#{staticPath}sticky#{gifIcon} title=Sticky class=stickyIcon>"
else
''
closed = if isClosed
" <img src=#{staticPath}closed#{gifIcon} alt=Closed title=Closed class=closedIcon>"
" <img src=#{staticPath}closed#{gifIcon} title=Closed class=closedIcon>"
else
''
if isOP and g.VIEW is 'index'
pageNum = Math.floor Index.liveThreadIDs.indexOf(postID) / Index.threadsNumPerPage
pageNum = Index.liveThreadIDs.indexOf(postID) // Index.threadsNumPerPage
pageIcon = " <span class=page-num title='This thread is on page #{pageNum} in the original index.'>Page #{pageNum}</span>"
replyLink = " &nbsp; <span>[<a href='/#{boardID}/res/#{threadID}' class=replylink>Reply</a>]</span>"
else
@ -254,9 +254,74 @@ Build =
[posts, files] = if Conf['Show Replies']
[data.omitted_posts, data.omitted_images]
else
# XXX data.images is not accurate.
[data.replies, data.omitted_images + data.last_replies.filter((data) -> !!data.ext).length]
[data.replies, data.images]
nodes.push Build.summary board.ID, data.no, posts, files
$.add root, nodes
root
catalogThread: (thread) ->
{staticPath, gifIcon} = Build
data = Index.liveThreadData[Index.liveThreadIDs.indexOf thread.ID]
postCount = data.replies + 1
fileCount = data.images + !!data.ext
pageCount = Index.liveThreadIDs.indexOf(thread.ID) // Index.threadsNumPerPage
subject = if thread.OP.info.subject
"<div class='subject'>#{thread.OP.info.subject}</div>"
else
''
comment = thread.OP.nodes.comment.innerHTML.replace /(<br>\s*){2,}/g, '<br>'
root = $.el 'div',
className: 'catalog-thread'
innerHTML: <%= importHTML('General/Thread-catalog-view') %>
root.dataset.fullID = thread.fullID
$.addClass root, 'pinned' if thread.isPinned
$.addClass root, thread.OP.highlights... if thread.OP.highlights
thumb = root.firstElementChild
if data.spoiler and !Conf['Reveal Spoilers']
src = "#{staticPath}spoiler"
if spoilerRange = Build.spoilerRange[thread.board]
# Randomize the spoiler image.
src += "-#{thread.board}" + Math.floor 1 + spoilerRange * Math.random()
src += '.png'
$.addClass thumb, 'spoiler-file'
else if data.filedeleted
src = "#{staticPath}filedeleted-res#{gifIcon}"
$.addClass thumb, 'deleted-file'
else if thread.OP.file
src = thread.OP.file.thumbURL
thumb.dataset.width = data.tn_w
thumb.dataset.height = data.tn_h
else
src = "#{staticPath}nofile.png"
$.addClass thumb, 'no-file'
thumb.style.backgroundImage = "url(#{src})"
if Conf['Open threads in a new tab']
thumb.target = '_blank'
for quotelink in $$ '.quotelink', root.lastElementChild
$.replace quotelink, [quotelink.childNodes...]
for pp in $$ '.prettyprint', root.lastElementChild
$.replace pp, $.tn pp.textContent
if thread.isSticky
$.add $('.thread-icons', root), $.el 'img',
src: "#{staticPath}sticky#{gifIcon}"
className: 'stickyIcon'
title: 'Sticky'
if thread.isClosed
$.add $('.thread-icons', root), $.el 'img',
src: "#{staticPath}closed#{gifIcon}"
className: 'closedIcon'
title: 'Closed'
if data.bumplimit
$.addClass $('.post-count', root), 'warning'
if data.imagelimit
$.addClass $('.file-count', root), 'warning'
root

View File

@ -0,0 +1,15 @@
class CatalogThread
@callbacks = []
toString: -> @ID
constructor: (root, @thread) ->
@ID = @thread.ID
@board = @thread.board
@nodes =
root: root
thumb: $ '.thumb', root
icons: $ '.thread-icons', root
postCount: $ '.post-count', root
fileCount: $ '.file-count', root
pageCount: $ '.page-count', root
@thread.catalogView = @

View File

@ -6,6 +6,7 @@ Config =
'Announcement Hiding': [true, 'Add button to hide 4chan announcements.']
'404 Redirect': [true, 'Redirect dead threads and images.']
'Keybinds': [true, 'Bind actions to keyboard shortcuts.']
'Linkify': [true, 'Convert text links into hyperlinks.']
'Time Formatting': [true, 'Localize and format timestamps.']
'Relative Post Dates': [false, 'Display dates like "3 minutes ago". Tooltip shows the timestamp.']
'File Info Formatting': [true, 'Reformat the file information.']
@ -24,11 +25,9 @@ Config =
'Auto-GIF': [false, 'Animate GIF thumbnails (disabled on /gif/, /wsg/).']
'Image Expansion': [true, 'Expand images inline.']
'Image Hover': [false, 'Show a floating expanded image on hover.']
'Image Hover in Catalog': [false, 'Show a floating expanded image on hover in the catalog.']
'Sauce': [true, 'Add sauce links to images.']
'Reveal Spoilers': [false, 'Reveal spoiler thumbnails.']
'Linkification':
'Linkify': [true, 'Convert text links into hyperlinks.']
'Clean Links': [true, 'Remove spoiler and code tags commonly used to bypass blocked links.']
'Menu':
'Menu': [true, 'Add a drop-down menu to posts.']
'Report Link': [true, 'Add a report link to the menu.']
@ -62,7 +61,6 @@ Config =
'Remember Spoiler': [false, 'Remember the spoiler state, instead of resetting after posting.']
'Hide Original Post Form': [true, 'Hide the normal post form.']
'Cooldown': [true, 'Indicate the remaining time before posting again.']
'Cooldown Prediction': [true, 'Decrease the cooldown time by taking into account upload speed. Disable it if it\'s inaccurate for you.']
<% if (type === 'crx') { %>
'Tab to Choose Files First': [false, 'Tab to the file input before the submit button.']
<% } %>
@ -74,9 +72,7 @@ Config =
'Quote Previewing': [true, 'Show quoted post on hover.']
'Quote Highlighting': [true, 'Highlight the previewed post.']
'Resurrect Quotes': [true, 'Link dead quotes to the archives.']
'Mark Quotes of You': [true, 'Add \'(You)\' to quotes linking to your posts.']
'Mark OP Quotes': [true, 'Add \'(OP)\' to OP quotes.']
'Mark Cross-thread Quotes': [true, 'Add \'(Cross-thread)\' to cross-threads quotes.']
'Quote Markers': [true, 'Add "(You)", "(OP)", "(Cross-thread)", "(Dead)" markers to quote links.']
imageExpansion:
'Fit width': [true, '']
'Fit height': [false, '']
@ -141,7 +137,11 @@ Config =
'Custom CSS': false
Index:
'Index Mode': 'paged'
'Previous Index Mode': 'paged'
'Index Sort': 'bump'
'Index Size': 'small'
'Threads per Page': 0
'Open threads in a new tab': false
'Show Replies': true
'Anchor Hidden Threads': true
'Refreshed Navigation': false
@ -149,7 +149,6 @@ Config =
'Header auto-hide': false
'Header auto-hide on scroll': false
'Bottom header': false
'Header catalog links': false
'Top Board List': false
'Bottom Board List': false
'Custom Board Navigation': true
@ -188,6 +187,10 @@ Config =
'Next page': ['Right', 'Jump to the next page.']
'Previous page': ['Left', 'Jump to the previous page.']
'Search form': ['Ctrl+Alt+s', 'Focus the search field on the board index.']
'Paged mode': ['Ctrl+1', 'Sets the index mode to paged.']
'All pages mode': ['Ctrl+2', 'Sets the index mode to all threads.']
'Catalog mode': ['Ctrl+3', 'Sets the index mode to catalog.']
'Cycle sort type': ['Ctrl+x', 'Cycle through index sort types.']
# Thread Navigation
'Next thread': ['Down', 'See next thread.']
'Previous thread': ['Up', 'See previous thread.']

View File

@ -1,5 +1,5 @@
class DataBoard
@keys = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads']
@keys = ['pinnedThreads', 'hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads']
constructor: (@key, sync, dontClean) ->
@data = Conf[key]
@ -58,30 +58,31 @@ class DataBoard
val or defaultValue
clean: ->
for boardID, val of @data.boards
@deleteIfEmpty {boardID}
now = Date.now()
if (@data.lastChecked or 0) < now - 2 * $.HOUR
@data.lastChecked = now
for boardID of @data.boards
@ajaxClean boardID
return if (@data.lastChecked or 0) > now - 2 * $.HOUR
for boardID of @data.boards
@deleteIfEmpty {boardID}
@ajaxClean boardID if boardID of @data.boards
@data.lastChecked = now
@save()
ajaxClean: (boardID) ->
$.cache "//a.4cdn.org/#{boardID}/threads.json", (e) =>
if e.target.status isnt 200
@delete boardID if e.target.status is 404
@delete {boardID} if e.target.status is 404
return
board = @data.boards[boardID]
threads = {}
for page in e.target.response
for thread in page.threads
if thread.no of board
threads[thread.no] = board[thread.no]
@data.boards[boardID] = threads
@deleteIfEmpty {boardID}
@save()
for thread in page.threads when thread.no of board
threads[thread.no] = board[thread.no]
count = Object.keys(threads).length
return if count is Object.keys(board).length # Nothing changed.
if count
@set {boardID, val: threads}
else
@delete {boardID}
onSync: (data) =>
@data = data or boards: {}

View File

@ -188,7 +188,7 @@ Get =
comment = bq.innerHTML
# greentext
.replace(/(^|>)(&gt;[^<$]*)(<|$)/g, '$1<span class=quote>$2</span>$3')
.replace /(^|>)(&gt;[^<$]*)(<|$)/g, '$1<span class=quote>$2</span>$3'
# quotes
.replace /((&gt;){2}(&gt;\/[a-z\d]+\/)?\d+)/g, '<span class=deadlink>$1</span>'
@ -233,5 +233,6 @@ Get =
thread = g.threads["#{boardID}.#{threadID}"] or
new Thread threadID, board
post = new Post Build.post(o, true), thread, board, {isArchived: true}
$('.page-num', post.nodes.info).hidden = true
Main.callbackNodes Post, [post]
Get.insert post, root, context

View File

@ -25,8 +25,6 @@ Header =
innerHTML: '<input type=checkbox name="Header auto-hide on scroll"> Auto-hide header on scroll'
barPositionToggler = $.el 'label',
innerHTML: '<input type=checkbox name="Bottom header"> Bottom header'
catalogToggler = $.el 'label',
innerHTML: '<input type=checkbox name="Header catalog links"> Use catalog board links'
topBoardToggler = $.el 'label',
innerHTML: '<input type=checkbox name="Top Board List"> Top original board list'
botBoardToggler = $.el 'label',
@ -40,7 +38,6 @@ Header =
@headerToggler = headerToggler.firstElementChild
@scrollHeaderToggler = scrollHeaderToggler.firstElementChild
@barPositionToggler = barPositionToggler.firstElementChild
@catalogToggler = catalogToggler.firstElementChild
@topBoardToggler = topBoardToggler.firstElementChild
@botBoardToggler = botBoardToggler.firstElementChild
@customNavToggler = customNavToggler.firstElementChild
@ -48,7 +45,6 @@ Header =
$.on @headerToggler, 'change', @toggleBarVisibility
$.on @scrollHeaderToggler, 'change', @toggleHideBarOnScroll
$.on @barPositionToggler, 'change', @toggleBarPosition
$.on @catalogToggler, 'change', @toggleCatalogLinks
$.on @topBoardToggler, 'change', @toggleOriginalBoardList
$.on @botBoardToggler, 'change', @toggleOriginalBoardList
$.on @customNavToggler, 'change', @toggleCustomNav
@ -74,7 +70,6 @@ Header =
{el: headerToggler}
{el: scrollHeaderToggler}
{el: barPositionToggler}
{el: catalogToggler}
{el: topBoardToggler}
{el: botBoardToggler}
{el: customNavToggler}
@ -92,9 +87,6 @@ Header =
if a = $ "a[href*='/#{g.BOARD}/']", $.id 'boardNavDesktopFoot'
a.className = 'current'
Header.setCatalogLinks Conf['Header catalog links']
$.sync 'Header catalog links', Header.setCatalogLinks
@enableDesktopNotifications()
setBoardList: ->
@ -120,10 +112,12 @@ Header =
list = $ '#custom-board-list', Header.bar
$.rmAll list
return unless text
as = $$ '#full-board-list a[title]', Header.bar
nodes = text.match(/[\w@]+(-(all|title|replace|full|index|catalog|text:"[^"]+"))*|[^\w@]+/g).map (t) ->
as = $$ '.boardList a[title]', Header.bar
re = /[\w@]+(-(all|title|replace|full|archive|(mode|sort|text):"[^"]+"))*|[^\w@]+/g
nodes = text.match(re).map (t) ->
if /^[^\w@]/.test t
return $.tn t
if /^toggle-all/.test t
a = $.el 'a',
className: 'show-board-list-button'
@ -131,31 +125,47 @@ Header =
href: 'javascript:;'
$.on a, 'click', Header.toggleBoardList
return a
board = if /^current/.test t
g.BOARD.ID
boardID = t.split('-')[0]
boardID = g.BOARD.ID if boardID is 'current'
for a in as when a.textContent is boardID
a = a.cloneNode()
break
return $.tn boardID if a.parentNode # Not a clone.
a.textContent = if /-title/.test(t) or /-replace/.test(t) and boardID is g.BOARD.ID
a.title
else if /-full/.test t
"/#{boardID}/ - #{a.title}"
else if m = t.match /-text:"([^"]+)"/
m[1]
else
t.match(/^[^-]+/)[0]
for a in as
if a.textContent is board
a = a.cloneNode true
boardID
a.textContent = if /-title/.test(t) or /-replace/.test(t) and $.hasClass a, 'current'
a.title
else if /-full/.test t
"/#{board}/ - #{a.title}"
else if m = t.match /-text:"(.+)"/
m[1]
else
a.textContent
if /-archive/.test t
if href = Redirect.to 'board', {boardID}
a.href = href
else
return a.firstChild # Its text node.
if m = t.match /-(index|catalog)/
a.dataset.only = m[1]
a.href = "//boards.4chan.org/#{board}/"
a.href += 'catalog' if m[1] is 'catalog'
if m = t.match /-mode:"([^"]+)"/
type = m[1].toLowerCase()
a.dataset.indexMode = switch type
when 'all threads' then 'all pages'
when 'paged', 'catalog' then type
else 'paged'
if m = t.match /-sort:"([^"]+)"/
type = m[1].toLowerCase()
a.dataset.indexSort = switch type
when 'bump order' then 'bump'
when 'last reply' then 'lastreply'
when 'creation date' then 'birth'
when 'reply count' then 'replycount'
when 'file count' then 'filecount'
else 'bump'
$.addClass a, 'navSmall' if board is '@'
return a
$.tn t
$.addClass a, 'navSmall' if boardID is '@'
a
$.add list, nodes
toggleBoardList: ->
@ -219,21 +229,6 @@ Header =
$.cb.checked.call @
Header.setBarPosition @checked
setCatalogLinks: (useCatalog) ->
Header.catalogToggler.checked = useCatalog
as = $$ [
'#board-list a'
'#boardNavDesktop a'
'#boardNavDesktopFoot a'
].join ', '
path = if useCatalog then 'catalog' else ''
for a in as when a.hostname is 'boards.4chan.org' and not a.dataset.only
a.pathname = "/#{a.pathname.split('/')[1]}/#{path}"
return
toggleCatalogLinks: ->
$.cb.checked.call @
Header.setCatalogLinks @checked
setTopBoardList: (show) ->
Header.topBoardToggler.checked = show
if show

View File

@ -1,6 +1,18 @@
Index =
showHiddenThreads: false
init: ->
return if g.VIEW isnt 'index' or g.BOARD.ID is 'f'
if g.VIEW isnt 'index'
$.ready @setupNavLinks
return
return if g.BOARD.ID is 'f'
@db = new DataBoard 'pinnedThreads'
Thread.callbacks.push
name: 'Thread Pinning'
cb: @threadNode
CatalogThread.callbacks.push
name: 'Catalog Features'
cb: @catalogNode
@button = $.el 'a',
className: 'index-refresh-shortcut fa fa-refresh'
@ -9,33 +21,20 @@ Index =
$.on @button, 'click', @update
Header.addShortcut @button, 1
modeEntry =
el: $.el 'span', textContent: 'Index mode'
threadNumEntry =
el: $.el 'span', textContent: 'Threads per page'
subEntries: [
{ el: $.el 'label', innerHTML: '<input type=radio name="Index Mode" value="paged"> Paged' }
{ el: $.el 'label', innerHTML: '<input type=radio name="Index Mode" value="all pages"> All threads' }
{ el: $.el 'label', innerHTML: '<input type=number min=0 name="Threads per Page">', title: 'Use 0 for default value' }
]
for label in modeEntry.subEntries
input = label.el.firstChild
input.checked = Conf['Index Mode'] is input.value
$.on input, 'change', $.cb.value
$.on input, 'change', @cb.mode
sortEntry =
el: $.el 'span', textContent: 'Sort by'
subEntries: [
{ el: $.el 'label', innerHTML: '<input type=radio name="Index Sort" value="bump"> Bump order' }
{ el: $.el 'label', innerHTML: '<input type=radio name="Index Sort" value="lastreply"> Last reply' }
{ el: $.el 'label', innerHTML: '<input type=radio name="Index Sort" value="birth"> Creation date' }
{ el: $.el 'label', innerHTML: '<input type=radio name="Index Sort" value="replycount"> Reply count' }
{ el: $.el 'label', innerHTML: '<input type=radio name="Index Sort" value="filecount"> File count' }
]
for label in sortEntry.subEntries
input = label.el.firstChild
input.checked = Conf['Index Sort'] is input.value
$.on input, 'change', $.cb.value
$.on input, 'change', @cb.sort
threadsNumInput = threadNumEntry.subEntries[0].el.firstChild
threadsNumInput.value = Conf['Threads per Page']
$.on threadsNumInput, 'change', $.cb.value
$.on threadsNumInput, 'change', @cb.threadsNum
targetEntry =
el: $.el 'label',
innerHTML: '<input type=checkbox name="Open threads in a new tab"> Open threads in a new tab'
title: 'Catalog-only setting.'
repliesEntry =
el: $.el 'label',
innerHTML: '<input type=checkbox name="Show Replies"> Show replies'
@ -47,12 +46,14 @@ Index =
el: $.el 'label',
innerHTML: '<input type=checkbox name="Refreshed Navigation"> Refreshed navigation'
title: 'Refresh index when navigating through pages.'
for label in [repliesEntry, anchorEntry, refNavEntry]
for label in [targetEntry, repliesEntry, anchorEntry, refNavEntry]
input = label.el.firstChild
{name} = input
input.checked = Conf[name]
$.on input, 'change', $.cb.checked
switch name
when 'Open threads in a new tab'
$.on input, 'change', @cb.target
when 'Show Replies'
$.on input, 'change', @cb.replies
when 'Anchor Hidden Threads'
@ -63,24 +64,41 @@ Index =
el: $.el 'span',
textContent: 'Index Navigation'
order: 90
subEntries: [modeEntry, sortEntry, repliesEntry, anchorEntry, refNavEntry]
subEntries: [threadNumEntry, targetEntry, repliesEntry, anchorEntry, refNavEntry]
$.addClass doc, 'index-loading'
@update()
@navLinks = $.el 'div',
id: 'nav-links'
innerHTML: <%= importHTML('General/Index-navlinks') %>
@searchInput = $ '#index-search', @navLinks
@hideLabel = $ '#hidden-label', @navLinks
@selectMode = $ '#index-mode', @navLinks
@selectSort = $ '#index-sort', @navLinks
@selectSize = $ '#index-size', @navLinks
$.on @searchInput, 'input', @onSearchInput
$.on $('#index-search-clear', @navLinks), 'click', @clearSearch
$.on $('#hidden-toggle a', @navLinks), 'click', @cb.toggleHiddenThreads
for select in [@selectMode, @selectSort, @selectSize]
select.value = Conf[select.name]
$.on select, 'change', $.cb.value
$.on @selectMode, 'change', @cb.mode
$.on @selectSort, 'change', @cb.sort
$.on @selectSize, 'change', @cb.size
@root = $.el 'div', className: 'board'
@pagelist = $.el 'div',
className: 'pagelist'
hidden: true
innerHTML: <%= importHTML('General/Index-pagelist') %>
@navLinks = $.el 'div',
className: 'navLinks'
innerHTML: <%= importHTML('General/Index-navlinks') %>
@searchInput = $ '#index-search', @navLinks
@currentPage = @getCurrentPage()
$.on window, 'popstate', @cb.popstate
$.on @pagelist, 'click', @cb.pageNav
$.on @searchInput, 'input', @onSearchInput
$.on $('#index-search-clear', @navLinks), 'click', @clearSearch
$.on $('#custom-board-list', Header.bar), 'click', @cb.headerNav
@cb.toggleCatalogMode()
$.asap (-> $('.board', doc) or d.readyState isnt 'loading'), ->
board = $ '.board'
$.replace board, Index.root
@ -95,18 +113,192 @@ Index =
for navLink in $$ '.navLinks'
$.rm navLink
$.after $.x('child::form/preceding-sibling::hr[1]'), Index.navLinks
$.before $.id('delform'), [Index.navLinks, $.x 'child::form/preceding-sibling::hr[1]']
$.rmClass doc, 'index-loading'
$.asap (-> $('.pagelist') or d.readyState isnt 'loading'), ->
$.replace $('.pagelist'), Index.pagelist
menu:
init: ->
return if g.VIEW isnt 'index' or !Conf['Menu'] or g.BOARD.ID is 'f'
$.event 'AddMenuEntry',
type: 'post'
el: $.el 'a', href: 'javascript:;'
order: 5
open: ({thread}) ->
return false if Conf['Index Mode'] isnt 'catalog'
@el.textContent = if thread.isHidden
'Unhide thread'
else
'Hide thread'
$.off @el, 'click', @cb if @cb
@cb = ->
$.event 'CloseMenu'
Index.toggleHide thread
$.on @el, 'click', @cb
true
$.event 'AddMenuEntry',
type: 'post'
el: $.el 'a', href: 'javascript:;'
order: 6
open: ({thread}) ->
return false if Conf['Index Mode'] isnt 'catalog'
@el.textContent = if thread.isPinned
'Unpin thread'
else
'Pin thread'
$.off @el, 'click', @cb if @cb
@cb = ->
$.event 'CloseMenu'
Index.togglePin thread
$.on @el, 'click', @cb
true
threadNode: ->
return unless Index.db.get {boardID: @board.ID, threadID: @ID}
@pin()
catalogNode: ->
$.on @nodes.thumb, 'click', Index.onClick
return if Conf['Image Hover in Catalog']
$.on @nodes.thumb, 'mouseover', Index.onOver
onClick: (e) ->
return if e.button isnt 0
thread = g.threads[@parentNode.dataset.fullID]
if e.shiftKey
Index.toggleHide thread
else if e.altKey
Index.togglePin thread
else
return
e.preventDefault()
onOver: (e) ->
# 4chan's less than stellar CSS forces us to include a .post and .postInfo
# in order to have proper styling for the .nameBlock's content.
{nodes} = g.threads[@parentNode.dataset.fullID].OP
el = $.el 'div',
innerHTML: '<div class=post><div class=postInfo>'
className: 'thread-info'
hidden: true
$.add el.firstElementChild.firstElementChild, [
$('.nameBlock', nodes.info).cloneNode true
$.tn ' '
nodes.date.cloneNode true
]
$.add d.body, el
UI.hover
root: @
el: el
latestEvent: e
endEvents: 'mouseout'
offsetX: 15
offsetY: -20
setTimeout (-> el.hidden = false if el.parentNode), .25 * $.SECOND
toggleHide: (thread) ->
$.rm thread.catalogView.nodes.root
if Index.showHiddenThreads
ThreadHiding.show thread
return unless ThreadHiding.db.get {boardID: thread.board.ID, threadID: thread.ID}
# Don't save when un-hiding filtered threads.
else
ThreadHiding.hide thread
ThreadHiding.saveHiddenState thread
togglePin: (thread) ->
data =
boardID: thread.board.ID
threadID: thread.ID
if thread.isPinned
thread.unpin()
Index.db.delete data
else
thread.pin()
data.val = true
Index.db.set data
Index.sort()
Index.buildIndex()
setIndexMode: (mode) ->
Index.selectMode.value = mode
$.event 'change', null, Index.selectMode
cycleSortType: ->
types = [Index.selectSort.options...].filter (option) -> !option.disabled
for type, i in types
break if type.selected
types[(i + 1) % types.length].selected = true
$.event 'change', null, Index.selectSort
addCatalogSwitch: ->
a = $.el 'a',
href: 'javascript:;'
textContent: 'Switch to <%= meta.name %>\'s catalog'
className: 'btn-wrap'
$.on a, 'click', ->
$.set 'Index Mode', 'catalog'
window.location = './'
$.add $.id('info'), a
setupNavLinks: ->
for el in $$ '.navLinks.desktop > a'
if el.getAttribute('href') is '.././catalog'
el.href = '.././'
$.on el, 'click', ->
switch @textContent
when 'Return'
$.set 'Index Mode', Conf['Previous Index Mode']
when 'Catalog'
$.set 'Index Mode', 'catalog'
return
cb:
mode: ->
Index.togglePagelist()
Index.buildIndex()
sort: ->
toggleCatalogMode: ->
if Conf['Index Mode'] is 'catalog'
$.addClass doc, 'catalog-mode'
else
$.rmClass doc, 'catalog-mode'
Index.cb.size()
toggleHiddenThreads: ->
$('#hidden-toggle a', Index.navLinks).textContent = if Index.showHiddenThreads = !Index.showHiddenThreads
'Hide'
else
'Show'
Index.sort()
Index.buildIndex()
mode: (e) ->
Index.cb.toggleCatalogMode()
Index.togglePagelist()
Index.buildIndex() if e
mode = Conf['Index Mode']
if mode not in ['catalog', Conf['Previous Index Mode']]
Conf['Previous Index Mode'] = mode
$.set 'Previous Index Mode', mode
return unless QR.nodes
if mode is 'catalog'
QR.hide()
else
QR.unhide()
sort: (e) ->
Index.sort()
Index.buildIndex() if e
size: (e) ->
if Conf['Index Mode'] isnt 'catalog'
$.rmClass Index.root, 'catalog-small'
$.rmClass Index.root, 'catalog-large'
else if Conf['Index Size'] is 'small'
$.addClass Index.root, 'catalog-small'
$.rmClass Index.root, 'catalog-large'
else
$.addClass Index.root, 'catalog-large'
$.rmClass Index.root, 'catalog-small'
Index.buildIndex() if e
threadsNum: ->
return unless Conf['Index Mode'] is 'paged'
Index.buildPagelist()
Index.buildIndex()
target: ->
for threadID, thread of g.BOARD.threads when thread.catalogView
{thumb} = thread.catalogView.nodes
if Conf['Open threads in a new tab']
thumb.target = '_blank'
else
thumb.removeAttribute 'target'
return
replies: ->
Index.buildThreads()
Index.sort()
@ -123,12 +315,40 @@ Index =
a = e.target
else
return
return if a.textContent is 'Catalog'
e.preventDefault()
return if Index.cb.indexNav a, true
Index.userPageNav +a.pathname.split('/')[2]
headerNav: (e) ->
a = e.target
return if e.button isnt 0 or a.nodeName isnt 'A' or a.hostname isnt 'boards.4chan.org'
# Save settings
onSameBoard = a.pathname.split('/')[1] is g.BOARD.ID
Index.cb.indexNav a, onSameBoard
# Do nav if this isn't a simple click, or different board.
return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or !onSameBoard
e.preventDefault()
indexNav: (a, onSameBoard) ->
{indexMode, indexSort} = a.dataset
if indexMode
$.set 'Index Mode', indexMode
Conf['Index Mode'] = indexMode
if g.VIEW is 'index' and onSameBoard
Index.selectMode.value = indexMode
Index.cb.mode()
if indexSort
$.set 'Index Sort', indexSort
Conf['Index Sort'] = indexSort
if g.VIEW is 'index' and onSameBoard
Index.selectSort.value = indexSort
Index.cb.sort()
if g.VIEW is 'index' and onSameBoard and (indexMode or indexSort)
Index.buildIndex()
Index.scrollToIndex()
return true
false
scrollToIndex: ->
Header.scrollToIfNeeded Index.root
Header.scrollToIfNeeded Index.navLinks
getCurrentPage: ->
+window.location.pathname.split('/')[2]
@ -148,11 +368,17 @@ Index =
Index.setPage()
Index.scrollToIndex()
getPagesNum: ->
if Index.isSearching
Math.ceil (Index.sortedNodes.length / 2) / Index.threadsNumPerPage
getThreadsNumPerPage: ->
if Conf['Threads per Page'] > 0
+Conf['Threads per Page']
else
Index.pagesNum
Index.threadsNumPerPage
getPagesNum: ->
numThreads = if Index.isSearching
Index.sortedNodes.length / 2
else
Index.liveThreadIDs.length
Math.ceil numThreads / Index.getThreadsNumPerPage()
getMaxPageNum: ->
Math.max 0, Index.getPagesNum() - 1
togglePagelist: ->
@ -193,6 +419,20 @@ Index =
$.before a, strong
$.add strong, a
updateHideLabel: ->
hiddenCount = 0
for threadID, thread of g.BOARD.threads when thread.isHidden
hiddenCount++ if thread.ID in Index.liveThreadIDs
unless hiddenCount
Index.hideLabel.hidden = true
Index.cb.toggleHiddenThreads() if Index.showHiddenThreads
return
Index.hideLabel.hidden = false
$('#hidden-count', Index.navLinks).textContent = if hiddenCount is 1
'1 hidden thread'
else
"#{hiddenCount} hidden threads"
update: (pageNum) ->
return unless navigator.onLine
Index.req?.abort()
@ -273,7 +513,6 @@ Index =
Index.buildIndex()
Index.setPage()
parseThreadList: (pages) ->
Index.pagesNum = pages.length
Index.threadsNumPerPage = pages[0].threads.length
Index.liveThreadData = pages.reduce ((arr, next) -> arr.concat next.threads), []
Index.liveThreadIDs = Index.liveThreadData.map (data) -> data.no
@ -288,7 +527,9 @@ Index =
threadRoot = Build.thread g.BOARD, threadData
Index.nodes.push threadRoot, $.el 'hr'
if thread = g.BOARD.threads[threadData.no]
thread.setPage Math.floor i / Index.threadsNumPerPage
thread.setPage i // Index.threadsNumPerPage
thread.setCount 'post', threadData.replies + 1, threadData.bumplimit
thread.setCount 'file', threadData.images + !!threadData.ext, threadData.imagelimit
thread.setStatus 'Sticky', !!threadData.sticky
thread.setStatus 'Closed', !!threadData.closed
else
@ -309,6 +550,7 @@ Index =
$.nodes Index.nodes
Main.callbackNodes Thread, threads
Main.callbackNodes Post, posts
Index.updateHideLabel()
$.event 'IndexRefresh'
buildReplies: (threadRoots) ->
posts = []
@ -334,16 +576,37 @@ Index =
Main.handleErrors errors if errors
Main.callbackNodes Post, posts
buildCatalogViews: ->
threads = Index.sortedNodes
.filter (n, i) -> !(i % 2)
.map (threadRoot) -> Get.threadFromRoot threadRoot
.filter (thread) -> !thread.isHidden isnt Index.showHiddenThreads
catalogThreads = []
for thread in threads when !thread.catalogView
catalogThreads.push new CatalogThread Build.catalogThread(thread), thread
Main.callbackNodes CatalogThread, catalogThreads
threads.map (thread) -> thread.catalogView.nodes.root
sizeCatalogViews: (nodes) ->
# XXX When browsers support CSS3 attr(), use it instead.
size = if Conf['Index Size'] is 'small' then 150 else 250
for node in nodes
thumb = node.firstElementChild
{width, height} = thumb.dataset
continue unless width
ratio = size / Math.max width, height
thumb.style.width = width * ratio + 'px'
thumb.style.height = height * ratio + 'px'
return
sort: ->
switch Conf['Index Sort']
when 'bump'
sortedThreadIDs = Index.liveThreadIDs
when 'lastreply'
sortedThreadIDs = [Index.liveThreadData...].sort((a, b) ->
a = a.last_replies[a.last_replies.length - 1] if 'last_replies' of a
b = b.last_replies[b.last_replies.length - 1] if 'last_replies' of b
sortedThreadIDs = [Index.liveThreadData...].sort (a, b) ->
[..., a] = a.last_replies if 'last_replies' of a
[..., b] = b.last_replies if 'last_replies' of b
b.no - a.no
).map (data) -> data.no
.map (data) -> data.no
when 'birth'
sortedThreadIDs = [Index.liveThreadIDs...].sort (a, b) -> b - a
when 'replycount'
@ -359,7 +622,7 @@ Index =
# Sticky threads
Index.sortOnTop (thread) -> thread.isSticky
# Highlighted threads
Index.sortOnTop((thread) -> thread.isOnTop) if Conf['Filter']
Index.sortOnTop (thread) -> thread.isOnTop or thread.isPinned
# Non-hidden threads
Index.sortOnTop((thread) -> !thread.isHidden) if Conf['Anchor Hidden Threads']
sortOnTop: (match) ->
@ -368,16 +631,20 @@ Index =
Index.sortedNodes.splice offset++ * 2, 0, Index.sortedNodes.splice(i, 2)...
return
buildIndex: ->
if Conf['Index Mode'] is 'paged'
pageNum = Index.getCurrentPage()
nodesPerPage = Index.threadsNumPerPage * 2
nodes = Index.sortedNodes[nodesPerPage * pageNum ... nodesPerPage * (pageNum + 1)]
else
nodes = Index.sortedNodes
switch Conf['Index Mode']
when 'paged'
pageNum = Index.getCurrentPage()
nodesPerPage = Index.getThreadsNumPerPage() * 2
nodes = Index.sortedNodes[nodesPerPage * pageNum ... nodesPerPage * (pageNum + 1)]
when 'catalog'
nodes = Index.buildCatalogViews()
Index.sizeCatalogViews nodes
else
nodes = Index.sortedNodes
$.rmAll Index.root
Index.buildReplies nodes if Conf['Show Replies']
$.event 'IndexBuild', nodes
Index.buildReplies nodes if Conf['Show Replies'] and Conf['Index Mode'] isnt 'catalog'
$.add Index.root, nodes
$.event 'IndexBuild', nodes
isSearching: false
clearSearch: ->

View File

@ -11,6 +11,9 @@ Main =
'catalog'
else
'index'
if g.VIEW is 'catalog'
$.ready Index.addCatalogSwitch
return
if g.VIEW is 'thread'
g.THREADID = +pathname[3]
@ -82,6 +85,7 @@ Main =
initFeature 'Strike-through Quotes', QuoteStrikeThrough
initFeature 'Quick Reply', QR
initFeature 'Menu', Menu
initFeature 'Index Generator (Menu)', Index.menu
initFeature 'Report Link', ReportLink
initFeature 'Thread Hiding (Menu)', ThreadHiding.menu
initFeature 'Reply Hiding (Menu)', PostHiding.menu
@ -92,9 +96,7 @@ Main =
initFeature 'Quote Inlining', QuoteInline
initFeature 'Quote Previewing', QuotePreview
initFeature 'Quote Backlinks', QuoteBacklink
initFeature 'Mark Quotes of You', QuoteYou
initFeature 'Mark OP Quotes', QuoteOP
initFeature 'Mark Cross-thread Quotes', QuoteCT
initFeature 'Quote Markers', QuoteMarkers
initFeature 'Anonymize', Anonymize
initFeature 'Color User IDs', IDColor
initFeature 'Time Formatting', Time
@ -131,10 +133,6 @@ Main =
$.addClass doc, 'fourchan-x', '<% if (type === 'crx') { %>blink<% } else { %>gecko<% } %>'
$.addStyle Main.css
if g.VIEW is 'catalog'
$.addClass doc, $.id('base-css').href.match(/catalog_(\w+)/)[1].replace('_new', '').replace /_+/g, '-'
return
style = 'yotsuba-b'
mainStyleSheet = $ 'link[title=switch]', d.head
styleSheets = $$ 'link[rel="alternate stylesheet"]', d.head

View File

@ -52,6 +52,9 @@ class Post
@parseQuotes()
@parseFile that
@isDead = false
@isHidden = false
@clones = []
g.posts[@fullID] = thread.posts[@] = board.posts[@] = @
@kill() if that.isArchived
@ -62,7 +65,6 @@ class Post
# Get the comment's text.
# <br> -> \n
# Remove:
# 'Comment too long'...
# EXIF data. (/p/)
# Rolls. (/tg/)
# Preceding and following new lines.
@ -149,17 +151,14 @@ class Post
$.rmClass node, 'desktop'
return
kill: (file, now) ->
now or= new Date()
kill: (file) ->
if file
return if @file.isDead
@file.isDead = true
@file.timeOfDeath = now
$.addClass @nodes.root, 'deleted-file'
else
return if @isDead
@isDead = true
@timeOfDeath = now
$.addClass @nodes.root, 'deleted-post'
unless strong = $ 'strong.warning', @nodes.info
@ -171,20 +170,20 @@ class Post
return if @isClone
for clone in @clones
clone.kill file, now
clone.kill file
return if file
# Get quotelinks/backlinks to this post
# and paint them (Dead).
for quotelink in Get.allQuotelinksLinkingTo @ when not $.hasClass quotelink, 'deadlink'
$.add quotelink, $.tn '\u00A0(Dead)'
$.addClass quotelink, 'deadlink'
continue unless Conf['Quote Markers']
QuoteMarkers.parseQuotelink Get.postFromNode(quotelink), quotelink, true
return
# XXX tmp fix for 4chan's racing condition
# giving us false-positive dead posts.
resurrect: ->
delete @isDead
delete @timeOfDeath
$.rmClass @nodes.root, 'deleted-post'
strong = $ 'strong.warning', @nodes.info
# no false-positive files
@ -197,10 +196,10 @@ class Post
for clone in @clones
clone.resurrect()
for quotelink in Get.allQuotelinksLinkingTo @
if $.hasClass quotelink, 'deadlink'
quotelink.textContent = quotelink.textContent.replace '\u00A0(Dead)', ''
$.rmClass quotelink, 'deadlink'
for quotelink in Get.allQuotelinksLinkingTo @ when $.hasClass quotelink, 'deadlink'
$.rmClass quotelink, 'deadlink'
continue unless Conf['Quote Markers']
QuoteMarkers.parseQuotelink Get.postFromNode(quotelink), quotelink, true
return
collect: ->

View File

@ -133,10 +133,7 @@ Settings =
button.textContent = "Hidden: #{hiddenNum}"
$.on button, 'click', ->
@textContent = 'Hidden: 0'
$.get 'hiddenThreads', {}, ({hiddenThreads}) ->
for boardID of hiddenThreads.boards
localStorage.removeItem "4chan-hide-t-#{boardID}"
$.delete ['hiddenThreads', 'hiddenPosts']
$.delete ['hiddenThreads', 'hiddenPosts']
$.after $('input[name="Stubs"]', section).parentNode.parentNode, div
export: ->
# Make sure to export the most recent data.
@ -195,8 +192,8 @@ Settings =
'Remember QR size': ''
'Quote Inline': 'Quote Inlining'
'Quote Preview': 'Quote Previewing'
'Indicate OP quote': 'Mark OP Quotes'
'Indicate Cross-thread Quotes': 'Mark Cross-thread Quotes'
'Indicate OP quote': ''
'Indicate Cross-thread Quotes': ''
# filter
'uniqueid': 'uniqueID'
'mod': 'capcode'

View File

@ -5,18 +5,30 @@ class Thread
constructor: (@ID, @board) ->
@fullID = "#{@board}.#{@ID}"
@posts = {}
@isDead = false
@isHidden = false
@isOnTop = false
@isPinned = false
@isSticky = false
@isClosed = false
@postLimit = false
@fileLimit = false
@OP = null
@catalogView = null
g.threads[@fullID] = board.threads[@] = @
setPage: (pageNum) ->
icon = $ '.page-num', @OP.nodes.post
icon = $ '.page-num', @OP.nodes.info
for key in ['title', 'textContent']
icon[key] = icon[key].replace /\d+/, pageNum
return
@catalogView.nodes.pageCount.textContent = pageNum if @catalogView
setCount: (type, count, reachedLimit) ->
return unless @catalogView
el = @catalogView.nodes["#{type}Count"]
el.textContent = count
(if reachedLimit then $.addClass else $.rmClass) el, 'warning'
setStatus: (type, status) ->
name = "is#{type}"
return if @[name] is status
@ -25,23 +37,33 @@ class Thread
typeLC = type.toLowerCase()
unless status
$.rm $ ".#{typeLC}Icon", @OP.nodes.info
$.rm $ ".#{typeLC}Icon", @catalogView.nodes.icons if @catalogView
return
icon = $.el 'img',
src: "//s.4cdn.org/image/#{typeLC}#{if window.devicePixelRatio >= 2 then '@2x' else ''}.gif"
alt: type
src: "#{Build.staticPath}#{typeLC}#{Build.gifIcon}"
title: type
className: "#{typeLC}Icon"
root = if type is 'Closed' and @isSticky
$ '.stickyIcon', @OP.nodes.info
else if g.VIEW is 'index'
$ '.page-num', @OP.nodes.info
$ '.page-num', @OP.nodes.info
else
$ '[title="Quote this post"]', @OP.nodes.info
$.after root, [$.tn(' '), icon]
return unless @catalogView
(if type is 'Sticky' and @isClosed then $.prepend else $.add) @catalogView.nodes.icons, icon.cloneNode()
pin: ->
@isPinned = true
$.addClass @catalogView.nodes.root, 'pinned' if @catalogView
unpin: ->
@isPinned = false
$.rmClass @catalogView.nodes.root, 'pinned' if @catalogView
kill: ->
@isDead = true
@timeOfDeath = Date.now()
collect: ->
for postID, post of @posts

View File

@ -47,6 +47,7 @@ UI = do ->
menu = @makeMenu()
currentMenu = menu
lastToggledButton = button
$.addClass button, 'open'
for entry in @entries
@insertEntry entry, menu, data
@ -99,6 +100,7 @@ UI = do ->
close: =>
$.rm currentMenu
$.rmClass lastToggledButton, 'open'
currentMenu = null
lastToggledButton = null
$.off d, 'click CloseMenu', @close
@ -191,7 +193,7 @@ UI = do ->
# prevent text selection
e.preventDefault()
if isTouching = e.type is 'touchstart'
e = e.changedTouches[e.changedTouches.length - 1]
[..., e] = e.changedTouches
# distance from pointer to el edge is constant; calculate it here.
el = $.x 'ancestor::div[contains(@class,"dialog")][1]', @
rect = el.getBoundingClientRect()
@ -271,7 +273,7 @@ UI = do ->
$.off d, 'mouseup', @up
$.set "#{@id}.position", @style.cssText
hoverstart = ({root, el, latestEvent, endEvents, asapTest, cb}) ->
hoverstart = ({root, el, latestEvent, endEvents, asapTest, cb, offsetX, offsetY}) ->
o = {
root
el
@ -281,14 +283,17 @@ UI = do ->
latestEvent
clientHeight: doc.clientHeight
clientWidth: doc.clientWidth
offsetX: offsetX or 45
offsetY: offsetY or -120
}
o.hover = hover.bind o
o.hoverend = hoverend.bind o
$.asap ->
!el.parentNode or asapTest()
, ->
o.hover o.latestEvent if el.parentNode
if asapTest
$.asap ->
!el.parentNode or asapTest()
, ->
o.hover o.latestEvent if el.parentNode
$.on root, endEvents, o.hoverend
$.on root, 'mousemove', o.hover
@ -302,7 +307,7 @@ UI = do ->
height = @el.offsetHeight
{clientX, clientY} = e
top = clientY - 120
top = clientY + @offsetY
top = if @clientHeight <= height or top <= 0
0
else if top + height >= @clientHeight
@ -310,10 +315,10 @@ UI = do ->
else
top
[left, right] = if clientX <= @clientWidth - 400
[clientX + 45 + 'px', null]
[left, right] = if clientX <= @clientWidth / 2
[clientX + @offsetX + 'px', null]
else
[null, @clientWidth - clientX + 45 + 'px']
[null, @clientWidth - clientX + @offsetX + 'px']
{style} = @
style.top = top + 'px'

View File

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

View File

@ -1,6 +1,6 @@
ImageExpand =
init: ->
return if g.VIEW is 'catalog' or !Conf['Image Expansion']
return if !Conf['Image Expansion']
@EAI = $.el 'a',
className: 'expand-all-shortcut fa fa-expand'
@ -133,7 +133,7 @@ ImageExpand =
timeoutID = setTimeout ImageExpand.expand, 10000, post
<% if (type === 'crx') { %>
$.ajax @src,
$.ajax post.file.URL,
onloadend: ->
return if @status isnt 404
clearTimeout timeoutID
@ -156,7 +156,7 @@ ImageExpand =
menu:
init: ->
return if g.VIEW is 'catalog' or !Conf['Image Expansion']
return if !Conf['Image Expansion']
el = $.el 'span',
textContent: 'Image Expansion'

View File

@ -1,15 +1,24 @@
ImageHover =
init: ->
return if g.VIEW is 'catalog' or !Conf['Image Hover']
Post.callbacks.push
name: 'Image Hover'
cb: @node
if Conf['Image Hover']
Post.callbacks.push
name: 'Image Hover'
cb: @node
if Conf['Image Hover in Catalog']
CatalogThread.callbacks.push
name: 'Image Hover'
cb: @catalogNode
node: ->
return unless @file?.isImage
$.on @file.thumb, 'mouseover', ImageHover.mouseover
catalogNode: ->
return unless @thread.OP.file?.isImage
$.on @nodes.thumb, 'mouseover', ImageHover.mouseover
mouseover: (e) ->
post = Get.postFromNode @
post = if $.hasClass @, 'thumb'
g.posts[@parentNode.dataset.fullID]
else
Get.postFromNode @
el = $.el 'img',
id: 'ihover'
src: post.file.URL
@ -39,7 +48,7 @@ ImageHover =
timeoutID = setTimeout (=> @src = post.file.URL + '?' + Date.now()), 3000
<% if (type === 'crx') { %>
$.ajax @src,
$.ajax post.file.URL,
onloadend: ->
return if @status isnt 404
clearTimeout timeoutID

View File

@ -1,6 +1,6 @@
RevealSpoilers =
init: ->
return if g.VIEW is 'catalog' or !Conf['Reveal Spoilers']
return if !Conf['Reveal Spoilers']
Post.callbacks.push
name: 'Reveal Spoilers'

View File

@ -1,6 +1,6 @@
Sauce =
init: ->
return if g.VIEW is 'catalog' or !Conf['Sauce']
return if !Conf['Sauce']
links = []
for link in Conf['sauces'].split '\n'
@ -15,7 +15,7 @@ Sauce =
name: 'Sauce'
cb: @node
createSauceLink: (link) ->
link = link.replace /%(T?URL|MD5|board)/g, (parameter) ->
link = link.replace /%(T?URL|MD5|board|name)/g, (parameter) ->
switch parameter
when '%TURL'
"' + encodeURIComponent(post.file.thumbURL) + '"
@ -25,6 +25,8 @@ Sauce =
"' + encodeURIComponent(post.file.MD5) + '"
when '%board'
"' + encodeURIComponent(post.board) + '"
when '%name'
"' + encodeURIComponent(post.file.name) + '"
else
parameter
text = if m = link.match(/;text:(.+)$/) then m[1] else link.match(/(\w+)\.\w+\//)[1]

View File

@ -1,6 +1,6 @@
Linkify =
init: ->
return if g.VIEW is 'catalog' or !Conf['Linkify']
return if !Conf['Linkify']
# gruber revised + magnet support
# http://df4.us/fv9
@ -60,11 +60,12 @@ Linkify =
# Replace already-linkified links,
# f.e.: https://boards.4chan.org/b/%
$.replace parent, anchor
Linkify.cleanLink anchor, link if Conf['Clean Links']
Linkify.cleanLink anchor, link
walker.currentNode = anchor.lastChild
else
walker.previousNode()
range.detach()
@nodes.comment.normalize()
find: (link, walker) ->
# Walk through the nodes until we find the entire link.
@ -134,6 +135,8 @@ Linkify =
cleanLink: (anchor, link) ->
{length} = link
for node in $$ 'wbr', anchor
$.rm node
for node in $$ 's, .prettyprint', anchor
$.replace node, [node.childNodes...] if length > node.textContent.length
return

View File

@ -1,6 +1,6 @@
ArchiveLink =
init: ->
return if g.VIEW is 'catalog' or !Conf['Menu'] or !Conf['Archive Link']
return if !Conf['Menu'] or !Conf['Archive Link']
div = $.el 'div',
textContent: 'Archive'

View File

@ -1,6 +1,6 @@
DeleteLink =
init: ->
return if g.VIEW is 'catalog' or !Conf['Menu'] or !Conf['Delete Link']
return if !Conf['Menu'] or !Conf['Delete Link']
div = $.el 'div',
className: 'delete-link'

View File

@ -1,6 +1,6 @@
DownloadLink =
init: ->
return if g.VIEW is 'catalog' or !Conf['Menu'] or !Conf['Download Link']
return if !Conf['Menu'] or !Conf['Download Link']
a = $.el 'a',
className: 'download-link'

View File

@ -1,36 +1,42 @@
Menu =
init: ->
return if g.VIEW is 'catalog' or !Conf['Menu']
return if !Conf['Menu']
a = $.el 'a',
className: 'menu-button'
innerHTML: '<i class="fa fa-bars"></i>'
href: 'javascript:;'
@frag = $.nodes [$.tn(' '), a]
@menu = new UI.Menu 'post'
Post.callbacks.push
name: 'Menu'
cb: @node
CatalogThread.callbacks.push
name: 'Menu'
cb: @catalogNode
node: ->
if @isClone
$.on $('.menu-button', @nodes.info), 'click', Menu.toggle
return
$.add @nodes.info, Menu.makeButton()
catalogNode: ->
$.add @nodes.thumb, Menu.makeButton()
makeButton: do ->
frag = null
->
unless frag
a = $.el 'a',
className: 'menu-button'
innerHTML: '[<i></i>]'
href: 'javascript:;'
frag = $.nodes [$.tn(' '), a]
clone = frag.cloneNode true
$.on clone.lastElementChild, 'click', Menu.toggle
clone
makeButton: ->
clone = Menu.frag.cloneNode true
$.on clone.lastElementChild, 'click', Menu.toggle
clone
toggle: (e) ->
try
# Posts, inlined posts, hidden replies.
post = Get.postFromNode @
catch
# Hidden threads.
post = Get.threadFromNode(@).OP
post = if fullID = @parentNode.parentNode.dataset.fullID
g.threads[fullID].OP
else
# Hidden threads.
Get.threadFromNode(@).OP
Menu.menu.toggle e, @, post

View File

@ -1,6 +1,6 @@
ReportLink =
init: ->
return if g.VIEW is 'catalog' or !Conf['Menu'] or !Conf['Report Link']
return if !Conf['Menu'] or !Conf['Report Link']
a = $.el 'a',
className: 'report-link'

View File

@ -1,6 +1,6 @@
Anonymize =
init: ->
return if g.VIEW is 'catalog' or !Conf['Anonymize']
return if !Conf['Anonymize']
Post.callbacks.push
name: 'Anonymize'

View File

@ -1,6 +1,6 @@
Dice =
init: ->
return if g.BOARD.ID isnt 'tg' or g.VIEW is 'catalog' or !Conf['Show Dice Roll']
return if g.BOARD.ID isnt 'tg' or !Conf['Show Dice Roll']
Post.callbacks.push
name: 'Show Dice Roll'
cb: @node

View File

@ -92,12 +92,4 @@ ExpandThread =
postsRoot.push root
Main.callbackNodes Post, posts
$.after a, postsRoot
postsCount = postsRoot.length
a.textContent = ExpandThread.text '-', postsCount, filesCount
# Enable 4chan features.
if Conf['Enable 4chan\'s Extension']
$.globalEval "Parser.parseThread(#{thread}, 1, #{postsCount})"
else
Fourchan.parseThread thread.ID, 1, postsCount
a.textContent = ExpandThread.text '-', postsRoot.length, filesCount

View File

@ -1,6 +1,6 @@
FileInfo =
init: ->
return if g.VIEW is 'catalog' or !Conf['File Info Formatting']
return if !Conf['File Info Formatting']
@funk = @createFunc Conf['fileInfo']
Post.callbacks.push

View File

@ -1,7 +1,5 @@
Fourchan =
init: ->
return if g.VIEW is 'catalog'
board = g.BOARD.ID
if board is 'g'
$.globalEval """
@ -42,10 +40,3 @@ Fourchan =
math: ->
return if @isClone or !$ '.math', @nodes.comment
$.event 'jsmath', @nodes.post, window
parseThread: (threadID, offset, limit) ->
# Fix /sci/
# Fix /g/
$.event '4chanParsingDone',
threadId: threadID
offset: offset
limit: limit

View File

@ -1,6 +1,6 @@
IDColor =
init: ->
return if g.VIEW is 'catalog' or !Conf['Color User IDs']
return if !Conf['Color User IDs']
@ids = {}
Post.callbacks.push

View File

@ -1,6 +1,6 @@
Keybinds =
init: ->
return if g.VIEW is 'catalog' or !Conf['Keybinds']
return if !Conf['Keybinds']
for hotkey of Conf.hotkeys
$.sync hotkey, Keybinds.sync
@ -88,6 +88,17 @@ Keybinds =
$('.prev button', Index.pagelist).click()
when Conf['Search form']
Index.searchInput.focus()
when Conf['Paged mode']
return unless g.VIEW is 'index' and Conf['Index Mode'] isnt 'paged'
Index.setIndexMode 'paged'
when Conf['All pages mode']
return unless g.VIEW is 'index' and Conf['Index Mode'] isnt 'all pages'
Index.setIndexMode 'all pages'
when Conf['Catalog mode']
return unless g.VIEW is 'index' and Conf['Index Mode'] isnt 'catalog'
Index.setIndexMode 'catalog'
when Conf['Cycle sort type']
Index.cycleSortType()
# Thread Navigation
when Conf['Next thread']
return if g.VIEW isnt 'index'

View File

@ -5,8 +5,6 @@ Nav =
return unless Conf['Index Navigation']
when 'thread'
return unless Conf['Reply Navigation']
else # catalog
return
span = $.el 'span',
id: 'navlinks'

View File

@ -1,6 +1,6 @@
Time =
init: ->
return if g.VIEW is 'catalog' or !Conf['Time Formatting']
return if !Conf['Time Formatting']
@funk = @createFunc Conf['time']
Post.callbacks.push

View File

@ -254,11 +254,3 @@ ThreadUpdater =
window.scrollTo 0, d.body.clientHeight
else
Header.scrollTo nodes[0]
# Enable 4chan features.
threadID = ThreadUpdater.thread.ID
{length} = $$ '.thread > .postContainer', ThreadUpdater.root
if Conf['Enable 4chan\'s Extension']
$.globalEval "Parser.parseThread(#{threadID}, #{-count})"
else
Fourchan.parseThread threadID, length - count, length

View File

@ -72,9 +72,8 @@ ThreadWatcher =
else if Conf['Auto Watch Reply']
ThreadWatcher.add board.threads[threadID]
onIndexRefresh: ->
{db} = ThreadWatcher
boardID = g.BOARD.ID
for threadID, data of db.data.boards[boardID] when not data.isDead and threadID not of g.BOARD.threads
for threadID, data of ThreadWatcher.db.data.boards[boardID] when not data.isDead and threadID not of g.BOARD.threads
if Conf['Auto Prune']
ThreadWatcher.db.delete {boardID, threadID}
else

View File

@ -139,7 +139,7 @@ Unread =
Unread.readArray Unread.postsQuotingYou
Unread.update() if e
saveLastReadPost: ->
saveLastReadPost: <% if (type === 'crx') { %>$.debounce 5 * $.SECOND,<% } %> ->
return if Unread.thread.isDead
Unread.db.set
boardID: Unread.thread.board.ID
@ -158,7 +158,7 @@ Unread =
count = Unread.posts.length
if Conf['Unread Count']
d.title = "#{if count or !Conf['Hide Unread Count at (0)'] then "(#{count}) " else ''}#{if g.DEAD then "/#{g.BOARD}/ - 404" else "#{Unread.title}"}"
d.title = "#{if count or !Conf['Hide Unread Count at (0)'] then "(#{count}) " else ''}#{if g.DEAD then Unread.title.replace '-', '- 404 -' else Unread.title}"
return unless Conf['Unread Tab Icon']

View File

@ -1,38 +1,25 @@
QR.captcha =
init: ->
return if d.cookie.indexOf('pass_enabled=1') >= 0
return unless @isEnabled = !!$.id 'captchaFormPart'
$.asap (-> $.id 'recaptcha_challenge_field_holder'), @ready.bind @
ready: ->
setLifetime = (e) => @lifetime = e.detail
$.on window, 'captcha:timeout', setLifetime
$.globalEval 'window.dispatchEvent(new CustomEvent("captcha:timeout", {detail: RecaptchaState.timeout}))'
$.off window, 'captcha:timeout', setLifetime
container = $.id 'captchaContainer'
return unless @isEnabled = !!container
imgContainer = $.el 'div',
className: 'captcha-img'
title: 'Reload reCAPTCHA'
innerHTML: '<img>'
hidden: true
input = $.el 'input',
className: 'captcha-input field'
title: 'Verification'
placeholder: 'Focus to load reCAPTCHA'
autocomplete: 'off'
spellcheck: false
@nodes =
challenge: $.id 'recaptcha_challenge_field_holder'
img: imgContainer.firstChild
input: input
img: imgContainer.firstChild
input: input
new MutationObserver(@load.bind @).observe @nodes.challenge,
childList: true
$.on imgContainer, 'click', @reload.bind @
$.on input, 'keydown', @keydown.bind @
$.get 'captchas', [], ({captchas}) =>
@sync captchas
$.sync 'captchas', @sync
# start with an uncached captcha
@reload()
$.on input, 'focus', @setup
<% if (type === 'userscript') { %>
# XXX Firefox lacks focusin/focusout support.
@ -42,6 +29,36 @@ QR.captcha =
$.addClass QR.nodes.el, 'has-captcha'
$.after QR.nodes.com.parentNode, [imgContainer, input]
@setupObserver = new MutationObserver @afterSetup
@setupObserver.observe container, childList: true
@afterSetup() # reCAPTCHA might have loaded before the QR.
setup: ->
$.globalEval 'loadRecaptcha()'
afterSetup: ->
return unless challenge = $.id 'recaptcha_challenge_field_holder'
QR.captcha.setupObserver.disconnect()
delete QR.captcha.setupObserver
setLifetime = (e) -> QR.captcha.lifetime = e.detail
$.on window, 'captcha:timeout', setLifetime
$.globalEval 'window.dispatchEvent(new CustomEvent("captcha:timeout", {detail: RecaptchaState.timeout}))'
$.off window, 'captcha:timeout', setLifetime
{img, input} = QR.captcha.nodes
img.parentNode.hidden = false
$.off input, 'focus', QR.captcha.setup
$.on input, 'keydown', QR.captcha.keydown.bind QR.captcha
$.on img.parentNode, 'click', QR.captcha.reload.bind QR.captcha
$.get 'captchas', [], ({captchas}) ->
QR.captcha.sync captchas
$.sync 'captchas', QR.captcha.sync
QR.captcha.nodes.challenge = challenge
new MutationObserver(QR.captcha.load.bind QR.captcha).observe challenge,
childList: true
QR.captcha.load()
sync: (captchas) ->
QR.captcha.captchas = captchas
QR.captcha.count()
@ -70,6 +87,7 @@ QR.captcha =
@reload()
$.set 'captchas', @captchas
clear: ->
return unless @captchas # not loaded yet.
now = Date.now()
for captcha, i in @captchas
break if captcha.timeout > now
@ -87,7 +105,7 @@ QR.captcha =
@nodes.input.value = null
@clear()
count: ->
count = @captchas.length
count = if @captchas then @captchas.length else 0
@nodes.input.placeholder = switch count
when 0
'Verification (Shift + Enter to cache)'

View File

@ -50,14 +50,15 @@ QR =
else
QR.status()
QR.persist() if Conf['Persistent QR']
return unless Conf['Persistent QR']
QR.open()
QR.hide() if Conf['Auto-Hide QR'] or g.VIEW is 'index' and Conf['Index Mode'] is 'catalog'
node: ->
if QR.db.get {boardID: @board.ID, threadID: @thread.ID, postID: @ID}
$.addClass @nodes.root, 'your-post'
$.on $('a[title="Quote this post"]', @nodes.info), 'click', QR.quote
persist: ->
QR.open()
QR.hide() if Conf['Auto-Hide QR'] or g.VIEW is 'catalog'
open: ->
if QR.nodes
QR.nodes.el.hidden = false

View File

@ -7,16 +7,13 @@ QR.cooldown =
$.off window, 'cooldown:timers', setTimers
for type of QR.cooldown.types
QR.cooldown.types[type] = +QR.cooldown.types[type]
QR.cooldown.upSpd = 0
QR.cooldown.upSpdAccuracy = .5
key = "cooldown.#{g.BOARD}"
$.get key, {}, (item) ->
QR.cooldown.cooldowns = item[key]
QR.cooldown.start()
$.sync key, QR.cooldown.sync
start: ->
return unless Conf['Cooldown']
return if QR.cooldown.isCounting
return if QR.cooldown.isCounting or !Object.keys(QR.cooldown.cooldowns).length
QR.cooldown.isCounting = true
QR.cooldown.count()
sync: (cooldowns) ->
@ -32,10 +29,6 @@ QR.cooldown =
if delay
cooldown = {delay}
else
if post.file
upSpd = post.file.size / ((start - req.uploadStartTime) / $.SECOND)
QR.cooldown.upSpdAccuracy = ((upSpd > QR.cooldown.upSpd * .9) + QR.cooldown.upSpdAccuracy) / 2
QR.cooldown.upSpd = upSpd
cooldown = {isReply, threadID}
QR.cooldown.cooldowns[start] = cooldown
$.set "cooldown.#{g.BOARD}", QR.cooldown.cooldowns
@ -48,7 +41,7 @@ QR.cooldown =
$.delete "cooldown.#{g.BOARD}"
count: ->
unless Object.keys(QR.cooldown.cooldowns).length
$.delete "#{g.BOARD}.cooldown"
$.delete "cooldown.#{g.BOARD}"
delete QR.cooldown.isCounting
delete QR.cooldown.seconds
QR.status()
@ -62,9 +55,10 @@ QR.cooldown =
isReply = post.thread isnt 'new'
hasFile = !!post.file
seconds = null
{types, cooldowns, upSpd, upSpdAccuracy} = QR.cooldown
{types, cooldowns} = QR.cooldown
for start, cooldown of cooldowns
start = +start
if 'delay' of cooldown
if cooldown.delay
seconds = Math.max seconds, cooldown.delay--
@ -76,8 +70,10 @@ QR.cooldown =
if isReply is cooldown.isReply
# Only cooldowns relevant to this post can set the seconds variable:
# reply cooldown with a reply, thread cooldown with a thread
elapsed = Math.floor (now - start) / $.SECOND
continue if elapsed < 0 # clock changed since then?
elapsed = (now - start) // $.SECOND
if elapsed < 0 # clock changed since then?
QR.cooldown.unset start
continue
type = unless isReply
'thread'
else if hasFile
@ -90,9 +86,6 @@ QR.cooldown =
type += '_intra' if isReply and +post.thread is cooldown.threadID
seconds = Math.max seconds, types[type] - elapsed
if seconds and Conf['Cooldown Prediction'] and hasFile and upSpd
seconds -= Math.floor post.file.size / upSpd * upSpdAccuracy
seconds = Math.max seconds, 0
# Update the status when we change posting type.
# Don't get stuck at some random number.
# Don't interfere with progress status updates.

View File

@ -35,7 +35,7 @@ QR.post = class
else
'new'
prev = QR.posts[QR.posts.length - 1]
[..., prev] = QR.posts
QR.posts.push @
@nodes.spoiler.checked = @spoiler = if prev and Conf['Remember Spoiler']
prev.spoiler
@ -153,7 +153,10 @@ QR.post = class
@filesize = $.bytesToString file.size
@nodes.label.hidden = false if QR.spoiler
URL.revokeObjectURL @URL
@showFileData() if @ is QR.selected
if @ is QR.selected
@showFileData()
else
@updateFilename()
unless /^image/.test file.type
@nodes.el.style.backgroundImage = null
return

View File

@ -3,19 +3,19 @@ QuoteBacklink =
# - previous, same, and following posts.
# - existing and yet-to-exist posts.
# - newly fetched posts.
# - in copies.
# - clones.
# XXX what about order for fetched posts?
#
# First callback creates backlinks and add them to relevant containers.
# Second callback adds relevant containers into posts.
# This is is so that fetched posts can get their backlinks,
# and that as much backlinks are appended in the background as possible.
# First callback creates a map of quoted -> [quoters],
# and append backlinks to posts that already have containers.
# Second callback creates, fill and append containers.
init: ->
return if g.VIEW is 'catalog' or !Conf['Quote Backlinks']
return if !Conf['Quote Backlinks']
format = Conf['backlink'].replace /%id/g, "' + id + '"
@funk = Function 'id', "return '#{format}'"
@containers = {}
@frag = $.nodes [$.tn(' '), $.el 'a', className: 'backlink']
@map = {}
Post.callbacks.push
name: 'Quote Backlinking Part 1'
cb: @firstNode
@ -23,35 +23,42 @@ QuoteBacklink =
name: 'Quote Backlinking Part 2'
cb: @secondNode
firstNode: ->
return if @isClone or !@quotes.length
a = $.el 'a',
href: "/#{@board}/res/#{@thread}#p#{@}"
className: if @isHidden then 'filtered backlink' else 'backlink'
textContent: QuoteBacklink.funk @ID
for quote in @quotes
containers = [QuoteBacklink.getContainer quote]
if (post = g.posts[quote]) and post.nodes.backlinkContainer
# Don't add OP clones when OP Backlinks is disabled,
# as the clones won't have the backlink containers.
for clone in post.clones
containers.push clone.nodes.backlinkContainer
for container in containers
link = a.cloneNode true
if Conf['Quote Previewing']
$.on link, 'mouseover', QuotePreview.mouseover
if Conf['Quote Inlining']
$.on link, 'click', QuoteInline.toggle
$.add container, [$.tn(' '), link]
return if @isClone
for quoteID in @quotes
(QuoteBacklink.map[quoteID] or= []).push @fullID
continue unless (post = g.posts[quoteID]) and container = post?.nodes.backlinkContainer
for post in [post].concat post.clones
$.add post.nodes.backlinkContainer, QuoteBacklink.buildBacklink post, @
return
secondNode: ->
if @isClone and (@origin.isReply or Conf['OP Backlinks'])
@nodes.backlinkContainer = $ '.container', @nodes.info
return
# Don't backlink the OP.
return unless @isReply or Conf['OP Backlinks']
container = QuoteBacklink.getContainer @fullID
@nodes.backlinkContainer = container
if @isClone
@nodes.backlinkContainer = $ '.backlink-container', @nodes.info
return unless Conf['Quote Markers']
for backlink in @nodes.backlinks
QuoteMarkers.parseQuotelink @, backlink, true, QuoteBacklink.funk Get.postDataFromLink(backlink).postID
return
@nodes.backlinkContainer = container = $.el 'span',
className: 'backlink-container'
if @fullID of QuoteBacklink.map
for quoteID in QuoteBacklink.map[@fullID]
if post = g.posts[quoteID] # Post hasn't been collected since.
$.add container, QuoteBacklink.buildBacklink @, post
$.add @nodes.info, container
getContainer: (id) ->
@containers[id] or=
$.el 'span', className: 'container'
buildBacklink: (quoted, quoter) ->
frag = QuoteBacklink.frag.cloneNode true
a = frag.lastElementChild
a.href = "/#{quoter.board}/res/#{quoter.thread}#p#{quoter}"
a.textContent = text = QuoteBacklink.funk quoter.ID
if quoter.isDead
$.addClass a, 'deadlink'
if quoter.isHidden
$.addClass a, 'filtered'
if Conf['Quote Markers']
QuoteMarkers.parseQuotelink quoted, a, false, text
if Conf['Quote Previewing']
$.on a, 'mouseover', QuotePreview.mouseover
if Conf['Quote Inlining']
$.on a, 'click', QuoteInline.toggle
frag

View File

@ -1,22 +0,0 @@
QuoteCT =
init: ->
return if g.VIEW is 'catalog' or !Conf['Mark Cross-thread Quotes']
# \u00A0 is nbsp
@text = '\u00A0(Cross-thread)'
Post.callbacks.push
name: 'Mark Cross-thread Quotes'
cb: @node
node: ->
# Stop there if it's a clone of a post in the same thread.
return if @isClone and @thread is @context.thread
{board, thread} = if @isClone then @context else @
for quotelink in @nodes.quotelinks
{boardID, threadID} = Get.postDataFromLink quotelink
continue unless threadID # deadlink
if @isClone
quotelink.textContent = quotelink.textContent.replace QuoteCT.text, ''
if boardID is board.ID and threadID isnt thread.ID
$.add quotelink, $.tn QuoteCT.text
return

View File

@ -1,6 +1,6 @@
QuoteInline =
init: ->
return if g.VIEW is 'catalog' or !Conf['Quote Inlining']
return if !Conf['Quote Inlining']
Post.callbacks.push
name: 'Quote Inlining'

View File

@ -0,0 +1,39 @@
QuoteMarkers =
init: ->
return if !Conf['Quote Markers']
Post.callbacks.push
name: 'Quote Markers'
cb: @node
node: ->
for quotelink in @nodes.quotelinks
QuoteMarkers.parseQuotelink @, quotelink, !!@isClone
return
parseQuotelink: (post, quotelink, mayReset, customText) ->
{board, thread} = if post.isClone then post.context else post
markers = []
{boardID, threadID, postID} = Get.postDataFromLink quotelink
if QR.db?.get {boardID, threadID, postID}
markers.push 'You'
if board.ID is boardID
if thread.ID is postID
markers.push 'OP'
if threadID and threadID isnt thread.ID # threadID is 0 for deadlinks
markers.push 'Cross-thread'
if $.hasClass quotelink, 'deadlink'
markers.push 'Dead'
text = if customText
customText
else if boardID is post.board.ID
">>#{postID}"
else
">>>/#{boardID}/#{postID}"
if markers.length
quotelink.textContent = "#{text}\u00A0(#{markers.join '/'})"
else if mayReset
quotelink.textContent = text

View File

@ -1,29 +0,0 @@
QuoteOP =
init: ->
return if g.VIEW is 'catalog' or !Conf['Mark OP Quotes']
# \u00A0 is nbsp
@text = '\u00A0(OP)'
Post.callbacks.push
name: 'Mark OP Quotes'
cb: @node
node: ->
# Stop there if it's a clone of a post in the same thread.
return if @isClone and @thread is @context.thread
# Stop there if there's no quotes in that post.
return unless (quotes = @quotes).length
{quotelinks} = @nodes
# rm (OP) from cross-thread quotes.
if @isClone and @thread.fullID in quotes
for quotelink in quotelinks
quotelink.textContent = quotelink.textContent.replace QuoteOP.text, ''
{fullID} = (if @isClone then @context else @).thread
# add (OP) to quotes quoting this context's OP.
return unless fullID in quotes
for quotelink in quotelinks
{boardID, postID} = Get.postDataFromLink quotelink
if "#{boardID}.#{postID}" is fullID
$.add quotelink, $.tn QuoteOP.text
return

View File

@ -1,6 +1,6 @@
QuotePreview =
init: ->
return if g.VIEW is 'catalog' or !Conf['Quote Previewing']
return if !Conf['Quote Previewing']
Post.callbacks.push
name: 'Quote Previewing'

View File

@ -1,6 +1,6 @@
QuoteStrikeThrough =
init: ->
return if g.VIEW is 'catalog' or !Conf['Reply Hiding'] and !Conf['Reply Hiding Link'] and !Conf['Filter']
return if !Conf['Reply Hiding'] and !Conf['Reply Hiding Link'] and !Conf['Filter']
Post.callbacks.push
name: 'Strike-through Quotes'

View File

@ -1,14 +0,0 @@
QuoteYou =
init: ->
return if g.VIEW is 'catalog' or !Conf['Mark Quotes of You'] or !Conf['Quick Reply']
# \u00A0 is nbsp
@text = '\u00A0(You)'
Post.callbacks.push
name: 'Mark Quotes of You'
cb: @node
node: ->
return if @isClone
for quotelink in @nodes.quotelinks when QR.db.get Get.postDataFromLink quotelink
$.add quotelink, $.tn QuoteYou.text
return

View File

@ -1,6 +1,6 @@
Quotify =
init: ->
return if g.VIEW is 'catalog' or !Conf['Resurrect Quotes']
return if !Conf['Resurrect Quotes']
Post.callbacks.push
name: 'Resurrect Quotes'
@ -37,28 +37,20 @@ Quotify =
quoteID = "#{boardID}.#{postID}"
if post = g.posts[quoteID]
unless post.isDead
# Don't (Dead) when quotifying in an archived post,
# and we know the post still exists.
a = $.el 'a',
href: "/#{boardID}/res/#{post.thread}#p#{postID}"
className: 'quotelink'
textContent: quote
else
# Replace the .deadlink span if we can redirect.
a = $.el 'a',
href: "/#{boardID}/res/#{post.thread}#p#{postID}"
className: 'quotelink deadlink'
target: '_blank'
textContent: "#{quote}\u00A0(Dead)"
$.extend a.dataset, {boardID, threadID: post.thread.ID, postID}
else if redirect = Redirect.to 'thread', {boardID, threadID: 0, postID}
# Don't add 'deadlink' when quotifying in an archived post,
# and we don't know if the post died yet.
a = $.el 'a',
href: "/#{boardID}/res/#{post.thread}#p#{postID}"
className: if post.isDead then 'quotelink deadlink' else 'quotelink'
textContent: quote
$.extend a.dataset, {boardID, threadID: post.thread.ID, postID}
else if redirect = Redirect.to 'thread', {boardID, postID}
# Replace the .deadlink span if we can redirect.
a = $.el 'a',
href: redirect
className: 'deadlink'
textContent: quote
target: '_blank'
textContent: "#{quote}\u00A0(Dead)"
if Redirect.to 'post', {boardID, postID}
# Make it function as a normal quote if we can fetch the post.
$.addClass a, 'quotelink'
@ -68,7 +60,7 @@ Quotify =
@quotes.push quoteID
unless a
deadlink.textContent = "#{quote}\u00A0(Dead)"
deadlink.textContent = "#{quote}\u00A0(Dead)" if Conf['Quote Markers']
return
$.replace deadlink, a