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:
commit
6c2c9b281a
12
CHANGELOG.md
12
CHANGELOG.md
@ -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
929
css/style.css
Normal 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;
|
||||
}
|
||||
@ -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",
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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>>>#{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>>>#{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 ''
|
||||
]
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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'> - </a></span> #{fourchannav.innerHTML}</span>"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) ->
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>"""
|
||||
5
src/General/html/Monitoring/ThreadWatcher.html
Normal file
5
src/General/html/Monitoring/ThreadWatcher.html
Normal 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>
|
||||
@ -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 = {}
|
||||
|
||||
@ -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: {}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) ->
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -16,7 +16,6 @@ QuoteYou =
|
||||
cb: @node
|
||||
|
||||
node: ->
|
||||
# Stop there if it's a clone.
|
||||
return if @isClone
|
||||
|
||||
if @info.yours
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user