Merge branch 'v3' into cr32
This commit is contained in:
commit
756722e184
69
CHANGELOG.md
69
CHANGELOG.md
@ -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>
|
||||

|
||||
- 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*
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -56,3 +56,6 @@
|
||||
:root.burichan .focused.entry {
|
||||
background: rgba(255, 255, 255, .33);
|
||||
}
|
||||
:root.burichan .thumb > .menu-button > i {
|
||||
background: #EEF2FF;
|
||||
}
|
||||
|
||||
@ -56,3 +56,6 @@
|
||||
:root.futaba .focused.entry {
|
||||
background: rgba(255, 255, 255, .33);
|
||||
}
|
||||
:root.futaba .thumb > .menu-button > i {
|
||||
background: #FFE;
|
||||
}
|
||||
|
||||
@ -56,3 +56,6 @@
|
||||
:root.photon .focused.entry {
|
||||
background: rgba(255, 255, 255, .33);
|
||||
}
|
||||
:root.photon .thumb > .menu-button > i {
|
||||
background: #EEE;
|
||||
}
|
||||
|
||||
202
css/style.css
202
css/style.css
@ -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;
|
||||
|
||||
@ -56,3 +56,6 @@
|
||||
:root.tomorrow .focused.entry {
|
||||
background: rgba(0, 0, 0, .33);
|
||||
}
|
||||
:root.tomorrow .thumb > .menu-button > i {
|
||||
background: #1D1F21;
|
||||
}
|
||||
|
||||
@ -56,3 +56,6 @@
|
||||
:root.yotsuba-b .focused.entry {
|
||||
background: rgba(255, 255, 255, .33);
|
||||
}
|
||||
:root.yotsuba-b .thumb > .menu-button > i {
|
||||
background: #EEF2FF;
|
||||
}
|
||||
|
||||
@ -56,3 +56,6 @@
|
||||
:root.yotsuba .focused.entry {
|
||||
background: rgba(255, 255, 255, .33);
|
||||
}
|
||||
:root.yotsuba .thumb > .menu-button > i {
|
||||
background: #FFE;
|
||||
}
|
||||
|
||||
@ -1,4 +1,25 @@
|
||||
[<a href="./catalog">Catalog</a>]
|
||||
[<time id="index-last-refresh" title="Last index refresh">...</time>]
|
||||
<input type="search" id="index-search" class="field" placeholder="Search">
|
||||
<a id="index-search-clear" class="fa fa-times-circle" href="javascript:;"></a>
|
||||
|
||||
<time id="index-last-refresh" title="Last index refresh">...</time>
|
||||
<span id="hidden-label" hidden> — <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>
|
||||
|
||||
@ -10,5 +10,5 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="pages cataloglink">
|
||||
<a href="./catalog">Catalog</a>
|
||||
<a href="./" data-index-mode="catalog">Catalog</a>
|
||||
</div>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -12,7 +12,5 @@
|
||||
</div>
|
||||
</nav>
|
||||
<hr>
|
||||
<div class="section-container">
|
||||
<section></section>
|
||||
</div>
|
||||
<section></section>
|
||||
</div>
|
||||
|
||||
7
html/General/Thread-catalog-view.html
Normal file
7
html/General/Thread-catalog-view.html
Normal 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>
|
||||
@ -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
BIN
img/changelog/3.16.0/0.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
@ -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"]
|
||||
}]
|
||||
|
||||
138
lib/$.coffee
138
lib/$.coffee
@ -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 ->
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
Recursive =
|
||||
recursives: {}
|
||||
init: ->
|
||||
return if g.VIEW is 'catalog'
|
||||
|
||||
Post.callbacks.push
|
||||
name: 'Recursive'
|
||||
cb: @node
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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 = " <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
|
||||
|
||||
15
src/General/CatalogThread.coffee
Normal file
15
src/General/CatalogThread.coffee
Normal 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 = @
|
||||
@ -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.']
|
||||
|
||||
@ -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: {}
|
||||
|
||||
@ -188,7 +188,7 @@ Get =
|
||||
|
||||
comment = bq.innerHTML
|
||||
# greentext
|
||||
.replace(/(^|>)(>[^<$]*)(<|$)/g, '$1<span class=quote>$2</span>$3')
|
||||
.replace /(^|>)(>[^<$]*)(<|$)/g, '$1<span class=quote>$2</span>$3'
|
||||
# quotes
|
||||
.replace /((>){2}(>\/[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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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: ->
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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: ->
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
Anonymize =
|
||||
init: ->
|
||||
return if g.VIEW is 'catalog' or !Conf['Anonymize']
|
||||
return if !Conf['Anonymize']
|
||||
|
||||
Post.callbacks.push
|
||||
name: 'Anonymize'
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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']
|
||||
|
||||
|
||||
@ -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)'
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
@ -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'
|
||||
|
||||
39
src/Quotelinks/QuoteMarkers.coffee
Normal file
39
src/Quotelinks/QuoteMarkers.coffee
Normal 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
|
||||
@ -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
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user