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.
|
- Added a `Reset Settings` button in the settings.
|
||||||
|
|
||||||
### 3.15.2 - *2014-01-22*
|
### 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>
|
1. Make sure both your **browser** and **4chan X** are up to date.<br>
|
||||||
Only **Chrome**, **Firefox** and **Opera** are supported.<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).
|
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.
|
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:
|
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.
|
1. Precise steps to reproduce the problem, with the expected and actual results.
|
||||||
2. [Console errors](https://github.com/MayhemYDG/4chan-x/wiki/FAQ#console-errors), if any.
|
2. [Console errors](https://github.com/MayhemYDG/4chan-x/wiki/FAQ#console-errors), if any.
|
||||||
3. 4chan X version, browser variant, browser version, and Greasemonkey version if you are using it.
|
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:
|
Respect these guidelines:
|
||||||
- Describe the issue clearly, put some effort into it. A one-liner isn't a good enough description.
|
- 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/Board.coffee'
|
||||||
'src/General/Thread.coffee'
|
'src/General/Thread.coffee'
|
||||||
|
'src/General/CatalogThread.coffee'
|
||||||
'src/General/Post.coffee'
|
'src/General/Post.coffee'
|
||||||
'src/General/Clone.coffee'
|
'src/General/Clone.coffee'
|
||||||
'src/General/DataBoard.coffee'
|
'src/General/DataBoard.coffee'
|
||||||
|
|||||||
@ -56,3 +56,6 @@
|
|||||||
:root.burichan .focused.entry {
|
:root.burichan .focused.entry {
|
||||||
background: rgba(255, 255, 255, .33);
|
background: rgba(255, 255, 255, .33);
|
||||||
}
|
}
|
||||||
|
:root.burichan .thumb > .menu-button > i {
|
||||||
|
background: #EEF2FF;
|
||||||
|
}
|
||||||
|
|||||||
@ -56,3 +56,6 @@
|
|||||||
:root.futaba .focused.entry {
|
:root.futaba .focused.entry {
|
||||||
background: rgba(255, 255, 255, .33);
|
background: rgba(255, 255, 255, .33);
|
||||||
}
|
}
|
||||||
|
:root.futaba .thumb > .menu-button > i {
|
||||||
|
background: #FFE;
|
||||||
|
}
|
||||||
|
|||||||
@ -56,3 +56,6 @@
|
|||||||
:root.photon .focused.entry {
|
:root.photon .focused.entry {
|
||||||
background: rgba(255, 255, 255, .33);
|
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 {
|
.tab-selected {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
.section-container {
|
#fourchanx-settings > section {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.section-container > section {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
.section-sauce ul,
|
.section-sauce ul,
|
||||||
@ -375,9 +367,15 @@ a[href="javascript:;"] {
|
|||||||
/* Index */
|
/* Index */
|
||||||
:root.index-loading .navLinks,
|
:root.index-loading .navLinks,
|
||||||
:root.index-loading .board,
|
: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;
|
display: none;
|
||||||
}
|
}
|
||||||
|
#nav-links {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
#index-search {
|
#index-search {
|
||||||
padding-right: 1.5em;
|
padding-right: 1.5em;
|
||||||
width: 100px;
|
width: 100px;
|
||||||
@ -389,7 +387,9 @@ a[href="javascript:;"] {
|
|||||||
}
|
}
|
||||||
#index-search-clear {
|
#index-search-clear {
|
||||||
color: gray;
|
color: gray;
|
||||||
margin-left: -1.25em;
|
position: relative;
|
||||||
|
left: -1.25em;
|
||||||
|
width: 0;
|
||||||
}
|
}
|
||||||
<% if (type === 'crx') { %>
|
<% if (type === 'crx') { %>
|
||||||
/* ``::-webkit-*'' selectors break selector lists on Firefox. */
|
/* ``::-webkit-*'' selectors break selector lists on Firefox. */
|
||||||
@ -401,6 +401,110 @@ a[href="javascript:;"] {
|
|||||||
.summary {
|
.summary {
|
||||||
text-decoration: none;
|
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 */
|
/* Announcement Hiding */
|
||||||
:root.hide-announcement #globalMessage,
|
:root.hide-announcement #globalMessage,
|
||||||
@ -428,7 +532,7 @@ a.hide-announcement {
|
|||||||
#updater > div:last-child {
|
#updater > div:last-child {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
#updater input[type=number] {
|
#updater input[type="number"] {
|
||||||
width: 4em;
|
width: 4em;
|
||||||
}
|
}
|
||||||
#updater:not(:hover) > div:not(.move) {
|
#updater:not(:hover) > div:not(.move) {
|
||||||
@ -597,6 +701,10 @@ a.hide-announcement {
|
|||||||
.filter-highlight > .reply {
|
.filter-highlight > .reply {
|
||||||
box-shadow: -5px 0 rgba(255, 0, 0, .5);
|
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 */
|
/* Thread & Reply Hiding */
|
||||||
.hide-thread-button,
|
.hide-thread-button,
|
||||||
@ -655,6 +763,7 @@ a.hide-announcement {
|
|||||||
}
|
}
|
||||||
.persona .field {
|
.persona .field {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
width: 0;
|
||||||
}
|
}
|
||||||
.persona .field:hover,
|
.persona .field:hover,
|
||||||
.persona .field:focus {
|
.persona .field:focus {
|
||||||
@ -679,28 +788,24 @@ a.hide-announcement {
|
|||||||
:root.gecko #dump-button {
|
:root.gecko #dump-button {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
#qr:not(.dump) #dump-list-container {
|
#qr:not(.dump) #dump-list,
|
||||||
|
#qr:not(.dump) #add-post {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
#dump-list-container {
|
#dump-list {
|
||||||
height: 100px;
|
counter-reset: qrpreviews;
|
||||||
|
width: 0;
|
||||||
|
min-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
position: relative;
|
position: relative;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
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 {
|
#dump-list:hover {
|
||||||
bottom: -12px;
|
padding-bottom: 12px;
|
||||||
|
margin-bottom: -12px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
@ -777,13 +882,11 @@ a.remove {
|
|||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
}
|
}
|
||||||
#add-post {
|
#add-post {
|
||||||
|
align-self: flex-end;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
height: 20px;
|
width: 1em;
|
||||||
width: 20px;
|
margin-top: -1em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
#qr textarea {
|
#qr textarea {
|
||||||
@ -819,23 +922,11 @@ a.remove {
|
|||||||
height: 57px;
|
height: 57px;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
}
|
}
|
||||||
#file-n-submit-container {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
#file-n-submit {
|
#file-n-submit {
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
#file-n-submit-container input[type='file'] {
|
#file-n-submit input {
|
||||||
/* Keep it to set an appropriate height to the container. */
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
#file-n-submit-container input {
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
#file-n-submit input[type='submit'] {
|
#file-n-submit input[type='submit'] {
|
||||||
@ -852,8 +943,9 @@ a.remove {
|
|||||||
#qr-no-file,
|
#qr-no-file,
|
||||||
#qr-filename,
|
#qr-filename,
|
||||||
#qr-filesize,
|
#qr-filesize,
|
||||||
|
#qr-filerm,
|
||||||
#qr-file-spoiler {
|
#qr-file-spoiler {
|
||||||
margin: 0 2px !important;
|
margin: 0 1px !important;
|
||||||
}
|
}
|
||||||
#qr-no-file {
|
#qr-no-file {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
@ -864,17 +956,19 @@ a.remove {
|
|||||||
-moz-appearance: none;
|
-moz-appearance: none;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
background: none;
|
background: none;
|
||||||
border: none !important;
|
border: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
width: 0;
|
||||||
|
padding: 0;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
#qr-filesize {
|
#qr-filesize {
|
||||||
font-size: .8em;
|
font-size: .8em;
|
||||||
}
|
}
|
||||||
#qr-filesize::before {
|
#qr-filesize::before {
|
||||||
content: " (";
|
content: "(";
|
||||||
}
|
}
|
||||||
#qr-filesize::after {
|
#qr-filesize::after {
|
||||||
content: ")";
|
content: ")";
|
||||||
@ -884,14 +978,6 @@ a.remove {
|
|||||||
.menu-button {
|
.menu-button {
|
||||||
position: relative;
|
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) {
|
@media screen and (resolution: 1dppx) {
|
||||||
.fa-bars {
|
.fa-bars {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@ -970,8 +1056,10 @@ a.remove {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* colored uid */
|
/* Other */
|
||||||
|
.linkified {
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
.posteruid.painted {
|
.posteruid.painted {
|
||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
border-radius: 1em;
|
border-radius: 1em;
|
||||||
|
|||||||
@ -56,3 +56,6 @@
|
|||||||
:root.tomorrow .focused.entry {
|
:root.tomorrow .focused.entry {
|
||||||
background: rgba(0, 0, 0, .33);
|
background: rgba(0, 0, 0, .33);
|
||||||
}
|
}
|
||||||
|
:root.tomorrow .thumb > .menu-button > i {
|
||||||
|
background: #1D1F21;
|
||||||
|
}
|
||||||
|
|||||||
@ -56,3 +56,6 @@
|
|||||||
:root.yotsuba-b .focused.entry {
|
:root.yotsuba-b .focused.entry {
|
||||||
background: rgba(255, 255, 255, .33);
|
background: rgba(255, 255, 255, .33);
|
||||||
}
|
}
|
||||||
|
:root.yotsuba-b .thumb > .menu-button > i {
|
||||||
|
background: #EEF2FF;
|
||||||
|
}
|
||||||
|
|||||||
@ -56,3 +56,6 @@
|
|||||||
:root.yotsuba .focused.entry {
|
:root.yotsuba .focused.entry {
|
||||||
background: rgba(255, 255, 255, .33);
|
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">
|
<input type="search" id="index-search" class="field" placeholder="Search">
|
||||||
<a id="index-search-clear" class="fa fa-times-circle" href="javascript:;"></a>
|
<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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="pages cataloglink">
|
<div class="pages cataloglink">
|
||||||
<a href="./catalog">Catalog</a>
|
<a href="./" data-index-mode="catalog">Catalog</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,13 +3,14 @@
|
|||||||
<div><input name="boardnav" class="field" spellcheck="false"></div>
|
<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>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>Board link: <code>board</code></div>
|
||||||
|
<div>Archive link: <code>board-archive</code></div>
|
||||||
<div>Title link: <code>board-title</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>Board link (Replace with title when on that board): <code>board-replace</code></div>
|
||||||
<div>Full text link: <code>board-full</code></div>
|
<div>Full text link: <code>board-full</code></div>
|
||||||
<div>Custom text link: <code>board-text:"VIP Board"</code></div>
|
<div>Custom text link: <code>board-text:"VIP Board"</code></div>
|
||||||
<div>Index-only link: <code>board-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>Catalog-only link: <code>board-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-index-text:"VIP Index"</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>
|
<div>Full board list toggle: <code>toggle-all</code></div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
@ -32,7 +33,7 @@
|
|||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>File Info Formatting <span class="warning" #{if Conf['File Info Formatting'] then 'hidden' else ''}>is disabled.</span></legend>
|
<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>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>Original file name: <code>%n</code> (truncated), <code>%N</code> (untruncated), <code>%t</code> (Unix timestamp)</div>
|
||||||
<div>Spoiler indicator: <code>%p</code></div>
|
<div>Spoiler indicator: <code>%p</code></div>
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
<li><code>%TURL</code>: Thumbnail URL.</li>
|
<li><code>%TURL</code>: Thumbnail URL.</li>
|
||||||
<li><code>%URL</code>: Full image URL.</li>
|
<li><code>%URL</code>: Full image URL.</li>
|
||||||
<li><code>%MD5</code>: MD5 hash.</li>
|
<li><code>%MD5</code>: MD5 hash.</li>
|
||||||
|
<li><code>%name</code>: Original file name.</li>
|
||||||
<li><code>%board</code>: Current board.</li>
|
<li><code>%board</code>: Current board.</li>
|
||||||
</ul>
|
</ul>
|
||||||
<textarea name="sauces" class="field" spellcheck="false"></textarea>
|
<textarea name="sauces" class="field" spellcheck="false"></textarea>
|
||||||
|
|||||||
@ -12,7 +12,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<hr>
|
<hr>
|
||||||
<div class="section-container">
|
<section></section>
|
||||||
<section></section>
|
|
||||||
</div>
|
|
||||||
</div>
|
</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>
|
<form>
|
||||||
<div class="persona">
|
<div class="persona">
|
||||||
<input type="button" id="dump-button" title="Dump list" value="+">
|
<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="name" name="name" list="list-name" placeholder="Name" class="field">
|
||||||
<input data-name="email" list="list-email" placeholder="E-mail" class="field" size="1">
|
<input data-name="email" name="email" list="list-email" placeholder="E-mail" class="field">
|
||||||
<input data-name="sub" list="list-sub" placeholder="Subject" class="field" size="1">
|
<input data-name="sub" name="sub" list="list-sub" placeholder="Subject" class="field">
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div id="dump-list"></div>
|
||||||
|
<a href="javascript:;" id="add-post" class="fa fa-plus" title="Add a post"></a>
|
||||||
<div class="textarea">
|
<div class="textarea">
|
||||||
<textarea data-name="com" placeholder="Comment" class="field"></textarea>
|
<textarea data-name="com" placeholder="Comment" class="field"></textarea>
|
||||||
<span id="char-count"></span>
|
<span id="char-count"></span>
|
||||||
</div>
|
</div>
|
||||||
<div id="file-n-submit-container">
|
<div id="file-n-submit">
|
||||||
<input type="file" multiple>
|
<input type="file" hidden multiple>
|
||||||
<div id="file-n-submit">
|
<input type="submit">
|
||||||
<input type="submit">
|
<input type="button" id="qr-file-button" value="Choose files">
|
||||||
<input type="button" id="qr-file-button" value="Choose files">
|
<span id="qr-no-file">No selected file</span>
|
||||||
<span id="qr-no-file">No selected file</span>
|
<input id="qr-filename" data-name="filename" spellcheck="false">
|
||||||
<input id="qr-filename" data-name="filename" spellcheck="false">
|
<span id="qr-filesize"></span>
|
||||||
<span id="qr-filesize"></span>
|
<a href="javascript:;" id="qr-filerm" class="fa fa-times-circle" title="Remove file"></a>
|
||||||
<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">
|
||||||
<input type="checkbox" id="qr-file-spoiler" title="Spoiler image">
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<datalist id="list-name"></datalist>
|
<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,
|
"http": true,
|
||||||
"https": true,
|
"https": true,
|
||||||
"software": "foolfuuka",
|
"software": "foolfuuka",
|
||||||
"boards": ["a", "co", "gd", "jp", "m", "sp", "tg", "tv", "v", "vg", "vp", "vr", "wsg"],
|
"boards": ["a", "biz", "co", "gd", "jp", "m", "sp", "tg", "tv", "v", "vg", "vp", "vr", "wsg"],
|
||||||
"files": ["a", "gd", "jp", "m", "tg", "vg", "vp", "vr", "wsg"]
|
"files": ["a", "biz", "gd", "jp", "m", "tg", "vg", "vp", "vr", "wsg"]
|
||||||
}, {
|
}, {
|
||||||
"uid": 1,
|
"uid": 1,
|
||||||
"name": "NSFW Foolz",
|
"name": "NSFW Foolz",
|
||||||
@ -86,8 +86,8 @@
|
|||||||
"http": false,
|
"http": false,
|
||||||
"https": true,
|
"https": true,
|
||||||
"software": "fuuka",
|
"software": "fuuka",
|
||||||
"boards": ["3", "cgl", "ck", "fa", "ic", "jp", "lit", "tg", "vr"],
|
"boards": ["3", "biz", "cgl", "ck", "fa", "ic", "jp", "lit", "tg", "vr"],
|
||||||
"files": ["3", "cgl", "ck", "fa", "ic", "jp", "lit", "tg", "vr"]
|
"files": ["3", "biz", "cgl", "ck", "fa", "ic", "jp", "lit", "tg", "vr"]
|
||||||
}, {
|
}, {
|
||||||
"uid": 15,
|
"uid": 15,
|
||||||
"name": "fgts",
|
"name": "fgts",
|
||||||
@ -95,8 +95,8 @@
|
|||||||
"http": true,
|
"http": true,
|
||||||
"https": true,
|
"https": true,
|
||||||
"software": "foolfuuka",
|
"software": "foolfuuka",
|
||||||
"boards": ["soc"],
|
"boards": ["r", "soc"],
|
||||||
"files": ["soc"]
|
"files": ["r", "soc"]
|
||||||
}, {
|
}, {
|
||||||
"uid": 16,
|
"uid": 16,
|
||||||
"name": "maware",
|
"name": "maware",
|
||||||
@ -123,6 +123,6 @@
|
|||||||
"https": true,
|
"https": true,
|
||||||
"withCredentials": true,
|
"withCredentials": true,
|
||||||
"software": "foolfuuka",
|
"software": "foolfuuka",
|
||||||
"boards": ["a", "co", "gd", "jp", "m", "s4s", "sp", "tg", "tv", "u", "v", "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", "gd", "jp", "m", "s4s", "tg", "u", "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...
|
el.classList.remove className...
|
||||||
$.hasClass = (el, className) ->
|
$.hasClass = (el, className) ->
|
||||||
el.classList.contains className
|
el.classList.contains className
|
||||||
$.rm = do ->
|
$.rm = (el) ->
|
||||||
if 'remove' of Element.prototype
|
el.remove()
|
||||||
(el) -> el.remove()
|
|
||||||
else
|
|
||||||
(el) -> el.parentNode?.removeChild el
|
|
||||||
$.rmAll = (root) ->
|
$.rmAll = (root) ->
|
||||||
# jsperf.com/emptify-element
|
# https://gist.github.com/MayhemYDG/8646194
|
||||||
for node in [root.childNodes...]
|
root.textContent = null
|
||||||
# HTMLSelectElement.remove !== Element.remove
|
|
||||||
root.removeChild node
|
|
||||||
return
|
|
||||||
$.tn = (s) ->
|
$.tn = (s) ->
|
||||||
d.createTextNode s
|
d.createTextNode s
|
||||||
$.nodes = (nodes) ->
|
$.nodes = (nodes) ->
|
||||||
@ -234,81 +228,91 @@ $.localKeys = [
|
|||||||
'usercss'
|
'usercss'
|
||||||
]
|
]
|
||||||
# https://developer.chrome.com/extensions/storage.html
|
# https://developer.chrome.com/extensions/storage.html
|
||||||
$.delete = (keys) ->
|
do ->
|
||||||
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 ->
|
|
||||||
items =
|
items =
|
||||||
sync: {}
|
|
||||||
local: {}
|
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) ->
|
setArea = (area) ->
|
||||||
data = items[area]
|
data = items[area]
|
||||||
return if !Object.keys(data).length or timeout[area]
|
return if !Object.keys(data).length or timeout[area] > Date.now()
|
||||||
items[area] = {}
|
|
||||||
chrome.storage[area].set data, ->
|
chrome.storage[area].set data, ->
|
||||||
if chrome.runtime.lastError
|
if chrome.runtime.lastError
|
||||||
c.error chrome.runtime.lastError.message
|
c.error chrome.runtime.lastError.message
|
||||||
for key, val of data when key not of items[area]
|
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
|
items[area][key] = val
|
||||||
timeout[area] = setTimeout setArea, $.MINUTE, area
|
setTimeout setArea, $.MINUTE, area
|
||||||
|
timeout[area] = Date.now() + $.MINUTE
|
||||||
return
|
return
|
||||||
delete timeout[area]
|
delete timeout[area]
|
||||||
|
items[area] = {}
|
||||||
|
|
||||||
setAll = $.debounce $.SECOND, ->
|
setSync = $.debounce $.SECOND, ->
|
||||||
for key in $.localKeys
|
setArea 'sync'
|
||||||
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
|
|
||||||
|
|
||||||
(key, val) ->
|
$.set = (key, val) ->
|
||||||
if typeof key is 'string'
|
if typeof key is 'string'
|
||||||
items.sync[key] = val
|
items.sync[key] = val
|
||||||
else
|
else
|
||||||
$.extend items.sync, key
|
$.extend items.sync, key
|
||||||
setAll()
|
for key in $.localKeys when key of items.sync
|
||||||
$.clear = (cb) ->
|
items.local[key] = items.sync[key]
|
||||||
count = 2
|
delete items.sync[key]
|
||||||
done = ->
|
setArea 'local'
|
||||||
if chrome.runtime.lastError
|
setSync()
|
||||||
c.error chrome.runtime.lastError.message
|
|
||||||
return
|
$.clear = (cb) ->
|
||||||
cb?() unless --count
|
items.local = {}
|
||||||
chrome.storage.local.clear done
|
items.sync = {}
|
||||||
chrome.storage.sync.clear done
|
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 { %>
|
<% } else { %>
|
||||||
# http://wiki.greasespot.net/Main_Page
|
# http://wiki.greasespot.net/Main_Page
|
||||||
$.sync = do ->
|
$.sync = do ->
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "4chan-X",
|
"name": "4chan-X",
|
||||||
"version": "3.15.2",
|
"version": "3.18.0",
|
||||||
"description": "Cross-browser extension for productive lurking on 4chan.",
|
"description": "Cross-browser extension for productive lurking on 4chan.",
|
||||||
"meta": {
|
"meta": {
|
||||||
"name": "4chan X",
|
"name": "4chan X",
|
||||||
@ -26,13 +26,13 @@
|
|||||||
"grunt-bump": "~0.0.13",
|
"grunt-bump": "~0.0.13",
|
||||||
"grunt-concurrent": "~0.4.3",
|
"grunt-concurrent": "~0.4.3",
|
||||||
"grunt-contrib-clean": "~0.5.0",
|
"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-compress": "~0.6.0",
|
||||||
"grunt-contrib-concat": "~0.3.0",
|
"grunt-contrib-concat": "~0.3.0",
|
||||||
"grunt-contrib-copy": "~0.5.0",
|
"grunt-contrib-copy": "~0.5.0",
|
||||||
"grunt-contrib-watch": "~0.5.3",
|
"grunt-contrib-watch": "~0.5.3",
|
||||||
"grunt-shell": "~0.6.4",
|
"grunt-shell": "~0.6.4",
|
||||||
"load-grunt-tasks": "~0.2.1"
|
"load-grunt-tasks": "~0.3.0"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@ -45,7 +45,7 @@ Redirect =
|
|||||||
cb?()
|
cb?()
|
||||||
|
|
||||||
to: (dest, data) ->
|
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
|
return '' unless archive
|
||||||
Redirect[dest] archive, data
|
Redirect[dest] archive, data
|
||||||
|
|
||||||
@ -80,6 +80,9 @@ Redirect =
|
|||||||
file: (archive, {boardID, filename}) ->
|
file: (archive, {boardID, filename}) ->
|
||||||
"#{Redirect.protocol archive}#{archive.domain}/#{boardID}/full_image/#{filename}"
|
"#{Redirect.protocol archive}#{archive.domain}/#{boardID}/full_image/#{filename}"
|
||||||
|
|
||||||
|
board: (archive, {boardID}) ->
|
||||||
|
"#{Redirect.protocol archive}#{archive.domain}/#{boardID}/"
|
||||||
|
|
||||||
search: (archive, {boardID, type, value}) ->
|
search: (archive, {boardID, type, value}) ->
|
||||||
type = if type is 'name'
|
type = if type is 'name'
|
||||||
'username'
|
'username'
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
Filter =
|
Filter =
|
||||||
filters: {}
|
filters: {}
|
||||||
init: ->
|
init: ->
|
||||||
return if g.VIEW is 'catalog' or !Conf['Filter']
|
return if !Conf['Filter']
|
||||||
|
|
||||||
for key of Config.filter
|
for key of Config.filter
|
||||||
@filters[key] = []
|
@filters[key] = []
|
||||||
@ -110,6 +110,8 @@ Filter =
|
|||||||
|
|
||||||
# Highlight
|
# Highlight
|
||||||
$.addClass @nodes.root, result.class
|
$.addClass @nodes.root, result.class
|
||||||
|
unless @highlights and result.class in @highlights
|
||||||
|
(@highlights or= []).push result.class
|
||||||
if !@isReply and result.top
|
if !@isReply and result.top
|
||||||
@thread.isOnTop = true
|
@thread.isOnTop = true
|
||||||
|
|
||||||
@ -164,7 +166,7 @@ Filter =
|
|||||||
|
|
||||||
menu:
|
menu:
|
||||||
init: ->
|
init: ->
|
||||||
return if g.VIEW is 'catalog' or !Conf['Menu'] or !Conf['Filter']
|
return if !Conf['Menu'] or !Conf['Filter']
|
||||||
|
|
||||||
div = $.el 'div',
|
div = $.el 'div',
|
||||||
textContent: 'Filter'
|
textContent: 'Filter'
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
PostHiding =
|
PostHiding =
|
||||||
init: ->
|
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'
|
@db = new DataBoard 'hiddenPosts'
|
||||||
Post.callbacks.push
|
Post.callbacks.push
|
||||||
@ -20,7 +20,7 @@ PostHiding =
|
|||||||
|
|
||||||
menu:
|
menu:
|
||||||
init: ->
|
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
|
# Hide
|
||||||
div = $.el 'div',
|
div = $.el 'div',
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
Recursive =
|
Recursive =
|
||||||
recursives: {}
|
recursives: {}
|
||||||
init: ->
|
init: ->
|
||||||
return if g.VIEW is 'catalog'
|
|
||||||
|
|
||||||
Post.callbacks.push
|
Post.callbacks.push
|
||||||
name: 'Recursive'
|
name: 'Recursive'
|
||||||
cb: @node
|
cb: @node
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
ThreadHiding =
|
ThreadHiding =
|
||||||
init: ->
|
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'
|
@db = new DataBoard 'hiddenThreads'
|
||||||
@syncCatalog()
|
$.on d, 'IndexRefresh', @onIndexRefresh
|
||||||
$.on d, 'IndexBuild', @onIndexBuild
|
|
||||||
Thread.callbacks.push
|
Thread.callbacks.push
|
||||||
name: 'Thread Hiding'
|
name: 'Thread Hiding'
|
||||||
cb: @node
|
cb: @node
|
||||||
@ -15,61 +14,17 @@ ThreadHiding =
|
|||||||
return unless Conf['Thread Hiding']
|
return unless Conf['Thread Hiding']
|
||||||
$.prepend @OP.nodes.root, ThreadHiding.makeButton @, 'hide'
|
$.prepend @OP.nodes.root, ThreadHiding.makeButton @, 'hide'
|
||||||
|
|
||||||
onIndexBuild: ({detail: nodes}) ->
|
onIndexRefresh: ->
|
||||||
for root, i in nodes by 2
|
for root, i in Index.nodes by 2
|
||||||
thread = Get.threadFromRoot root
|
thread = Get.threadFromRoot root
|
||||||
continue unless thread.isHidden
|
continue unless thread.isHidden
|
||||||
unless thread.stub
|
unless thread.stub
|
||||||
nodes[i + 1].hidden = true
|
Index.nodes[i + 1].hidden = true
|
||||||
else unless root.contains thread.stub
|
else unless root.contains thread.stub
|
||||||
# When we come back to a page, the stub is already there.
|
# When we come back to a page, the stub is already there.
|
||||||
ThreadHiding.makeStub thread, root
|
ThreadHiding.makeStub thread, root
|
||||||
return
|
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:
|
menu:
|
||||||
init: ->
|
init: ->
|
||||||
return if g.VIEW isnt 'index' or !Conf['Menu'] or !Conf['Thread Hiding Link']
|
return if g.VIEW isnt 'index' or !Conf['Menu'] or !Conf['Thread Hiding Link']
|
||||||
@ -91,7 +46,7 @@ ThreadHiding =
|
|||||||
el: div
|
el: div
|
||||||
order: 20
|
order: 20
|
||||||
open: ({thread, isReply}) ->
|
open: ({thread, isReply}) ->
|
||||||
if isReply or thread.isHidden
|
if isReply or thread.isHidden or Conf['Index Mode'] is 'catalog'
|
||||||
return false
|
return false
|
||||||
ThreadHiding.menu.thread = thread
|
ThreadHiding.menu.thread = thread
|
||||||
true
|
true
|
||||||
@ -130,19 +85,15 @@ ThreadHiding =
|
|||||||
$.prepend root, thread.stub
|
$.prepend root, thread.stub
|
||||||
|
|
||||||
saveHiddenState: (thread, makeStub) ->
|
saveHiddenState: (thread, makeStub) ->
|
||||||
hiddenThreadsOnCatalog = JSON.parse(localStorage.getItem "4chan-hide-t-#{g.BOARD}") or {}
|
|
||||||
if thread.isHidden
|
if thread.isHidden
|
||||||
ThreadHiding.db.set
|
ThreadHiding.db.set
|
||||||
boardID: thread.board.ID
|
boardID: thread.board.ID
|
||||||
threadID: thread.ID
|
threadID: thread.ID
|
||||||
val: {makeStub}
|
val: {makeStub}
|
||||||
hiddenThreadsOnCatalog[thread] = true
|
|
||||||
else
|
else
|
||||||
ThreadHiding.db.delete
|
ThreadHiding.db.delete
|
||||||
boardID: thread.board.ID
|
boardID: thread.board.ID
|
||||||
threadID: thread.ID
|
threadID: thread.ID
|
||||||
delete hiddenThreadsOnCatalog[thread]
|
|
||||||
localStorage.setItem "4chan-hide-t-#{g.BOARD}", JSON.stringify hiddenThreadsOnCatalog
|
|
||||||
|
|
||||||
toggle: (thread) ->
|
toggle: (thread) ->
|
||||||
unless thread instanceof Thread
|
unless thread instanceof Thread
|
||||||
@ -157,6 +108,7 @@ ThreadHiding =
|
|||||||
return if thread.isHidden
|
return if thread.isHidden
|
||||||
threadRoot = thread.OP.nodes.root.parentNode
|
threadRoot = thread.OP.nodes.root.parentNode
|
||||||
thread.isHidden = true
|
thread.isHidden = true
|
||||||
|
Index.updateHideLabel()
|
||||||
|
|
||||||
unless makeStub
|
unless makeStub
|
||||||
threadRoot.hidden = threadRoot.nextElementSibling.hidden = true # <hr>
|
threadRoot.hidden = threadRoot.nextElementSibling.hidden = true # <hr>
|
||||||
@ -171,3 +123,4 @@ ThreadHiding =
|
|||||||
threadRoot = thread.OP.nodes.root.parentNode
|
threadRoot = thread.OP.nodes.root.parentNode
|
||||||
threadRoot.nextElementSibling.hidden =
|
threadRoot.nextElementSibling.hidden =
|
||||||
threadRoot.hidden = thread.isHidden = false
|
threadRoot.hidden = thread.isHidden = false
|
||||||
|
Index.updateHideLabel()
|
||||||
|
|||||||
@ -110,18 +110,18 @@ Build =
|
|||||||
flag = unless flagCode
|
flag = unless flagCode
|
||||||
''
|
''
|
||||||
else if boardID is 'pol'
|
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
|
else
|
||||||
" <span title='#{flagName}' class='flag flag-#{flagCode.toLowerCase()}'></span>"
|
" <span title='#{flagName}' class='flag flag-#{flagCode.toLowerCase()}'></span>"
|
||||||
|
|
||||||
if file?.isDeleted
|
if file?.isDeleted
|
||||||
fileHTML = if isOP
|
fileHTML = if isOP
|
||||||
"<div class=file id=f#{postID}><span class=fileThumb>" +
|
"<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>"
|
"</span></div>"
|
||||||
else
|
else
|
||||||
"<div class=file id=f#{postID}><span class=fileThumb>" +
|
"<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>"
|
"</span></div>"
|
||||||
else if file
|
else if file
|
||||||
fileSize = $.bytesToString file.size
|
fileSize = $.bytesToString file.size
|
||||||
@ -167,16 +167,16 @@ Build =
|
|||||||
fileHTML = ''
|
fileHTML = ''
|
||||||
|
|
||||||
sticky = if isSticky
|
sticky = if isSticky
|
||||||
" <img src=#{staticPath}sticky#{gifIcon} alt=Sticky title=Sticky class=stickyIcon>"
|
" <img src=#{staticPath}sticky#{gifIcon} title=Sticky class=stickyIcon>"
|
||||||
else
|
else
|
||||||
''
|
''
|
||||||
closed = if isClosed
|
closed = if isClosed
|
||||||
" <img src=#{staticPath}closed#{gifIcon} alt=Closed title=Closed class=closedIcon>"
|
" <img src=#{staticPath}closed#{gifIcon} title=Closed class=closedIcon>"
|
||||||
else
|
else
|
||||||
''
|
''
|
||||||
|
|
||||||
if isOP and g.VIEW is 'index'
|
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>"
|
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>"
|
replyLink = " <span>[<a href='/#{boardID}/res/#{threadID}' class=replylink>Reply</a>]</span>"
|
||||||
else
|
else
|
||||||
@ -254,9 +254,74 @@ Build =
|
|||||||
[posts, files] = if Conf['Show Replies']
|
[posts, files] = if Conf['Show Replies']
|
||||||
[data.omitted_posts, data.omitted_images]
|
[data.omitted_posts, data.omitted_images]
|
||||||
else
|
else
|
||||||
# XXX data.images is not accurate.
|
[data.replies, data.images]
|
||||||
[data.replies, data.omitted_images + data.last_replies.filter((data) -> !!data.ext).length]
|
|
||||||
nodes.push Build.summary board.ID, data.no, posts, files
|
nodes.push Build.summary board.ID, data.no, posts, files
|
||||||
|
|
||||||
$.add root, nodes
|
$.add root, nodes
|
||||||
root
|
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.']
|
'Announcement Hiding': [true, 'Add button to hide 4chan announcements.']
|
||||||
'404 Redirect': [true, 'Redirect dead threads and images.']
|
'404 Redirect': [true, 'Redirect dead threads and images.']
|
||||||
'Keybinds': [true, 'Bind actions to keyboard shortcuts.']
|
'Keybinds': [true, 'Bind actions to keyboard shortcuts.']
|
||||||
|
'Linkify': [true, 'Convert text links into hyperlinks.']
|
||||||
'Time Formatting': [true, 'Localize and format timestamps.']
|
'Time Formatting': [true, 'Localize and format timestamps.']
|
||||||
'Relative Post Dates': [false, 'Display dates like "3 minutes ago". Tooltip shows the timestamp.']
|
'Relative Post Dates': [false, 'Display dates like "3 minutes ago". Tooltip shows the timestamp.']
|
||||||
'File Info Formatting': [true, 'Reformat the file information.']
|
'File Info Formatting': [true, 'Reformat the file information.']
|
||||||
@ -24,11 +25,9 @@ Config =
|
|||||||
'Auto-GIF': [false, 'Animate GIF thumbnails (disabled on /gif/, /wsg/).']
|
'Auto-GIF': [false, 'Animate GIF thumbnails (disabled on /gif/, /wsg/).']
|
||||||
'Image Expansion': [true, 'Expand images inline.']
|
'Image Expansion': [true, 'Expand images inline.']
|
||||||
'Image Hover': [false, 'Show a floating expanded image on hover.']
|
'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.']
|
'Sauce': [true, 'Add sauce links to images.']
|
||||||
'Reveal Spoilers': [false, 'Reveal spoiler thumbnails.']
|
'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':
|
||||||
'Menu': [true, 'Add a drop-down menu to posts.']
|
'Menu': [true, 'Add a drop-down menu to posts.']
|
||||||
'Report Link': [true, 'Add a report link to the menu.']
|
'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.']
|
'Remember Spoiler': [false, 'Remember the spoiler state, instead of resetting after posting.']
|
||||||
'Hide Original Post Form': [true, 'Hide the normal post form.']
|
'Hide Original Post Form': [true, 'Hide the normal post form.']
|
||||||
'Cooldown': [true, 'Indicate the remaining time before posting again.']
|
'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') { %>
|
<% if (type === 'crx') { %>
|
||||||
'Tab to Choose Files First': [false, 'Tab to the file input before the submit button.']
|
'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 Previewing': [true, 'Show quoted post on hover.']
|
||||||
'Quote Highlighting': [true, 'Highlight the previewed post.']
|
'Quote Highlighting': [true, 'Highlight the previewed post.']
|
||||||
'Resurrect Quotes': [true, 'Link dead quotes to the archives.']
|
'Resurrect Quotes': [true, 'Link dead quotes to the archives.']
|
||||||
'Mark Quotes of You': [true, 'Add \'(You)\' to quotes linking to your posts.']
|
'Quote Markers': [true, 'Add "(You)", "(OP)", "(Cross-thread)", "(Dead)" markers to quote links.']
|
||||||
'Mark OP Quotes': [true, 'Add \'(OP)\' to OP quotes.']
|
|
||||||
'Mark Cross-thread Quotes': [true, 'Add \'(Cross-thread)\' to cross-threads quotes.']
|
|
||||||
imageExpansion:
|
imageExpansion:
|
||||||
'Fit width': [true, '']
|
'Fit width': [true, '']
|
||||||
'Fit height': [false, '']
|
'Fit height': [false, '']
|
||||||
@ -141,7 +137,11 @@ Config =
|
|||||||
'Custom CSS': false
|
'Custom CSS': false
|
||||||
Index:
|
Index:
|
||||||
'Index Mode': 'paged'
|
'Index Mode': 'paged'
|
||||||
|
'Previous Index Mode': 'paged'
|
||||||
'Index Sort': 'bump'
|
'Index Sort': 'bump'
|
||||||
|
'Index Size': 'small'
|
||||||
|
'Threads per Page': 0
|
||||||
|
'Open threads in a new tab': false
|
||||||
'Show Replies': true
|
'Show Replies': true
|
||||||
'Anchor Hidden Threads': true
|
'Anchor Hidden Threads': true
|
||||||
'Refreshed Navigation': false
|
'Refreshed Navigation': false
|
||||||
@ -149,7 +149,6 @@ Config =
|
|||||||
'Header auto-hide': false
|
'Header auto-hide': false
|
||||||
'Header auto-hide on scroll': false
|
'Header auto-hide on scroll': false
|
||||||
'Bottom header': false
|
'Bottom header': false
|
||||||
'Header catalog links': false
|
|
||||||
'Top Board List': false
|
'Top Board List': false
|
||||||
'Bottom Board List': false
|
'Bottom Board List': false
|
||||||
'Custom Board Navigation': true
|
'Custom Board Navigation': true
|
||||||
@ -188,6 +187,10 @@ Config =
|
|||||||
'Next page': ['Right', 'Jump to the next page.']
|
'Next page': ['Right', 'Jump to the next page.']
|
||||||
'Previous page': ['Left', 'Jump to the previous page.']
|
'Previous page': ['Left', 'Jump to the previous page.']
|
||||||
'Search form': ['Ctrl+Alt+s', 'Focus the search field on the board index.']
|
'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
|
# Thread Navigation
|
||||||
'Next thread': ['Down', 'See next thread.']
|
'Next thread': ['Down', 'See next thread.']
|
||||||
'Previous thread': ['Up', 'See previous thread.']
|
'Previous thread': ['Up', 'See previous thread.']
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
class DataBoard
|
class DataBoard
|
||||||
@keys = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads']
|
@keys = ['pinnedThreads', 'hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads']
|
||||||
|
|
||||||
constructor: (@key, sync, dontClean) ->
|
constructor: (@key, sync, dontClean) ->
|
||||||
@data = Conf[key]
|
@data = Conf[key]
|
||||||
@ -58,30 +58,31 @@ class DataBoard
|
|||||||
val or defaultValue
|
val or defaultValue
|
||||||
|
|
||||||
clean: ->
|
clean: ->
|
||||||
for boardID, val of @data.boards
|
|
||||||
@deleteIfEmpty {boardID}
|
|
||||||
|
|
||||||
now = Date.now()
|
now = Date.now()
|
||||||
if (@data.lastChecked or 0) < now - 2 * $.HOUR
|
return if (@data.lastChecked or 0) > now - 2 * $.HOUR
|
||||||
@data.lastChecked = now
|
|
||||||
for boardID of @data.boards
|
|
||||||
@ajaxClean boardID
|
|
||||||
|
|
||||||
|
for boardID of @data.boards
|
||||||
|
@deleteIfEmpty {boardID}
|
||||||
|
@ajaxClean boardID if boardID of @data.boards
|
||||||
|
|
||||||
|
@data.lastChecked = now
|
||||||
@save()
|
@save()
|
||||||
ajaxClean: (boardID) ->
|
ajaxClean: (boardID) ->
|
||||||
$.cache "//a.4cdn.org/#{boardID}/threads.json", (e) =>
|
$.cache "//a.4cdn.org/#{boardID}/threads.json", (e) =>
|
||||||
if e.target.status isnt 200
|
if e.target.status isnt 200
|
||||||
@delete boardID if e.target.status is 404
|
@delete {boardID} if e.target.status is 404
|
||||||
return
|
return
|
||||||
board = @data.boards[boardID]
|
board = @data.boards[boardID]
|
||||||
threads = {}
|
threads = {}
|
||||||
for page in e.target.response
|
for page in e.target.response
|
||||||
for thread in page.threads
|
for thread in page.threads when thread.no of board
|
||||||
if thread.no of board
|
threads[thread.no] = board[thread.no]
|
||||||
threads[thread.no] = board[thread.no]
|
count = Object.keys(threads).length
|
||||||
@data.boards[boardID] = threads
|
return if count is Object.keys(board).length # Nothing changed.
|
||||||
@deleteIfEmpty {boardID}
|
if count
|
||||||
@save()
|
@set {boardID, val: threads}
|
||||||
|
else
|
||||||
|
@delete {boardID}
|
||||||
|
|
||||||
onSync: (data) =>
|
onSync: (data) =>
|
||||||
@data = data or boards: {}
|
@data = data or boards: {}
|
||||||
|
|||||||
@ -188,7 +188,7 @@ Get =
|
|||||||
|
|
||||||
comment = bq.innerHTML
|
comment = bq.innerHTML
|
||||||
# greentext
|
# greentext
|
||||||
.replace(/(^|>)(>[^<$]*)(<|$)/g, '$1<span class=quote>$2</span>$3')
|
.replace /(^|>)(>[^<$]*)(<|$)/g, '$1<span class=quote>$2</span>$3'
|
||||||
# quotes
|
# quotes
|
||||||
.replace /((>){2}(>\/[a-z\d]+\/)?\d+)/g, '<span class=deadlink>$1</span>'
|
.replace /((>){2}(>\/[a-z\d]+\/)?\d+)/g, '<span class=deadlink>$1</span>'
|
||||||
|
|
||||||
@ -233,5 +233,6 @@ Get =
|
|||||||
thread = g.threads["#{boardID}.#{threadID}"] or
|
thread = g.threads["#{boardID}.#{threadID}"] or
|
||||||
new Thread threadID, board
|
new Thread threadID, board
|
||||||
post = new Post Build.post(o, true), thread, board, {isArchived: true}
|
post = new Post Build.post(o, true), thread, board, {isArchived: true}
|
||||||
|
$('.page-num', post.nodes.info).hidden = true
|
||||||
Main.callbackNodes Post, [post]
|
Main.callbackNodes Post, [post]
|
||||||
Get.insert post, root, context
|
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'
|
innerHTML: '<input type=checkbox name="Header auto-hide on scroll"> Auto-hide header on scroll'
|
||||||
barPositionToggler = $.el 'label',
|
barPositionToggler = $.el 'label',
|
||||||
innerHTML: '<input type=checkbox name="Bottom header"> Bottom header'
|
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',
|
topBoardToggler = $.el 'label',
|
||||||
innerHTML: '<input type=checkbox name="Top Board List"> Top original board list'
|
innerHTML: '<input type=checkbox name="Top Board List"> Top original board list'
|
||||||
botBoardToggler = $.el 'label',
|
botBoardToggler = $.el 'label',
|
||||||
@ -40,7 +38,6 @@ Header =
|
|||||||
@headerToggler = headerToggler.firstElementChild
|
@headerToggler = headerToggler.firstElementChild
|
||||||
@scrollHeaderToggler = scrollHeaderToggler.firstElementChild
|
@scrollHeaderToggler = scrollHeaderToggler.firstElementChild
|
||||||
@barPositionToggler = barPositionToggler.firstElementChild
|
@barPositionToggler = barPositionToggler.firstElementChild
|
||||||
@catalogToggler = catalogToggler.firstElementChild
|
|
||||||
@topBoardToggler = topBoardToggler.firstElementChild
|
@topBoardToggler = topBoardToggler.firstElementChild
|
||||||
@botBoardToggler = botBoardToggler.firstElementChild
|
@botBoardToggler = botBoardToggler.firstElementChild
|
||||||
@customNavToggler = customNavToggler.firstElementChild
|
@customNavToggler = customNavToggler.firstElementChild
|
||||||
@ -48,7 +45,6 @@ Header =
|
|||||||
$.on @headerToggler, 'change', @toggleBarVisibility
|
$.on @headerToggler, 'change', @toggleBarVisibility
|
||||||
$.on @scrollHeaderToggler, 'change', @toggleHideBarOnScroll
|
$.on @scrollHeaderToggler, 'change', @toggleHideBarOnScroll
|
||||||
$.on @barPositionToggler, 'change', @toggleBarPosition
|
$.on @barPositionToggler, 'change', @toggleBarPosition
|
||||||
$.on @catalogToggler, 'change', @toggleCatalogLinks
|
|
||||||
$.on @topBoardToggler, 'change', @toggleOriginalBoardList
|
$.on @topBoardToggler, 'change', @toggleOriginalBoardList
|
||||||
$.on @botBoardToggler, 'change', @toggleOriginalBoardList
|
$.on @botBoardToggler, 'change', @toggleOriginalBoardList
|
||||||
$.on @customNavToggler, 'change', @toggleCustomNav
|
$.on @customNavToggler, 'change', @toggleCustomNav
|
||||||
@ -74,7 +70,6 @@ Header =
|
|||||||
{el: headerToggler}
|
{el: headerToggler}
|
||||||
{el: scrollHeaderToggler}
|
{el: scrollHeaderToggler}
|
||||||
{el: barPositionToggler}
|
{el: barPositionToggler}
|
||||||
{el: catalogToggler}
|
|
||||||
{el: topBoardToggler}
|
{el: topBoardToggler}
|
||||||
{el: botBoardToggler}
|
{el: botBoardToggler}
|
||||||
{el: customNavToggler}
|
{el: customNavToggler}
|
||||||
@ -92,9 +87,6 @@ Header =
|
|||||||
if a = $ "a[href*='/#{g.BOARD}/']", $.id 'boardNavDesktopFoot'
|
if a = $ "a[href*='/#{g.BOARD}/']", $.id 'boardNavDesktopFoot'
|
||||||
a.className = 'current'
|
a.className = 'current'
|
||||||
|
|
||||||
Header.setCatalogLinks Conf['Header catalog links']
|
|
||||||
$.sync 'Header catalog links', Header.setCatalogLinks
|
|
||||||
|
|
||||||
@enableDesktopNotifications()
|
@enableDesktopNotifications()
|
||||||
|
|
||||||
setBoardList: ->
|
setBoardList: ->
|
||||||
@ -120,10 +112,12 @@ Header =
|
|||||||
list = $ '#custom-board-list', Header.bar
|
list = $ '#custom-board-list', Header.bar
|
||||||
$.rmAll list
|
$.rmAll list
|
||||||
return unless text
|
return unless text
|
||||||
as = $$ '#full-board-list a[title]', Header.bar
|
as = $$ '.boardList a[title]', Header.bar
|
||||||
nodes = text.match(/[\w@]+(-(all|title|replace|full|index|catalog|text:"[^"]+"))*|[^\w@]+/g).map (t) ->
|
re = /[\w@]+(-(all|title|replace|full|archive|(mode|sort|text):"[^"]+"))*|[^\w@]+/g
|
||||||
|
nodes = text.match(re).map (t) ->
|
||||||
if /^[^\w@]/.test t
|
if /^[^\w@]/.test t
|
||||||
return $.tn t
|
return $.tn t
|
||||||
|
|
||||||
if /^toggle-all/.test t
|
if /^toggle-all/.test t
|
||||||
a = $.el 'a',
|
a = $.el 'a',
|
||||||
className: 'show-board-list-button'
|
className: 'show-board-list-button'
|
||||||
@ -131,31 +125,47 @@ Header =
|
|||||||
href: 'javascript:;'
|
href: 'javascript:;'
|
||||||
$.on a, 'click', Header.toggleBoardList
|
$.on a, 'click', Header.toggleBoardList
|
||||||
return a
|
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
|
else
|
||||||
t.match(/^[^-]+/)[0]
|
boardID
|
||||||
for a in as
|
|
||||||
if a.textContent is board
|
|
||||||
a = a.cloneNode true
|
|
||||||
|
|
||||||
a.textContent = if /-title/.test(t) or /-replace/.test(t) and $.hasClass a, 'current'
|
if /-archive/.test t
|
||||||
a.title
|
if href = Redirect.to 'board', {boardID}
|
||||||
else if /-full/.test t
|
a.href = href
|
||||||
"/#{board}/ - #{a.title}"
|
else
|
||||||
else if m = t.match /-text:"(.+)"/
|
return a.firstChild # Its text node.
|
||||||
m[1]
|
|
||||||
else
|
|
||||||
a.textContent
|
|
||||||
|
|
||||||
if m = t.match /-(index|catalog)/
|
if m = t.match /-mode:"([^"]+)"/
|
||||||
a.dataset.only = m[1]
|
type = m[1].toLowerCase()
|
||||||
a.href = "//boards.4chan.org/#{board}/"
|
a.dataset.indexMode = switch type
|
||||||
a.href += 'catalog' if m[1] is 'catalog'
|
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 '@'
|
$.addClass a, 'navSmall' if boardID is '@'
|
||||||
return a
|
a
|
||||||
$.tn t
|
|
||||||
$.add list, nodes
|
$.add list, nodes
|
||||||
|
|
||||||
toggleBoardList: ->
|
toggleBoardList: ->
|
||||||
@ -219,21 +229,6 @@ Header =
|
|||||||
$.cb.checked.call @
|
$.cb.checked.call @
|
||||||
Header.setBarPosition @checked
|
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) ->
|
setTopBoardList: (show) ->
|
||||||
Header.topBoardToggler.checked = show
|
Header.topBoardToggler.checked = show
|
||||||
if show
|
if show
|
||||||
|
|||||||
@ -1,6 +1,18 @@
|
|||||||
Index =
|
Index =
|
||||||
|
showHiddenThreads: false
|
||||||
init: ->
|
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',
|
@button = $.el 'a',
|
||||||
className: 'index-refresh-shortcut fa fa-refresh'
|
className: 'index-refresh-shortcut fa fa-refresh'
|
||||||
@ -9,33 +21,20 @@ Index =
|
|||||||
$.on @button, 'click', @update
|
$.on @button, 'click', @update
|
||||||
Header.addShortcut @button, 1
|
Header.addShortcut @button, 1
|
||||||
|
|
||||||
modeEntry =
|
threadNumEntry =
|
||||||
el: $.el 'span', textContent: 'Index mode'
|
el: $.el 'span', textContent: 'Threads per page'
|
||||||
subEntries: [
|
subEntries: [
|
||||||
{ el: $.el 'label', innerHTML: '<input type=radio name="Index Mode" value="paged"> Paged' }
|
{ el: $.el 'label', innerHTML: '<input type=number min=0 name="Threads per Page">', title: 'Use 0 for default value' }
|
||||||
{ el: $.el 'label', innerHTML: '<input type=radio name="Index Mode" value="all pages"> All threads' }
|
|
||||||
]
|
]
|
||||||
for label in modeEntry.subEntries
|
threadsNumInput = threadNumEntry.subEntries[0].el.firstChild
|
||||||
input = label.el.firstChild
|
threadsNumInput.value = Conf['Threads per Page']
|
||||||
input.checked = Conf['Index Mode'] is input.value
|
$.on threadsNumInput, 'change', $.cb.value
|
||||||
$.on input, 'change', $.cb.value
|
$.on threadsNumInput, 'change', @cb.threadsNum
|
||||||
$.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
|
|
||||||
|
|
||||||
|
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 =
|
repliesEntry =
|
||||||
el: $.el 'label',
|
el: $.el 'label',
|
||||||
innerHTML: '<input type=checkbox name="Show Replies"> Show replies'
|
innerHTML: '<input type=checkbox name="Show Replies"> Show replies'
|
||||||
@ -47,12 +46,14 @@ Index =
|
|||||||
el: $.el 'label',
|
el: $.el 'label',
|
||||||
innerHTML: '<input type=checkbox name="Refreshed Navigation"> Refreshed navigation'
|
innerHTML: '<input type=checkbox name="Refreshed Navigation"> Refreshed navigation'
|
||||||
title: 'Refresh index when navigating through pages.'
|
title: 'Refresh index when navigating through pages.'
|
||||||
for label in [repliesEntry, anchorEntry, refNavEntry]
|
for label in [targetEntry, repliesEntry, anchorEntry, refNavEntry]
|
||||||
input = label.el.firstChild
|
input = label.el.firstChild
|
||||||
{name} = input
|
{name} = input
|
||||||
input.checked = Conf[name]
|
input.checked = Conf[name]
|
||||||
$.on input, 'change', $.cb.checked
|
$.on input, 'change', $.cb.checked
|
||||||
switch name
|
switch name
|
||||||
|
when 'Open threads in a new tab'
|
||||||
|
$.on input, 'change', @cb.target
|
||||||
when 'Show Replies'
|
when 'Show Replies'
|
||||||
$.on input, 'change', @cb.replies
|
$.on input, 'change', @cb.replies
|
||||||
when 'Anchor Hidden Threads'
|
when 'Anchor Hidden Threads'
|
||||||
@ -63,24 +64,41 @@ Index =
|
|||||||
el: $.el 'span',
|
el: $.el 'span',
|
||||||
textContent: 'Index Navigation'
|
textContent: 'Index Navigation'
|
||||||
order: 90
|
order: 90
|
||||||
subEntries: [modeEntry, sortEntry, repliesEntry, anchorEntry, refNavEntry]
|
subEntries: [threadNumEntry, targetEntry, repliesEntry, anchorEntry, refNavEntry]
|
||||||
|
|
||||||
$.addClass doc, 'index-loading'
|
$.addClass doc, 'index-loading'
|
||||||
@update()
|
@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'
|
@root = $.el 'div', className: 'board'
|
||||||
@pagelist = $.el 'div',
|
@pagelist = $.el 'div',
|
||||||
className: 'pagelist'
|
className: 'pagelist'
|
||||||
hidden: true
|
hidden: true
|
||||||
innerHTML: <%= importHTML('General/Index-pagelist') %>
|
innerHTML: <%= importHTML('General/Index-pagelist') %>
|
||||||
@navLinks = $.el 'div',
|
|
||||||
className: 'navLinks'
|
|
||||||
innerHTML: <%= importHTML('General/Index-navlinks') %>
|
|
||||||
@searchInput = $ '#index-search', @navLinks
|
|
||||||
@currentPage = @getCurrentPage()
|
@currentPage = @getCurrentPage()
|
||||||
$.on window, 'popstate', @cb.popstate
|
$.on window, 'popstate', @cb.popstate
|
||||||
$.on @pagelist, 'click', @cb.pageNav
|
$.on @pagelist, 'click', @cb.pageNav
|
||||||
$.on @searchInput, 'input', @onSearchInput
|
$.on $('#custom-board-list', Header.bar), 'click', @cb.headerNav
|
||||||
$.on $('#index-search-clear', @navLinks), 'click', @clearSearch
|
|
||||||
|
@cb.toggleCatalogMode()
|
||||||
|
|
||||||
$.asap (-> $('.board', doc) or d.readyState isnt 'loading'), ->
|
$.asap (-> $('.board', doc) or d.readyState isnt 'loading'), ->
|
||||||
board = $ '.board'
|
board = $ '.board'
|
||||||
$.replace board, Index.root
|
$.replace board, Index.root
|
||||||
@ -95,18 +113,192 @@ Index =
|
|||||||
|
|
||||||
for navLink in $$ '.navLinks'
|
for navLink in $$ '.navLinks'
|
||||||
$.rm navLink
|
$.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'
|
$.rmClass doc, 'index-loading'
|
||||||
$.asap (-> $('.pagelist') or d.readyState isnt 'loading'), ->
|
$.asap (-> $('.pagelist') or d.readyState isnt 'loading'), ->
|
||||||
$.replace $('.pagelist'), Index.pagelist
|
$.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:
|
cb:
|
||||||
mode: ->
|
toggleCatalogMode: ->
|
||||||
Index.togglePagelist()
|
if Conf['Index Mode'] is 'catalog'
|
||||||
Index.buildIndex()
|
$.addClass doc, 'catalog-mode'
|
||||||
sort: ->
|
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.sort()
|
||||||
Index.buildIndex()
|
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: ->
|
replies: ->
|
||||||
Index.buildThreads()
|
Index.buildThreads()
|
||||||
Index.sort()
|
Index.sort()
|
||||||
@ -123,12 +315,40 @@ Index =
|
|||||||
a = e.target
|
a = e.target
|
||||||
else
|
else
|
||||||
return
|
return
|
||||||
return if a.textContent is 'Catalog'
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
return if Index.cb.indexNav a, true
|
||||||
Index.userPageNav +a.pathname.split('/')[2]
|
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: ->
|
scrollToIndex: ->
|
||||||
Header.scrollToIfNeeded Index.root
|
Header.scrollToIfNeeded Index.navLinks
|
||||||
|
|
||||||
getCurrentPage: ->
|
getCurrentPage: ->
|
||||||
+window.location.pathname.split('/')[2]
|
+window.location.pathname.split('/')[2]
|
||||||
@ -148,11 +368,17 @@ Index =
|
|||||||
Index.setPage()
|
Index.setPage()
|
||||||
Index.scrollToIndex()
|
Index.scrollToIndex()
|
||||||
|
|
||||||
getPagesNum: ->
|
getThreadsNumPerPage: ->
|
||||||
if Index.isSearching
|
if Conf['Threads per Page'] > 0
|
||||||
Math.ceil (Index.sortedNodes.length / 2) / Index.threadsNumPerPage
|
+Conf['Threads per Page']
|
||||||
else
|
else
|
||||||
Index.pagesNum
|
Index.threadsNumPerPage
|
||||||
|
getPagesNum: ->
|
||||||
|
numThreads = if Index.isSearching
|
||||||
|
Index.sortedNodes.length / 2
|
||||||
|
else
|
||||||
|
Index.liveThreadIDs.length
|
||||||
|
Math.ceil numThreads / Index.getThreadsNumPerPage()
|
||||||
getMaxPageNum: ->
|
getMaxPageNum: ->
|
||||||
Math.max 0, Index.getPagesNum() - 1
|
Math.max 0, Index.getPagesNum() - 1
|
||||||
togglePagelist: ->
|
togglePagelist: ->
|
||||||
@ -193,6 +419,20 @@ Index =
|
|||||||
$.before a, strong
|
$.before a, strong
|
||||||
$.add strong, a
|
$.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) ->
|
update: (pageNum) ->
|
||||||
return unless navigator.onLine
|
return unless navigator.onLine
|
||||||
Index.req?.abort()
|
Index.req?.abort()
|
||||||
@ -273,7 +513,6 @@ Index =
|
|||||||
Index.buildIndex()
|
Index.buildIndex()
|
||||||
Index.setPage()
|
Index.setPage()
|
||||||
parseThreadList: (pages) ->
|
parseThreadList: (pages) ->
|
||||||
Index.pagesNum = pages.length
|
|
||||||
Index.threadsNumPerPage = pages[0].threads.length
|
Index.threadsNumPerPage = pages[0].threads.length
|
||||||
Index.liveThreadData = pages.reduce ((arr, next) -> arr.concat next.threads), []
|
Index.liveThreadData = pages.reduce ((arr, next) -> arr.concat next.threads), []
|
||||||
Index.liveThreadIDs = Index.liveThreadData.map (data) -> data.no
|
Index.liveThreadIDs = Index.liveThreadData.map (data) -> data.no
|
||||||
@ -288,7 +527,9 @@ Index =
|
|||||||
threadRoot = Build.thread g.BOARD, threadData
|
threadRoot = Build.thread g.BOARD, threadData
|
||||||
Index.nodes.push threadRoot, $.el 'hr'
|
Index.nodes.push threadRoot, $.el 'hr'
|
||||||
if thread = g.BOARD.threads[threadData.no]
|
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 'Sticky', !!threadData.sticky
|
||||||
thread.setStatus 'Closed', !!threadData.closed
|
thread.setStatus 'Closed', !!threadData.closed
|
||||||
else
|
else
|
||||||
@ -309,6 +550,7 @@ Index =
|
|||||||
$.nodes Index.nodes
|
$.nodes Index.nodes
|
||||||
Main.callbackNodes Thread, threads
|
Main.callbackNodes Thread, threads
|
||||||
Main.callbackNodes Post, posts
|
Main.callbackNodes Post, posts
|
||||||
|
Index.updateHideLabel()
|
||||||
$.event 'IndexRefresh'
|
$.event 'IndexRefresh'
|
||||||
buildReplies: (threadRoots) ->
|
buildReplies: (threadRoots) ->
|
||||||
posts = []
|
posts = []
|
||||||
@ -334,16 +576,37 @@ Index =
|
|||||||
|
|
||||||
Main.handleErrors errors if errors
|
Main.handleErrors errors if errors
|
||||||
Main.callbackNodes Post, posts
|
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: ->
|
sort: ->
|
||||||
switch Conf['Index Sort']
|
switch Conf['Index Sort']
|
||||||
when 'bump'
|
when 'bump'
|
||||||
sortedThreadIDs = Index.liveThreadIDs
|
sortedThreadIDs = Index.liveThreadIDs
|
||||||
when 'lastreply'
|
when 'lastreply'
|
||||||
sortedThreadIDs = [Index.liveThreadData...].sort((a, b) ->
|
sortedThreadIDs = [Index.liveThreadData...].sort (a, b) ->
|
||||||
a = a.last_replies[a.last_replies.length - 1] if 'last_replies' of a
|
[..., a] = a.last_replies if 'last_replies' of a
|
||||||
b = b.last_replies[b.last_replies.length - 1] if 'last_replies' of b
|
[..., b] = b.last_replies if 'last_replies' of b
|
||||||
b.no - a.no
|
b.no - a.no
|
||||||
).map (data) -> data.no
|
.map (data) -> data.no
|
||||||
when 'birth'
|
when 'birth'
|
||||||
sortedThreadIDs = [Index.liveThreadIDs...].sort (a, b) -> b - a
|
sortedThreadIDs = [Index.liveThreadIDs...].sort (a, b) -> b - a
|
||||||
when 'replycount'
|
when 'replycount'
|
||||||
@ -359,7 +622,7 @@ Index =
|
|||||||
# Sticky threads
|
# Sticky threads
|
||||||
Index.sortOnTop (thread) -> thread.isSticky
|
Index.sortOnTop (thread) -> thread.isSticky
|
||||||
# Highlighted threads
|
# Highlighted threads
|
||||||
Index.sortOnTop((thread) -> thread.isOnTop) if Conf['Filter']
|
Index.sortOnTop (thread) -> thread.isOnTop or thread.isPinned
|
||||||
# Non-hidden threads
|
# Non-hidden threads
|
||||||
Index.sortOnTop((thread) -> !thread.isHidden) if Conf['Anchor Hidden Threads']
|
Index.sortOnTop((thread) -> !thread.isHidden) if Conf['Anchor Hidden Threads']
|
||||||
sortOnTop: (match) ->
|
sortOnTop: (match) ->
|
||||||
@ -368,16 +631,20 @@ Index =
|
|||||||
Index.sortedNodes.splice offset++ * 2, 0, Index.sortedNodes.splice(i, 2)...
|
Index.sortedNodes.splice offset++ * 2, 0, Index.sortedNodes.splice(i, 2)...
|
||||||
return
|
return
|
||||||
buildIndex: ->
|
buildIndex: ->
|
||||||
if Conf['Index Mode'] is 'paged'
|
switch Conf['Index Mode']
|
||||||
pageNum = Index.getCurrentPage()
|
when 'paged'
|
||||||
nodesPerPage = Index.threadsNumPerPage * 2
|
pageNum = Index.getCurrentPage()
|
||||||
nodes = Index.sortedNodes[nodesPerPage * pageNum ... nodesPerPage * (pageNum + 1)]
|
nodesPerPage = Index.getThreadsNumPerPage() * 2
|
||||||
else
|
nodes = Index.sortedNodes[nodesPerPage * pageNum ... nodesPerPage * (pageNum + 1)]
|
||||||
nodes = Index.sortedNodes
|
when 'catalog'
|
||||||
|
nodes = Index.buildCatalogViews()
|
||||||
|
Index.sizeCatalogViews nodes
|
||||||
|
else
|
||||||
|
nodes = Index.sortedNodes
|
||||||
$.rmAll Index.root
|
$.rmAll Index.root
|
||||||
Index.buildReplies nodes if Conf['Show Replies']
|
Index.buildReplies nodes if Conf['Show Replies'] and Conf['Index Mode'] isnt 'catalog'
|
||||||
$.event 'IndexBuild', nodes
|
|
||||||
$.add Index.root, nodes
|
$.add Index.root, nodes
|
||||||
|
$.event 'IndexBuild', nodes
|
||||||
|
|
||||||
isSearching: false
|
isSearching: false
|
||||||
clearSearch: ->
|
clearSearch: ->
|
||||||
|
|||||||
@ -11,6 +11,9 @@ Main =
|
|||||||
'catalog'
|
'catalog'
|
||||||
else
|
else
|
||||||
'index'
|
'index'
|
||||||
|
if g.VIEW is 'catalog'
|
||||||
|
$.ready Index.addCatalogSwitch
|
||||||
|
return
|
||||||
if g.VIEW is 'thread'
|
if g.VIEW is 'thread'
|
||||||
g.THREADID = +pathname[3]
|
g.THREADID = +pathname[3]
|
||||||
|
|
||||||
@ -82,6 +85,7 @@ Main =
|
|||||||
initFeature 'Strike-through Quotes', QuoteStrikeThrough
|
initFeature 'Strike-through Quotes', QuoteStrikeThrough
|
||||||
initFeature 'Quick Reply', QR
|
initFeature 'Quick Reply', QR
|
||||||
initFeature 'Menu', Menu
|
initFeature 'Menu', Menu
|
||||||
|
initFeature 'Index Generator (Menu)', Index.menu
|
||||||
initFeature 'Report Link', ReportLink
|
initFeature 'Report Link', ReportLink
|
||||||
initFeature 'Thread Hiding (Menu)', ThreadHiding.menu
|
initFeature 'Thread Hiding (Menu)', ThreadHiding.menu
|
||||||
initFeature 'Reply Hiding (Menu)', PostHiding.menu
|
initFeature 'Reply Hiding (Menu)', PostHiding.menu
|
||||||
@ -92,9 +96,7 @@ Main =
|
|||||||
initFeature 'Quote Inlining', QuoteInline
|
initFeature 'Quote Inlining', QuoteInline
|
||||||
initFeature 'Quote Previewing', QuotePreview
|
initFeature 'Quote Previewing', QuotePreview
|
||||||
initFeature 'Quote Backlinks', QuoteBacklink
|
initFeature 'Quote Backlinks', QuoteBacklink
|
||||||
initFeature 'Mark Quotes of You', QuoteYou
|
initFeature 'Quote Markers', QuoteMarkers
|
||||||
initFeature 'Mark OP Quotes', QuoteOP
|
|
||||||
initFeature 'Mark Cross-thread Quotes', QuoteCT
|
|
||||||
initFeature 'Anonymize', Anonymize
|
initFeature 'Anonymize', Anonymize
|
||||||
initFeature 'Color User IDs', IDColor
|
initFeature 'Color User IDs', IDColor
|
||||||
initFeature 'Time Formatting', Time
|
initFeature 'Time Formatting', Time
|
||||||
@ -131,10 +133,6 @@ Main =
|
|||||||
$.addClass doc, 'fourchan-x', '<% if (type === 'crx') { %>blink<% } else { %>gecko<% } %>'
|
$.addClass doc, 'fourchan-x', '<% if (type === 'crx') { %>blink<% } else { %>gecko<% } %>'
|
||||||
$.addStyle Main.css
|
$.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'
|
style = 'yotsuba-b'
|
||||||
mainStyleSheet = $ 'link[title=switch]', d.head
|
mainStyleSheet = $ 'link[title=switch]', d.head
|
||||||
styleSheets = $$ 'link[rel="alternate stylesheet"]', d.head
|
styleSheets = $$ 'link[rel="alternate stylesheet"]', d.head
|
||||||
|
|||||||
@ -52,6 +52,9 @@ class Post
|
|||||||
@parseQuotes()
|
@parseQuotes()
|
||||||
@parseFile that
|
@parseFile that
|
||||||
|
|
||||||
|
@isDead = false
|
||||||
|
@isHidden = false
|
||||||
|
|
||||||
@clones = []
|
@clones = []
|
||||||
g.posts[@fullID] = thread.posts[@] = board.posts[@] = @
|
g.posts[@fullID] = thread.posts[@] = board.posts[@] = @
|
||||||
@kill() if that.isArchived
|
@kill() if that.isArchived
|
||||||
@ -62,7 +65,6 @@ class Post
|
|||||||
# Get the comment's text.
|
# Get the comment's text.
|
||||||
# <br> -> \n
|
# <br> -> \n
|
||||||
# Remove:
|
# Remove:
|
||||||
# 'Comment too long'...
|
|
||||||
# EXIF data. (/p/)
|
# EXIF data. (/p/)
|
||||||
# Rolls. (/tg/)
|
# Rolls. (/tg/)
|
||||||
# Preceding and following new lines.
|
# Preceding and following new lines.
|
||||||
@ -149,17 +151,14 @@ class Post
|
|||||||
$.rmClass node, 'desktop'
|
$.rmClass node, 'desktop'
|
||||||
return
|
return
|
||||||
|
|
||||||
kill: (file, now) ->
|
kill: (file) ->
|
||||||
now or= new Date()
|
|
||||||
if file
|
if file
|
||||||
return if @file.isDead
|
return if @file.isDead
|
||||||
@file.isDead = true
|
@file.isDead = true
|
||||||
@file.timeOfDeath = now
|
|
||||||
$.addClass @nodes.root, 'deleted-file'
|
$.addClass @nodes.root, 'deleted-file'
|
||||||
else
|
else
|
||||||
return if @isDead
|
return if @isDead
|
||||||
@isDead = true
|
@isDead = true
|
||||||
@timeOfDeath = now
|
|
||||||
$.addClass @nodes.root, 'deleted-post'
|
$.addClass @nodes.root, 'deleted-post'
|
||||||
|
|
||||||
unless strong = $ 'strong.warning', @nodes.info
|
unless strong = $ 'strong.warning', @nodes.info
|
||||||
@ -171,20 +170,20 @@ class Post
|
|||||||
|
|
||||||
return if @isClone
|
return if @isClone
|
||||||
for clone in @clones
|
for clone in @clones
|
||||||
clone.kill file, now
|
clone.kill file
|
||||||
|
|
||||||
return if file
|
return if file
|
||||||
# Get quotelinks/backlinks to this post
|
# Get quotelinks/backlinks to this post
|
||||||
# and paint them (Dead).
|
# and paint them (Dead).
|
||||||
for quotelink in Get.allQuotelinksLinkingTo @ when not $.hasClass quotelink, 'deadlink'
|
for quotelink in Get.allQuotelinksLinkingTo @ when not $.hasClass quotelink, 'deadlink'
|
||||||
$.add quotelink, $.tn '\u00A0(Dead)'
|
|
||||||
$.addClass quotelink, 'deadlink'
|
$.addClass quotelink, 'deadlink'
|
||||||
|
continue unless Conf['Quote Markers']
|
||||||
|
QuoteMarkers.parseQuotelink Get.postFromNode(quotelink), quotelink, true
|
||||||
return
|
return
|
||||||
# XXX tmp fix for 4chan's racing condition
|
# XXX tmp fix for 4chan's racing condition
|
||||||
# giving us false-positive dead posts.
|
# giving us false-positive dead posts.
|
||||||
resurrect: ->
|
resurrect: ->
|
||||||
delete @isDead
|
delete @isDead
|
||||||
delete @timeOfDeath
|
|
||||||
$.rmClass @nodes.root, 'deleted-post'
|
$.rmClass @nodes.root, 'deleted-post'
|
||||||
strong = $ 'strong.warning', @nodes.info
|
strong = $ 'strong.warning', @nodes.info
|
||||||
# no false-positive files
|
# no false-positive files
|
||||||
@ -197,10 +196,10 @@ class Post
|
|||||||
for clone in @clones
|
for clone in @clones
|
||||||
clone.resurrect()
|
clone.resurrect()
|
||||||
|
|
||||||
for quotelink in Get.allQuotelinksLinkingTo @
|
for quotelink in Get.allQuotelinksLinkingTo @ when $.hasClass quotelink, 'deadlink'
|
||||||
if $.hasClass quotelink, 'deadlink'
|
$.rmClass quotelink, 'deadlink'
|
||||||
quotelink.textContent = quotelink.textContent.replace '\u00A0(Dead)', ''
|
continue unless Conf['Quote Markers']
|
||||||
$.rmClass quotelink, 'deadlink'
|
QuoteMarkers.parseQuotelink Get.postFromNode(quotelink), quotelink, true
|
||||||
return
|
return
|
||||||
|
|
||||||
collect: ->
|
collect: ->
|
||||||
|
|||||||
@ -133,10 +133,7 @@ Settings =
|
|||||||
button.textContent = "Hidden: #{hiddenNum}"
|
button.textContent = "Hidden: #{hiddenNum}"
|
||||||
$.on button, 'click', ->
|
$.on button, 'click', ->
|
||||||
@textContent = 'Hidden: 0'
|
@textContent = 'Hidden: 0'
|
||||||
$.get 'hiddenThreads', {}, ({hiddenThreads}) ->
|
$.delete ['hiddenThreads', 'hiddenPosts']
|
||||||
for boardID of hiddenThreads.boards
|
|
||||||
localStorage.removeItem "4chan-hide-t-#{boardID}"
|
|
||||||
$.delete ['hiddenThreads', 'hiddenPosts']
|
|
||||||
$.after $('input[name="Stubs"]', section).parentNode.parentNode, div
|
$.after $('input[name="Stubs"]', section).parentNode.parentNode, div
|
||||||
export: ->
|
export: ->
|
||||||
# Make sure to export the most recent data.
|
# Make sure to export the most recent data.
|
||||||
@ -195,8 +192,8 @@ Settings =
|
|||||||
'Remember QR size': ''
|
'Remember QR size': ''
|
||||||
'Quote Inline': 'Quote Inlining'
|
'Quote Inline': 'Quote Inlining'
|
||||||
'Quote Preview': 'Quote Previewing'
|
'Quote Preview': 'Quote Previewing'
|
||||||
'Indicate OP quote': 'Mark OP Quotes'
|
'Indicate OP quote': ''
|
||||||
'Indicate Cross-thread Quotes': 'Mark Cross-thread Quotes'
|
'Indicate Cross-thread Quotes': ''
|
||||||
# filter
|
# filter
|
||||||
'uniqueid': 'uniqueID'
|
'uniqueid': 'uniqueID'
|
||||||
'mod': 'capcode'
|
'mod': 'capcode'
|
||||||
|
|||||||
@ -5,18 +5,30 @@ class Thread
|
|||||||
constructor: (@ID, @board) ->
|
constructor: (@ID, @board) ->
|
||||||
@fullID = "#{@board}.#{@ID}"
|
@fullID = "#{@board}.#{@ID}"
|
||||||
@posts = {}
|
@posts = {}
|
||||||
|
@isDead = false
|
||||||
|
@isHidden = false
|
||||||
|
@isOnTop = false
|
||||||
|
@isPinned = false
|
||||||
@isSticky = false
|
@isSticky = false
|
||||||
@isClosed = false
|
@isClosed = false
|
||||||
@postLimit = false
|
@postLimit = false
|
||||||
@fileLimit = false
|
@fileLimit = false
|
||||||
|
|
||||||
|
@OP = null
|
||||||
|
@catalogView = null
|
||||||
|
|
||||||
g.threads[@fullID] = board.threads[@] = @
|
g.threads[@fullID] = board.threads[@] = @
|
||||||
|
|
||||||
setPage: (pageNum) ->
|
setPage: (pageNum) ->
|
||||||
icon = $ '.page-num', @OP.nodes.post
|
icon = $ '.page-num', @OP.nodes.info
|
||||||
for key in ['title', 'textContent']
|
for key in ['title', 'textContent']
|
||||||
icon[key] = icon[key].replace /\d+/, pageNum
|
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) ->
|
setStatus: (type, status) ->
|
||||||
name = "is#{type}"
|
name = "is#{type}"
|
||||||
return if @[name] is status
|
return if @[name] is status
|
||||||
@ -25,23 +37,33 @@ class Thread
|
|||||||
typeLC = type.toLowerCase()
|
typeLC = type.toLowerCase()
|
||||||
unless status
|
unless status
|
||||||
$.rm $ ".#{typeLC}Icon", @OP.nodes.info
|
$.rm $ ".#{typeLC}Icon", @OP.nodes.info
|
||||||
|
$.rm $ ".#{typeLC}Icon", @catalogView.nodes.icons if @catalogView
|
||||||
return
|
return
|
||||||
|
|
||||||
icon = $.el 'img',
|
icon = $.el 'img',
|
||||||
src: "//s.4cdn.org/image/#{typeLC}#{if window.devicePixelRatio >= 2 then '@2x' else ''}.gif"
|
src: "#{Build.staticPath}#{typeLC}#{Build.gifIcon}"
|
||||||
alt: type
|
|
||||||
title: type
|
title: type
|
||||||
className: "#{typeLC}Icon"
|
className: "#{typeLC}Icon"
|
||||||
root = if type is 'Closed' and @isSticky
|
root = if type is 'Closed' and @isSticky
|
||||||
$ '.stickyIcon', @OP.nodes.info
|
$ '.stickyIcon', @OP.nodes.info
|
||||||
else if g.VIEW is 'index'
|
else if g.VIEW is 'index'
|
||||||
$ '.page-num', @OP.nodes.info
|
$ '.page-num', @OP.nodes.info
|
||||||
else
|
else
|
||||||
$ '[title="Quote this post"]', @OP.nodes.info
|
$ '[title="Quote this post"]', @OP.nodes.info
|
||||||
$.after root, [$.tn(' '), icon]
|
$.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: ->
|
kill: ->
|
||||||
@isDead = true
|
@isDead = true
|
||||||
@timeOfDeath = Date.now()
|
|
||||||
|
|
||||||
collect: ->
|
collect: ->
|
||||||
for postID, post of @posts
|
for postID, post of @posts
|
||||||
|
|||||||
@ -47,6 +47,7 @@ UI = do ->
|
|||||||
menu = @makeMenu()
|
menu = @makeMenu()
|
||||||
currentMenu = menu
|
currentMenu = menu
|
||||||
lastToggledButton = button
|
lastToggledButton = button
|
||||||
|
$.addClass button, 'open'
|
||||||
|
|
||||||
for entry in @entries
|
for entry in @entries
|
||||||
@insertEntry entry, menu, data
|
@insertEntry entry, menu, data
|
||||||
@ -99,6 +100,7 @@ UI = do ->
|
|||||||
|
|
||||||
close: =>
|
close: =>
|
||||||
$.rm currentMenu
|
$.rm currentMenu
|
||||||
|
$.rmClass lastToggledButton, 'open'
|
||||||
currentMenu = null
|
currentMenu = null
|
||||||
lastToggledButton = null
|
lastToggledButton = null
|
||||||
$.off d, 'click CloseMenu', @close
|
$.off d, 'click CloseMenu', @close
|
||||||
@ -191,7 +193,7 @@ UI = do ->
|
|||||||
# prevent text selection
|
# prevent text selection
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if isTouching = e.type is 'touchstart'
|
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.
|
# distance from pointer to el edge is constant; calculate it here.
|
||||||
el = $.x 'ancestor::div[contains(@class,"dialog")][1]', @
|
el = $.x 'ancestor::div[contains(@class,"dialog")][1]', @
|
||||||
rect = el.getBoundingClientRect()
|
rect = el.getBoundingClientRect()
|
||||||
@ -271,7 +273,7 @@ UI = do ->
|
|||||||
$.off d, 'mouseup', @up
|
$.off d, 'mouseup', @up
|
||||||
$.set "#{@id}.position", @style.cssText
|
$.set "#{@id}.position", @style.cssText
|
||||||
|
|
||||||
hoverstart = ({root, el, latestEvent, endEvents, asapTest, cb}) ->
|
hoverstart = ({root, el, latestEvent, endEvents, asapTest, cb, offsetX, offsetY}) ->
|
||||||
o = {
|
o = {
|
||||||
root
|
root
|
||||||
el
|
el
|
||||||
@ -281,14 +283,17 @@ UI = do ->
|
|||||||
latestEvent
|
latestEvent
|
||||||
clientHeight: doc.clientHeight
|
clientHeight: doc.clientHeight
|
||||||
clientWidth: doc.clientWidth
|
clientWidth: doc.clientWidth
|
||||||
|
offsetX: offsetX or 45
|
||||||
|
offsetY: offsetY or -120
|
||||||
}
|
}
|
||||||
o.hover = hover.bind o
|
o.hover = hover.bind o
|
||||||
o.hoverend = hoverend.bind o
|
o.hoverend = hoverend.bind o
|
||||||
|
|
||||||
$.asap ->
|
if asapTest
|
||||||
!el.parentNode or asapTest()
|
$.asap ->
|
||||||
, ->
|
!el.parentNode or asapTest()
|
||||||
o.hover o.latestEvent if el.parentNode
|
, ->
|
||||||
|
o.hover o.latestEvent if el.parentNode
|
||||||
|
|
||||||
$.on root, endEvents, o.hoverend
|
$.on root, endEvents, o.hoverend
|
||||||
$.on root, 'mousemove', o.hover
|
$.on root, 'mousemove', o.hover
|
||||||
@ -302,7 +307,7 @@ UI = do ->
|
|||||||
height = @el.offsetHeight
|
height = @el.offsetHeight
|
||||||
{clientX, clientY} = e
|
{clientX, clientY} = e
|
||||||
|
|
||||||
top = clientY - 120
|
top = clientY + @offsetY
|
||||||
top = if @clientHeight <= height or top <= 0
|
top = if @clientHeight <= height or top <= 0
|
||||||
0
|
0
|
||||||
else if top + height >= @clientHeight
|
else if top + height >= @clientHeight
|
||||||
@ -310,10 +315,10 @@ UI = do ->
|
|||||||
else
|
else
|
||||||
top
|
top
|
||||||
|
|
||||||
[left, right] = if clientX <= @clientWidth - 400
|
[left, right] = if clientX <= @clientWidth / 2
|
||||||
[clientX + 45 + 'px', null]
|
[clientX + @offsetX + 'px', null]
|
||||||
else
|
else
|
||||||
[null, @clientWidth - clientX + 45 + 'px']
|
[null, @clientWidth - clientX + @offsetX + 'px']
|
||||||
|
|
||||||
{style} = @
|
{style} = @
|
||||||
style.top = top + 'px'
|
style.top = top + 'px'
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
AutoGIF =
|
AutoGIF =
|
||||||
init: ->
|
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
|
Post.callbacks.push
|
||||||
name: 'Auto-GIF'
|
name: 'Auto-GIF'
|
||||||
cb: @node
|
cb: @node
|
||||||
|
CatalogThread.callbacks.push
|
||||||
|
name: 'Auto-GIF'
|
||||||
|
cb: @catalogNode
|
||||||
node: ->
|
node: ->
|
||||||
return if @isClone or @isHidden or @thread.isHidden or !@file?.isImage
|
return if @isClone or @isHidden or @thread.isHidden or !@file?.isImage
|
||||||
{thumb, URL} = @file
|
{thumb, URL} = @file
|
||||||
@ -13,8 +16,20 @@ AutoGIF =
|
|||||||
# Revealed spoilers do not have height/width set, this fixes auto-gifs dimensions.
|
# Revealed spoilers do not have height/width set, this fixes auto-gifs dimensions.
|
||||||
{style} = thumb
|
{style} = thumb
|
||||||
style.maxHeight = style.maxWidth = if @isReply then '125px' else '250px'
|
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'
|
gif = $.el 'img'
|
||||||
$.on gif, 'load', ->
|
$.on gif, 'load', ->
|
||||||
# Replace the thumbnail once the GIF has finished loading.
|
# 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
|
gif.src = URL
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
ImageExpand =
|
ImageExpand =
|
||||||
init: ->
|
init: ->
|
||||||
return if g.VIEW is 'catalog' or !Conf['Image Expansion']
|
return if !Conf['Image Expansion']
|
||||||
|
|
||||||
@EAI = $.el 'a',
|
@EAI = $.el 'a',
|
||||||
className: 'expand-all-shortcut fa fa-expand'
|
className: 'expand-all-shortcut fa fa-expand'
|
||||||
@ -133,7 +133,7 @@ ImageExpand =
|
|||||||
|
|
||||||
timeoutID = setTimeout ImageExpand.expand, 10000, post
|
timeoutID = setTimeout ImageExpand.expand, 10000, post
|
||||||
<% if (type === 'crx') { %>
|
<% if (type === 'crx') { %>
|
||||||
$.ajax @src,
|
$.ajax post.file.URL,
|
||||||
onloadend: ->
|
onloadend: ->
|
||||||
return if @status isnt 404
|
return if @status isnt 404
|
||||||
clearTimeout timeoutID
|
clearTimeout timeoutID
|
||||||
@ -156,7 +156,7 @@ ImageExpand =
|
|||||||
|
|
||||||
menu:
|
menu:
|
||||||
init: ->
|
init: ->
|
||||||
return if g.VIEW is 'catalog' or !Conf['Image Expansion']
|
return if !Conf['Image Expansion']
|
||||||
|
|
||||||
el = $.el 'span',
|
el = $.el 'span',
|
||||||
textContent: 'Image Expansion'
|
textContent: 'Image Expansion'
|
||||||
|
|||||||
@ -1,15 +1,24 @@
|
|||||||
ImageHover =
|
ImageHover =
|
||||||
init: ->
|
init: ->
|
||||||
return if g.VIEW is 'catalog' or !Conf['Image Hover']
|
if Conf['Image Hover']
|
||||||
|
Post.callbacks.push
|
||||||
Post.callbacks.push
|
name: 'Image Hover'
|
||||||
name: 'Image Hover'
|
cb: @node
|
||||||
cb: @node
|
if Conf['Image Hover in Catalog']
|
||||||
|
CatalogThread.callbacks.push
|
||||||
|
name: 'Image Hover'
|
||||||
|
cb: @catalogNode
|
||||||
node: ->
|
node: ->
|
||||||
return unless @file?.isImage
|
return unless @file?.isImage
|
||||||
$.on @file.thumb, 'mouseover', ImageHover.mouseover
|
$.on @file.thumb, 'mouseover', ImageHover.mouseover
|
||||||
|
catalogNode: ->
|
||||||
|
return unless @thread.OP.file?.isImage
|
||||||
|
$.on @nodes.thumb, 'mouseover', ImageHover.mouseover
|
||||||
mouseover: (e) ->
|
mouseover: (e) ->
|
||||||
post = Get.postFromNode @
|
post = if $.hasClass @, 'thumb'
|
||||||
|
g.posts[@parentNode.dataset.fullID]
|
||||||
|
else
|
||||||
|
Get.postFromNode @
|
||||||
el = $.el 'img',
|
el = $.el 'img',
|
||||||
id: 'ihover'
|
id: 'ihover'
|
||||||
src: post.file.URL
|
src: post.file.URL
|
||||||
@ -39,7 +48,7 @@ ImageHover =
|
|||||||
|
|
||||||
timeoutID = setTimeout (=> @src = post.file.URL + '?' + Date.now()), 3000
|
timeoutID = setTimeout (=> @src = post.file.URL + '?' + Date.now()), 3000
|
||||||
<% if (type === 'crx') { %>
|
<% if (type === 'crx') { %>
|
||||||
$.ajax @src,
|
$.ajax post.file.URL,
|
||||||
onloadend: ->
|
onloadend: ->
|
||||||
return if @status isnt 404
|
return if @status isnt 404
|
||||||
clearTimeout timeoutID
|
clearTimeout timeoutID
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
RevealSpoilers =
|
RevealSpoilers =
|
||||||
init: ->
|
init: ->
|
||||||
return if g.VIEW is 'catalog' or !Conf['Reveal Spoilers']
|
return if !Conf['Reveal Spoilers']
|
||||||
|
|
||||||
Post.callbacks.push
|
Post.callbacks.push
|
||||||
name: 'Reveal Spoilers'
|
name: 'Reveal Spoilers'
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
Sauce =
|
Sauce =
|
||||||
init: ->
|
init: ->
|
||||||
return if g.VIEW is 'catalog' or !Conf['Sauce']
|
return if !Conf['Sauce']
|
||||||
|
|
||||||
links = []
|
links = []
|
||||||
for link in Conf['sauces'].split '\n'
|
for link in Conf['sauces'].split '\n'
|
||||||
@ -15,7 +15,7 @@ Sauce =
|
|||||||
name: 'Sauce'
|
name: 'Sauce'
|
||||||
cb: @node
|
cb: @node
|
||||||
createSauceLink: (link) ->
|
createSauceLink: (link) ->
|
||||||
link = link.replace /%(T?URL|MD5|board)/g, (parameter) ->
|
link = link.replace /%(T?URL|MD5|board|name)/g, (parameter) ->
|
||||||
switch parameter
|
switch parameter
|
||||||
when '%TURL'
|
when '%TURL'
|
||||||
"' + encodeURIComponent(post.file.thumbURL) + '"
|
"' + encodeURIComponent(post.file.thumbURL) + '"
|
||||||
@ -25,6 +25,8 @@ Sauce =
|
|||||||
"' + encodeURIComponent(post.file.MD5) + '"
|
"' + encodeURIComponent(post.file.MD5) + '"
|
||||||
when '%board'
|
when '%board'
|
||||||
"' + encodeURIComponent(post.board) + '"
|
"' + encodeURIComponent(post.board) + '"
|
||||||
|
when '%name'
|
||||||
|
"' + encodeURIComponent(post.file.name) + '"
|
||||||
else
|
else
|
||||||
parameter
|
parameter
|
||||||
text = if m = link.match(/;text:(.+)$/) then m[1] else link.match(/(\w+)\.\w+\//)[1]
|
text = if m = link.match(/;text:(.+)$/) then m[1] else link.match(/(\w+)\.\w+\//)[1]
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
Linkify =
|
Linkify =
|
||||||
init: ->
|
init: ->
|
||||||
return if g.VIEW is 'catalog' or !Conf['Linkify']
|
return if !Conf['Linkify']
|
||||||
|
|
||||||
# gruber revised + magnet support
|
# gruber revised + magnet support
|
||||||
# http://df4.us/fv9
|
# http://df4.us/fv9
|
||||||
@ -60,11 +60,12 @@ Linkify =
|
|||||||
# Replace already-linkified links,
|
# Replace already-linkified links,
|
||||||
# f.e.: https://boards.4chan.org/b/%
|
# f.e.: https://boards.4chan.org/b/%
|
||||||
$.replace parent, anchor
|
$.replace parent, anchor
|
||||||
Linkify.cleanLink anchor, link if Conf['Clean Links']
|
Linkify.cleanLink anchor, link
|
||||||
walker.currentNode = anchor.lastChild
|
walker.currentNode = anchor.lastChild
|
||||||
else
|
else
|
||||||
walker.previousNode()
|
walker.previousNode()
|
||||||
range.detach()
|
range.detach()
|
||||||
|
@nodes.comment.normalize()
|
||||||
|
|
||||||
find: (link, walker) ->
|
find: (link, walker) ->
|
||||||
# Walk through the nodes until we find the entire link.
|
# Walk through the nodes until we find the entire link.
|
||||||
@ -134,6 +135,8 @@ Linkify =
|
|||||||
|
|
||||||
cleanLink: (anchor, link) ->
|
cleanLink: (anchor, link) ->
|
||||||
{length} = link
|
{length} = link
|
||||||
|
for node in $$ 'wbr', anchor
|
||||||
|
$.rm node
|
||||||
for node in $$ 's, .prettyprint', anchor
|
for node in $$ 's, .prettyprint', anchor
|
||||||
$.replace node, [node.childNodes...] if length > node.textContent.length
|
$.replace node, [node.childNodes...] if length > node.textContent.length
|
||||||
return
|
return
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
ArchiveLink =
|
ArchiveLink =
|
||||||
init: ->
|
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',
|
div = $.el 'div',
|
||||||
textContent: 'Archive'
|
textContent: 'Archive'
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
DeleteLink =
|
DeleteLink =
|
||||||
init: ->
|
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',
|
div = $.el 'div',
|
||||||
className: 'delete-link'
|
className: 'delete-link'
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
DownloadLink =
|
DownloadLink =
|
||||||
init: ->
|
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',
|
a = $.el 'a',
|
||||||
className: 'download-link'
|
className: 'download-link'
|
||||||
|
|||||||
@ -1,36 +1,42 @@
|
|||||||
Menu =
|
Menu =
|
||||||
init: ->
|
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'
|
@menu = new UI.Menu 'post'
|
||||||
Post.callbacks.push
|
Post.callbacks.push
|
||||||
name: 'Menu'
|
name: 'Menu'
|
||||||
cb: @node
|
cb: @node
|
||||||
|
CatalogThread.callbacks.push
|
||||||
|
name: 'Menu'
|
||||||
|
cb: @catalogNode
|
||||||
|
|
||||||
node: ->
|
node: ->
|
||||||
if @isClone
|
if @isClone
|
||||||
$.on $('.menu-button', @nodes.info), 'click', Menu.toggle
|
$.on $('.menu-button', @nodes.info), 'click', Menu.toggle
|
||||||
return
|
return
|
||||||
$.add @nodes.info, Menu.makeButton()
|
$.add @nodes.info, Menu.makeButton()
|
||||||
|
catalogNode: ->
|
||||||
|
$.add @nodes.thumb, Menu.makeButton()
|
||||||
|
|
||||||
makeButton: do ->
|
makeButton: ->
|
||||||
frag = null
|
clone = Menu.frag.cloneNode true
|
||||||
->
|
$.on clone.lastElementChild, 'click', Menu.toggle
|
||||||
unless frag
|
clone
|
||||||
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
|
|
||||||
|
|
||||||
toggle: (e) ->
|
toggle: (e) ->
|
||||||
try
|
try
|
||||||
# Posts, inlined posts, hidden replies.
|
# Posts, inlined posts, hidden replies.
|
||||||
post = Get.postFromNode @
|
post = Get.postFromNode @
|
||||||
catch
|
catch
|
||||||
# Hidden threads.
|
post = if fullID = @parentNode.parentNode.dataset.fullID
|
||||||
post = Get.threadFromNode(@).OP
|
g.threads[fullID].OP
|
||||||
|
else
|
||||||
|
# Hidden threads.
|
||||||
|
Get.threadFromNode(@).OP
|
||||||
Menu.menu.toggle e, @, post
|
Menu.menu.toggle e, @, post
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
ReportLink =
|
ReportLink =
|
||||||
init: ->
|
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',
|
a = $.el 'a',
|
||||||
className: 'report-link'
|
className: 'report-link'
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
Anonymize =
|
Anonymize =
|
||||||
init: ->
|
init: ->
|
||||||
return if g.VIEW is 'catalog' or !Conf['Anonymize']
|
return if !Conf['Anonymize']
|
||||||
|
|
||||||
Post.callbacks.push
|
Post.callbacks.push
|
||||||
name: 'Anonymize'
|
name: 'Anonymize'
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
Dice =
|
Dice =
|
||||||
init: ->
|
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
|
Post.callbacks.push
|
||||||
name: 'Show Dice Roll'
|
name: 'Show Dice Roll'
|
||||||
cb: @node
|
cb: @node
|
||||||
|
|||||||
@ -92,12 +92,4 @@ ExpandThread =
|
|||||||
postsRoot.push root
|
postsRoot.push root
|
||||||
Main.callbackNodes Post, posts
|
Main.callbackNodes Post, posts
|
||||||
$.after a, postsRoot
|
$.after a, postsRoot
|
||||||
|
a.textContent = ExpandThread.text '-', postsRoot.length, filesCount
|
||||||
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
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
FileInfo =
|
FileInfo =
|
||||||
init: ->
|
init: ->
|
||||||
return if g.VIEW is 'catalog' or !Conf['File Info Formatting']
|
return if !Conf['File Info Formatting']
|
||||||
|
|
||||||
@funk = @createFunc Conf['fileInfo']
|
@funk = @createFunc Conf['fileInfo']
|
||||||
Post.callbacks.push
|
Post.callbacks.push
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
Fourchan =
|
Fourchan =
|
||||||
init: ->
|
init: ->
|
||||||
return if g.VIEW is 'catalog'
|
|
||||||
|
|
||||||
board = g.BOARD.ID
|
board = g.BOARD.ID
|
||||||
if board is 'g'
|
if board is 'g'
|
||||||
$.globalEval """
|
$.globalEval """
|
||||||
@ -42,10 +40,3 @@ Fourchan =
|
|||||||
math: ->
|
math: ->
|
||||||
return if @isClone or !$ '.math', @nodes.comment
|
return if @isClone or !$ '.math', @nodes.comment
|
||||||
$.event 'jsmath', @nodes.post, window
|
$.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 =
|
IDColor =
|
||||||
init: ->
|
init: ->
|
||||||
return if g.VIEW is 'catalog' or !Conf['Color User IDs']
|
return if !Conf['Color User IDs']
|
||||||
@ids = {}
|
@ids = {}
|
||||||
|
|
||||||
Post.callbacks.push
|
Post.callbacks.push
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
Keybinds =
|
Keybinds =
|
||||||
init: ->
|
init: ->
|
||||||
return if g.VIEW is 'catalog' or !Conf['Keybinds']
|
return if !Conf['Keybinds']
|
||||||
|
|
||||||
for hotkey of Conf.hotkeys
|
for hotkey of Conf.hotkeys
|
||||||
$.sync hotkey, Keybinds.sync
|
$.sync hotkey, Keybinds.sync
|
||||||
@ -88,6 +88,17 @@ Keybinds =
|
|||||||
$('.prev button', Index.pagelist).click()
|
$('.prev button', Index.pagelist).click()
|
||||||
when Conf['Search form']
|
when Conf['Search form']
|
||||||
Index.searchInput.focus()
|
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
|
# Thread Navigation
|
||||||
when Conf['Next thread']
|
when Conf['Next thread']
|
||||||
return if g.VIEW isnt 'index'
|
return if g.VIEW isnt 'index'
|
||||||
|
|||||||
@ -5,8 +5,6 @@ Nav =
|
|||||||
return unless Conf['Index Navigation']
|
return unless Conf['Index Navigation']
|
||||||
when 'thread'
|
when 'thread'
|
||||||
return unless Conf['Reply Navigation']
|
return unless Conf['Reply Navigation']
|
||||||
else # catalog
|
|
||||||
return
|
|
||||||
|
|
||||||
span = $.el 'span',
|
span = $.el 'span',
|
||||||
id: 'navlinks'
|
id: 'navlinks'
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
Time =
|
Time =
|
||||||
init: ->
|
init: ->
|
||||||
return if g.VIEW is 'catalog' or !Conf['Time Formatting']
|
return if !Conf['Time Formatting']
|
||||||
|
|
||||||
@funk = @createFunc Conf['time']
|
@funk = @createFunc Conf['time']
|
||||||
Post.callbacks.push
|
Post.callbacks.push
|
||||||
|
|||||||
@ -254,11 +254,3 @@ ThreadUpdater =
|
|||||||
window.scrollTo 0, d.body.clientHeight
|
window.scrollTo 0, d.body.clientHeight
|
||||||
else
|
else
|
||||||
Header.scrollTo nodes[0]
|
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']
|
else if Conf['Auto Watch Reply']
|
||||||
ThreadWatcher.add board.threads[threadID]
|
ThreadWatcher.add board.threads[threadID]
|
||||||
onIndexRefresh: ->
|
onIndexRefresh: ->
|
||||||
{db} = ThreadWatcher
|
|
||||||
boardID = g.BOARD.ID
|
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']
|
if Conf['Auto Prune']
|
||||||
ThreadWatcher.db.delete {boardID, threadID}
|
ThreadWatcher.db.delete {boardID, threadID}
|
||||||
else
|
else
|
||||||
|
|||||||
@ -139,7 +139,7 @@ Unread =
|
|||||||
Unread.readArray Unread.postsQuotingYou
|
Unread.readArray Unread.postsQuotingYou
|
||||||
Unread.update() if e
|
Unread.update() if e
|
||||||
|
|
||||||
saveLastReadPost: ->
|
saveLastReadPost: <% if (type === 'crx') { %>$.debounce 5 * $.SECOND,<% } %> ->
|
||||||
return if Unread.thread.isDead
|
return if Unread.thread.isDead
|
||||||
Unread.db.set
|
Unread.db.set
|
||||||
boardID: Unread.thread.board.ID
|
boardID: Unread.thread.board.ID
|
||||||
@ -158,7 +158,7 @@ Unread =
|
|||||||
count = Unread.posts.length
|
count = Unread.posts.length
|
||||||
|
|
||||||
if Conf['Unread Count']
|
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']
|
return unless Conf['Unread Tab Icon']
|
||||||
|
|
||||||
|
|||||||
@ -1,38 +1,25 @@
|
|||||||
QR.captcha =
|
QR.captcha =
|
||||||
init: ->
|
init: ->
|
||||||
return if d.cookie.indexOf('pass_enabled=1') >= 0
|
return if d.cookie.indexOf('pass_enabled=1') >= 0
|
||||||
return unless @isEnabled = !!$.id 'captchaFormPart'
|
container = $.id 'captchaContainer'
|
||||||
$.asap (-> $.id 'recaptcha_challenge_field_holder'), @ready.bind @
|
return unless @isEnabled = !!container
|
||||||
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
|
|
||||||
|
|
||||||
imgContainer = $.el 'div',
|
imgContainer = $.el 'div',
|
||||||
className: 'captcha-img'
|
className: 'captcha-img'
|
||||||
title: 'Reload reCAPTCHA'
|
title: 'Reload reCAPTCHA'
|
||||||
innerHTML: '<img>'
|
innerHTML: '<img>'
|
||||||
|
hidden: true
|
||||||
input = $.el 'input',
|
input = $.el 'input',
|
||||||
className: 'captcha-input field'
|
className: 'captcha-input field'
|
||||||
title: 'Verification'
|
title: 'Verification'
|
||||||
|
placeholder: 'Focus to load reCAPTCHA'
|
||||||
autocomplete: 'off'
|
autocomplete: 'off'
|
||||||
spellcheck: false
|
spellcheck: false
|
||||||
@nodes =
|
@nodes =
|
||||||
challenge: $.id 'recaptcha_challenge_field_holder'
|
img: imgContainer.firstChild
|
||||||
img: imgContainer.firstChild
|
input: input
|
||||||
input: input
|
|
||||||
|
|
||||||
new MutationObserver(@load.bind @).observe @nodes.challenge,
|
$.on input, 'focus', @setup
|
||||||
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()
|
|
||||||
|
|
||||||
<% if (type === 'userscript') { %>
|
<% if (type === 'userscript') { %>
|
||||||
# XXX Firefox lacks focusin/focusout support.
|
# XXX Firefox lacks focusin/focusout support.
|
||||||
@ -42,6 +29,36 @@ QR.captcha =
|
|||||||
|
|
||||||
$.addClass QR.nodes.el, 'has-captcha'
|
$.addClass QR.nodes.el, 'has-captcha'
|
||||||
$.after QR.nodes.com.parentNode, [imgContainer, input]
|
$.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) ->
|
sync: (captchas) ->
|
||||||
QR.captcha.captchas = captchas
|
QR.captcha.captchas = captchas
|
||||||
QR.captcha.count()
|
QR.captcha.count()
|
||||||
@ -70,6 +87,7 @@ QR.captcha =
|
|||||||
@reload()
|
@reload()
|
||||||
$.set 'captchas', @captchas
|
$.set 'captchas', @captchas
|
||||||
clear: ->
|
clear: ->
|
||||||
|
return unless @captchas # not loaded yet.
|
||||||
now = Date.now()
|
now = Date.now()
|
||||||
for captcha, i in @captchas
|
for captcha, i in @captchas
|
||||||
break if captcha.timeout > now
|
break if captcha.timeout > now
|
||||||
@ -87,7 +105,7 @@ QR.captcha =
|
|||||||
@nodes.input.value = null
|
@nodes.input.value = null
|
||||||
@clear()
|
@clear()
|
||||||
count: ->
|
count: ->
|
||||||
count = @captchas.length
|
count = if @captchas then @captchas.length else 0
|
||||||
@nodes.input.placeholder = switch count
|
@nodes.input.placeholder = switch count
|
||||||
when 0
|
when 0
|
||||||
'Verification (Shift + Enter to cache)'
|
'Verification (Shift + Enter to cache)'
|
||||||
|
|||||||
@ -50,14 +50,15 @@ QR =
|
|||||||
else
|
else
|
||||||
QR.status()
|
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: ->
|
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
|
$.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: ->
|
open: ->
|
||||||
if QR.nodes
|
if QR.nodes
|
||||||
QR.nodes.el.hidden = false
|
QR.nodes.el.hidden = false
|
||||||
|
|||||||
@ -7,16 +7,13 @@ QR.cooldown =
|
|||||||
$.off window, 'cooldown:timers', setTimers
|
$.off window, 'cooldown:timers', setTimers
|
||||||
for type of QR.cooldown.types
|
for type of QR.cooldown.types
|
||||||
QR.cooldown.types[type] = +QR.cooldown.types[type]
|
QR.cooldown.types[type] = +QR.cooldown.types[type]
|
||||||
QR.cooldown.upSpd = 0
|
|
||||||
QR.cooldown.upSpdAccuracy = .5
|
|
||||||
key = "cooldown.#{g.BOARD}"
|
key = "cooldown.#{g.BOARD}"
|
||||||
$.get key, {}, (item) ->
|
$.get key, {}, (item) ->
|
||||||
QR.cooldown.cooldowns = item[key]
|
QR.cooldown.cooldowns = item[key]
|
||||||
QR.cooldown.start()
|
QR.cooldown.start()
|
||||||
$.sync key, QR.cooldown.sync
|
$.sync key, QR.cooldown.sync
|
||||||
start: ->
|
start: ->
|
||||||
return unless Conf['Cooldown']
|
return if QR.cooldown.isCounting or !Object.keys(QR.cooldown.cooldowns).length
|
||||||
return if QR.cooldown.isCounting
|
|
||||||
QR.cooldown.isCounting = true
|
QR.cooldown.isCounting = true
|
||||||
QR.cooldown.count()
|
QR.cooldown.count()
|
||||||
sync: (cooldowns) ->
|
sync: (cooldowns) ->
|
||||||
@ -32,10 +29,6 @@ QR.cooldown =
|
|||||||
if delay
|
if delay
|
||||||
cooldown = {delay}
|
cooldown = {delay}
|
||||||
else
|
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}
|
cooldown = {isReply, threadID}
|
||||||
QR.cooldown.cooldowns[start] = cooldown
|
QR.cooldown.cooldowns[start] = cooldown
|
||||||
$.set "cooldown.#{g.BOARD}", QR.cooldown.cooldowns
|
$.set "cooldown.#{g.BOARD}", QR.cooldown.cooldowns
|
||||||
@ -48,7 +41,7 @@ QR.cooldown =
|
|||||||
$.delete "cooldown.#{g.BOARD}"
|
$.delete "cooldown.#{g.BOARD}"
|
||||||
count: ->
|
count: ->
|
||||||
unless Object.keys(QR.cooldown.cooldowns).length
|
unless Object.keys(QR.cooldown.cooldowns).length
|
||||||
$.delete "#{g.BOARD}.cooldown"
|
$.delete "cooldown.#{g.BOARD}"
|
||||||
delete QR.cooldown.isCounting
|
delete QR.cooldown.isCounting
|
||||||
delete QR.cooldown.seconds
|
delete QR.cooldown.seconds
|
||||||
QR.status()
|
QR.status()
|
||||||
@ -62,9 +55,10 @@ QR.cooldown =
|
|||||||
isReply = post.thread isnt 'new'
|
isReply = post.thread isnt 'new'
|
||||||
hasFile = !!post.file
|
hasFile = !!post.file
|
||||||
seconds = null
|
seconds = null
|
||||||
{types, cooldowns, upSpd, upSpdAccuracy} = QR.cooldown
|
{types, cooldowns} = QR.cooldown
|
||||||
|
|
||||||
for start, cooldown of cooldowns
|
for start, cooldown of cooldowns
|
||||||
|
start = +start
|
||||||
if 'delay' of cooldown
|
if 'delay' of cooldown
|
||||||
if cooldown.delay
|
if cooldown.delay
|
||||||
seconds = Math.max seconds, cooldown.delay--
|
seconds = Math.max seconds, cooldown.delay--
|
||||||
@ -76,8 +70,10 @@ QR.cooldown =
|
|||||||
if isReply is cooldown.isReply
|
if isReply is cooldown.isReply
|
||||||
# Only cooldowns relevant to this post can set the seconds variable:
|
# Only cooldowns relevant to this post can set the seconds variable:
|
||||||
# reply cooldown with a reply, thread cooldown with a thread
|
# reply cooldown with a reply, thread cooldown with a thread
|
||||||
elapsed = Math.floor (now - start) / $.SECOND
|
elapsed = (now - start) // $.SECOND
|
||||||
continue if elapsed < 0 # clock changed since then?
|
if elapsed < 0 # clock changed since then?
|
||||||
|
QR.cooldown.unset start
|
||||||
|
continue
|
||||||
type = unless isReply
|
type = unless isReply
|
||||||
'thread'
|
'thread'
|
||||||
else if hasFile
|
else if hasFile
|
||||||
@ -90,9 +86,6 @@ QR.cooldown =
|
|||||||
type += '_intra' if isReply and +post.thread is cooldown.threadID
|
type += '_intra' if isReply and +post.thread is cooldown.threadID
|
||||||
seconds = Math.max seconds, types[type] - elapsed
|
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.
|
# Update the status when we change posting type.
|
||||||
# Don't get stuck at some random number.
|
# Don't get stuck at some random number.
|
||||||
# Don't interfere with progress status updates.
|
# Don't interfere with progress status updates.
|
||||||
|
|||||||
@ -35,7 +35,7 @@ QR.post = class
|
|||||||
else
|
else
|
||||||
'new'
|
'new'
|
||||||
|
|
||||||
prev = QR.posts[QR.posts.length - 1]
|
[..., prev] = QR.posts
|
||||||
QR.posts.push @
|
QR.posts.push @
|
||||||
@nodes.spoiler.checked = @spoiler = if prev and Conf['Remember Spoiler']
|
@nodes.spoiler.checked = @spoiler = if prev and Conf['Remember Spoiler']
|
||||||
prev.spoiler
|
prev.spoiler
|
||||||
@ -153,7 +153,10 @@ QR.post = class
|
|||||||
@filesize = $.bytesToString file.size
|
@filesize = $.bytesToString file.size
|
||||||
@nodes.label.hidden = false if QR.spoiler
|
@nodes.label.hidden = false if QR.spoiler
|
||||||
URL.revokeObjectURL @URL
|
URL.revokeObjectURL @URL
|
||||||
@showFileData() if @ is QR.selected
|
if @ is QR.selected
|
||||||
|
@showFileData()
|
||||||
|
else
|
||||||
|
@updateFilename()
|
||||||
unless /^image/.test file.type
|
unless /^image/.test file.type
|
||||||
@nodes.el.style.backgroundImage = null
|
@nodes.el.style.backgroundImage = null
|
||||||
return
|
return
|
||||||
|
|||||||
@ -3,19 +3,19 @@ QuoteBacklink =
|
|||||||
# - previous, same, and following posts.
|
# - previous, same, and following posts.
|
||||||
# - existing and yet-to-exist posts.
|
# - existing and yet-to-exist posts.
|
||||||
# - newly fetched posts.
|
# - newly fetched posts.
|
||||||
# - in copies.
|
# - clones.
|
||||||
# XXX what about order for fetched posts?
|
# XXX what about order for fetched posts?
|
||||||
#
|
#
|
||||||
# First callback creates backlinks and add them to relevant containers.
|
# First callback creates a map of quoted -> [quoters],
|
||||||
# Second callback adds relevant containers into posts.
|
# and append backlinks to posts that already have containers.
|
||||||
# This is is so that fetched posts can get their backlinks,
|
# Second callback creates, fill and append containers.
|
||||||
# and that as much backlinks are appended in the background as possible.
|
|
||||||
init: ->
|
init: ->
|
||||||
return if g.VIEW is 'catalog' or !Conf['Quote Backlinks']
|
return if !Conf['Quote Backlinks']
|
||||||
|
|
||||||
format = Conf['backlink'].replace /%id/g, "' + id + '"
|
format = Conf['backlink'].replace /%id/g, "' + id + '"
|
||||||
@funk = Function 'id', "return '#{format}'"
|
@funk = Function 'id', "return '#{format}'"
|
||||||
@containers = {}
|
@frag = $.nodes [$.tn(' '), $.el 'a', className: 'backlink']
|
||||||
|
@map = {}
|
||||||
Post.callbacks.push
|
Post.callbacks.push
|
||||||
name: 'Quote Backlinking Part 1'
|
name: 'Quote Backlinking Part 1'
|
||||||
cb: @firstNode
|
cb: @firstNode
|
||||||
@ -23,35 +23,42 @@ QuoteBacklink =
|
|||||||
name: 'Quote Backlinking Part 2'
|
name: 'Quote Backlinking Part 2'
|
||||||
cb: @secondNode
|
cb: @secondNode
|
||||||
firstNode: ->
|
firstNode: ->
|
||||||
return if @isClone or !@quotes.length
|
return if @isClone
|
||||||
a = $.el 'a',
|
for quoteID in @quotes
|
||||||
href: "/#{@board}/res/#{@thread}#p#{@}"
|
(QuoteBacklink.map[quoteID] or= []).push @fullID
|
||||||
className: if @isHidden then 'filtered backlink' else 'backlink'
|
continue unless (post = g.posts[quoteID]) and container = post?.nodes.backlinkContainer
|
||||||
textContent: QuoteBacklink.funk @ID
|
for post in [post].concat post.clones
|
||||||
for quote in @quotes
|
$.add post.nodes.backlinkContainer, QuoteBacklink.buildBacklink post, @
|
||||||
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
|
return
|
||||||
secondNode: ->
|
secondNode: ->
|
||||||
if @isClone and (@origin.isReply or Conf['OP Backlinks'])
|
|
||||||
@nodes.backlinkContainer = $ '.container', @nodes.info
|
|
||||||
return
|
|
||||||
# Don't backlink the OP.
|
# Don't backlink the OP.
|
||||||
return unless @isReply or Conf['OP Backlinks']
|
return unless @isReply or Conf['OP Backlinks']
|
||||||
container = QuoteBacklink.getContainer @fullID
|
if @isClone
|
||||||
@nodes.backlinkContainer = container
|
@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
|
$.add @nodes.info, container
|
||||||
getContainer: (id) ->
|
buildBacklink: (quoted, quoter) ->
|
||||||
@containers[id] or=
|
frag = QuoteBacklink.frag.cloneNode true
|
||||||
$.el 'span', className: 'container'
|
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 =
|
QuoteInline =
|
||||||
init: ->
|
init: ->
|
||||||
return if g.VIEW is 'catalog' or !Conf['Quote Inlining']
|
return if !Conf['Quote Inlining']
|
||||||
|
|
||||||
Post.callbacks.push
|
Post.callbacks.push
|
||||||
name: 'Quote Inlining'
|
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 =
|
QuotePreview =
|
||||||
init: ->
|
init: ->
|
||||||
return if g.VIEW is 'catalog' or !Conf['Quote Previewing']
|
return if !Conf['Quote Previewing']
|
||||||
|
|
||||||
Post.callbacks.push
|
Post.callbacks.push
|
||||||
name: 'Quote Previewing'
|
name: 'Quote Previewing'
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
QuoteStrikeThrough =
|
QuoteStrikeThrough =
|
||||||
init: ->
|
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
|
Post.callbacks.push
|
||||||
name: 'Strike-through Quotes'
|
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 =
|
Quotify =
|
||||||
init: ->
|
init: ->
|
||||||
return if g.VIEW is 'catalog' or !Conf['Resurrect Quotes']
|
return if !Conf['Resurrect Quotes']
|
||||||
|
|
||||||
Post.callbacks.push
|
Post.callbacks.push
|
||||||
name: 'Resurrect Quotes'
|
name: 'Resurrect Quotes'
|
||||||
@ -37,28 +37,20 @@ Quotify =
|
|||||||
quoteID = "#{boardID}.#{postID}"
|
quoteID = "#{boardID}.#{postID}"
|
||||||
|
|
||||||
if post = g.posts[quoteID]
|
if post = g.posts[quoteID]
|
||||||
unless post.isDead
|
# Don't add 'deadlink' when quotifying in an archived post,
|
||||||
# Don't (Dead) when quotifying in an archived post,
|
# and we don't know if the post died yet.
|
||||||
# and we know the post still exists.
|
a = $.el 'a',
|
||||||
a = $.el 'a',
|
href: "/#{boardID}/res/#{post.thread}#p#{postID}"
|
||||||
href: "/#{boardID}/res/#{post.thread}#p#{postID}"
|
className: if post.isDead then 'quotelink deadlink' else 'quotelink'
|
||||||
className: 'quotelink'
|
textContent: quote
|
||||||
textContent: quote
|
$.extend a.dataset, {boardID, threadID: post.thread.ID, postID}
|
||||||
else
|
else if redirect = Redirect.to 'thread', {boardID, postID}
|
||||||
# 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}
|
|
||||||
# Replace the .deadlink span if we can redirect.
|
# Replace the .deadlink span if we can redirect.
|
||||||
a = $.el 'a',
|
a = $.el 'a',
|
||||||
href: redirect
|
href: redirect
|
||||||
className: 'deadlink'
|
className: 'deadlink'
|
||||||
|
textContent: quote
|
||||||
target: '_blank'
|
target: '_blank'
|
||||||
textContent: "#{quote}\u00A0(Dead)"
|
|
||||||
if Redirect.to 'post', {boardID, postID}
|
if Redirect.to 'post', {boardID, postID}
|
||||||
# Make it function as a normal quote if we can fetch the post.
|
# Make it function as a normal quote if we can fetch the post.
|
||||||
$.addClass a, 'quotelink'
|
$.addClass a, 'quotelink'
|
||||||
@ -68,7 +60,7 @@ Quotify =
|
|||||||
@quotes.push quoteID
|
@quotes.push quoteID
|
||||||
|
|
||||||
unless a
|
unless a
|
||||||
deadlink.textContent = "#{quote}\u00A0(Dead)"
|
deadlink.textContent = "#{quote}\u00A0(Dead)" if Conf['Quote Markers']
|
||||||
return
|
return
|
||||||
|
|
||||||
$.replace deadlink, a
|
$.replace deadlink, a
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user