Merge branch 'v3'

Conflicts:
	builds/crx/script.js
	src/General/Config.coffee
	src/General/Settings.coffee
	src/Miscellaneous/ExpandComment.coffee
	src/Monitoring/ThreadWatcher.coffee
This commit is contained in:
Zixaphir 2013-08-12 19:22:37 -07:00
commit 6c2c9b281a
31 changed files with 3309 additions and 1196 deletions

View File

@ -16,6 +16,18 @@
**MayhemYDG**:
- **New feature**: `Show Dice Roll` (with @carboncopy)
- 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 flag filtering on /sp/ and /int/.
- Update archives. (with @woxxy and @proplex)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

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

View File

@ -50,8 +50,8 @@ Redirect =
http: true
https: true
software: 'foolfuuka'
boards: ['adv', 'asp', 'cm', 'i', 'lgbt', 'n', 'o', 'p', 's4s', 't', 'trv']
files: ['adv', 'asp', 'cm', 'i', 'lgbt', 'n', 'o', 'p', 's4s', 't', 'trv']
boards: ['adv', 'asp', 'cm', 'd', 'e', 'i', 'lgbt', 'n', 'o', 'p', 'pol', 's', 's4s', 't', 'trv', 'y']
files: ['cm', 'd', 'e', 'i', 'n', 'o', 'p', 's', 'trv', 'y']
'Foolz Beta':
domain: 'beta.foolz.us'

View File

@ -27,7 +27,7 @@ Build =
date: data.now
dateUTC: data.time
comment: data.com
capReps: data.capcode_replies
capcodeReplies: data.capcode_replies
# thread status
isSticky: !!data.sticky
isClosed: !!data.closed
@ -59,7 +59,7 @@ Build =
postID, threadID, boardID
name, capcode, tripcode, uniqueID, email, subject, flagCode, flagName, date, dateUTC
isSticky, isClosed
comment, capReps
comment, capcodeReplies
file
} = o
isOP = postID is threadID
@ -191,26 +191,6 @@ Build =
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',
id: "pc#{postID}"
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
quote.href = "/#{boardID}/res/#{href}" # Fix pathnames
Build.capcodeReplies {boardID, threadID, root: container, capcodeReplies}
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

@ -53,12 +53,6 @@ Config =
true
'Show dice that were entered into the email field.'
]
<% if (type !== 'crx') { %>
'Check for Updates': [
true
'Check for updated versions of <%= meta.name %>.'
]
<% } %>
'Color User IDs': [
false
'Assign unique colors to user IDs on boards that use them'
@ -239,14 +233,6 @@ Config =
true
'Bookmark threads.'
]
'Auto Watch': [
true
'Automatically watch threads you start.'
]
'Auto Watch Reply': [
false
'Automatically watch threads you reply to.'
]
'Posting':
'Header Shortcut': [
@ -754,6 +740,24 @@ Config =
['before', 'after']
]
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:
name: """
# Filter any namefags:

View File

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

View File

@ -163,6 +163,7 @@ Main =
'Thread Updater': ThreadUpdater
'Thread Stats': ThreadStats
'Thread Watcher': ThreadWatcher
'Thread Watcher (Menu)': ThreadWatcher.menu
'Index Navigation': Nav
'Keybinds': Keybinds
'Show Dice Roll': Dice
@ -204,9 +205,6 @@ Main =
Main.callbackNodes Thread, threads
Main.callbackNodesDB Post, posts, ->
$.event '4chanXInitFinished'
<% if (type !== 'crx') { %>
Main.checkUpdate()
<% } %>
if styleSelector = $.id 'styleSelector'
passLink = $.el 'a',
@ -226,9 +224,6 @@ Main =
new Notification 'warning', 'Cookies need to be enabled on 4chan for <%= meta.name %> to properly function.', 30
$.event '4chanXInitFinished'
<% if (type !== 'crx') { %>
Main.checkUpdate()
<% } %>
callbackNodes: (klass, nodes) ->
# get the nodes' length only once
@ -302,27 +297,6 @@ Main =
obj.callback.isAddon = true
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) ->
unless errors instanceof Array
error = errors

View File

@ -29,7 +29,8 @@ Settings =
else
$.on d, '4chanXInitFinished', Settings.open
$.set
lastchecked: Date.now()
archives: Conf['archives']
lastarchivecheck: now
previousversion: g.VERSION
Settings.addSection 'Style', Settings.style
@ -173,7 +174,6 @@ Settings =
data =
version: g.VERSION
date: now
Conf['WatchedThreads'] = {}
for db in DataBoards
Conf[db] = boards: {}
# Make sure to export the most recent data.
@ -220,7 +220,9 @@ Settings =
reader.readAsText file
loadSettings: (data) ->
version = data.version.split '.'
if data.Conf['WatchedThreads']
data.Conf['watchedThreads'] = boards: ThreadWatcher.convert data.Conf['WatchedThreads']
delete data.Conf['WatchedThreads']
$.set data.Conf
convertSettings: (data, map) ->

View File

@ -1,7 +1,7 @@
#navtopright .exlinksOptionsLink::after,
#main-menu,
body > div.navLinks > a:first-of-type::after,
.slideout-watcher #watcher::after,
.slideout-watcher #thread-watcher::after,
.announcements-slideout #globalMessage::after,
#boardNavDesktopFoot::after,
#img-controls,
@ -26,7 +26,7 @@ body::after {
.invisible-icons #navtopright .exlinksOptionsLink::after,
.invisible-icons #main-menu,
.invisible-icons body > div.navLinks > a:first-of-type::after,
.invisible-icons.slideout-watcher #watcher::after,
.invisible-icons.slideout-watcher #thread-watcher::after,
.invisible-icons.announcements-slideout #globalMessage::after,
.invisible-icons #boardNavDesktopFoot::after,
.invisible-icons #img-controls,
@ -36,7 +36,7 @@ body::after {
}
#navtopright .exlinksOptionsLink,
body > div.navLinks > a:first-of-type,
#{if Conf['Slideout Watcher'] then '#watcher,' else ''}
#{if Conf['Slideout Watcher'] then '#thread-watcher,' else ''}
#{if Conf['Announcements'] is 'slideout' then '#globalMessage,' else ''}
#boardNavDesktopFoot,
#catalog {
@ -44,7 +44,7 @@ body > div.navLinks > a:first-of-type,
}
#navtopright .exlinksOptionsLink:hover,
body > div.navLinks > a:first-of-type:hover,
.slideout-watcher #watcher:hover,
.slideout-watcher #thread-watcher:hover,
.announcements-slideout #globalMessage:hover,
#boardNavDesktopFoot:hover,
#img-controls,
@ -60,7 +60,7 @@ body > div.navLinks > a:first-of-type::after {
cursor: pointer;
background-position: 0 -15px;
}
.slideout-watcher #watcher::after {
.slideout-watcher #thread-watcher::after {
background-position: 0 -30px;
}
.announcements-slideout #globalMessage::after {
@ -90,7 +90,7 @@ body > div.navLinks > a:first-of-type::after {
#main-menu:hover,
#navtopright .exlinksOptionsLink:hover::after,
#qr #qrtab,
.slideout-watcher #watcher:hover::after,
.slideout-watcher #thread-watcher:hover::after,
.thumbnail#selected,
div.navLinks > a:first-of-type:hover::after,
#catalog:hover::after,

View File

@ -14,7 +14,7 @@ body::after {
#{align}: #{position[i++]}px;
}
/* Watcher */
.slideout-watcher #watcher::after {
.slideout-watcher #thread-watcher::after {
#{align}: #{position[i++]}px;
}
/* ExLinks */
@ -55,7 +55,7 @@ body::after {
#boardNavDesktopFoot::after,
#navtopright .exlinksOptionsLink::after,
#main-menu,
.slideout-watcher #watcher::after,
.slideout-watcher #thread-watcher::after,
.announcements-slideout #globalMessage::after,
#img-controls,
#fappeTyme,
@ -64,7 +64,7 @@ div.navLinks > a:first-of-type::after,
top: 1px !important;
}
.slideout-watcher #globalMessage,
.slideout-watcher #watcher,
.slideout-watcher #thread-watcher,
#boardNavDesktopFoot {
top: 16px !important;
}

View File

@ -20,8 +20,8 @@ body::after {
top: #{position[i++]}px;
}
/* Watcher */
.slideout-watcher #watcher,
.slideout-watcher #watcher::after {
.slideout-watcher #thread-watcher,
.slideout-watcher #thread-watcher::after {
top: #{position[i++]}px !important;
}
/* ExLinks */
@ -58,21 +58,21 @@ body::after {
#globalMessage::after,
#img-controls,
#fappeTyme,
.slideout-watcher #watcher::after,
.slideout-watcher #thread-watcher::after,
#catalog::after,
div.navLinks > a:first-of-type::after {
#{align}: 3px !important;
}
#boardNavDesktopFoot,
#globalMessage,
.slideout-watcher #watcher.dialog {
.slideout-watcher #thread-watcher.dialog {
<%= sizing %>: border-box;
width: 232px !important;
#{align}: 18px !important;
}
.sidebar-large #boardNavDesktopFoot,
.sidebar-large #globalMessage,
.sidebar-large #watcher {
.sidebar-large #thread-watcher {
width: 288px !important;
}
.fourchan-ss-navigation.fixed.top #header-bar,

View File

@ -482,7 +482,7 @@ th {
.icons-4chan-ss #navtopright .exlinksOptionsLink::after,
.icons-4chan-ss #main-menu,
.icons-4chan-ss .navLinks > a:first-of-type::after,
.icons-4chan-ss #watcher::after,
.icons-4chan-ss #thread-watcher::after,
.icons-4chan-ss #globalMessage::after,
.icons-4chan-ss #boardNavDesktopFoot::after,
.icons-4chan-ss #img-controls,
@ -493,7 +493,7 @@ th {
.icons-oneechan #navtopright .exlinksOptionsLink::after,
.icons-oneechan #main-menu,
.icons-oneechan .navLinks > a:first-of-type::after,
.icons-oneechan #watcher::after,
.icons-oneechan #thread-watcher::after,
.icons-oneechan #globalMessage::after,
.icons-oneechan #boardNavDesktopFoot::after,
.icons-oneechan #img-controls,
@ -751,53 +751,53 @@ th {
display: none;
}
/* Watcher */
#watcher {
#thread-watcher {
position: fixed;
z-index: 14;
padding: 2px;
}
#watcher {
#thread-watcher {
width: 200px;
}
#watcher:not(:hover) {
#thread-watcher:not(:hover) {
max-height: 200px;
overflow: hidden;
}
.rounded-edges #watcher {
.rounded-edges #thread-watcher {
border-radius: 3px;
}
#watcher > div {
#thread-watcher > div {
max-height: 1.3em;
overflow: hidden;
}
.slideout-watcher #watcher {
.slideout-watcher #thread-watcher {
<%= sizing %>: border-box;
width: 248px;
}
.slideout-watcher.sidebar-large #boardNavDesktopFoot {
width: 299px;
}
.slideout-watcher.sidebar-location-right #watcher {
.slideout-watcher.sidebar-location-right #thread-watcher {
left: auto !important;
right: 2px !important;
}
.slideout-watcher.sidebar-location-left #watcher {
.slideout-watcher.sidebar-location-left #thread-watcher {
right: auto !important;
left: 2px !important;
}
.slideout-watcher #watcher .move {
.slideout-watcher #thread-watcher .move {
cursor: default;
}
.slideout-watcher.underline-links #watcher .move {
.slideout-watcher.underline-links #thread-watcher .move {
text-decoration: underline;
}
.slideout-watcher #watcher > div {
.slideout-watcher #thread-watcher > div {
overflow: hidden;
}
.slideout-watcher #watcher:hover {
.slideout-watcher #thread-watcher:hover {
overflow-y: auto;
}
.slideout-watcher #watcher:not(:hover) {
.slideout-watcher #thread-watcher:not(:hover) {
height: 0;
overflow: hidden;
border: 0 none;

View File

@ -97,10 +97,10 @@ a {
#qr {
z-index: 30;
}
#watcher {
#thread-watcher {
z-index: 8;
}
:root.fixed-watcher #watcher {
:root.fixed-watcher #thread-watcher {
z-index: 20;
}
.fixed #header-bar {
@ -465,10 +465,10 @@ a.hide-announcement {
}
/* Thread Watcher */
#watcher {
#thread-watcher {
position: absolute;
}
#watcher {
#thread-watcher {
padding-bottom: 3px;
overflow: hidden;
white-space: nowrap;
@ -476,27 +476,27 @@ a.hide-announcement {
max-height: 92%;
overflow-y: auto;
}
:root.fixed-watcher #watcher {
:root.fixed-watcher #thread-watcher {
position: fixed;
}
:root:not(.fixed-watcher) #watcher:not(:hover) {
:root:not(.fixed-watcher) #thread-watcher:not(:hover) {
max-height: 210px;
overflow-y: hidden;
}
#watcher > .move {
#thread-watcher > .move {
padding-top: 3px;
}
#watcher > div {
#thread-watcher > div {
max-width: 250px;
overflow: hidden;
padding-left: 3px;
padding-right: 3px;
text-overflow: ellipsis;
}
#watcher a {
#thread-watcher a {
text-decoration: none;
}
#watcher .move>.close {
#thread-watcher .move>.close {
position: absolute;
right: 0px;
top: 0px;

View File

@ -165,8 +165,8 @@ textarea.field:focus {
#menu,
#selectrice,
#themeConf,
#watcher,
#watcher:hover,
#thread-watcher,
#thread-watcher:hover,
.announcements-slideout #globalMessage,
.dialog,
.post-form-style-float #qr,
@ -343,7 +343,7 @@ a .name {
#navtopright .exlinksOptionsLink::after,
#main-menu,
.navLinks > a:first-of-type::after,
#watcher::after,
#thread-watcher::after,
#globalMessage::after,
#boardNavDesktopFoot::after,
#img-controls,

View File

@ -54,6 +54,6 @@
#{if isOP then '' else fileHTML}
<blockquote class=postMessage id=m#{postID}>#{comment or ''}#{capcodeReplies}</blockquote>#{" "}
<blockquote class=postMessage id=m#{postID}>#{comment or ''}</blockquote>#{" "}
</div>"""

View File

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

View File

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

View File

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

View File

@ -1,6 +1,16 @@
Polyfill =
init: ->
Polyfill.toBlob()
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: ->
# page visibility API
return unless 'webkitHidden' of document

View File

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

View File

@ -4,23 +4,20 @@ Linkify =
@regString = if Conf['Allow False Positives']
///(
\b(
[-a-z]+://
|
[a-z]{3,}\.[-a-z0-9]+\.[a-z]
|
[-a-z0-9]+\.[a-z]
|
[\d]+\.[\d]+\.[\d]+\.[\d]+/
|
[a-z]{3,}:[a-z0-9?]
|
[^\s@]+@[a-z0-9.-]+\.[a-z0-9]
)
[^\s'"]+
)///gi
[-a-z]+://
|
[a-z]{3,}\.[-a-z0-9]+\.[a-z]
|
[-a-z0-9]+\.[a-z]
|
[\d]+\.[\d]+\.[\d]+\.[\d]+/
|
[a-z]{3,}:[a-z0-9?]
|
[^\s@]+@[a-z0-9.-]+\.[a-z0-9]
)///i
else
/(((magnet|mailto)\:|(www\.)|(news|(ht|f)tp(s?))\:\/\/){1}\S+)/gi
/(((magnet|mailto)\:|(www\.)|(news|(ht|f)tp(s?))\:\/\/){1})/i
if Conf['Comment Expansion']
ExpandComment.callbacks.push @node
@ -43,16 +40,44 @@ Linkify =
return
test = /[^\s'"]+/g
space = /[\s'"]/
snapshot = $.X './/br|.//text()', @nodes.comment
i = 0
while node = snapshot.snapshotItem i++
links = []
{data} = node
continue if node.parentElement.nodeName is "A" or not data
continue if node.parentElement.nodeName is "A"
links = []
while result = test.exec data
{index} = result
endNode = node
if (length = index + result[0].length) is data.length
if Linkify.regString.test node.data
Linkify.regString.lastIndex = 0
Linkify.gatherLinks snapshot, @, node, links, i
while (saved = snapshot.snapshotItem i++)
break if saved.nodeName is 'BR'
endNode = saved
{length} = saved.data
if end = space.exec saved.data
length = end.index
i--
break
if length is endNode.data.length then test.lastIndex = 0
range = Linkify.makeRange node, endNode, index, length
if link = Linkify.regString.exec text = range.toString()
if lIndex = link.index
range.setStart node, lIndex + index
links.push [range, text]
break
else
if link = Linkify.regString.exec result[0]
range = Linkify.makeRange node, node, link.index, link.length
links.push [range, link]
for range in links.reverse()
@nodes.links.push Linkify.makeLink range, @
@ -68,66 +93,29 @@ Linkify =
return
gatherLinks: (snapshot, post, node, links, i) ->
{data} = node
len = data.length
while (match = Linkify.regString.exec data)
{index} = match
link = match[0]
len2 = index + link.length
break if len is len2
range = document.createRange();
range.setStart node, index
range.setEnd node, len2
links.push range
Linkify.regString.lastIndex = 0
if match
links.push Linkify.seek snapshot, post, node, links, match, i
return
seek: (snapshot, post, node, links, match, i) ->
link = match[0]
range = document.createRange()
range.setStart node, match.index
while (next = snapshot.snapshotItem i++) and next.nodeName isnt 'BR'
node = next
data = node.data
if result = /[\s'"]/.exec data
{index} = result
range.setEnd node, index
Linkify.regString.lastIndex = index
Linkify.gatherLinks snapshot, post, node, links, i
return range
if range.collapsed
range.setEnd node, node.data.length
makeRange: (startNode, endNode, startOffset, endOffset) ->
range = document.createRange();
range.setStart startNode, startOffset
range.setEnd endNode, endOffset
range
makeLink: (range) ->
link = range.toString()
link =
if link.contains ':'
link
makeLink: ([range, text]) ->
text
text =
if text.contains ':'
text
else (
if link.contains '@'
if text.contains '@'
'mailto:'
else
'http://'
) + link
) + text
a = $.el 'a',
className: 'linkify'
rel: 'nofollow noreferrer'
target: '_blank'
href: link
href: text
$.add a, range.extractContents()
range.insertNode a
a

View File

@ -19,9 +19,8 @@ ExpandComment =
cb: (e) ->
e.preventDefault()
post = Get.postFromNode @
ExpandComment.expand post
ExpandComment.expand Get.postFromNode @
expand: (post) ->
if post.nodes.longComment and !post.nodes.longComment.parentNode
$.replace post.nodes.shortComment, post.nodes.longComment
@ -61,6 +60,11 @@ ExpandComment =
href = quote.getAttribute 'href'
continue if href[0] is '/' # Cross-board quote, or board link
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
$.replace comment, clone
post.nodes.comment = post.nodes.longComment = clone

View File

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

View File

@ -64,7 +64,6 @@ ThreadUpdater =
ThreadUpdater.root = @OP.nodes.root.parentNode
ThreadUpdater.lastPost = +ThreadUpdater.root.lastElementChild.id.match(/\d+/)[0]
ThreadUpdater.outdateCount = 0
ThreadUpdater.lastModified = '0'
ThreadUpdater.cb.interval.call $.el 'input', value: Conf['Interval']
@ -137,9 +136,7 @@ ThreadUpdater =
when 200
g.DEAD = false
ThreadUpdater.parse JSON.parse(req.response).posts
ThreadUpdater.lastModified = req.getResponseHeader 'Last-Modified'
if Conf['Auto Update']
ThreadUpdater.set 'timer', ThreadUpdater.getInterval()
ThreadUpdater.set 'timer', ThreadUpdater.getInterval()
when 404
g.DEAD = true
ThreadUpdater.set 'timer', null
@ -150,14 +147,8 @@ ThreadUpdater =
404: true
thread: ThreadUpdater.thread
else
if Conf['Auto Update']
ThreadUpdater.outdateCount++
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.
###
ThreadUpdater.outdateCount++
ThreadUpdater.set 'timer', ThreadUpdater.getInterval()
[text, klass] = if req.status is 304
[null, null]
else
@ -219,7 +210,7 @@ ThreadUpdater =
ThreadUpdater.req.abort()
url = "//api.4chan.org/#{ThreadUpdater.thread.board}/res/#{ThreadUpdater.thread}.json"
ThreadUpdater.req = $.ajax url, onloadend: ThreadUpdater.cb.load,
headers: 'If-Modified-Since': ThreadUpdater.lastModified
whenModified: true
updateThreadStatus: (title, OP) ->
titleLC = title.toLowerCase()

View File

@ -2,98 +2,282 @@ ThreadWatcher =
init: ->
return unless Conf['Thread Watcher']
@dialog = UI.dialog 'watcher', 'top: 50px; left: 0px;',
'<div class=move>Thread Watcher</div>'
@db = new DataBoard 'watchedThreads', @refresh, true
@dialog = UI.dialog 'thread-watcher', 'top: 50px; left: 0px;', """
<%= grunt.file.read('src/General/html/Monitoring/ThreadWatcher.html').replace(/>\s+</g, '><').trim() %>
"""
@status = $ '#watcher-status', @dialog
@list = @dialog.lastElementChild
$.on d, 'QRPostSuccessful', @cb.post
$.sync 'WatchedThreads', @refresh
$.on d, 'ThreadUpdate', @cb.threadUpdate if g.VIEW is 'thread'
$.on d, '4chanXInitFinished', @ready
$.ready ->
ThreadWatcher.refresh()
$.add d.body, ThreadWatcher.dialog
now = Date.now()
if (@db.data.lastChecked or 0) < now - 2 * $.HOUR
@db.data.lastChecked = now
ThreadWatcher.fetchAllStatus()
@db.save()
# XXX tmp conversion from old to new format
$.get 'WatchedThreads', null, ({WatchedThreads}) ->
return unless WatchedThreads
for boardID, threads of ThreadWatcher.convert WatchedThreads
for threadID, data of threads
ThreadWatcher.db.set {boardID, threadID, val: data}
$.delete 'WatchedThreads'
Thread::callbacks.push
name: 'Thread Watcher'
cb: @node
node: ->
favicon = $.el 'a',
className: 'watch-thread-link'
href: 'javascript:;'
$.on favicon, 'click', ThreadWatcher.cb.toggle
$.before $('input', @OP.nodes.post), favicon
return if g.VIEW isnt 'thread'
$.get 'AutoWatch', 0, (item) =>
return if item['AutoWatch'] isnt @ID
ThreadWatcher.watch @
toggler = $.el 'img',
className: 'watcher-toggler'
$.on toggler, 'click', ThreadWatcher.cb.toggle
$.before $('input', @OP.nodes.post), toggler
ready: ->
$.off d, '4chanXInitFinished', ThreadWatcher.ready
return unless Main.isThisPageLegit()
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'
refresh: (watched) ->
unless watched
$.get 'WatchedThreads', {}, (item) ->
ThreadWatcher.refresh item['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
$.addClass favicon, 'watched'
else
$.rmClass favicon, 'watched'
return
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: ->
ThreadWatcher.toggle Get.postFromNode(@).thread
x: ->
thread = @nextElementSibling.pathname.split '/'
ThreadWatcher.unwatch thread[1], thread[3]
rm: ->
[boardID, threadID] = @parentNode.dataset.fullID.split '.'
ThreadWatcher.rm boardID, +threadID
post: (e) ->
{board, postID, threadID} = e.detail
if postID is threadID
if Conf['Auto Watch']
$.set 'AutoWatch', threadID
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) ->
unless $.hasClass $('.watch-thread-link', thread.OP.nodes.post), 'watched'
ThreadWatcher.watch thread
boardID = thread.board.ID
threadID = thread.ID
if ThreadWatcher.db.get {boardID, threadID}
ThreadWatcher.rm boardID, threadID
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) ->
$.get 'WatchedThreads', {}, (item) ->
watched = item['WatchedThreads']
delete watched[board][threadID]
delete watched[board] unless Object.keys(watched[board]).length
ThreadWatcher.refresh watched
$.set 'WatchedThreads', watched
convert: (oldFormat) ->
newFormat = {}
for boardID, threads of oldFormat
for threadID, data of threads
(newFormat[boardID] or= {})[threadID] = excerpt: data.textContent
newFormat
watch: (thread) ->
$.get 'WatchedThreads', {}, (item) ->
watched = item['WatchedThreads']
watched[thread.board] or= {}
watched[thread.board][thread] =
href: "/#{thread.board}/res/#{thread}"
textContent: Get.threadExcerpt thread
ThreadWatcher.refresh watched
$.set 'WatchedThreads', watched
menu:
refreshers: []
init: ->
return if !Conf['Thread Watcher']
menu = new UI.Menu 'thread watcher'
$.on $('.menu-button', ThreadWatcher.dialog), 'click', (e) ->
menu.toggle e, @, ThreadWatcher
@addHeaderMenuEntry()
@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

@ -665,21 +665,9 @@ QR =
cv.width = img.width = width
cv.getContext('2d').drawImage img, 0, 0, width, height
URL.revokeObjectURL fileURL
applyBlob = (blob) =>
cv.toBlob (blob) =>
@URL = URL.createObjectURL blob
@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
img.src = fileURL

View File

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