Merge branch 'v3' of git://github.com/MayhemYDG/4chan-x into v3

Conflicts:
	CHANGELOG.md
	css/style.css
	src/Archive/Redirect.coffee
	src/General/Build.coffee
	src/General/Config.coffee
	src/General/Header.coffee
	src/General/Main.coffee
	src/General/Settings.coffee
	src/General/lib/$.coffee
	src/General/lib/databoard.class
	src/Monitoring/ThreadStats.coffee
	src/Monitoring/ThreadUpdater.coffee
	src/Monitoring/ThreadWatcher.coffee
This commit is contained in:
Zixaphir 2013-08-12 18:26:39 -07:00
commit e0895c594a
21 changed files with 2458 additions and 705 deletions

View File

@ -1,6 +1,18 @@
**MayhemYDG**: **MayhemYDG**:
- **New feature**: `Show Dice Roll` (with @carboncopy) - **New feature**: `Show Dice Roll` (with @carboncopy)
- Shows dice that were entered into the email field on /tg/. - Shows dice that were entered into the email field on /tg/.
- **Thread Watcher** improvements:
- It is now possible to open all watched threads via the `Open all threads` button in the Thread Watcher's menu.
- Added the `Current Board` setting to switch between showing watched threads from the current board or all boards, disabled by default.
- About dead (404'd) threads:
- Dead threads will be typographically indicated with a strikethrough.
- Dead threads will directly link to the corresponding archive when available.
- A button to prune all 404'd threads from the list is now available.
- Added the `Auto Prune` setting to automatically prune 404'd threads, disabled by default.
- The current thread is now highlighted in the list of watched threads.
- Watching the current thread can be done in the Header's menu too.
- Removed the `Check for Updates` setting:
- Your browser/userscript manager should handle updates itself automatically.
- Fix impossibility to create new threads when in dead threads. - Fix impossibility to create new threads when in dead threads.
- Fix flag filtering on /sp/ and /int/. - Fix flag filtering on /sp/ and /int/.
- Update archives. (with @woxxy and @proplex) - Update archives. (with @woxxy and @proplex)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

929
css/style.css Normal file
View File

@ -0,0 +1,929 @@
/* General */
.dialog {
box-shadow: 0 1px 2px rgba(0, 0, 0, .15);
border: 1px solid;
display: block;
padding: 0;
}
.field {
background-color: #FFF;
border: 1px solid #CCC;
-moz-box-sizing: border-box;
box-sizing: border-box;
color: #333;
font-family: inherit;
font-size: 13px;
margin: 0;
padding: 2px 4px 3px;
outline: none;
transition: color .25s, border-color .25s, -webkit-flex .25s;
transition: color .25s, border-color .25s, flex .25s;
}
.field::-moz-placeholder,
.field:hover::-moz-placeholder {
color: #AAA !important;
}
.field:hover {
border-color: #999;
}
.field:hover, .field:focus {
color: #000;
}
.field[disabled] {
background-color: #F2F2F2;
color: #888;
}
.move {
cursor: move;
}
label, .watcher-toggler {
cursor: pointer;
}
a[href="javascript:;"] {
text-decoration: none;
}
.warning {
color: red;
}
/* 4chan style fixes */
.opContainer, .op {
display: block !important;
}
.post {
overflow: visible !important;
}
[hidden] {
display: none !important;
}
/* fixed, z-index */
#overlay,
#qp, #ihover,
#updater, #thread-stats,
#navlinks, #header,
#qr {
position: fixed;
}
#overlay {
z-index: 999;
}
#notifications {
z-index: 70;
}
#qp, #ihover {
z-index: 60;
}
#menu {
z-index: 50;
}
#navlinks, #updater, #thread-stats {
z-index: 40;
}
#qr {
z-index: 30;
}
#thread-watcher:hover {
z-index: 20;
}
#header {
z-index: 10;
}
#thread-watcher {
z-index: 5;
}
/* Header */
:root.top-header body {
margin-top: 2em;
}
:root.bottom-header body {
margin-bottom: 2em;
}
:root.fourchan-x #navtopright,
:root.fourchan-x #navbotright,
:root.fourchan-x:not(.show-original-top-board-list) #boardNavDesktop,
:root.fourchan-x:not(.show-original-bot-board-list) #boardNavDesktopFoot {
display: none !important;
}
#header {
right: 0;
left: 0;
}
#header.top {
top: 0;
}
#header.bottom {
bottom: 0;
}
#header-bar {
border-width: 0;
display: -webkit-flex;
display: flex;
padding: 3px 4px 4px;
position: relative;
transition: all .1s .05s ease-in-out;
}
#header.top #header-bar {
border-bottom-width: 1px;
}
#header.bottom #header-bar {
box-shadow: 0 -1px 2px rgba(0, 0, 0, .15);
border-top-width: 1px;
}
#header.bottom .menu-button i {
border-top: none;
border-bottom: 6px solid;
}
#board-list {
-webkit-flex: 1;
flex: 1;
text-align: center;
}
#header-bar.autohide:not(:hover) {
box-shadow: none;
transition: all .8s .6s cubic-bezier(.55, .055, .675, .19);
}
#header.top #header-bar.autohide:not(:hover) {
margin-bottom: -1em;
-webkit-transform: translateY(-100%);
transform: translateY(-100%);
}
#header.bottom #header-bar.autohide:not(:hover) {
-webkit-transform: translateY(100%);
transform: translateY(100%);
}
#toggle-header-bar {
left: 0;
right: 0;
height: 10px;
position: absolute;
}
#header.top #toggle-header-bar {
cursor: n-resize;
bottom: -8px;
}
#header.bottom #toggle-header-bar {
cursor: s-resize;
top: -8px;
}
#header-bar.autohide:not(:hover) #toggle-header-bar,
#toggle-header-bar:hover {
height: 18px;
}
#header.top #header-bar.autohide:not(:hover) #toggle-header-bar,
#header.top #toggle-header-bar:hover {
bottom: -16px;
}
#header.bottom #header-bar.autohide:not(:hover) #toggle-header-bar,
#header.bottom #toggle-header-bar:hover {
top: -16px;
}
#header.top #header-bar.autohide #toggle-header-bar {
cursor: s-resize;
}
#header.bottom #header-bar.autohide #toggle-header-bar {
cursor: n-resize;
}
#header-bar a:not(.entry) {
text-decoration: none;
padding: 1px;
}
#shortcuts:empty {
display: none;
}
.shortcut:not(:last-child)::after {
content: " / ";
}
.brackets-wrap::before {
content: "\\00a0[";
}
.brackets-wrap::after {
content: "]\\00a0";
}
.expand-all-shortcut {
opacity: .35;
}
/* Notifications */
#notifications {
height: 0;
text-align: center;
}
#header.bottom #notifications {
position: fixed;
top: 0;
left: 0;
width: 100%;
}
.notification {
color: #FFF;
font-weight: 700;
text-shadow: 0 1px 2px rgba(0, 0, 0, .5);
box-shadow: 0 1px 2px rgba(0, 0, 0, .15);
border-radius: 2px;
margin: 1px auto;
width: 500px;
max-width: 100%;
position: relative;
transition: all .25s ease-in-out;
}
.notification.error {
background-color: hsla(0, 100%, 38%, .9);
}
.notification.warning {
background-color: hsla(36, 100%, 38%, .9);
}
.notification.info {
background-color: hsla(200, 100%, 38%, .9);
}
.notification.success {
background-color: hsla(104, 100%, 38%, .9);
}
.notification a {
color: white;
}
.notification > .close {
padding: 6px;
top: 0;
right: 0;
position: absolute;
}
.message {
-moz-box-sizing: border-box;
box-sizing: border-box;
padding: 6px 20px;
max-height: 200px;
width: 100%;
overflow: auto;
}
/* Settings */
:root.fourchan-x body {
-moz-box-sizing: border-box;
box-sizing: border-box;
}
#overlay {
background-color: rgba(0, 0, 0, .5);
display: -webkit-flex;
display: flex;
position: fixed;
top: 0;
left: 0;
height: 100%;
width: 100%;
}
#fourchanx-settings {
-moz-box-sizing: border-box;
box-sizing: border-box;
box-shadow: 0 0 15px rgba(0, 0, 0, .15);
height: 600px;
max-height: 100%;
width: 900px;
max-width: 100%;
margin: auto;
padding: 3px;
display: -webkit-flex;
display: flex;
-webkit-flex-direction: column;
flex-direction: column;
}
#fourchanx-settings > nav {
display: -webkit-flex;
display: flex;
padding: 2px 2px 0;
}
#fourchanx-settings > nav a {
text-decoration: underline;
}
#fourchanx-settings > nav a.close {
text-decoration: none;
padding: 2px;
}
.sections-list {
-webkit-flex: 1;
flex: 1;
}
.tab-selected {
font-weight: 700;
}
.section-container {
-webkit-flex: 1;
flex: 1;
position: relative;
}
.section-container > section {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
overflow: auto;
}
.section-sauce ul,
.section-rice ul {
list-style: none;
margin: 0;
padding: 8px;
}
.section-sauce li,
.section-rice li {
padding-left: 4px;
}
.section-main label {
text-decoration: underline;
}
.section-filter ul,
.section-qr ul {
padding: 0;
}
.section-filter li,
.section-qr li {
margin: 10px 40px;
}
.section-filter textarea {
height: 500px;
}
.section-qr textarea {
height: 200px;
}
.section-sauce textarea {
height: 350px;
}
.section-rice .field[name="boardnav"] {
width: 100%;
}
.section-rice textarea {
height: 150px;
}
.section-archives table {
width: 100%;
}
.section-archives th:not(:first-child) {
width: 30%;
}
.section-archives td {
text-align: center;
}
.section-archives select {
width: 90%;
}
.section-keybinds .field {
font-family: monospace;
}
#fourchanx-settings fieldset {
border: 1px solid;
border-radius: 3px;
}
#fourchanx-settings legend {
font-weight: 700;
}
#fourchanx-settings textarea {
font-family: monospace;
min-width: 100%;
max-width: 100%;
}
#fourchanx-settings code {
color: #000;
background-color: #FFF;
padding: 0 2px;
}
.unscroll {
overflow: hidden;
}
/* Announcement Hiding */
:root.hide-announcement #globalMessage,
:root.hide-announcement-enabled #toggleMsgBtn {
display: none;
}
a.hide-announcement {
float: left;
}
/* Unread */
#unread-line {
margin: 0;
}
/* Thread Updater */
#updater:not(:hover) {
background: none;
border: none;
box-shadow: none;
}
#updater > .move {
padding: 0 3px;
}
#updater > div:last-child {
text-align: center;
}
#updater input[type=number] {
width: 4em;
}
#updater:not(:hover) > div:not(.move) {
display: none;
}
#updater input[type="button"] {
width: 100%;
}
.new {
color: limegreen;
}
/* Thread Watcher */
#thread-watcher {
max-width: 200px;
min-width: 150px;
padding: 3px;
position: absolute;
}
#thread-watcher > div:first-child {
display: -webkit-flex;
display: flex;
-webkit-align-items: center;
align-items: center;
}
#thread-watcher .move {
-webkit-flex: 1;
flex: 1;
}
#watcher-status:not(:empty)::before {
content: "(";
}
#watcher-status:not(:empty)::after {
content: ")";
}
#watched-threads:not(:hover) {
max-height: 150px;
overflow: hidden;
}
#watched-threads div {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#watched-threads .current {
font-weight: 700;
}
#watched-threads a {
text-decoration: none;
}
#watched-threads .dead-thread a[title] {
text-decoration: line-through;
}
/* Thread Stats */
#thread-stats {
background: none;
border: none;
box-shadow: none;
}
/* Quote */
.deadlink {
text-decoration: none !important;
}
.backlink.deadlink:not(.forwardlink),
.quotelink.deadlink:not(.forwardlink) {
text-decoration: underline !important;
}
.inlined {
opacity: .5;
}
#qp input, .forwarded {
display: none;
}
.quotelink.forwardlink,
.backlink.forwardlink {
text-decoration: none;
border-bottom: 1px dashed;
}
.filtered {
text-decoration: underline line-through;
}
.inline {
border: 1px solid;
display: table;
margin: 2px 0;
}
.inline .post {
border: 0 !important;
background-color: transparent !important;
display: table !important;
margin: 0 !important;
padding: 1px 2px !important;
}
#qp > .opContainer::after {
content: '';
clear: both;
display: table;
}
#qp .post {
border: none;
margin: 0;
padding: 2px 2px 5px;
}
#qp img {
max-height: 80vh;
max-width: 50vw;
}
.qphl {
outline: 2px solid rgba(216, 94, 49, .7);
}
/* File */
.fileText:hover .fntrunc,
.fileText:not(:hover) .fnfull,
.expanded-image > .post > .file > .fileThumb > img[data-md5],
:not(.expanded-image) > .post > .file > .fileThumb > .full-image {
display: none;
}
.expanding {
opacity: .5;
}
.expanded-image {
clear: both;
}
.expanded-image > .op > .file::after {
content: '';
clear: both;
display: table;
}
:root.fit-height .full-image {
max-height: 100vh;
}
:root.fit-width .full-image {
max-width: 100%;
}
:root.gecko.fit-width .full-image {
width: 100%;
}
#ihover {
-moz-box-sizing: border-box;
box-sizing: border-box;
max-height: 100%;
max-width: 75%;
padding-bottom: 16px;
}
/* Index/Reply Navigation */
#navlinks {
font-size: 16px;
top: 25px;
right: 10px;
}
/* Filter */
.opContainer.filter-highlight {
box-shadow: inset 5px 0 rgba(255, 0, 0, .5);
}
.filter-highlight > .reply {
box-shadow: -5px 0 rgba(255, 0, 0, .5);
}
/* Thread & Reply Hiding */
.hide-thread-button,
.hide-reply-button {
float: left;
margin-right: 2px;
}
.stub ~ * {
display: none !important;
}
.stub input {
display: inline-block;
}
/* QR */
:root.hide-original-post-form #postForm,
:root.hide-original-post-form .postingMode,
:root.hide-original-post-form #togglePostForm,
#qr.autohide:not(.has-focus):not(:hover) > form {
display: none;
}
#qr select, #dump-button, .remove, .captcha-img {
cursor: pointer;
}
#qr > div {
min-width: 300px;
display: -webkit-flex;
display: flex;
-webkit-align-items: center;
align-items: center;
}
#qr .move {
-webkit-align-self: stretch;
align-self: stretch;
-webkit-flex: 1;
flex: 1;
}
#qr select {
margin: 0;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
border: none;
background: none;
}
#qr option {
color: #000;
background-color: #F7F7F7;
}
#qr .close {
padding: 0 3px;
}
#qr > form {
display: -webkit-flex;
display: flex;
-webkit-flex-direction: column;
flex-direction: column;
}
.persona {
display: -webkit-flex;
display: flex;
}
.persona .field {
-webkit-flex: 1;
flex: 1;
}
.persona .field:hover,
.persona .field:focus {
-webkit-flex: 3;
flex: 3;
}
#dump-button {
background: linear-gradient(#EEE, #CCC);
border: 1px solid #CCC;
margin: 0;
padding: 2px 4px 3px;
outline: none;
width: 30px;
}
#dump-button:hover,
#dump-button:focus {
background: linear-gradient(#FFF, #DDD);
}
#dump-button:active,
.dump #dump-button:not(:hover):not(:focus) {
background: linear-gradient(#CCC, #DDD);
}
:root.gecko #dump-button {
padding: 0;
}
#qr:not(.dump) #dump-list-container {
display: none;
}
#dump-list-container {
height: 100px;
position: relative;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
#dump-list {
counter-reset: qrpreviews;
top: 0;
right: 0;
bottom: 0;
left: 0;
overflow: hidden;
position: absolute;
white-space: nowrap;
}
#dump-list:hover {
bottom: -12px;
overflow-x: auto;
z-index: 1;
}
#dump-list::-webkit-scrollbar {
height: 12px;
}
#dump-list::-webkit-scrollbar-thumb {
border: 1px solid;
}
.qr-preview {
background-position: 50% 20%;
background-size: cover;
border: 1px solid #808080;
color: #FFF !important;
font-size: 12px;
-moz-box-sizing: border-box;
box-sizing: border-box;
cursor: move;
display: inline-block;
height: 92px;
width: 92px;
margin: 4px;
padding: 2px;
opacity: .6;
outline: none;
overflow: hidden;
position: relative;
text-shadow: 0 1px 1px #000;
transition: opacity .25s ease-in-out;
vertical-align: top;
white-space: pre;
}
.qr-preview:hover,
.qr-preview:focus {
opacity: .9;
color: #FFF !important;
}
.qr-preview#selected {
opacity: 1;
}
.qr-preview::before {
counter-increment: qrpreviews;
content: counter(qrpreviews);
font-weight: 700;
text-shadow: 0 0 3px #000, 0 0 5px #000;
position: absolute;
top: 3px;
right: 3px;
}
.qr-preview.drag {
border-color: red;
border-style: dashed;
}
.qr-preview.over {
border-color: #FFF;
border-style: dashed;
}
.remove {
color: #E00 !important;
font-weight: 700;
padding: 3px;
}
.remove:hover::after {
content: ' Remove';
}
.qr-preview > label {
background: rgba(0, 0, 0, .5);
right: 0;
bottom: 0;
left: 0;
position: absolute;
text-align: center;
}
.qr-preview > label > input {
margin: 1px 0;
vertical-align: bottom;
}
#add-post {
display: inline-block;
font-size: 30px;
height: 30px;
width: 30px;
line-height: 1;
text-align: center;
position: absolute;
right: 0;
bottom: 0;
z-index: 1;
}
#qr textarea {
min-height: 160px;
min-width: 100%;
display: block;
}
#qr.has-captcha textarea {
min-height: 120px;
}
.textarea {
position: relative;
}
#char-count {
color: #000;
background: hsla(0, 0%, 100%, .5);
font-size: 8pt;
position: absolute;
bottom: 1px;
right: 1px;
pointer-events: none;
}
#char-count.warning {
color: red;
}
.captcha-img {
background: #FFF;
outline: 1px solid #CCC;
outline-offset: -1px;
}
.captcha-img > img {
display: block;
height: 57px;
width: 300px;
}
#file-n-submit > input {
margin: 0;
}
#file-n-submit.has-file #qr-no-file {
visibility: hidden;
}
#file-n-submit:not(.has-file) #qr-filename,
#file-n-submit:not(.has-file) #qr-file-spoiler,
#file-n-submit:not(.has-file) #qr-filerm {
display: none;
}
#file-n-submit {
display: -webkit-flex;
display: flex;
-webkit-flex-direction: row;
flex-direction: row;
-webkit-align-items: center;
align-items: center;
}
#qr-no-file, #qr-filename-container {
-webkit-flex: 1;
flex: 1;
}
#qr-filename-container {
cursor: default;
position: relative;
margin-left: 2px;
}
#qr-filename {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
#qr-filerm {
padding: 0 2px;
}
#file-n-submit > #qr-file-spoiler {
margin: 0 2px;
}
#file-n-submit input[type='submit'] {
min-width: 40px;
-webkit-order: 1;
order: 1;
}
/* Menu */
.menu-button {
display: inline-block;
position: relative;
}
.menu-button i {
border-top: 6px solid;
border-right: 4px solid transparent;
border-left: 4px solid transparent;
display: inline-block;
margin: 2px;
vertical-align: middle;
}
#menu {
border-bottom: 0;
display: -webkit-flex;
display: flex;
margin: 2px 0;
-webkit-flex-direction: column;
flex-direction: column;
position: absolute;
outline: none;
}
.entry {
cursor: pointer;
outline: none;
padding: 3px 7px;
position: relative;
text-decoration: none;
white-space: nowrap;
}
.entry.disabled {
color: graytext !important;
}
.entry.has-submenu {
padding-right: 20px;
}
.has-submenu::after {
content: '';
border-left: 6px solid;
border-top: 4px solid transparent;
border-bottom: 4px solid transparent;
display: inline-block;
margin: 4px;
position: absolute;
right: 3px;
}
.has-submenu:not(.focused) > .submenu {
display: none;
}
.submenu {
border-bottom: 0;
display: -webkit-flex;
display: flex;
-webkit-flex-direction: column;
flex-direction: column;
position: absolute;
margin: -1px 0;
}
.entry input {
margin: 0;
}

View File

@ -0,0 +1,5 @@
<div>
<span class="move">Thread Watcher <span id="watcher-status"></span></span>
<a class="menu-button" href="javascript:;">[<i></i>]</a>
</div>
<div id="watched-threads"></div>

View File

@ -50,8 +50,8 @@
"http": true, "http": true,
"https": true, "https": true,
"software": "foolfuuka", "software": "foolfuuka",
"boards": ["adv", "asp", "cm", "e", "i", "lgbt", "n", "o", "p", "pol", "s", "s4s", "t", "trv", "y"], "boards": ["adv", "asp", "cm", "d", "e", "i", "lgbt", "n", "o", "p", "pol", "s", "s4s", "t", "trv", "y"],
"files": ["adv", "asp", "cm", "e", "i", "lgbt", "n", "o", "p", "s", "s4s", "t", "trv", "y"] "files": ["cm", "d", "e", "i", "n", "o", "p", "s", "trv", "y"]
}, { }, {
"uid": 12, "uid": 12,
"name": "fap archive", "name": "fap archive",

View File

@ -27,7 +27,7 @@ Build =
date: data.now date: data.now
dateUTC: data.time dateUTC: data.time
comment: data.com comment: data.com
capReps: data.capcode_replies capcodeReplies: data.capcode_replies
# thread status # thread status
isSticky: !!data.sticky isSticky: !!data.sticky
isClosed: !!data.closed isClosed: !!data.closed
@ -59,7 +59,7 @@ Build =
postID, threadID, boardID postID, threadID, boardID
name, capcode, tripcode, uniqueID, email, subject, flagCode, flagName, date, dateUTC name, capcode, tripcode, uniqueID, email, subject, flagCode, flagName, date, dateUTC
isSticky, isClosed isSticky, isClosed
comment, capReps comment, capcodeReplies
file file
} = o } = o
isOP = postID is threadID isOP = postID is threadID
@ -191,26 +191,6 @@ Build =
else else
'' ''
capcodeReplies = ''
if capReps
generateCapcodeReplies = (capcodeType, array) ->
"<span class=smaller><span class=bold>#{
switch capcodeType
when 'admin'
'Administrator'
when 'mod'
'Moderator'
when 'developer'
'Developer'
} Repl#{if array.length > 1 then 'ies' else 'y'}:</span> #{
array.map (ID) ->
"<a href='/#{boardID}/res/#{threadID}#p#{ID}' class=quotelink>&gt;&gt;#{ID}</a>"
.join ' '
}</span><br>"
for capcodeType, array of capReps
capcodeReplies += generateCapcodeReplies capcodeType, array
capcodeReplies = "<br><br><span class=capcodeReplies>#{capcodeReplies}</span>"
container = $.el 'div', container = $.el 'div',
id: "pc#{postID}" id: "pc#{postID}"
className: "postContainer #{if isOP then 'op' else 'reply'}Container" className: "postContainer #{if isOP then 'op' else 'reply'}Container"
@ -221,4 +201,36 @@ Build =
continue if href[0] is '/' # Cross-board quote, or board link continue if href[0] is '/' # Cross-board quote, or board link
quote.href = "/#{boardID}/res/#{href}" # Fix pathnames quote.href = "/#{boardID}/res/#{href}" # Fix pathnames
Build.capcodeReplies {boardID, threadID, root: container, capcodeReplies}
container container
capcodeReplies: ({boardID, threadID, bq, root, capcodeReplies}) ->
return unless capcodeReplies
generateCapcodeReplies = (capcodeType, array) ->
"<span class=smaller><span class=bold>#{
switch capcodeType
when 'admin'
'Administrator'
when 'mod'
'Moderator'
when 'developer'
'Developer'
} Repl#{if array.length > 1 then 'ies' else 'y'}:</span> #{
array.map (ID) ->
"<a href='/#{boardID}/res/#{threadID}#p#{ID}' class=quotelink>&gt;&gt;#{ID}</a>"
.join ' '
}</span><br>"
html = []
for capcodeType, array of capcodeReplies
html.push generateCapcodeReplies capcodeType, array
bq or= $ 'blockquote', root
$.add bq, [
$.el 'br'
$.el 'br'
$.el 'span',
className: 'capcodeReplies'
innerHTML: html.join ''
]

View File

@ -57,12 +57,6 @@ Config =
true true
'Show dice that were entered into the email field.' 'Show dice that were entered into the email field.'
] ]
<% if (type !== 'crx') { %>
'Check for Updates': [
true
'Check for updated versions of <%= meta.name %>.'
]
<% } %>
'Show Updated Notifications': [ 'Show Updated Notifications': [
true true
'Show notifications when 4chan X is successfully updated.' 'Show notifications when 4chan X is successfully updated.'
@ -255,14 +249,6 @@ Config =
true true
'Adds a shortcut for the thread watcher, hides the watcher by default, and makes it scroll with the page.' 'Adds a shortcut for the thread watcher, hides the watcher by default, and makes it scroll with the page.'
] ]
'Auto Watch': [
true
'Automatically watch threads you start.'
]
'Auto Watch Reply': [
false
'Automatically watch threads you reply to.'
]
'Posting': 'Posting':
'Quick Reply': [ 'Quick Reply': [
@ -399,7 +385,25 @@ Config =
false false
'Advance to next post when contracting an expanded image.' 'Advance to next post when contracting an expanded image.'
] ]
threadWatcher:
'Current Board': [
false
'Only show watched threads from the current board.'
]
'Auto Watch': [
true
'Automatically watch threads you start.'
]
'Auto Watch Reply': [
false
'Automatically watch threads you reply to.'
]
'Auto Prune': [
false
'Automatically prune 404\'d threads.'
]
filter: filter:
name: """ name: """
# Filter any namefags: # Filter any namefags:

View File

@ -109,7 +109,6 @@ Header =
fourchannav = $.id 'boardNavDesktop' fourchannav = $.id 'boardNavDesktop'
if a = $ "a[href*='/#{g.BOARD}/']", fourchannav if a = $ "a[href*='/#{g.BOARD}/']", fourchannav
a.className = 'current' a.className = 'current'
boardList = $.el 'span', boardList = $.el 'span',
id: 'board-list' id: 'board-list'
innerHTML: "<span id=custom-board-list></span><span id=full-board-list hidden><span class='hide-board-list-container fourchanx-link'><a href=javascript:; class='hide-board-list-button'>&nbsp;-&nbsp;</a></span> #{fourchannav.innerHTML}</span>" innerHTML: "<span id=custom-board-list></span><span id=full-board-list hidden><span class='hide-board-list-container fourchanx-link'><a href=javascript:; class='hide-board-list-button'>&nbsp;-&nbsp;</a></span> #{fourchannav.innerHTML}</span>"

View File

@ -126,6 +126,7 @@ Main =
'Thread Stats': ThreadStats 'Thread Stats': ThreadStats
'Thread Updater': ThreadUpdater 'Thread Updater': ThreadUpdater
'Thread Watcher': ThreadWatcher 'Thread Watcher': ThreadWatcher
'Thread Watcher (Menu)': ThreadWatcher.menu
'Index Navigation': Nav 'Index Navigation': Nav
'Keybinds': Keybinds 'Keybinds': Keybinds
'Show Dice Roll': Dice 'Show Dice Roll': Dice
@ -205,9 +206,6 @@ Main =
Main.callbackNodes Thread, threads Main.callbackNodes Thread, threads
Main.callbackNodesDB Post, posts, -> Main.callbackNodesDB Post, posts, ->
$.event '4chanXInitFinished' $.event '4chanXInitFinished'
<% if (type !== 'crx') { %>
Main.checkUpdate()
<% } %>
if styleSelector = $.id 'styleSelector' if styleSelector = $.id 'styleSelector'
passLink = $.el 'a', passLink = $.el 'a',
@ -227,9 +225,6 @@ Main =
new Notification 'warning', 'Cookies need to be enabled on 4chan for <%= meta.name %> to properly function.', 30 new Notification 'warning', 'Cookies need to be enabled on 4chan for <%= meta.name %> to properly function.', 30
$.event '4chanXInitFinished' $.event '4chanXInitFinished'
<% if (type !== 'crx') { %>
Main.checkUpdate()
<% } %>
callbackNodes: (klass, nodes) -> callbackNodes: (klass, nodes) ->
# get the nodes' length only once # get the nodes' length only once
@ -303,27 +298,6 @@ Main =
obj.callback.isAddon = true obj.callback.isAddon = true
Klass::callbacks.push obj.callback Klass::callbacks.push obj.callback
<% if (type !== 'crx') { %>
message: (e) ->
{version} = e.data
if version and version isnt g.VERSION
el = $.el 'span',
innerHTML: "Update: <%= meta.name %> v#{version} is out, get it <a href=<%= meta.page %> target=_blank>here</a>."
new Notification 'info', el, 120
checkUpdate: ->
return unless Conf['Check for Updates'] and Main.isThisPageLegit()
now = Date.now()
$.get 'lastchecked', 0, ({lastchecked}) ->
if (lastchecked > now - $.DAY)
return
$.ready ->
$.on window, 'message', Main.message
$.set 'lastchecked', now
$.add d.head, $.el 'script',
src: '<%= meta.repo %>raw/<%= meta.mainBranch %>/latest.js'
<% } %>
handleErrors: (errors) -> handleErrors: (errors) ->
unless errors instanceof Array unless errors instanceof Array
error = errors error = errors

View File

@ -21,7 +21,8 @@ Settings =
else else
$.on d, '4chanXInitFinished', Settings.open $.on d, '4chanXInitFinished', Settings.open
$.set $.set
lastchecked: Date.now() archives: Conf['archives']
lastarchivecheck: now
previousversion: g.VERSION previousversion: g.VERSION
Settings.addSection 'Main', Settings.main Settings.addSection 'Main', Settings.main
@ -153,7 +154,6 @@ Settings =
data = data =
version: g.VERSION version: g.VERSION
date: now date: now
Conf['WatchedThreads'] = {}
for db in DataBoards for db in DataBoards
Conf[db] = boards: {} Conf[db] = boards: {}
# Make sure to export the most recent data. # Make sure to export the most recent data.
@ -265,13 +265,10 @@ Settings =
for key, val of Config.hotkeys when key of data.Conf for key, val of Config.hotkeys when key of data.Conf
data.Conf[key] = data.Conf[key].replace(/ctrl|alt|meta/g, (s) -> "#{s[0].toUpperCase()}#{s[1..]}").replace /(^|.+\+)[A-Z]$/g, (s) -> data.Conf[key] = data.Conf[key].replace(/ctrl|alt|meta/g, (s) -> "#{s[0].toUpperCase()}#{s[1..]}").replace /(^|.+\+)[A-Z]$/g, (s) ->
"Shift+#{s[0...-1]}#{s[-1..].toLowerCase()}" "Shift+#{s[0...-1]}#{s[-1..].toLowerCase()}"
data.Conf.WatchedThreads = data.WatchedThreads data.Conf['WatchedThreads'] = data.WatchedThreads
else if version[0] is '3' if data.Conf['WatchedThreads']
data = Settings.convertSettings data, data.Conf['watchedThreads'] = boards: ThreadWatcher.convert data.Conf['WatchedThreads']
'Reply Hiding': 'Reply Hiding Buttons' delete data.Conf['WatchedThreads']
'Thread Hiding': 'Thread Hiding Buttons'
'Bottom header': 'Bottom Header'
'Unread Tab Icon': 'Unread Favicon'
$.set data.Conf $.set data.Conf
convertSettings: (data, map) -> convertSettings: (data, map) ->
for prevKey, newKey of map for prevKey, newKey of map

View File

@ -57,18 +57,23 @@ $.extend = (object, properties) ->
object[key] = val object[key] = val
return return
$.ajax = (url, options, extra={}) -> $.ajax = do ->
{type, headers, upCallbacks, form, sync} = extra # Status Code 304: Not modified
r = new XMLHttpRequest() # With the `If-Modified-Since` header we only receive the HTTP headers and no body for 304 responses.
r.overrideMimeType 'text/html' # This saves a lot of bandwidth and CPU time for both the users and the servers.
type or= form and 'post' or 'get' lastModified = {}
r.open type, url, !sync (url, options, extra={}) ->
for key, val of headers {type, whenModified, upCallbacks, form, sync} = extra
r.setRequestHeader key, val r = new XMLHttpRequest()
$.extend r, options type or= form and 'post' or 'get'
$.extend r.upload, upCallbacks r.open type, url, !sync
r.send form if whenModified
r r.setRequestHeader 'If-Modified-Since', lastModified[url] or '0'
$.on r, 'load', -> lastModified[url] = r.getResponseHeader 'Last-Modified'
$.extend r, options
$.extend r.upload, upCallbacks
r.send form
r
$.cache = do -> $.cache = do ->
reqs = {} reqs = {}

View File

@ -1,10 +1,10 @@
DataBoards = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts'] DataBoards = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads']
class DataBoard class DataBoard
constructor: (@key, sync) -> constructor: (@key, sync, dontClean) ->
@data = Conf[key] @data = Conf[key]
$.sync key, @onSync.bind @ $.sync key, @onSync.bind @
@clean() @clean() unless dontClean
return unless sync return unless sync
# Chrome also fires the onChanged callback on the current tab, # Chrome also fires the onChanged callback on the current tab,
# so we only start syncing when we're ready. # so we only start syncing when we're ready.
@ -13,6 +13,9 @@ class DataBoard
@sync = sync @sync = sync
$.on d, '4chanXInitFinished', init $.on d, '4chanXInitFinished', init
save: ->
$.set @key, @data
delete: ({boardID, threadID, postID}) -> delete: ({boardID, threadID, postID}) ->
if postID if postID
delete @data.boards[boardID][threadID][postID] delete @data.boards[boardID][threadID][postID]
@ -22,7 +25,7 @@ class DataBoard
@deleteIfEmpty {boardID} @deleteIfEmpty {boardID}
else else
delete @data.boards[boardID] delete @data.boards[boardID]
$.set @key, @data @save()
deleteIfEmpty: ({boardID, threadID}) -> deleteIfEmpty: ({boardID, threadID}) ->
if threadID if threadID
@ -39,7 +42,7 @@ class DataBoard
(@data.boards[boardID] or= {})[threadID] = val (@data.boards[boardID] or= {})[threadID] = val
else else
@data.boards[boardID] = val @data.boards[boardID] = val
$.set @key, @data @save()
get: ({boardID, threadID, postID, defaultValue}) -> get: ({boardID, threadID, postID, defaultValue}) ->
if board = @data.boards[boardID] if board = @data.boards[boardID]
@ -67,8 +70,7 @@ class DataBoard
@data.lastChecked = now @data.lastChecked = now
for boardID of @data.boards for boardID of @data.boards
@ajaxClean boardID @ajaxClean boardID
@save()
$.set @key, @data
ajaxClean: (boardID) -> ajaxClean: (boardID) ->
$.cache "//api.4chan.org/#{boardID}/threads.json", (e) => $.cache "//api.4chan.org/#{boardID}/threads.json", (e) =>
@ -84,7 +86,7 @@ class DataBoard
threads[thread.no] = board[thread.no] threads[thread.no] = board[thread.no]
@data.boards[boardID] = threads @data.boards[boardID] = threads
@deleteIfEmpty {boardID} @deleteIfEmpty {boardID}
$.set @key, @data @save()
onSync: (data) -> onSync: (data) ->
@data = data or boards: {} @data = data or boards: {}

View File

@ -1,6 +1,16 @@
Polyfill = Polyfill =
init: -> init: ->
Polyfill.toBlob()
Polyfill.visibility() Polyfill.visibility()
toBlob: ->
HTMLCanvasElement::toBlob or= (cb) ->
data = atob @toDataURL()[22..]
# DataUrl to Binary code from Aeosynth's 4chan X repo
l = data.length
ui8a = new Uint8Array l
for i in [0...l]
ui8a[i] = data.charCodeAt i
cb new Blob [ui8a], type: 'image/png'
visibility: -> visibility: ->
# page visibility API # page visibility API
return unless 'webkitHidden' of document return unless 'webkitHidden' of document

View File

@ -169,8 +169,8 @@ ImageExpand =
{createSubEntry} = ImageExpand.menu {createSubEntry} = ImageExpand.menu
subEntries = [] subEntries = []
for key, conf of Config.imageExpansion for name, conf of Config.imageExpansion
subEntries.push createSubEntry key, conf subEntries.push createSubEntry name, conf[1]
$.event 'AddMenuEntry', $.event 'AddMenuEntry',
type: 'header' type: 'header'
@ -178,17 +178,16 @@ ImageExpand =
order: 105 order: 105
subEntries: subEntries subEntries: subEntries
createSubEntry: (type, config) -> createSubEntry: (name, desc) ->
label = $.el 'label', label = $.el 'label',
innerHTML: "<input type=checkbox name='#{type}'> #{type}" innerHTML: "<input type=checkbox name='#{name}'> #{name}"
title: desc
input = label.firstElementChild input = label.firstElementChild
if type in ['Fit width', 'Fit height'] if name in ['Fit width', 'Fit height']
$.on input, 'change', ImageExpand.cb.setFitness $.on input, 'change', ImageExpand.cb.setFitness
if config input.checked = Conf[name]
label.title = config[1] $.event 'change', null, input
input.checked = Conf[type] $.on input, 'change', $.cb.checked
$.event 'change', null, input
$.on input, 'change', $.cb.checked
el: label el: label
menuToggle: (e) -> menuToggle: (e) ->

View File

@ -16,8 +16,7 @@ ExpandComment =
callbacks: [] callbacks: []
cb: (e) -> cb: (e) ->
e.preventDefault() e.preventDefault()
post = Get.postFromNode @ ExpandComment.expand Get.postFromNode @
ExpandComment.expand post
expand: (post) -> expand: (post) ->
if post.nodes.longComment and !post.nodes.longComment.parentNode if post.nodes.longComment and !post.nodes.longComment.parentNode
$.replace post.nodes.shortComment, post.nodes.longComment $.replace post.nodes.shortComment, post.nodes.longComment
@ -55,6 +54,11 @@ ExpandComment =
href = quote.getAttribute 'href' href = quote.getAttribute 'href'
continue if href[0] is '/' # Cross-board quote, or board link continue if href[0] is '/' # Cross-board quote, or board link
quote.href = "/#{post.board}/res/#{href}" # Fix pathnames quote.href = "/#{post.board}/res/#{href}" # Fix pathnames
Build.capcodeReplies
boardID: post.board.ID
threadID: post.thread.ID
bq: clone
capcodeReplies: postObj.capcode_replies
post.nodes.shortComment = comment post.nodes.shortComment = comment
$.replace comment, clone $.replace comment, clone
post.nodes.comment = post.nodes.longComment = clone post.nodes.comment = post.nodes.longComment = clone

View File

@ -18,7 +18,6 @@ ThreadStats =
@postCountEl = $ '#post-count', sc @postCountEl = $ '#post-count', sc
@fileCountEl = $ '#file-count', sc @fileCountEl = $ '#file-count', sc
@pageCountEl = $ '#page-count', sc @pageCountEl = $ '#page-count', sc
@lastModified = '0'
Thread::callbacks.push Thread::callbacks.push
name: 'Thread Stats' name: 'Thread Stats'
@ -55,12 +54,10 @@ ThreadStats =
return return
setTimeout ThreadStats.fetchPage, 2 * $.MINUTE setTimeout ThreadStats.fetchPage, 2 * $.MINUTE
$.ajax "//api.4chan.org/#{ThreadStats.thread.board}/threads.json", onload: ThreadStats.onThreadsLoad, $.ajax "//api.4chan.org/#{ThreadStats.thread.board}/threads.json", onload: ThreadStats.onThreadsLoad,
headers: 'If-Modified-Since': ThreadStats.lastModified whenModified: true
onThreadsLoad: -> onThreadsLoad: ->
return if !Conf["Page Count in Stats"] return unless Conf["Page Count in Stats"] and @status is 200
ThreadStats.lastModified = @getResponseHeader 'Last-Modified'
return if @status isnt 200
pages = JSON.parse @response pages = JSON.parse @response
for page in pages for page in pages
for thread in page.threads for thread in page.threads

View File

@ -64,7 +64,6 @@ ThreadUpdater =
ThreadUpdater.root = @OP.nodes.root.parentNode ThreadUpdater.root = @OP.nodes.root.parentNode
ThreadUpdater.lastPost = +ThreadUpdater.root.lastElementChild.id.match(/\d+/)[0] ThreadUpdater.lastPost = +ThreadUpdater.root.lastElementChild.id.match(/\d+/)[0]
ThreadUpdater.outdateCount = 0 ThreadUpdater.outdateCount = 0
ThreadUpdater.lastModified = '0'
ThreadUpdater.cb.interval.call $.el 'input', value: Conf['Interval'] ThreadUpdater.cb.interval.call $.el 'input', value: Conf['Interval']
@ -136,9 +135,7 @@ ThreadUpdater =
when 200 when 200
g.DEAD = false g.DEAD = false
ThreadUpdater.parse JSON.parse(req.response).posts ThreadUpdater.parse JSON.parse(req.response).posts
ThreadUpdater.lastModified = req.getResponseHeader 'Last-Modified' ThreadUpdater.set 'timer', ThreadUpdater.getInterval()
if Conf['Auto Update']
ThreadUpdater.set 'timer', ThreadUpdater.getInterval()
when 404 when 404
g.DEAD = true g.DEAD = true
ThreadUpdater.set 'timer', null ThreadUpdater.set 'timer', null
@ -149,14 +146,8 @@ ThreadUpdater =
404: true 404: true
thread: ThreadUpdater.thread thread: ThreadUpdater.thread
else else
if Conf['Auto Update'] ThreadUpdater.outdateCount++
ThreadUpdater.outdateCount++ ThreadUpdater.set 'timer', ThreadUpdater.getInterval()
ThreadUpdater.set 'timer', ThreadUpdater.getInterval()
###
Status Code 304: Not modified
By sending the `If-Modified-Since` header we get a proper status code, and no response.
This saves bandwidth for both the user and the servers and avoid unnecessary computation.
###
[text, klass] = if req.status is 304 [text, klass] = if req.status is 304
[null, null] [null, null]
else else
@ -218,7 +209,7 @@ ThreadUpdater =
ThreadUpdater.req.abort() ThreadUpdater.req.abort()
url = "//api.4chan.org/#{ThreadUpdater.thread.board}/res/#{ThreadUpdater.thread}.json" url = "//api.4chan.org/#{ThreadUpdater.thread.board}/res/#{ThreadUpdater.thread}.json"
ThreadUpdater.req = $.ajax url, onloadend: ThreadUpdater.cb.load, ThreadUpdater.req = $.ajax url, onloadend: ThreadUpdater.cb.load,
headers: 'If-Modified-Since': ThreadUpdater.lastModified whenModified: true
updateThreadStatus: (title, OP) -> updateThreadStatus: (title, OP) ->
titleLC = title.toLowerCase() titleLC = title.toLowerCase()

View File

@ -1,113 +1,286 @@
ThreadWatcher = ThreadWatcher =
init: -> init: ->
return unless Conf['Thread Watcher'] return if !Conf['Thread Watcher']
@shortcut = sc = $.el 'a',
textContent: 'Watcher'
id: 'watcher-link'
href: 'javascript:;'
className: 'disabled'
@dialog = UI.dialog 'watcher', 'top: 50px; left: 0px;', @db = new DataBoard 'watchedThreads', @refresh, true
'<div class=move>Thread Watcher<a class=close href=javascript:;>×</a></div>' @dialog = UI.dialog 'thread-watcher', 'top: 50px; left: 0px;', """
<%= grunt.file.read('html/Monitoring/ThreadWatcher.html').replace(/>\s+</g, '><').trim() %>
"""
@status = $ '#watcher-status', @dialog
@list = @dialog.lastElementChild
$.on d, 'QRPostSuccessful', @cb.post $.on d, 'QRPostSuccessful', @cb.post
$.sync 'WatchedThreads', @refresh $.on d, 'ThreadUpdate', @cb.threadUpdate if g.VIEW is 'thread'
$.on sc, 'click', @toggleWatcher $.on d, '4chanXInitFinished', @ready
$.on $('.move>.close', ThreadWatcher.dialog), 'click', @toggleWatcher
if Conf['Toggleable Thread Watcher'] now = Date.now()
Header.addShortcut sc if (@db.data.lastChecked or 0) < now - 2 * $.HOUR
$.addClass doc, 'fixed-watcher' @db.data.lastChecked = now
ThreadWatcher.fetchAllStatus()
@db.save()
$.ready -> # XXX tmp conversion from old to new format
ThreadWatcher.refresh() $.get 'WatchedThreads', null, ({WatchedThreads}) ->
$.add d.body, ThreadWatcher.dialog return unless WatchedThreads
if Conf['Toggleable Thread Watcher'] for boardID, threads of ThreadWatcher.convert WatchedThreads
ThreadWatcher.dialog.hidden = true for threadID, data of threads
ThreadWatcher.db.set {boardID, threadID, val: data}
$.delete 'WatchedThreads'
Thread::callbacks.push Thread::callbacks.push
name: 'Thread Watcher' name: 'Thread Watcher'
cb: @node cb: @node
node: -> node: ->
favicon = $.el 'a', toggler = $.el 'img',
className: 'watch-thread-link' className: 'watcher-toggler'
href: 'javascript:;' $.on toggler, 'click', ThreadWatcher.cb.toggle
$.on favicon, 'click', ThreadWatcher.cb.toggle $.before $('input', @OP.nodes.post), toggler
$.before $('input', @OP.nodes.post), favicon
return if g.VIEW isnt 'thread' ready: ->
$.get 'AutoWatch', 0, (item) => $.off d, '4chanXInitFinished', ThreadWatcher.ready
return if item['AutoWatch'] isnt @ID return unless Main.isThisPageLegit()
ThreadWatcher.watch @ ThreadWatcher.refresh()
$.add d.body, ThreadWatcher.dialog
return unless Conf['Auto Watch']
$.get 'AutoWatch', 0, ({AutoWatch}) ->
return unless thread = g.BOARD.threads[AutoWatch]
ThreadWatcher.add thread
$.delete 'AutoWatch' $.delete 'AutoWatch'
refresh: (watched) ->
unless watched
$.get 'WatchedThreads', {}, ({WatchedThreads}) ->
ThreadWatcher.refresh WatchedThreads
return
nodes = [$('.move', ThreadWatcher.dialog)]
for board of watched
for id, props of watched[board]
x = $.el 'a',
textContent: '×'
className: 'close'
href: 'javascript:;'
$.on x, 'click', ThreadWatcher.cb.x
link = $.el 'a', props
link.title = link.textContent
div = $.el 'div'
$.add div, [x, $.tn(' '), link]
nodes.push div
$.rmAll ThreadWatcher.dialog
$.add ThreadWatcher.dialog, nodes
watched = watched[g.BOARD] or {}
for ID, thread of g.BOARD.threads
favicon = $ '.watch-thread-link', thread.OP.nodes.post
$[if ID of watched then 'addClass' else 'rmClass'] favicon, 'watched'
return
toggleWatcher: -> toggleWatcher: ->
$.toggleClass ThreadWatcher.shortcut, 'disabled' $.toggleClass ThreadWatcher.shortcut, 'disabled'
ThreadWatcher.dialog.hidden = !ThreadWatcher.dialog.hidden ThreadWatcher.dialog.hidden = !ThreadWatcher.dialog.hidden
cb: cb:
openAll: ->
return if $.hasClass @, 'disabled'
for a in $$ 'a[title]', ThreadWatcher.list
$.open a.href
$.event 'CloseMenu'
checkThreads: ->
return if $.hasClass @, 'disabled'
ThreadWatcher.fetchAllStatus()
pruneDeads: ->
return if $.hasClass @, 'disabled'
for {boardID, threadID, data} in ThreadWatcher.getAll() when data.isDead
delete ThreadWatcher.db.data.boards[boardID][threadID]
ThreadWatcher.db.deleteIfEmpty {boardID}
ThreadWatcher.db.save()
ThreadWatcher.refresh()
$.event 'CloseMenu'
toggle: -> toggle: ->
ThreadWatcher.toggle Get.postFromNode(@).thread ThreadWatcher.toggle Get.postFromNode(@).thread
x: -> rm: ->
thread = @nextElementSibling.pathname.split '/' [boardID, threadID] = @parentNode.dataset.fullID.split '.'
ThreadWatcher.unwatch thread[1], thread[3] ThreadWatcher.rm boardID, +threadID
post: (e) -> post: (e) ->
{board, postID, threadID} = e.detail {board, postID, threadID} = e.detail
if postID is threadID if postID is threadID
if Conf['Auto Watch'] if Conf['Auto Watch']
$.set 'AutoWatch', threadID $.set 'AutoWatch', threadID
else if Conf['Auto Watch Reply'] else if Conf['Auto Watch Reply']
ThreadWatcher.watch board.threads[threadID] ThreadWatcher.add board.threads[threadID]
threadUpdate: (e) ->
{thread} = e.detail
return unless e.detail[404] and ThreadWatcher.db.get {boardID: thread.board.ID, threadID: thread.ID}
# Update 404 status.
ThreadWatcher.add thread
fetchCount:
fetched: 0
fetching: 0
fetchAllStatus: ->
ThreadWatcher.status.textContent = '...'
for thread in ThreadWatcher.getAll()
ThreadWatcher.fetchStatus thread
return
fetchStatus: ({boardID, threadID, data}) ->
return if data.isDead
{fetchCount} = ThreadWatcher
fetchCount.fetching++
$.ajax "//api.4chan.org/#{boardID}/res/#{threadID}.json",
onloadend: ->
fetchCount.fetched++
if fetchCount.fetched is fetchCount.fetching
fetchCount.fetched = 0
fetchCount.fetching = 0
status = ''
else
status = "#{Math.round fetchCount.fetched / fetchCount.fetching * 100}%"
ThreadWatcher.status.textContent = status
return if @status isnt 404
if Conf['Auto Prune']
ThreadWatcher.rm boardID, threadID
else
data.isDead = true
ThreadWatcher.db.set {boardID, threadID, val: data}
ThreadWatcher.refresh()
,
type: 'head'
getAll: ->
all = []
for boardID, threads of ThreadWatcher.db.data.boards
if Conf['Current Board'] and boardID isnt g.BOARD.ID
continue
for threadID, data of threads
all.push {boardID, threadID, data}
all
makeLine: (boardID, threadID, data) ->
x = $.el 'a',
textContent: '×'
href: 'javascript:;'
$.on x, 'click', ThreadWatcher.cb.rm
if data.isDead
href = Redirect.to 'thread', {boardID, threadID}
link = $.el 'a',
href: href or "/#{boardID}/res/#{threadID}"
textContent: data.excerpt
title: data.excerpt
div = $.el 'div'
fullID = "#{boardID}.#{threadID}"
div.dataset.fullID = fullID
$.addClass div, 'current' if g.VIEW is 'thread' and fullID is "#{g.BOARD}.#{g.THREADID}"
$.addClass div, 'dead-thread' if data.isDead
$.add div, [x, $.tn(' '), link]
div
refresh: ->
nodes = []
for {boardID, threadID, data} in ThreadWatcher.getAll()
nodes.push ThreadWatcher.makeLine boardID, threadID, data
{list} = ThreadWatcher
$.rmAll list
$.add list, nodes
for threadID, thread of g.BOARD.threads
toggler = $ '.watcher-toggler', thread.OP.nodes.post
toggler.src = if ThreadWatcher.db.get {boardID: thread.board.ID, threadID}
Favicon.default
else
Favicon.empty
for refresher in ThreadWatcher.menu.refreshers
refresher()
return
toggle: (thread) -> toggle: (thread) ->
unless $.hasClass $('.watch-thread-link', thread.OP.nodes.post), 'watched' boardID = thread.board.ID
ThreadWatcher.watch thread threadID = thread.ID
if ThreadWatcher.db.get {boardID, threadID}
ThreadWatcher.rm boardID, threadID
else else
ThreadWatcher.unwatch thread.board, thread.ID ThreadWatcher.add thread
add: (thread) ->
data = {}
boardID = thread.board.ID
threadID = thread.ID
if thread.isDead
if Conf['Auto Prune'] and ThreadWatcher.db.get {boardID, threadID}
ThreadWatcher.rm boardID, threadID
return
data.isDead = true
data.excerpt = Get.threadExcerpt thread
ThreadWatcher.db.set {boardID, threadID, val: data}
ThreadWatcher.refresh()
rm: (boardID, threadID) ->
ThreadWatcher.db.delete {boardID, threadID}
ThreadWatcher.refresh()
unwatch: (board, threadID) -> convert: (oldFormat) ->
$.get 'WatchedThreads', {}, (item) -> newFormat = {}
watched = item['WatchedThreads'] for boardID, threads of oldFormat
delete watched[board][threadID] for threadID, data of threads
delete watched[board] unless Object.keys(watched[board]).length (newFormat[boardID] or= {})[threadID] = excerpt: data.textContent
ThreadWatcher.refresh watched newFormat
$.set 'WatchedThreads', watched
watch: (thread) -> menu:
$.get 'WatchedThreads', {}, (item) -> refreshers: []
watched = item['WatchedThreads'] init: ->
watched[thread.board] or= {} return if !Conf['Thread Watcher']
watched[thread.board][thread] = menu = new UI.Menu 'thread watcher'
href: "/#{thread.board}/res/#{thread}" $.on $('.menu-button', ThreadWatcher.dialog), 'click', (e) ->
textContent: Get.threadExcerpt thread menu.toggle e, @, ThreadWatcher
ThreadWatcher.refresh watched @addHeaderMenuEntry()
$.set 'WatchedThreads', watched @addMenuEntries()
addHeaderMenuEntry: ->
return if g.VIEW isnt 'thread'
entryEl = $.el 'a',
href: 'javascript:;'
$.event 'AddMenuEntry',
type: 'header'
el: entryEl
order: 60
$.on entryEl, 'click', -> ThreadWatcher.toggle g.threads["#{g.BOARD}.#{g.THREADID}"]
@refreshers.push ->
[addClass, rmClass, text] = if $ '.current', ThreadWatcher.list
['unwatch-thread', 'watch-thread', 'Unwatch thread']
else
['watch-thread', 'unwatch-thread', 'Watch thread']
$.addClass entryEl, addClass
$.rmClass entryEl, rmClass
entryEl.textContent = text
addMenuEntries: ->
entries = []
# `Open all` entry
entries.push
cb: ThreadWatcher.cb.openAll
entry:
type: 'thread watcher'
el: $.el 'a',
textContent: 'Open all threads'
refresh: -> (if ThreadWatcher.list.firstElementChild then $.rmClass else $.addClass) @el, 'disabled'
# `Check 404'd threads` entry
entries.push
cb: ThreadWatcher.cb.checkThreads
entry:
type: 'thread watcher'
el: $.el 'a',
textContent: 'Check 404\'d threads'
refresh: -> (if $('div:not(.dead-thread)', ThreadWatcher.list) then $.rmClass else $.addClass) @el, 'disabled'
# `Prune 404'd threads` entry
entries.push
cb: ThreadWatcher.cb.pruneDeads
entry:
type: 'thread watcher'
el: $.el 'a',
textContent: 'Prune 404\'d threads'
refresh: -> (if $('.dead-thread', ThreadWatcher.list) then $.rmClass else $.addClass) @el, 'disabled'
# `Settings` entries:
subEntries = []
for name, conf of Config.threadWatcher
subEntries.push @createSubEntry name, conf[1]
entries.push
entry:
type: 'thread watcher'
el: $.el 'span',
textContent: 'Settings'
subEntries: subEntries
for {entry, cb, refresh} in entries
entry.el.href = 'javascript:;' if entry.el.nodeName is 'A'
$.on entry.el, 'click', cb if cb
@refreshers.push refresh.bind entry if refresh
$.event 'AddMenuEntry', entry
createSubEntry: (name, desc) ->
entry =
type: 'thread watcher'
el: $.el 'label',
innerHTML: "<input type=checkbox name='#{name}'> #{name}"
title: desc
input = entry.el.firstElementChild
input.checked = Conf[name]
$.on input, 'change', $.cb.checked
$.on input, 'change', ThreadWatcher.refresh if name is 'Current Board'
entry

View File

@ -660,21 +660,9 @@ QR =
cv.width = img.width = width cv.width = img.width = width
cv.getContext('2d').drawImage img, 0, 0, width, height cv.getContext('2d').drawImage img, 0, 0, width, height
URL.revokeObjectURL fileURL URL.revokeObjectURL fileURL
applyBlob = (blob) => cv.toBlob (blob) =>
@URL = URL.createObjectURL blob @URL = URL.createObjectURL blob
@nodes.el.style.backgroundImage = "url(#{@URL})" @nodes.el.style.backgroundImage = "url(#{@URL})"
if cv.toBlob
cv.toBlob applyBlob
return
data = atob cv.toDataURL().split(',')[1]
# DataUrl to Binary code from Aeosynth's 4chan X repo
l = data.length
ui8a = new Uint8Array l
for i in [0...l]
ui8a[i] = data.charCodeAt i
applyBlob new Blob [ui8a], type: 'image/png'
fileURL = URL.createObjectURL @file fileURL = URL.createObjectURL @file
img.src = fileURL img.src = fileURL

View File

@ -16,7 +16,6 @@ QuoteYou =
cb: @node cb: @node
node: -> node: ->
# Stop there if it's a clone.
return if @isClone return if @isClone
if @info.yours if @info.yours