Merge branch 'v3'
Conflicts: CHANGELOG.md LICENSE builds/4chan-X.meta.js builds/4chan-X.user.js builds/appchan-x.user.js builds/crx/manifest.json builds/crx/script.js latest.js package.json src/Filtering/ThreadHiding.coffee src/General/Config.coffee src/General/Header.coffee src/General/Main.coffee src/General/Settings.coffee src/General/css/font-awesome.css src/General/lib/notice.class src/General/meta/metadata.js src/Images/Gallery.coffee src/Images/ImageExpand.coffee src/Monitoring/Favicon.coffee src/Monitoring/ThreadWatcher.coffee src/Posting/QuickReply.coffee worksforme.png
This commit is contained in:
commit
34140232fb
52
CHANGELOG.md
52
CHANGELOG.md
@ -1,3 +1,4 @@
|
|||||||
|
<<<<<<< HEAD
|
||||||
### v2.5.1
|
### v2.5.1
|
||||||
*2013-10-20*
|
*2013-10-20*
|
||||||
**Zixaphir**:
|
**Zixaphir**:
|
||||||
@ -5,6 +6,57 @@
|
|||||||
|
|
||||||
## v2.5.0
|
## v2.5.0
|
||||||
*2013-10-19*
|
*2013-10-19*
|
||||||
|
=======
|
||||||
|
|
||||||
|
**MayhemYDG**:
|
||||||
|
- Tiny posting cooldown adjustment:
|
||||||
|
- You can post an image reply immediately after a non-image reply.
|
||||||
|
- **New option**: `Auto-hide header on scroll`.
|
||||||
|
- Added support for `4cdn.org`.
|
||||||
|
- More index navigation improvements:
|
||||||
|
- Searching in the index is now possible and will show matched OPs by:
|
||||||
|
<ul>
|
||||||
|
<li> comment
|
||||||
|
<li> subject
|
||||||
|
<li> filename
|
||||||
|
<li> name
|
||||||
|
<li> tripcode
|
||||||
|
<li> e-mail
|
||||||
|
</ul>
|
||||||
|
- The page number on which threads are will now be displayed in OPs, to easily identify where threads are located when:
|
||||||
|
<ul>
|
||||||
|
<li> searching through the index.
|
||||||
|
<li> using different index modes and sorting types.
|
||||||
|
<li> threads highlighted by the filter are moved to the top and move other threads down.
|
||||||
|
</ul>
|
||||||
|
- The elapsed time since the last index refresh is now indicated at the top of the index.
|
||||||
|
- New setting: `Show replies`, enabled by default. Disable it to only show OPs in the index.
|
||||||
|
- The index refreshing notification will now only appear on initial page load with slow connections.
|
||||||
|
- Index navigation improvements:
|
||||||
|
- You can now refresh the index page you are on with the refresh shortcut in the header bar or the same keybind for refreshing threads.
|
||||||
|
- You can now switch between paged and all-threads index modes via the "Index Navigation" header sub-menu:<br>
|
||||||
|

|
||||||
|
- Threads in the index can now be sorted by:
|
||||||
|
<ul>
|
||||||
|
<li> Bump order
|
||||||
|
<li> Last reply
|
||||||
|
<li> Creation date
|
||||||
|
<li> Reply count
|
||||||
|
<li> File count
|
||||||
|
</ul>
|
||||||
|
- Navigating across index pages is now instantaneous.
|
||||||
|
- Added a keybind to open the catalog search field on index pages.
|
||||||
|
- Various minor fixes
|
||||||
|
|
||||||
|
### v1.2.43
|
||||||
|
*2013-11-10*
|
||||||
|
|
||||||
|
**noface**:
|
||||||
|
- Strawpoll.me embedding support (as usual, only works on HTTP 4chan due to lack of HTTPS)
|
||||||
|
|
||||||
|
### v1.2.42
|
||||||
|
*2013-10-22*
|
||||||
|
>>>>>>> v3
|
||||||
|
|
||||||
**Zixaphir**:
|
**Zixaphir**:
|
||||||
- Better MediaCru.sh embedding
|
- Better MediaCru.sh embedding
|
||||||
|
|||||||
@ -1,11 +1,17 @@
|
|||||||
module.exports = (grunt) ->
|
module.exports = (grunt) ->
|
||||||
|
|
||||||
|
importHTML = (filename) ->
|
||||||
|
"\"\"\"#{grunt.file.read("src/General/html/#{filename}.html").replace(/^\s+|\s+$</gm, '').replace(/\n/g, '')}\"\"\""
|
||||||
|
|
||||||
# Project configuration.
|
# Project configuration.
|
||||||
grunt.initConfig
|
grunt.initConfig
|
||||||
pkg: grunt.file.readJSON 'package.json'
|
pkg: grunt.file.readJSON 'package.json'
|
||||||
concat:
|
concat:
|
||||||
options: process: Object.create(null, data:
|
options: process: Object.create(null, data:
|
||||||
get: -> grunt.config 'pkg'
|
get: ->
|
||||||
|
pkg = grunt.config 'pkg'
|
||||||
|
pkg.importHTML = importHTML
|
||||||
|
pkg
|
||||||
enumerable: true
|
enumerable: true
|
||||||
)
|
)
|
||||||
coffee:
|
coffee:
|
||||||
@ -14,6 +20,7 @@ module.exports = (grunt) ->
|
|||||||
'src/General/Globals.coffee'
|
'src/General/Globals.coffee'
|
||||||
'src/General/lib/*.coffee'
|
'src/General/lib/*.coffee'
|
||||||
'src/General/Header.coffee'
|
'src/General/Header.coffee'
|
||||||
|
'src/General/Index.coffee'
|
||||||
'src/General/Build.coffee'
|
'src/General/Build.coffee'
|
||||||
'src/General/Get.coffee'
|
'src/General/Get.coffee'
|
||||||
'src/General/UI.coffee'
|
'src/General/UI.coffee'
|
||||||
@ -89,6 +96,8 @@ module.exports = (grunt) ->
|
|||||||
stdout: true
|
stdout: true
|
||||||
stderr: true
|
stderr: true
|
||||||
failOnError: true
|
failOnError: true
|
||||||
|
checkout:
|
||||||
|
command: 'git checkout <%= pkg.meta.mainBranch %>'
|
||||||
commit:
|
commit:
|
||||||
command: [
|
command: [
|
||||||
'git commit -am "Release <%= pkg.meta.name %> v<%= pkg.version %>."'
|
'git commit -am "Release <%= pkg.meta.name %> v<%= pkg.version %>."'
|
||||||
|
|||||||
2
LICENSE
2
LICENSE
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* appchan x - Version 2.5.1 - 2013-10-28
|
* appchan x - Version 2.5.1 - 2013-11-23
|
||||||
*
|
*
|
||||||
* Licensed under the MIT license.
|
* Licensed under the MIT license.
|
||||||
* https://github.com/zixaphir/appchan-x/blob/master/LICENSE
|
* https://github.com/zixaphir/appchan-x/blob/master/LICENSE
|
||||||
|
|||||||
21
builds/4chan-X.meta.js
Executable file
21
builds/4chan-X.meta.js
Executable file
@ -0,0 +1,21 @@
|
|||||||
|
// ==UserScript==
|
||||||
|
// @name 4chan X
|
||||||
|
// @version 1.2.43
|
||||||
|
// @minGMVer 1.12
|
||||||
|
// @minFFVer 22
|
||||||
|
// @namespace 4chan-X
|
||||||
|
// @description Cross-browser userscript for maximum lurking on 4chan.
|
||||||
|
// @license MIT; https://github.com/seaweedchan/4chan-x/blob/master/LICENSE
|
||||||
|
// @match *://boards.4chan.org/*
|
||||||
|
// @match *://sys.4chan.org/*
|
||||||
|
// @match *://a.4cdn.org/*
|
||||||
|
// @match *://i.4cdn.org/*
|
||||||
|
// @grant GM_getValue
|
||||||
|
// @grant GM_setValue
|
||||||
|
// @grant GM_deleteValue
|
||||||
|
// @grant GM_openInTab
|
||||||
|
// @run-at document-start
|
||||||
|
// @updateURL https://github.com/seaweedchan/4chan-x/raw/stable/builds/4chan-X.meta.js
|
||||||
|
// @downloadURL https://github.com/seaweedchan/4chan-x/raw/stable/builds/4chan-X.user.js
|
||||||
|
// @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwAgMAAAAqbBEUAAAACVBMVEUAAGcAAABmzDNZt9VtAAAAAXRSTlMAQObYZgAAAHFJREFUKFOt0LENACEIBdBv4Qju4wgWanEj3D6OcIVMKaitYHEU/jwTCQj8W75kiVCSBvdQ5/AvfVHBin11BgdRq3ysBgfwBDRrj3MCIA+oAQaku/Q1cNctrAmyDl577tOThYt/Y1RBM4DgOHzM0HFTAyLukH/cmRnqAAAAAElFTkSuQmCC
|
||||||
|
// ==/UserScript==
|
||||||
12904
builds/4chan-X.user.js
Normal file
12904
builds/4chan-X.user.js
Normal file
File diff suppressed because one or more lines are too long
@ -1,13 +1,15 @@
|
|||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name appchan x
|
// @name appchan x
|
||||||
// @version 2.5.1
|
// @version 2.5.1
|
||||||
// @minGMVer 1.13
|
// @minGMVer 1.12
|
||||||
// @minFFVer 22
|
// @minFFVer 22
|
||||||
// @namespace zixaphir
|
// @namespace zixaphir
|
||||||
// @description The most comprehensive 4chan userscript.
|
// @description The most comprehensive 4chan userscript.
|
||||||
// @license MIT; https://github.com/zixaphir/appchan-x/blob/master/LICENSE
|
// @license MIT; https://github.com/zixaphir/appchan-x/blob/master/LICENSE
|
||||||
// @match *://*.4chan.org/*
|
// @match *://boards.4chan.org/*
|
||||||
// @match *://4chan.org/*
|
// @match *://sys.4chan.org/*
|
||||||
|
// @match *://a.4cdn.org/*
|
||||||
|
// @match *://i.4cdn.org/*
|
||||||
// @grant GM_getValue
|
// @grant GM_getValue
|
||||||
// @grant GM_setValue
|
// @grant GM_setValue
|
||||||
// @grant GM_deleteValue
|
// @grant GM_deleteValue
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -10,12 +10,12 @@
|
|||||||
},
|
},
|
||||||
"content_scripts": [{
|
"content_scripts": [{
|
||||||
"js": ["script.js"],
|
"js": ["script.js"],
|
||||||
"matches": ["*://*.4chan.org/*","*://4chan.org/*"],
|
"matches": ["*://boards.4chan.org/*","*://sys.4chan.org/*","*://a.4cdn.org/*","*://i.4cdn.org/*"],
|
||||||
"all_frames": true,
|
"all_frames": true,
|
||||||
"run_at": "document_start"
|
"run_at": "document_start"
|
||||||
}],
|
}],
|
||||||
"homepage_url": "http://zixaphir.github.com/appchan-x/",
|
"homepage_url": "http://zixaphir.github.com/appchan-x/",
|
||||||
"minimum_chrome_version": "27",
|
"minimum_chrome_version": "29",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"storage"
|
"storage"
|
||||||
]
|
]
|
||||||
|
|||||||
1748
builds/crx/script.js
1748
builds/crx/script.js
File diff suppressed because one or more lines are too long
1325
changelog-old
Normal file
1325
changelog-old
Normal file
File diff suppressed because it is too large
Load Diff
968
css/style.css
Normal file
968
css/style.css
Normal file
@ -0,0 +1,968 @@
|
|||||||
|
/* 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, 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;
|
||||||
|
}
|
||||||
|
.field::-webkit-search-decoration {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.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: flex;
|
||||||
|
padding: 3px;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
#board-list {
|
||||||
|
flex: 1;
|
||||||
|
align-self: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
#header-bar.autohide:not(:hover) {
|
||||||
|
box-shadow: none;
|
||||||
|
transition: all .8s .6s cubic-bezier(.55, .055, .675, .19);
|
||||||
|
}
|
||||||
|
#header-bar.scroll:not(:hover) {
|
||||||
|
transition: -webkit-transform .2s !important;
|
||||||
|
transition: transform .2s !important;
|
||||||
|
}
|
||||||
|
#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: -10px;
|
||||||
|
}
|
||||||
|
#header.bottom #toggle-header-bar {
|
||||||
|
cursor: s-resize;
|
||||||
|
top: -10px;
|
||||||
|
}
|
||||||
|
#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;
|
||||||
|
}
|
||||||
|
.shortcut:not(:last-child)::after {
|
||||||
|
content: " / ";
|
||||||
|
}
|
||||||
|
.brackets-wrap::before {
|
||||||
|
content: " [ ";
|
||||||
|
}
|
||||||
|
.brackets-wrap::after {
|
||||||
|
content: " ] ";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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: 7px;
|
||||||
|
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: 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: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
#fourchanx-settings > nav {
|
||||||
|
display: flex;
|
||||||
|
padding: 2px 2px 0;
|
||||||
|
}
|
||||||
|
#fourchanx-settings > nav a {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
#fourchanx-settings > nav a.close {
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 0 2px;
|
||||||
|
}
|
||||||
|
.sections-list {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.tab-selected {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.section-container {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Index */
|
||||||
|
:root.index-loading .navLinks,
|
||||||
|
:root.index-loading .board,
|
||||||
|
:root.index-loading .pagelist {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
#index-search {
|
||||||
|
padding-right: 1.5em;
|
||||||
|
width: 100px;
|
||||||
|
transition: color .25s, border-color .25s, width .25s;
|
||||||
|
}
|
||||||
|
#index-search:focus,
|
||||||
|
#index-search[data-searching] {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
#index-search-clear {
|
||||||
|
color: gray;
|
||||||
|
margin-left: -1.25em;
|
||||||
|
}
|
||||||
|
<% if (type === 'crx') { %>
|
||||||
|
/* ``::-webkit-*'' selectors break selector lists on Firefox. */
|
||||||
|
#index-search::-webkit-search-cancel-button,
|
||||||
|
<% } %>
|
||||||
|
#index-search:not([data-searching]) + #index-search-clear {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.summary {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
#thread-watcher .move {
|
||||||
|
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: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
#qr .move {
|
||||||
|
align-self: stretch;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
#qr select[data-name=thread] {
|
||||||
|
margin: 0;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
#qr option {
|
||||||
|
color: #000;
|
||||||
|
background-color: #F7F7F7;
|
||||||
|
}
|
||||||
|
#qr .close {
|
||||||
|
padding: 0 3px;
|
||||||
|
}
|
||||||
|
#qr > form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.persona {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.persona .field {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.persona .field:hover,
|
||||||
|
.persona .field:focus {
|
||||||
|
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 0 2px #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;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.qr-preview.over {
|
||||||
|
border-color: #FFF;
|
||||||
|
border-style: dashed;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
a.remove {
|
||||||
|
color: #E00 !important;
|
||||||
|
padding: 1px;
|
||||||
|
}
|
||||||
|
.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 {
|
||||||
|
font-size: 20px;
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
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-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
#file-n-submit {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
#file-n-submit-container input[type='file'] {
|
||||||
|
/* Keep it to set an appropriate height to the container. */
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
#file-n-submit-container input {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
#file-n-submit input[type='submit'] {
|
||||||
|
order: 1;
|
||||||
|
}
|
||||||
|
#file-n-submit.has-file #qr-no-file,
|
||||||
|
#file-n-submit:not(.has-file) #qr-filename,
|
||||||
|
#file-n-submit:not(.has-file) #qr-filesize,
|
||||||
|
#file-n-submit:not(.has-file) #qr-file-spoiler,
|
||||||
|
#file-n-submit:not(.has-file) #qr-filerm,
|
||||||
|
#qr-filename:focus ~ #qr-filesize {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
#qr-no-file,
|
||||||
|
#qr-filename,
|
||||||
|
#qr-filesize,
|
||||||
|
#qr-file-spoiler {
|
||||||
|
margin: 0 2px !important;
|
||||||
|
}
|
||||||
|
#qr-no-file {
|
||||||
|
cursor: default;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
#qr-filename {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
background: none;
|
||||||
|
border: none !important;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
flex: 1;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
#qr-filesize {
|
||||||
|
font-size: .8em;
|
||||||
|
}
|
||||||
|
#qr-filesize::before {
|
||||||
|
content: " (";
|
||||||
|
}
|
||||||
|
#qr-filesize::after {
|
||||||
|
content: ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Menu */
|
||||||
|
.menu-button {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.menu-button i:not(.fa-bars) {
|
||||||
|
border-top: 6px solid;
|
||||||
|
border-right: 4px solid transparent;
|
||||||
|
border-left: 4px solid transparent;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 2px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
@media screen and (resolution: 1dppx) {
|
||||||
|
.fa-bars {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
#shortcuts .fa-bars {
|
||||||
|
vertical-align: -1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#menu {
|
||||||
|
border-bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
margin: 2px 0;
|
||||||
|
flex-direction: column;
|
||||||
|
position: absolute;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
#menu.top {
|
||||||
|
top: 100%;
|
||||||
|
}
|
||||||
|
#menu.bottom {
|
||||||
|
bottom: 100%;
|
||||||
|
}
|
||||||
|
#menu.left {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
#menu.right {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
.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: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: absolute;
|
||||||
|
margin: -1px 0;
|
||||||
|
}
|
||||||
|
.submenu.top {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.submenu.bottom {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
.submenu.left {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
.submenu.right {
|
||||||
|
right: 100%;
|
||||||
|
}
|
||||||
|
.entry input {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* colored uid */
|
||||||
|
|
||||||
|
.posteruid.painted {
|
||||||
|
padding: 0 5px;
|
||||||
|
border-radius: 1em;
|
||||||
|
font-size: 0.8em;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
18
html/General/Settings.html
Normal file
18
html/General/Settings.html
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<div id="fourchanx-settings" class="dialog">
|
||||||
|
<nav>
|
||||||
|
<div class="sections-list"></div>
|
||||||
|
<div class="credits">
|
||||||
|
<a href="<%= meta.page %>" target="_blank"><%= meta.name %></a>
|
||||||
|
|
|
||||||
|
<a href="<%= meta.repo %>blob/<%= meta.mainBranch %>/CHANGELOG.md" target="_blank">#{g.VERSION}</a>
|
||||||
|
|
|
||||||
|
<a href="<%= meta.repo %>blob/<%= meta.mainBranch %>/CONTRIBUTING.md#reporting-bugs-and-suggestions" target="_blank">Issues</a>
|
||||||
|
|
|
||||||
|
<a href="javascript:;" class="close fa fa-times" title="Close"></a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<hr>
|
||||||
|
<div class="section-container">
|
||||||
|
<section></section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
17
html/Monitoring/ThreadUpdater.html
Normal file
17
html/Monitoring/ThreadUpdater.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<div class="move">
|
||||||
|
<span id="update-status"></span> <span id="update-timer"></span>
|
||||||
|
</div>
|
||||||
|
#{html}
|
||||||
|
<div>
|
||||||
|
<label title="Controls whether *this* thread automatically updates or not">
|
||||||
|
<input type="checkbox" name="Auto Update This" #{if Conf['Auto Update'] then "checked" else ""}> Auto Update This
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
<input type="number" name="Interval" class="field" min="5" value="#{Conf['Interval']}"> Refresh rate (s)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input value="Refresh thread" type="button" name="Update">
|
||||||
|
</div>
|
||||||
5
html/Monitoring/ThreadWatcher.html
Normal file
5
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" href="javascript:;"><i class="fa fa-bars"></i></a>
|
||||||
|
</div>
|
||||||
|
<div id="watched-threads"></div>
|
||||||
39
html/Posting/QR.html
Normal file
39
html/Posting/QR.html
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<div>
|
||||||
|
<input type="checkbox" id="autohide" title="Auto-hide">
|
||||||
|
<select data-name="thread" title="Create a new thread / Reply">
|
||||||
|
<option value="new">New thread</option>
|
||||||
|
</select>
|
||||||
|
<span class="move"></span>
|
||||||
|
<a href="javascript:;" class="close fa fa-times" title="Close"></a>
|
||||||
|
</div>
|
||||||
|
<form>
|
||||||
|
<div class="persona">
|
||||||
|
<input type="button" id="dump-button" title="Dump list" value="+">
|
||||||
|
<input data-name="name" list="list-name" placeholder="Name" class="field" size="1">
|
||||||
|
<input data-name="email" list="list-email" placeholder="E-mail" class="field" size="1">
|
||||||
|
<input data-name="sub" list="list-sub" placeholder="Subject" class="field" size="1">
|
||||||
|
</div>
|
||||||
|
<div id="dump-list-container">
|
||||||
|
<div id="dump-list"></div>
|
||||||
|
<a href="javascript:;" id="add-post" class="fa fa-plus" title="Add a post"></a>
|
||||||
|
</div>
|
||||||
|
<div class="textarea">
|
||||||
|
<textarea data-name="com" placeholder="Comment" class="field"></textarea>
|
||||||
|
<span id="char-count"></span>
|
||||||
|
</div>
|
||||||
|
<div id="file-n-submit-container">
|
||||||
|
<input type="file" multiple>
|
||||||
|
<div id="file-n-submit">
|
||||||
|
<input type="submit">
|
||||||
|
<input type="button" id="qr-file-button" value="Choose files">
|
||||||
|
<span id="qr-no-file">No selected file</span>
|
||||||
|
<input id="qr-filename" data-name="filename" spellcheck="false">
|
||||||
|
<span id="qr-filesize"></span>
|
||||||
|
<a href="javascript:;" id="qr-filerm" class="fa fa-times-circle" title="Remove file"></a>
|
||||||
|
<input type="checkbox" id="qr-file-spoiler" title="Spoiler image">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<datalist id="list-name"></datalist>
|
||||||
|
<datalist id="list-email"></datalist>
|
||||||
|
<datalist id="list-sub"></datalist>
|
||||||
BIN
img/changelog/3.12.0/0.png
Normal file
BIN
img/changelog/3.12.0/0.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 56 KiB |
@ -32,8 +32,8 @@
|
|||||||
"http": true,
|
"http": true,
|
||||||
"https": true,
|
"https": true,
|
||||||
"software": "foolfuuka",
|
"software": "foolfuuka",
|
||||||
"boards": ["hr", "tg", "tv", "x"],
|
"boards": ["hr", "s4s", "tg", "tv", "x"],
|
||||||
"files": ["hr", "tg", "tv", "x"]
|
"files": ["hr", "s4s", "tg", "tv", "x"]
|
||||||
}, {
|
}, {
|
||||||
"uid": 4,
|
"uid": 4,
|
||||||
"name": "Nyafuu",
|
"name": "Nyafuu",
|
||||||
@ -43,15 +43,6 @@
|
|||||||
"software": "foolfuuka",
|
"software": "foolfuuka",
|
||||||
"boards": ["c", "w", "wg"],
|
"boards": ["c", "w", "wg"],
|
||||||
"files": ["c", "w", "wg"]
|
"files": ["c", "w", "wg"]
|
||||||
}, {
|
|
||||||
"uid": 12,
|
|
||||||
"name": "FapArchive",
|
|
||||||
"domain": "fuuka.worldathleticproject.org",
|
|
||||||
"http": true,
|
|
||||||
"https": true,
|
|
||||||
"software": "foolfuuka",
|
|
||||||
"boards": ["adv", "b", "cm", "d", "e", "h", "hc", "lgbt", "pol", "r", "s", "s4s", "soc", "trv", "u", "y"],
|
|
||||||
"files": ["b", "cm", "d", "e", "h", "hc", "pol", "r", "s", "s4s", "soc", "u", "y"]
|
|
||||||
}, {
|
}, {
|
||||||
"uid": 7,
|
"uid": 7,
|
||||||
"name": "Install Gentoo",
|
"name": "Install Gentoo",
|
||||||
@ -96,6 +87,15 @@
|
|||||||
"https": true,
|
"https": true,
|
||||||
"withCredentials": true,
|
"withCredentials": true,
|
||||||
"software": "foolfuuka",
|
"software": "foolfuuka",
|
||||||
"boards": ["a", "co", "d", "gd", "h", "jp", "m", "mlp", "sp", "tg", "tv", "u", "v", "vg", "vp", "vr", "wsg"],
|
"boards": ["a", "co", "gd", "jp", "m", "s4s", "sp", "tg", "tv", "u", "v", "vg", "vp", "vr", "wsg"],
|
||||||
"files": ["a", "d", "gd", "h", "jp", "m", "tg", "u", "vg", "vp", "vr", "wsg"]
|
"files": ["a", "gd", "jp", "m", "s4s", "tg", "u", "vg", "vp", "vr", "wsg"]
|
||||||
|
}, {
|
||||||
|
"uid": 14,
|
||||||
|
"name": "Bui's Archive",
|
||||||
|
"domain": "archive.bui.pm",
|
||||||
|
"http": true,
|
||||||
|
"https": true,
|
||||||
|
"software": "foolfuuka",
|
||||||
|
"boards": ["b"],
|
||||||
|
"files": ["b"]
|
||||||
}]
|
}]
|
||||||
|
|||||||
17
package.json
17
package.json
@ -10,16 +10,23 @@
|
|||||||
"buildsPath": "builds/",
|
"buildsPath": "builds/",
|
||||||
"mainBranch": "master",
|
"mainBranch": "master",
|
||||||
"matches": [
|
"matches": [
|
||||||
"*://*.4chan.org/*",
|
"*://boards.4chan.org/*",
|
||||||
"*://4chan.org/*"
|
"*://sys.4chan.org/*",
|
||||||
|
"*://a.4cdn.org/*",
|
||||||
|
"*://i.4cdn.org/*"
|
||||||
],
|
],
|
||||||
"files": {
|
"files": {
|
||||||
"metajs": "appchan-x.meta.js",
|
"metajs": "appchan-x.meta.js",
|
||||||
"userjs": "appchan-x.user.js"
|
"userjs": "appchan-x.user.js"
|
||||||
|
},
|
||||||
|
"min": {
|
||||||
|
"chrome": "29",
|
||||||
|
"firefox": "22",
|
||||||
|
"greasemonkey": "1.12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"font-awesome": "git://github.com/MayhemYDG/Font-Awesome.git#df4285951124f9ca1f3907438462e5ba9e464bcb",
|
"font-awesome": "https://github.com/FortAwesome/Font-Awesome/archive/v4.0.3.tar.gz",
|
||||||
"grunt": "~0.4.1",
|
"grunt": "~0.4.1",
|
||||||
"grunt-bump": "~0.0.11",
|
"grunt-bump": "~0.0.11",
|
||||||
"grunt-concurrent": "~0.4.0",
|
"grunt-concurrent": "~0.4.0",
|
||||||
@ -29,8 +36,8 @@
|
|||||||
"grunt-contrib-concat": "~0.3.0",
|
"grunt-contrib-concat": "~0.3.0",
|
||||||
"grunt-contrib-copy": "~0.4.1",
|
"grunt-contrib-copy": "~0.4.1",
|
||||||
"grunt-contrib-watch": "~0.5.3",
|
"grunt-contrib-watch": "~0.5.3",
|
||||||
"grunt-shell": "~0.4.0",
|
"grunt-shell": "~0.6.0",
|
||||||
"load-grunt-tasks": "~0.1.0"
|
"load-grunt-tasks": "~0.2.0"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@ -113,13 +113,8 @@ Filter =
|
|||||||
|
|
||||||
# Highlight
|
# Highlight
|
||||||
$.addClass @nodes.root, result.class
|
$.addClass @nodes.root, result.class
|
||||||
if !@isReply and result.top and g.VIEW is 'index'
|
if !@isReply and result.top
|
||||||
# Put the highlighted OPs' thread on top of the board page...
|
@thread.isOnTop = true
|
||||||
thisThread = @nodes.root.parentNode
|
|
||||||
# ...before the first non highlighted thread.
|
|
||||||
if firstThread = $ 'div[class="postContainer opContainer"]'
|
|
||||||
unless firstThread is @nodes.root
|
|
||||||
$.before firstThread.parentNode, [thisThread, thisThread.nextElementSibling]
|
|
||||||
|
|
||||||
name: (post) ->
|
name: (post) ->
|
||||||
if 'name' of post.info
|
if 'name' of post.info
|
||||||
|
|||||||
@ -4,6 +4,7 @@ ThreadHiding =
|
|||||||
|
|
||||||
@db = new DataBoard 'hiddenThreads'
|
@db = new DataBoard 'hiddenThreads'
|
||||||
@syncCatalog()
|
@syncCatalog()
|
||||||
|
$.on d, 'IndexBuild', @onIndexBuild
|
||||||
Thread.callbacks.push
|
Thread.callbacks.push
|
||||||
name: 'Thread Hiding'
|
name: 'Thread Hiding'
|
||||||
cb: @node
|
cb: @node
|
||||||
@ -14,6 +15,17 @@ ThreadHiding =
|
|||||||
return unless Conf['Thread Hiding Buttons']
|
return unless Conf['Thread Hiding Buttons']
|
||||||
$.prepend @OP.nodes.info, ThreadHiding.makeButton @, 'hide'
|
$.prepend @OP.nodes.info, ThreadHiding.makeButton @, 'hide'
|
||||||
|
|
||||||
|
onIndexBuild: ({detail: nodes}) ->
|
||||||
|
for root, i in nodes by 2
|
||||||
|
thread = Get.threadFromRoot root
|
||||||
|
continue unless thread.isHidden
|
||||||
|
unless thread.stub
|
||||||
|
nodes[i + 1].hidden = true
|
||||||
|
else unless root.contains thread.stub
|
||||||
|
# When we come back to a page, the stub is already there.
|
||||||
|
ThreadHiding.makeStub thread, root
|
||||||
|
return
|
||||||
|
|
||||||
syncCatalog: ->
|
syncCatalog: ->
|
||||||
# Sync hidden threads from the catalog into the index.
|
# Sync hidden threads from the catalog into the index.
|
||||||
hiddenThreads = ThreadHiding.db.get
|
hiddenThreads = ThreadHiding.db.get
|
||||||
@ -43,7 +55,7 @@ ThreadHiding =
|
|||||||
# We need to clean hidden threads on the catalog ourselves,
|
# We need to clean hidden threads on the catalog ourselves,
|
||||||
# otherwise if we don't visit the catalog regularly
|
# otherwise if we don't visit the catalog regularly
|
||||||
# it will pollute the localStorage and our data.
|
# it will pollute the localStorage and our data.
|
||||||
$.cache "//api.4chan.org/#{g.BOARD}/threads.json", ->
|
$.cache "//a.4cdn.org/#{g.BOARD}/threads.json", ->
|
||||||
return unless @status is 200
|
return unless @status is 200
|
||||||
threads = {}
|
threads = {}
|
||||||
for page in JSON.parse @response
|
for page in JSON.parse @response
|
||||||
@ -139,6 +151,23 @@ ThreadHiding =
|
|||||||
a.dataset.fullID = thread.fullID
|
a.dataset.fullID = thread.fullID
|
||||||
$.on a, 'click', ThreadHiding.toggle
|
$.on a, 'click', ThreadHiding.toggle
|
||||||
a
|
a
|
||||||
|
makeStub: (thread, root) ->
|
||||||
|
numReplies = $$('.thread > .replyContainer', root).length
|
||||||
|
numReplies += +summary.textContent.match /\d+/ if summary = $ '.summary', root
|
||||||
|
opInfo = if Conf['Anonymize']
|
||||||
|
'Anonymous'
|
||||||
|
else
|
||||||
|
$('.nameBlock', thread.OP.nodes.info).textContent
|
||||||
|
|
||||||
|
a = ThreadHiding.makeButton thread, 'show'
|
||||||
|
$.add a, $.tn " #{opInfo} (#{if numReplies is 1 then '1 reply' else "#{numReplies} replies"})"
|
||||||
|
thread.stub = $.el 'div',
|
||||||
|
className: 'stub'
|
||||||
|
if Conf['Menu']
|
||||||
|
$.add thread.stub, [a, $.tn(' '), Menu.makeButton()]
|
||||||
|
else
|
||||||
|
$.add thread.stub, a
|
||||||
|
$.prepend root, thread.stub
|
||||||
|
|
||||||
saveHiddenState: (thread, makeStub) ->
|
saveHiddenState: (thread, makeStub) ->
|
||||||
hiddenThreadsOnCatalog = JSON.parse(localStorage.getItem "4chan-hide-t-#{g.BOARD}") or {}
|
hiddenThreadsOnCatalog = JSON.parse(localStorage.getItem "4chan-hide-t-#{g.BOARD}") or {}
|
||||||
@ -165,37 +194,13 @@ ThreadHiding =
|
|||||||
ThreadHiding.saveHiddenState thread
|
ThreadHiding.saveHiddenState thread
|
||||||
|
|
||||||
hide: (thread, makeStub=Conf['Stubs']) ->
|
hide: (thread, makeStub=Conf['Stubs']) ->
|
||||||
{OP} = thread
|
return if thread.isHidden
|
||||||
threadRoot = OP.nodes.root.parentNode
|
threadRoot = thread.OP.nodes.root.parentNode
|
||||||
thread.isHidden = true
|
thread.isHidden = true
|
||||||
|
|
||||||
unless makeStub
|
return threadRoot.hidden = threadRoot.nextElementSibling.hidden = true unless makeStub # <hr>
|
||||||
threadRoot.hidden = threadRoot.nextElementSibling.hidden = true # <hr>
|
|
||||||
return
|
|
||||||
|
|
||||||
numReplies = (
|
ThreadHiding.makeStub thread, threadRoot
|
||||||
if span = $ '.summary', threadRoot
|
|
||||||
+span.textContent.match /\d+/
|
|
||||||
else
|
|
||||||
0
|
|
||||||
) +
|
|
||||||
$$('.opContainer ~ .replyContainer', threadRoot).length
|
|
||||||
numReplies = if numReplies is 1 then '1 reply' else "#{numReplies or 'no'} replies"
|
|
||||||
opInfo =
|
|
||||||
if Conf['Anonymize']
|
|
||||||
'Anonymous'
|
|
||||||
else
|
|
||||||
$('.nameBlock', OP.nodes.info).textContent
|
|
||||||
|
|
||||||
a = ThreadHiding.makeButton thread, 'show'
|
|
||||||
$.add a, $.tn " #{opInfo} (#{numReplies})"
|
|
||||||
thread.stub = $.el 'div',
|
|
||||||
className: 'stub'
|
|
||||||
$.add thread.stub, if Conf['Menu']
|
|
||||||
[a, $.tn(' '), Menu.makeButton()]
|
|
||||||
else
|
|
||||||
a
|
|
||||||
$.prepend threadRoot, thread.stub
|
|
||||||
|
|
||||||
show: (thread) ->
|
show: (thread) ->
|
||||||
if thread.stub
|
if thread.stub
|
||||||
|
|||||||
@ -36,14 +36,14 @@ Build =
|
|||||||
name: data.filename + data.ext
|
name: data.filename + data.ext
|
||||||
timestamp: "#{data.tim}#{data.ext}"
|
timestamp: "#{data.tim}#{data.ext}"
|
||||||
url: if boardID is 'f'
|
url: if boardID is 'f'
|
||||||
"//images.4channel.org/#{boardID}/src/#{data.filename}#{data.ext}"
|
"//i.4cdn.org/#{boardID}/src/#{data.filename}#{data.ext}"
|
||||||
else
|
else
|
||||||
"//images.4chan.org/#{boardID}/src/#{data.tim}#{data.ext}"
|
"//i.4cdn.org/#{boardID}/src/#{data.tim}#{data.ext}"
|
||||||
height: data.h
|
height: data.h
|
||||||
width: data.w
|
width: data.w
|
||||||
MD5: data.md5
|
MD5: data.md5
|
||||||
size: data.fsize
|
size: data.fsize
|
||||||
turl: "//thumbs.4chan.org/#{boardID}/thumb/#{data.tim}s.jpg"
|
turl: "//t.4cdn.org/#{boardID}/thumb/#{data.tim}s.jpg"
|
||||||
theight: data.tn_h
|
theight: data.tn_h
|
||||||
twidth: data.tn_w
|
twidth: data.tn_w
|
||||||
isSpoiler: !!data.spoiler
|
isSpoiler: !!data.spoiler
|
||||||
@ -63,7 +63,11 @@ Build =
|
|||||||
} = o
|
} = o
|
||||||
isOP = postID is threadID
|
isOP = postID is threadID
|
||||||
|
|
||||||
staticPath = '//static.4chan.org/image/'
|
staticPath = '//s.4cdn.org/image/'
|
||||||
|
gifIcon = if window.devicePixelRatio >= 2
|
||||||
|
'@2x.gif'
|
||||||
|
else
|
||||||
|
'.gif'
|
||||||
|
|
||||||
if email
|
if email
|
||||||
emailStart = '<a href="mailto:' + email + '" class="useremail">'
|
emailStart = '<a href="mailto:' + email + '" class="useremail">'
|
||||||
@ -86,21 +90,21 @@ Build =
|
|||||||
capcodeClass = " capcodeAdmin"
|
capcodeClass = " capcodeAdmin"
|
||||||
capcodeStart = " <strong class='capcode hand id_admin'" +
|
capcodeStart = " <strong class='capcode hand id_admin'" +
|
||||||
"title='Highlight posts by the Administrator'>## Admin</strong>"
|
"title='Highlight posts by the Administrator'>## Admin</strong>"
|
||||||
capcode = " <img src='#{staticPath}adminicon.gif' " +
|
capcode = " <img src='#{staticPath}adminicon#{gifIcon}' " +
|
||||||
"alt='This user is the 4chan Administrator.' " +
|
"alt='This user is the 4chan Administrator.' " +
|
||||||
"title='This user is the 4chan Administrator.' class=identityIcon>"
|
"title='This user is the 4chan Administrator.' class=identityIcon>"
|
||||||
when 'mod'
|
when 'mod'
|
||||||
capcodeClass = " capcodeMod"
|
capcodeClass = " capcodeMod"
|
||||||
capcodeStart = " <strong class='capcode hand id_mod' " +
|
capcodeStart = " <strong class='capcode hand id_mod' " +
|
||||||
"title='Highlight posts by Moderators'>## Mod</strong>"
|
"title='Highlight posts by Moderators'>## Mod</strong>"
|
||||||
capcode = " <img src='#{staticPath}modicon.gif' " +
|
capcode = " <img src='#{staticPath}modicon#{gifIcon}' " +
|
||||||
"alt='This user is a 4chan Moderator.' " +
|
"alt='This user is a 4chan Moderator.' " +
|
||||||
"title='This user is a 4chan Moderator.' class=identityIcon>"
|
"title='This user is a 4chan Moderator.' class=identityIcon>"
|
||||||
when 'developer'
|
when 'developer'
|
||||||
capcodeClass = " capcodeDeveloper"
|
capcodeClass = " capcodeDeveloper"
|
||||||
capcodeStart = " <strong class='capcode hand id_developer' " +
|
capcodeStart = " <strong class='capcode hand id_developer' " +
|
||||||
"title='Highlight posts by Developers'>## Developer</strong>"
|
"title='Highlight posts by Developers'>## Developer</strong>"
|
||||||
capcode = " <img src='#{staticPath}developericon.gif' " +
|
capcode = " <img src='#{staticPath}developericon#{gifIcon}' " +
|
||||||
"alt='This user is a 4chan Developer.' " +
|
"alt='This user is a 4chan Developer.' " +
|
||||||
"title='This user is a 4chan Developer.' class=identityIcon>"
|
"title='This user is a 4chan Developer.' class=identityIcon>"
|
||||||
else
|
else
|
||||||
@ -118,11 +122,11 @@ Build =
|
|||||||
if file?.isDeleted
|
if file?.isDeleted
|
||||||
fileHTML = if isOP
|
fileHTML = if isOP
|
||||||
"<div class=file id=f#{postID}><div class=fileInfo></div><span class=fileThumb>" +
|
"<div class=file id=f#{postID}><div class=fileInfo></div><span class=fileThumb>" +
|
||||||
"<img src='#{staticPath}filedeleted.gif' alt='File deleted.' class=fileDeletedRes>" +
|
"<img src='#{staticPath}filedeleted#{gifIcon}' alt='File deleted.' class=fileDeleted>" +
|
||||||
"</span></div>"
|
"</span></div>"
|
||||||
else
|
else
|
||||||
"<div class=file id=f#{postID}><span class=fileThumb>" +
|
"<div class=file id=f#{postID}><span class=fileThumb>" +
|
||||||
"<img src='#{staticPath}filedeleted-res.gif' alt='File deleted.' class=fileDeletedRes>" +
|
"<img src='#{staticPath}filedeleted-res#{gifIcon}' alt='File deleted.' class=fileDeletedRes>" +
|
||||||
"</span></div>"
|
"</span></div>"
|
||||||
else if file
|
else if file
|
||||||
ext = file.name[-3..]
|
ext = file.name[-3..]
|
||||||
@ -182,14 +186,21 @@ Build =
|
|||||||
''
|
''
|
||||||
|
|
||||||
sticky = if isSticky
|
sticky = if isSticky
|
||||||
" <img src=#{staticPath}sticky.gif alt=Sticky title=Sticky class=stickyIcon>"
|
" <img src=#{staticPath}sticky#{gifIcon} alt=Sticky title=Sticky class=stickyIcon>"
|
||||||
else
|
else
|
||||||
''
|
''
|
||||||
closed = if isClosed
|
closed = if isClosed
|
||||||
" <img src=#{staticPath}closed.gif alt=Closed title=Closed class=closedIcon>"
|
" <img src=#{staticPath}closed#{gifIcon} alt=Closed title=Closed class=closedIcon>"
|
||||||
else
|
else
|
||||||
''
|
''
|
||||||
|
|
||||||
|
if isOP and g.VIEW is 'index'
|
||||||
|
pageNum = Math.floor Index.liveThreadIDs.indexOf(postID) / Index.threadsNumPerPage
|
||||||
|
pageIcon = " <span class=page-num title='This thread is on page #{pageNum} in the original index.'>Page #{pageNum}</span>"
|
||||||
|
replyLink = " <span>[<a href='/#{boardID}/res/#{threadID}' class=replylink>Reply</a>]</span>"
|
||||||
|
else
|
||||||
|
pageIcon = replyLink = ''
|
||||||
|
|
||||||
container = $.el 'div',
|
container = $.el 'div',
|
||||||
id: "pc#{postID}"
|
id: "pc#{postID}"
|
||||||
className: "postContainer #{if isOP then 'op' else 'reply'}Container"
|
className: "postContainer #{if isOP then 'op' else 'reply'}Container"
|
||||||
@ -201,3 +212,34 @@ Build =
|
|||||||
quote.href = "/#{boardID}/res/#{href}" # Fix pathnames
|
quote.href = "/#{boardID}/res/#{href}" # Fix pathnames
|
||||||
|
|
||||||
container
|
container
|
||||||
|
|
||||||
|
summary: (boardID, threadID, posts, files) ->
|
||||||
|
text = []
|
||||||
|
text.push "#{posts} post#{if posts > 1 then 's' else ''}"
|
||||||
|
text.push "and #{files} image repl#{if files > 1 then 'ies' else 'y'}" if files
|
||||||
|
text.push 'omitted.'
|
||||||
|
$.el 'a',
|
||||||
|
className: 'summary'
|
||||||
|
textContent: text.join ' '
|
||||||
|
href: "/#{boardID}/res/#{threadID}"
|
||||||
|
thread: (board, data) ->
|
||||||
|
Build.spoilerRange[board] = data.custom_spoiler
|
||||||
|
|
||||||
|
if (OP = board.posts[data.no]) and root = OP.nodes.root.parentNode
|
||||||
|
$.rmAll root
|
||||||
|
else
|
||||||
|
root = $.el 'div',
|
||||||
|
className: 'thread'
|
||||||
|
id: "t#{data.no}"
|
||||||
|
|
||||||
|
nodes = [if OP then OP.nodes.root else Build.postFromObject data, board.ID]
|
||||||
|
if data.omitted_posts or !Conf['Show Replies'] and data.replies
|
||||||
|
[posts, files] = if Conf['Show Replies']
|
||||||
|
[data.omitted_posts, data.omitted_images]
|
||||||
|
else
|
||||||
|
# XXX data.images is not accurate.
|
||||||
|
[data.replies, data.omitted_images + data.last_replies.filter((data) -> !!data.ext).length]
|
||||||
|
nodes.push Build.summary board.ID, data.no, posts, files
|
||||||
|
|
||||||
|
$.add root, nodes
|
||||||
|
root
|
||||||
|
|||||||
@ -37,10 +37,6 @@ Config =
|
|||||||
true
|
true
|
||||||
'Reformat the file information.'
|
'Reformat the file information.'
|
||||||
]
|
]
|
||||||
'Comment Expansion': [
|
|
||||||
true
|
|
||||||
'Add buttons to expand long comments.'
|
|
||||||
]
|
|
||||||
'Thread Expansion': [
|
'Thread Expansion': [
|
||||||
true
|
true
|
||||||
'Add buttons to expand threads.'
|
'Add buttons to expand threads.'
|
||||||
@ -846,13 +842,19 @@ http://iqdb.org/?url=%TURL
|
|||||||
|
|
||||||
'Custom CSS': false
|
'Custom CSS': false
|
||||||
|
|
||||||
|
Index:
|
||||||
|
'Index Mode': 'paged'
|
||||||
|
'Index Sort': 'bump'
|
||||||
|
'Show Replies': true
|
||||||
|
|
||||||
Header:
|
Header:
|
||||||
'Fixed Header': true
|
'Fixed Header': true
|
||||||
'Header auto-hide': false
|
'Header auto-hide': false
|
||||||
'Bottom Header': false
|
'Header auto-hide on scroll': false
|
||||||
'Header catalog links': false
|
'Bottom Header': false
|
||||||
'Bottom Board List': true
|
'Header catalog links': false
|
||||||
'Shortcut Icons': false
|
'Bottom Board List': true
|
||||||
|
'Shortcut Icons': false
|
||||||
'Custom Board Navigation': true
|
'Custom Board Navigation': true
|
||||||
|
|
||||||
boardnav: """
|
boardnav: """
|
||||||
@ -998,6 +1000,10 @@ box-shadow: inset 2px 2px 2px rgba(0,0,0,0.2);
|
|||||||
'Shift+c'
|
'Shift+c'
|
||||||
'Open the catalog of the current board'
|
'Open the catalog of the current board'
|
||||||
]
|
]
|
||||||
|
'Search form': [
|
||||||
|
'Ctrl+Alt+s'
|
||||||
|
'Focus the search field on the board index.'
|
||||||
|
]
|
||||||
# Thread Navigation
|
# Thread Navigation
|
||||||
'Next thread': [
|
'Next thread': [
|
||||||
'Shift+Down'
|
'Shift+Down'
|
||||||
|
|||||||
@ -70,7 +70,7 @@ Get =
|
|||||||
|
|
||||||
root.textContent = "Loading post No.#{postID}..."
|
root.textContent = "Loading post No.#{postID}..."
|
||||||
if threadID
|
if threadID
|
||||||
$.cache "//api.4chan.org/#{boardID}/res/#{threadID}.json", ->
|
$.cache "//a.4cdn.org/#{boardID}/res/#{threadID}.json", ->
|
||||||
Get.fetchedPost @, boardID, threadID, postID, root, context
|
Get.fetchedPost @, boardID, threadID, postID, root, context
|
||||||
else if url = Redirect.to 'post', {boardID, postID}
|
else if url = Redirect.to 'post', {boardID, postID}
|
||||||
$.cache url,
|
$.cache url,
|
||||||
@ -198,7 +198,7 @@ Get =
|
|||||||
width: data.media.media_w
|
width: data.media.media_w
|
||||||
MD5: data.media.media_hash
|
MD5: data.media.media_hash
|
||||||
size: data.media.media_size
|
size: data.media.media_size
|
||||||
turl: data.media.thumb_link or "//thumbs.4chan.org/#{boardID}/thumb/#{data.media.preview_orig}"
|
turl: data.media.thumb_link or "//t.4cdn.org/#{boardID}/thumb/#{data.media.preview_orig}"
|
||||||
theight: data.media.preview_h
|
theight: data.media.preview_h
|
||||||
twidth: data.media.preview_w
|
twidth: data.media.preview_w
|
||||||
isSpoiler: data.media.spoiler is '1'
|
isSpoiler: data.media.spoiler is '1'
|
||||||
|
|||||||
@ -10,6 +10,8 @@ Header =
|
|||||||
innerHTML: '<input type=checkbox name="Fixed Header"> Fixed Header'
|
innerHTML: '<input type=checkbox name="Fixed Header"> Fixed Header'
|
||||||
headerToggler = $.el 'label',
|
headerToggler = $.el 'label',
|
||||||
innerHTML: '<input type=checkbox name="Header auto-hide"> Auto-hide header'
|
innerHTML: '<input type=checkbox name="Header auto-hide"> Auto-hide header'
|
||||||
|
scrollHeaderToggler = $.el 'label',
|
||||||
|
innerHTML: '<input type=checkbox name="Header auto-hide on scroll"> Auto-hide header on scroll'
|
||||||
barPositionToggler = $.el 'label',
|
barPositionToggler = $.el 'label',
|
||||||
innerHTML: '<input type=checkbox name="Bottom header"> Bottom header'
|
innerHTML: '<input type=checkbox name="Bottom header"> Bottom header'
|
||||||
customNavToggler = $.el 'label',
|
customNavToggler = $.el 'label',
|
||||||
@ -18,23 +20,33 @@ Header =
|
|||||||
textContent: 'Edit custom board navigation'
|
textContent: 'Edit custom board navigation'
|
||||||
href: 'javascript:;'
|
href: 'javascript:;'
|
||||||
|
|
||||||
@barFixedToggler = barFixedToggler.firstElementChild
|
@barFixedToggler = barFixedToggler.firstElementChild
|
||||||
@barPositionToggler = barPositionToggler.firstElementChild
|
@scrollHeaderToggler = scrollHeaderToggler.firstElementChild
|
||||||
@headerToggler = headerToggler.firstElementChild
|
@barPositionToggler = barPositionToggler.firstElementChild
|
||||||
@customNavToggler = customNavToggler.firstElementChild
|
@headerToggler = headerToggler.firstElementChild
|
||||||
|
@customNavToggler = customNavToggler.firstElementChild
|
||||||
|
|
||||||
@setBarFixed Conf['Fixed Header']
|
@setBarFixed Conf['Fixed Header']
|
||||||
@setBarVisibility Conf['Header auto-hide']
|
@setBarVisibility Conf['Header auto-hide']
|
||||||
|
|
||||||
$.on menuButton, 'click', @menuToggle
|
$.on menuButton, 'click', @menuToggle
|
||||||
|
$.on @headerToggler, 'change', @toggleBarVisibility
|
||||||
$.on @barFixedToggler, 'change', @toggleBarFixed
|
$.on @barFixedToggler, 'change', @toggleBarFixed
|
||||||
$.on @barPositionToggler, 'change', @toggleBarPosition
|
$.on @barPositionToggler, 'change', @toggleBarPosition
|
||||||
$.on @headerToggler, 'change', @toggleBarVisibility
|
$.on @headerToggler, 'change', @toggleBarVisibility
|
||||||
$.on @customNavToggler, 'change', @toggleCustomNav
|
$.on @customNavToggler, 'change', @toggleCustomNav
|
||||||
$.on editCustomNav, 'click', @editCustomNav
|
$.on editCustomNav, 'click', @editCustomNav
|
||||||
|
|
||||||
$.sync 'Fixed Header', Header.setBarFixed
|
@setBarFixed Conf['Fixed Header']
|
||||||
$.sync 'Bottom Header', Header.setBarPosition
|
@setHideBarOnScroll Conf['Header auto-hide on scroll']
|
||||||
|
@setBarVisibility Conf['Header auto-hide']
|
||||||
|
|
||||||
|
$.sync 'Fixed Header', @setBarFixed
|
||||||
|
$.sync 'Header auto-hide on scroll', @setHideBarOnScroll
|
||||||
|
$.sync 'Bottom Header', @setBarPosition
|
||||||
|
$.sync 'Header auto-hide', @setBarVisibility
|
||||||
|
|
||||||
|
@addShortcut menuButton
|
||||||
|
|
||||||
$.event 'AddMenuEntry',
|
$.event 'AddMenuEntry',
|
||||||
type: 'header'
|
type: 'header'
|
||||||
@ -44,6 +56,7 @@ Header =
|
|||||||
subEntries: [
|
subEntries: [
|
||||||
{el: barFixedToggler}
|
{el: barFixedToggler}
|
||||||
{el: headerToggler}
|
{el: headerToggler}
|
||||||
|
{el: scrollHeaderToggler}
|
||||||
{el: barPositionToggler}
|
{el: barPositionToggler}
|
||||||
{el: customNavToggler}
|
{el: customNavToggler}
|
||||||
{el: editCustomNav}
|
{el: editCustomNav}
|
||||||
@ -173,26 +186,6 @@ Header =
|
|||||||
custom.hidden = !showBoardList
|
custom.hidden = !showBoardList
|
||||||
full.hidden = showBoardList
|
full.hidden = showBoardList
|
||||||
|
|
||||||
setBarPosition: (bottom) ->
|
|
||||||
Header.barPositionToggler.checked = bottom
|
|
||||||
if bottom
|
|
||||||
$.rmClass doc, 'top'
|
|
||||||
$.addClass doc, 'bottom'
|
|
||||||
$.after Header.bar, Header.notify
|
|
||||||
else
|
|
||||||
$.rmClass doc, 'bottom'
|
|
||||||
$.addClass doc, 'top'
|
|
||||||
$.add Header.bar, Header.notify
|
|
||||||
Style.padding()
|
|
||||||
|
|
||||||
toggleBarPosition: ->
|
|
||||||
$.event 'CloseMenu'
|
|
||||||
|
|
||||||
Header.setBarPosition @checked
|
|
||||||
|
|
||||||
Conf['Bottom Header'] = @checked
|
|
||||||
$.set 'Bottom Header', @checked
|
|
||||||
|
|
||||||
setBarFixed: (fixed) ->
|
setBarFixed: (fixed) ->
|
||||||
Header.barFixedToggler.checked = fixed
|
Header.barFixedToggler.checked = fixed
|
||||||
if fixed
|
if fixed
|
||||||
@ -210,21 +203,6 @@ Header =
|
|||||||
Conf['Fixed Header'] = @checked
|
Conf['Fixed Header'] = @checked
|
||||||
$.set 'Fixed Header', @checked
|
$.set 'Fixed Header', @checked
|
||||||
|
|
||||||
setShortcutIcons: (show) ->
|
|
||||||
Header.shortcutToggler.checked = show
|
|
||||||
if show
|
|
||||||
$.addClass doc, 'shortcut-icons'
|
|
||||||
else
|
|
||||||
$.rmClass doc, 'shortcut-icons'
|
|
||||||
|
|
||||||
toggleShortcutIcons: ->
|
|
||||||
$.event 'CloseMenu'
|
|
||||||
|
|
||||||
Header.setShortcutIcons @checked
|
|
||||||
|
|
||||||
Conf['Shortcut Icons'] = @checked
|
|
||||||
$.set 'Shortcut Icons', @checked
|
|
||||||
|
|
||||||
setBarVisibility: (hide) ->
|
setBarVisibility: (hide) ->
|
||||||
Header.headerToggler.checked = hide
|
Header.headerToggler.checked = hide
|
||||||
$.event 'CloseMenu'
|
$.event 'CloseMenu'
|
||||||
@ -247,6 +225,46 @@ Header =
|
|||||||
'remain visible.'}"
|
'remain visible.'}"
|
||||||
new Notice 'info', message, 2
|
new Notice 'info', message, 2
|
||||||
|
|
||||||
|
setHideBarOnScroll: (hide) ->
|
||||||
|
Header.scrollHeaderToggler.checked = hide
|
||||||
|
if hide
|
||||||
|
$.on window, 'scroll', Header.hideBarOnScroll
|
||||||
|
return
|
||||||
|
$.off window, 'scroll', Header.hideBarOnScroll
|
||||||
|
$.rmClass Header.bar, 'scroll'
|
||||||
|
$.rmClass Header.bar, 'autohide' unless Conf['Header auto-hide']
|
||||||
|
|
||||||
|
toggleHideBarOnScroll: (e) ->
|
||||||
|
hide = @checked
|
||||||
|
$.set 'Header auto-hide on scroll', hide
|
||||||
|
Header.setHideBarOnScroll hide
|
||||||
|
|
||||||
|
hideBarOnScroll: ->
|
||||||
|
offsetY = window.pageYOffset
|
||||||
|
if offsetY > (Header.previousOffset or 0)
|
||||||
|
$.addClass Header.bar, 'autohide'
|
||||||
|
$.addClass Header.bar, 'scroll'
|
||||||
|
else
|
||||||
|
$.rmClass Header.bar, 'autohide'
|
||||||
|
$.rmClass Header.bar, 'scroll'
|
||||||
|
Header.previousOffset = offsetY
|
||||||
|
|
||||||
|
setBarPosition: (bottom) ->
|
||||||
|
Header.barPositionToggler.checked = bottom
|
||||||
|
$.event 'CloseMenu'
|
||||||
|
if bottom
|
||||||
|
$.addClass doc, 'bottom-header'
|
||||||
|
$.rmClass doc, 'top-header'
|
||||||
|
Header.bar.parentNode.className = 'bottom'
|
||||||
|
else
|
||||||
|
$.addClass doc, 'top-header'
|
||||||
|
$.rmClass doc, 'bottom-header'
|
||||||
|
Header.bar.parentNode.className = 'top'
|
||||||
|
|
||||||
|
toggleBarPosition: ->
|
||||||
|
$.cb.checked.call @
|
||||||
|
Header.setBarPosition @checked
|
||||||
|
|
||||||
setCustomNav: (show) ->
|
setCustomNav: (show) ->
|
||||||
Header.customNavToggler.checked = show
|
Header.customNavToggler.checked = show
|
||||||
cust = $ '#custom-board-list', Header.bar
|
cust = $ '#custom-board-list', Header.bar
|
||||||
@ -267,29 +285,47 @@ Header =
|
|||||||
$('input[name=boardnav]', settings).focus()
|
$('input[name=boardnav]', settings).focus()
|
||||||
|
|
||||||
hashScroll: ->
|
hashScroll: ->
|
||||||
return unless (hash = @location.hash[1..]) and post = $.id hash
|
hash = @location.hash[1..]
|
||||||
|
return unless /^p\d+$/.test(hash) and post = $.id hash
|
||||||
return if (Get.postFromRoot post).isHidden
|
return if (Get.postFromRoot post).isHidden
|
||||||
Header.scrollToPost post
|
|
||||||
|
|
||||||
scrollToPost: (post) ->
|
Header.scrollTo post
|
||||||
{top} = post.getBoundingClientRect()
|
scrollTo: (root, down, needed) ->
|
||||||
|
if down
|
||||||
|
x = Header.getBottomOf root
|
||||||
|
window.scrollBy 0, -x unless needed and x >= 0
|
||||||
|
else
|
||||||
|
x = Header.getTopOf root
|
||||||
|
window.scrollBy 0, x unless needed and x >= 0
|
||||||
|
scrollToIfNeeded: (root, down) ->
|
||||||
|
Header.scrollTo root, down, true
|
||||||
|
getTopOf: (root) ->
|
||||||
|
{top} = root.getBoundingClientRect()
|
||||||
if Conf['Fixed Header'] and not Conf['Bottom Header']
|
if Conf['Fixed Header'] and not Conf['Bottom Header']
|
||||||
headRect = Header.bar.getBoundingClientRect()
|
headRect = Header.toggle.getBoundingClientRect()
|
||||||
top -= headRect.top + headRect.height
|
top -= headRect.top + headRect.height
|
||||||
window.scrollBy 0, top
|
top
|
||||||
|
getBottomOf: (root) ->
|
||||||
|
{clientHeight} = doc
|
||||||
|
bottom = clientHeight - root.getBoundingClientRect().bottom
|
||||||
|
if Conf['Bottom header']
|
||||||
|
headRect = Header.toggle.getBoundingClientRect()
|
||||||
|
bottom -= clientHeight - headRect.bottom + headRect.height
|
||||||
|
bottom
|
||||||
|
|
||||||
addShortcut: (el, icon) ->
|
addShortcut: (el, icon) ->
|
||||||
$.addClass el, 'shortcut'
|
$.addClass el, 'shortcut'
|
||||||
|
|
||||||
$.add Header[if icon then 'icons' else 'stats'], el
|
$.add Header[if icon then 'icons' else 'stats'], el
|
||||||
|
|
||||||
|
|
||||||
menuToggle: (e) ->
|
menuToggle: (e) ->
|
||||||
Header.menu.toggle e, @, g
|
Header.menu.toggle e, @, g
|
||||||
|
|
||||||
createNotification: (e) ->
|
createNotification: (e) ->
|
||||||
{type, content, lifetime, cb} = e.detail
|
{type, content, lifetime, cb} = e.detail
|
||||||
notif = new Notice type, content, lifetime
|
notice = new Notice type, content, lifetime
|
||||||
cb notif if cb
|
cb notice if cb
|
||||||
|
|
||||||
areNotificationsEnabled: false
|
areNotificationsEnabled: false
|
||||||
enableDesktopNotifications: ->
|
enableDesktopNotifications: ->
|
||||||
|
|||||||
393
src/General/Index.coffee
Normal file
393
src/General/Index.coffee
Normal file
@ -0,0 +1,393 @@
|
|||||||
|
Index =
|
||||||
|
init: ->
|
||||||
|
return if g.VIEW isnt 'index' or g.BOARD.ID is 'f'
|
||||||
|
|
||||||
|
@button = $.el 'a',
|
||||||
|
className: 'index-refresh-shortcut fa fa-refresh'
|
||||||
|
title: 'Refresh Index'
|
||||||
|
href: 'javascript:;'
|
||||||
|
$.on @button, 'click', @update
|
||||||
|
Header.addShortcut @button, 1
|
||||||
|
|
||||||
|
modeEntry =
|
||||||
|
el: $.el 'span', textContent: 'Index mode'
|
||||||
|
subEntries: [
|
||||||
|
{ el: $.el 'label', innerHTML: '<input type=radio name="Index Mode" value="paged"> Paged' }
|
||||||
|
{ el: $.el 'label', innerHTML: '<input type=radio name="Index Mode" value="all pages"> All threads' }
|
||||||
|
]
|
||||||
|
for label in modeEntry.subEntries
|
||||||
|
input = label.el.firstChild
|
||||||
|
input.checked = Conf['Index Mode'] is input.value
|
||||||
|
$.on input, 'change', $.cb.value
|
||||||
|
$.on input, 'change', @cb.mode
|
||||||
|
|
||||||
|
sortEntry =
|
||||||
|
el: $.el 'span', textContent: 'Sort by'
|
||||||
|
subEntries: [
|
||||||
|
{ el: $.el 'label', innerHTML: '<input type=radio name="Index Sort" value="bump"> Bump order' }
|
||||||
|
{ el: $.el 'label', innerHTML: '<input type=radio name="Index Sort" value="lastreply"> Last reply' }
|
||||||
|
{ el: $.el 'label', innerHTML: '<input type=radio name="Index Sort" value="birth"> Creation date' }
|
||||||
|
{ el: $.el 'label', innerHTML: '<input type=radio name="Index Sort" value="replycount"> Reply count' }
|
||||||
|
{ el: $.el 'label', innerHTML: '<input type=radio name="Index Sort" value="filecount"> File count' }
|
||||||
|
]
|
||||||
|
for label in sortEntry.subEntries
|
||||||
|
input = label.el.firstChild
|
||||||
|
input.checked = Conf['Index Sort'] is input.value
|
||||||
|
$.on input, 'change', $.cb.value
|
||||||
|
$.on input, 'change', @cb.sort
|
||||||
|
|
||||||
|
repliesEntry =
|
||||||
|
el: $.el 'label', innerHTML: '<input type=checkbox name="Show Replies"> Show replies'
|
||||||
|
input = repliesEntry.el.firstChild
|
||||||
|
input.checked = Conf['Show Replies']
|
||||||
|
$.on input, 'change', $.cb.checked
|
||||||
|
$.on input, 'change', @cb.replies
|
||||||
|
|
||||||
|
$.event 'AddMenuEntry',
|
||||||
|
type: 'header'
|
||||||
|
el: $.el 'span',
|
||||||
|
textContent: 'Index Navigation'
|
||||||
|
order: 90
|
||||||
|
subEntries: [modeEntry, sortEntry, repliesEntry]
|
||||||
|
|
||||||
|
$.addClass doc, 'index-loading'
|
||||||
|
@update()
|
||||||
|
@root = $.el 'div', className: 'board'
|
||||||
|
@pagelist = $.el 'div',
|
||||||
|
className: 'pagelist'
|
||||||
|
hidden: true
|
||||||
|
innerHTML: <%= importHTML('Features/Index-pagelist') %>
|
||||||
|
@navLinks = $.el 'div',
|
||||||
|
className: 'navLinks'
|
||||||
|
innerHTML: <%= importHTML('Features/Index-navlinks') %>
|
||||||
|
@searchInput = $ '#index-search', @navLinks
|
||||||
|
@currentPage = @getCurrentPage()
|
||||||
|
$.on window, 'popstate', @cb.popstate
|
||||||
|
$.on @pagelist, 'click', @cb.pageNav
|
||||||
|
$.on @searchInput, 'input', @onSearchInput
|
||||||
|
$.on $('#index-search-clear', @navLinks), 'click', @clearSearch
|
||||||
|
$.asap (-> $('.board', doc) or d.readyState isnt 'loading'), ->
|
||||||
|
board = $ '.board'
|
||||||
|
$.replace board, Index.root
|
||||||
|
# Hacks:
|
||||||
|
# - When removing an element from the document during page load,
|
||||||
|
# its ancestors will still be correctly created inside of it.
|
||||||
|
# - Creating loadable elements inside of an origin-less document
|
||||||
|
# will not download them.
|
||||||
|
# - Combine the two and you get a download canceller!
|
||||||
|
# Does not work on Firefox unfortunately. bugzil.la/939713
|
||||||
|
d.implementation.createDocument(null, null, null).appendChild board
|
||||||
|
|
||||||
|
for navLink in $$ '.navLinks'
|
||||||
|
$.rm navLink
|
||||||
|
$.after $.x('child::form/preceding-sibling::hr[1]'), Index.navLinks
|
||||||
|
$.rmClass doc, 'index-loading'
|
||||||
|
$.asap (-> $('.pagelist') or d.readyState isnt 'loading'), ->
|
||||||
|
$.replace $('.pagelist'), Index.pagelist
|
||||||
|
|
||||||
|
cb:
|
||||||
|
mode: ->
|
||||||
|
Index.togglePagelist()
|
||||||
|
Index.buildIndex()
|
||||||
|
sort: ->
|
||||||
|
Index.sort()
|
||||||
|
Index.buildIndex()
|
||||||
|
replies: ->
|
||||||
|
Index.buildThreads()
|
||||||
|
Index.sort()
|
||||||
|
Index.buildIndex()
|
||||||
|
popstate: (e) ->
|
||||||
|
pageNum = Index.getCurrentPage()
|
||||||
|
Index.pageLoad pageNum if Index.currentPage isnt pageNum
|
||||||
|
pageNav: (e) ->
|
||||||
|
return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or e.button isnt 0
|
||||||
|
switch e.target.nodeName
|
||||||
|
when 'BUTTON'
|
||||||
|
a = e.target.parentNode
|
||||||
|
when 'A'
|
||||||
|
a = e.target
|
||||||
|
else
|
||||||
|
return
|
||||||
|
return if a.textContent is 'Catalog'
|
||||||
|
e.preventDefault()
|
||||||
|
Index.pageNav +a.pathname.split('/')[2]
|
||||||
|
|
||||||
|
scrollToIndex: ->
|
||||||
|
Header.scrollToIfNeeded Index.root
|
||||||
|
|
||||||
|
getCurrentPage: ->
|
||||||
|
+window.location.pathname.split('/')[2]
|
||||||
|
pageNav: (pageNum) ->
|
||||||
|
return if Index.currentPage is pageNum
|
||||||
|
history.pushState null, '', if pageNum is 0 then './' else pageNum
|
||||||
|
Index.pageLoad pageNum
|
||||||
|
pageLoad: (pageNum) ->
|
||||||
|
Index.currentPage = pageNum
|
||||||
|
return if Conf['Index Mode'] isnt 'paged'
|
||||||
|
Index.buildIndex()
|
||||||
|
Index.setPage()
|
||||||
|
Index.scrollToIndex()
|
||||||
|
|
||||||
|
getPagesNum: ->
|
||||||
|
if Index.isSearching
|
||||||
|
Math.ceil (Index.sortedNodes.length / 2) / Index.threadsNumPerPage
|
||||||
|
else
|
||||||
|
Index.pagesNum
|
||||||
|
getMaxPageNum: ->
|
||||||
|
Math.max 0, Index.getPagesNum() - 1
|
||||||
|
togglePagelist: ->
|
||||||
|
Index.pagelist.hidden = Conf['Index Mode'] isnt 'paged'
|
||||||
|
buildPagelist: ->
|
||||||
|
pagesRoot = $ '.pages', Index.pagelist
|
||||||
|
maxPageNum = Index.getMaxPageNum()
|
||||||
|
if pagesRoot.childElementCount isnt maxPageNum + 1
|
||||||
|
nodes = []
|
||||||
|
for i in [0..maxPageNum] by 1
|
||||||
|
a = $.el 'a',
|
||||||
|
textContent: i
|
||||||
|
href: if i then i else './'
|
||||||
|
nodes.push $.tn('['), a, $.tn '] '
|
||||||
|
$.rmAll pagesRoot
|
||||||
|
$.add pagesRoot, nodes
|
||||||
|
Index.togglePagelist()
|
||||||
|
setPage: ->
|
||||||
|
pageNum = Index.getCurrentPage()
|
||||||
|
maxPageNum = Index.getMaxPageNum()
|
||||||
|
pagesRoot = $ '.pages', Index.pagelist
|
||||||
|
# Previous/Next buttons
|
||||||
|
prev = pagesRoot.previousSibling.firstChild
|
||||||
|
next = pagesRoot.nextSibling.firstChild
|
||||||
|
href = Math.max pageNum - 1, 0
|
||||||
|
prev.href = if href is 0 then './' else href
|
||||||
|
prev.firstChild.disabled = href is pageNum
|
||||||
|
href = Math.min pageNum + 1, maxPageNum
|
||||||
|
next.href = if href is 0 then './' else href
|
||||||
|
next.firstChild.disabled = href is pageNum
|
||||||
|
# <strong> current page
|
||||||
|
if strong = $ 'strong', pagesRoot
|
||||||
|
return if +strong.textContent is pageNum
|
||||||
|
$.replace strong, strong.firstChild
|
||||||
|
else
|
||||||
|
strong = $.el 'strong'
|
||||||
|
a = pagesRoot.children[pageNum]
|
||||||
|
$.before a, strong
|
||||||
|
$.add strong, a
|
||||||
|
|
||||||
|
update: ->
|
||||||
|
return unless navigator.onLine
|
||||||
|
Index.req?.abort()
|
||||||
|
Index.notice?.close()
|
||||||
|
if d.readyState isnt 'loading'
|
||||||
|
Index.notice = new Notice 'info', 'Refreshing index...'
|
||||||
|
else
|
||||||
|
# Delay the notice on initial page load
|
||||||
|
# and only display it for slow connections.
|
||||||
|
now = Date.now()
|
||||||
|
$.ready ->
|
||||||
|
setTimeout (->
|
||||||
|
return unless Index.req and !Index.notice
|
||||||
|
Index.notice = new Notice 'info', 'Refreshing index...'
|
||||||
|
), 5 * $.SECOND - (Date.now() - now)
|
||||||
|
Index.req = $.ajax "//a.4cdn.org/#{g.BOARD}/catalog.json",
|
||||||
|
onabort: Index.load
|
||||||
|
onloadend: Index.load
|
||||||
|
,
|
||||||
|
whenModified: true
|
||||||
|
$.addClass Index.button, 'fa-spin'
|
||||||
|
load: (e) ->
|
||||||
|
$.rmClass Index.button, 'fa-spin'
|
||||||
|
{req, notice} = Index
|
||||||
|
delete Index.req
|
||||||
|
delete Index.notice
|
||||||
|
|
||||||
|
if e.type is 'abort'
|
||||||
|
req.onloadend = null
|
||||||
|
notice.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
try
|
||||||
|
Index.parse JSON.parse req.response if req.status is 200
|
||||||
|
catch err
|
||||||
|
c.error 'Index failure:', err.stack
|
||||||
|
# network error or non-JSON content for example.
|
||||||
|
if notice
|
||||||
|
notice.setType 'error'
|
||||||
|
notice.el.lastElementChild.textContent = 'Index refresh failed.'
|
||||||
|
setTimeout notice.close, 2 * $.SECOND
|
||||||
|
else
|
||||||
|
new Notice 'error', 'Index refresh failed.', 2
|
||||||
|
return
|
||||||
|
|
||||||
|
if notice
|
||||||
|
notice.setType 'success'
|
||||||
|
notice.el.lastElementChild.textContent = 'Index refreshed!'
|
||||||
|
setTimeout notice.close, $.SECOND
|
||||||
|
|
||||||
|
timeEl = $ '#index-last-refresh', Index.navLinks
|
||||||
|
timeEl.dataset.utc = e.timeStamp <% if (type === 'userscript') { %>/ 1000<% } %>
|
||||||
|
RelativeDates.update timeEl
|
||||||
|
Index.scrollToIndex()
|
||||||
|
parse: (pages) ->
|
||||||
|
Index.parseThreadList pages
|
||||||
|
Index.buildThreads()
|
||||||
|
Index.sort()
|
||||||
|
Index.buildIndex()
|
||||||
|
Index.buildPagelist()
|
||||||
|
Index.setPage()
|
||||||
|
parseThreadList: (pages) ->
|
||||||
|
Index.pagesNum = pages.length
|
||||||
|
Index.threadsNumPerPage = pages[0].threads.length
|
||||||
|
Index.liveThreadData = pages.reduce ((arr, next) -> arr.concat next.threads), []
|
||||||
|
Index.liveThreadIDs = Index.liveThreadData.map (data) -> data.no
|
||||||
|
for threadID, thread of g.BOARD.threads when thread.ID not in Index.liveThreadIDs
|
||||||
|
thread.collect()
|
||||||
|
return
|
||||||
|
buildThreads: ->
|
||||||
|
Index.nodes = []
|
||||||
|
threads = []
|
||||||
|
posts = []
|
||||||
|
for threadData, i in Index.liveThreadData
|
||||||
|
threadRoot = Build.thread g.BOARD, threadData
|
||||||
|
Index.nodes.push threadRoot, $.el 'hr'
|
||||||
|
if thread = g.BOARD.threads[threadData.no]
|
||||||
|
thread.setPage Math.floor i / Index.threadsNumPerPage
|
||||||
|
thread.setStatus 'Sticky', !!threadData.sticky
|
||||||
|
thread.setStatus 'Closed', !!threadData.closed
|
||||||
|
else
|
||||||
|
thread = new Thread threadData.no, g.BOARD
|
||||||
|
threads.push thread
|
||||||
|
continue if thread.ID of thread.posts
|
||||||
|
try
|
||||||
|
posts.push new Post $('.opContainer', threadRoot), thread, g.BOARD
|
||||||
|
catch err
|
||||||
|
# Skip posts that we failed to parse.
|
||||||
|
errors = [] unless errors
|
||||||
|
errors.push
|
||||||
|
message: "Parsing of Post No.#{thread} failed. Post will be skipped."
|
||||||
|
error: err
|
||||||
|
Main.handleErrors errors if errors
|
||||||
|
|
||||||
|
# Add the threads and <hr>s in a container to make sure all features work.
|
||||||
|
$.nodes Index.nodes
|
||||||
|
Main.callbackNodes Thread, threads
|
||||||
|
Main.callbackNodes Post, posts
|
||||||
|
$.event 'IndexRefresh'
|
||||||
|
buildReplies: (threadRoots) ->
|
||||||
|
posts = []
|
||||||
|
for threadRoot in threadRoots by 2
|
||||||
|
thread = Get.threadFromRoot threadRoot
|
||||||
|
i = Index.liveThreadIDs.indexOf thread.ID
|
||||||
|
continue unless lastReplies = Index.liveThreadData[i].last_replies
|
||||||
|
nodes = []
|
||||||
|
for data in lastReplies
|
||||||
|
if post = thread.posts[data.no]
|
||||||
|
nodes.push post.nodes.root
|
||||||
|
continue
|
||||||
|
nodes.push node = Build.postFromObject data, thread.board.ID
|
||||||
|
try
|
||||||
|
posts.push new Post node, thread, thread.board
|
||||||
|
catch err
|
||||||
|
# Skip posts that we failed to parse.
|
||||||
|
errors = [] unless errors
|
||||||
|
errors.push
|
||||||
|
message: "Parsing of Post No.#{data.no} failed. Post will be skipped."
|
||||||
|
error: err
|
||||||
|
$.add threadRoot, nodes
|
||||||
|
|
||||||
|
Main.handleErrors errors if errors
|
||||||
|
Main.callbackNodes Post, posts
|
||||||
|
sort: ->
|
||||||
|
switch Conf['Index Sort']
|
||||||
|
when 'bump'
|
||||||
|
sortedThreadIDs = Index.liveThreadIDs
|
||||||
|
when 'lastreply'
|
||||||
|
sortedThreadIDs = [Index.liveThreadData...].sort((a, b) ->
|
||||||
|
a = a.last_replies[a.last_replies.length - 1] if 'last_replies' of a
|
||||||
|
b = b.last_replies[b.last_replies.length - 1] if 'last_replies' of b
|
||||||
|
b.no - a.no
|
||||||
|
).map (data) -> data.no
|
||||||
|
when 'birth'
|
||||||
|
sortedThreadIDs = [Index.liveThreadIDs...].sort (a, b) -> b - a
|
||||||
|
when 'replycount'
|
||||||
|
sortedThreadIDs = [Index.liveThreadData...].sort((a, b) -> b.replies - a.replies).map (data) -> data.no
|
||||||
|
when 'filecount'
|
||||||
|
sortedThreadIDs = [Index.liveThreadData...].sort((a, b) -> b.images - a.images).map (data) -> data.no
|
||||||
|
Index.sortedNodes = []
|
||||||
|
for threadID in sortedThreadIDs
|
||||||
|
i = Index.liveThreadIDs.indexOf(threadID) * 2
|
||||||
|
Index.sortedNodes.push Index.nodes[i], Index.nodes[i + 1]
|
||||||
|
if Index.isSearching
|
||||||
|
Index.sortedNodes = Index.querySearch(Index.searchInput.value) or Index.sortedNodes
|
||||||
|
# Put the sticky threads on top of the index.
|
||||||
|
offset = 0
|
||||||
|
for threadRoot, i in Index.sortedNodes by 2 when Get.threadFromRoot(threadRoot).isSticky
|
||||||
|
Index.sortedNodes.splice offset++ * 2, 0, Index.sortedNodes.splice(i, 2)...
|
||||||
|
return unless Conf['Filter']
|
||||||
|
# Put the highlighted thread & <hr> on top of the index
|
||||||
|
# while keeping the original order they appear in.
|
||||||
|
offset = 0
|
||||||
|
for threadRoot, i in Index.sortedNodes by 2 when Get.threadFromRoot(threadRoot).isOnTop
|
||||||
|
Index.sortedNodes.splice offset++ * 2, 0, Index.sortedNodes.splice(i, 2)...
|
||||||
|
return
|
||||||
|
buildIndex: ->
|
||||||
|
if Conf['Index Mode'] is 'paged'
|
||||||
|
pageNum = Index.getCurrentPage()
|
||||||
|
nodesPerPage = Index.threadsNumPerPage * 2
|
||||||
|
nodes = Index.sortedNodes[nodesPerPage * pageNum ... nodesPerPage * (pageNum + 1)]
|
||||||
|
else
|
||||||
|
nodes = Index.sortedNodes
|
||||||
|
$.rmAll Index.root
|
||||||
|
Index.buildReplies nodes if Conf['Show Replies']
|
||||||
|
$.event 'IndexBuild', nodes
|
||||||
|
$.add Index.root, nodes
|
||||||
|
|
||||||
|
isSearching: false
|
||||||
|
clearSearch: ->
|
||||||
|
Index.searchInput.value = null
|
||||||
|
Index.onSearchInput()
|
||||||
|
Index.searchInput.focus()
|
||||||
|
onSearchInput: ->
|
||||||
|
if Index.isSearching = !!Index.searchInput.value.trim()
|
||||||
|
unless Index.searchInput.dataset.searching
|
||||||
|
Index.searchInput.dataset.searching = 1
|
||||||
|
Index.pageBeforeSearch = Index.getCurrentPage()
|
||||||
|
pageNum = 0
|
||||||
|
else
|
||||||
|
pageNum = Index.getCurrentPage()
|
||||||
|
else
|
||||||
|
pageNum = Index.pageBeforeSearch
|
||||||
|
delete Index.pageBeforeSearch
|
||||||
|
<% if (type === 'userscript') { %>
|
||||||
|
# XXX https://github.com/greasemonkey/greasemonkey/issues/1571
|
||||||
|
Index.searchInput.removeAttribute 'data-searching'
|
||||||
|
<% } else { %>
|
||||||
|
delete Index.searchInput.dataset.searching
|
||||||
|
<% } %>
|
||||||
|
Index.sort()
|
||||||
|
# Go to the last available page if we were past the limit.
|
||||||
|
pageNum = Math.min pageNum, Index.getMaxPageNum() if Conf['Index Mode'] is 'paged'
|
||||||
|
Index.buildPagelist()
|
||||||
|
if Index.currentPage is pageNum
|
||||||
|
Index.buildIndex()
|
||||||
|
Index.setPage()
|
||||||
|
else
|
||||||
|
Index.pageNav pageNum
|
||||||
|
querySearch: (query) ->
|
||||||
|
return unless keywords = query.toLowerCase().match /\S+/g
|
||||||
|
Index.search keywords
|
||||||
|
search: (keywords) ->
|
||||||
|
found = []
|
||||||
|
for threadRoot, i in Index.sortedNodes by 2
|
||||||
|
if Index.searchMatch Get.threadFromRoot(threadRoot), keywords
|
||||||
|
found.push Index.sortedNodes[i], Index.sortedNodes[i + 1]
|
||||||
|
found
|
||||||
|
searchMatch: (thread, keywords) ->
|
||||||
|
{info, file} = thread.OP
|
||||||
|
text = []
|
||||||
|
for key in ['comment', 'subject', 'name', 'tripcode', 'email']
|
||||||
|
text.push info[key] if key of info
|
||||||
|
text.push file.name if file
|
||||||
|
text = text.join(' ').toLowerCase()
|
||||||
|
for keyword in keywords
|
||||||
|
return false if -1 is text.indexOf keyword
|
||||||
|
return true
|
||||||
@ -74,14 +74,14 @@ Main =
|
|||||||
g.VIEW = 'home'
|
g.VIEW = 'home'
|
||||||
Style.init()
|
Style.init()
|
||||||
return
|
return
|
||||||
when 'api.4chan.org'
|
when 'a.4cdn.org'
|
||||||
return
|
return
|
||||||
when 'sys.4chan.org'
|
when 'sys.4chan.org'
|
||||||
g.VIEW = 'report'
|
g.VIEW = 'report'
|
||||||
Style.init()
|
Style.init()
|
||||||
Report.init()
|
Report.init()
|
||||||
return
|
return
|
||||||
when 'images.4chan.org'
|
when 'i.4cdn.org'
|
||||||
$.ready ->
|
$.ready ->
|
||||||
if Conf['404 Redirect'] and ['4chan - Temporarily Offline', '4chan - 404 Not Found'].contains d.title
|
if Conf['404 Redirect'] and ['4chan - Temporarily Offline', '4chan - 404 Not Found'].contains d.title
|
||||||
Redirect.init()
|
Redirect.init()
|
||||||
@ -106,7 +106,6 @@ Main =
|
|||||||
return
|
return
|
||||||
|
|
||||||
# c.time 'All initializations'
|
# c.time 'All initializations'
|
||||||
|
|
||||||
init
|
init
|
||||||
'Polyfill': Polyfill
|
'Polyfill': Polyfill
|
||||||
'Emoji': Emoji
|
'Emoji': Emoji
|
||||||
@ -119,6 +118,7 @@ Main =
|
|||||||
'Header': Header
|
'Header': Header
|
||||||
'Catalog Links': CatalogLinks
|
'Catalog Links': CatalogLinks
|
||||||
'Settings': Settings
|
'Settings': Settings
|
||||||
|
'Index Generator': Index
|
||||||
'Announcement Hiding': PSAHiding
|
'Announcement Hiding': PSAHiding
|
||||||
'Fourchan thingies': Fourchan
|
'Fourchan thingies': Fourchan
|
||||||
'Color User IDs': IDColor
|
'Color User IDs': IDColor
|
||||||
@ -159,7 +159,6 @@ Main =
|
|||||||
'Reveal Spoiler Thumbnails': RevealSpoilers
|
'Reveal Spoiler Thumbnails': RevealSpoilers
|
||||||
'Image Loading': ImageLoader
|
'Image Loading': ImageLoader
|
||||||
'Image Hover': ImageHover
|
'Image Hover': ImageHover
|
||||||
'Comment Expansion': ExpandComment
|
|
||||||
'Thread Expansion': ExpandThread
|
'Thread Expansion': ExpandThread
|
||||||
'Thread Excerpt': ThreadExcerpt
|
'Thread Excerpt': ThreadExcerpt
|
||||||
'Favicon': Favicon
|
'Favicon': Favicon
|
||||||
@ -172,8 +171,6 @@ Main =
|
|||||||
'Index Navigation': Nav
|
'Index Navigation': Nav
|
||||||
'Keybinds': Keybinds
|
'Keybinds': Keybinds
|
||||||
'Show Dice Roll': Dice
|
'Show Dice Roll': Dice
|
||||||
'Infinite Scrolling': InfiniScroll
|
|
||||||
|
|
||||||
# c.timeEnd 'All initializations'
|
# c.timeEnd 'All initializations'
|
||||||
|
|
||||||
$.on d, 'AddCallback', Main.addCallback
|
$.on d, 'AddCallback', Main.addCallback
|
||||||
@ -189,26 +186,21 @@ Main =
|
|||||||
location.replace href or "/#{g.BOARD}/"
|
location.replace href or "/#{g.BOARD}/"
|
||||||
return
|
return
|
||||||
|
|
||||||
if board = $ '.board'
|
if g.VIEW is 'thread' and threadRoot = $ '.thread'
|
||||||
threads = []
|
thread = new Thread +threadRoot.id[1..], g.BOARD
|
||||||
posts = []
|
posts = []
|
||||||
|
for postRoot in $$ '.thread > .postContainer', threadRoot
|
||||||
for threadRoot in $$ '.board > .thread', board
|
try
|
||||||
thread = new Thread +threadRoot.id[1..], g.BOARD
|
posts.push new Post postRoot, thread, g.BOARD
|
||||||
threads.push thread
|
catch err
|
||||||
for postRoot in $$ '.thread > .postContainer', threadRoot
|
# Skip posts that we failed to parse.
|
||||||
try
|
errors = [] unless errors
|
||||||
posts.push new Post postRoot, thread, g.BOARD
|
errors.push
|
||||||
catch err
|
message: "Parsing of Post No.#{postRoot.id.match /\d+/} failed. Post will be skipped."
|
||||||
# Skip posts that we failed to parse.
|
error: err
|
||||||
unless errors
|
|
||||||
errors = []
|
|
||||||
errors.push
|
|
||||||
message: "Parsing of Post No.#{postRoot.id.match(/\d+/)} failed. Post will be skipped."
|
|
||||||
error: err
|
|
||||||
Main.handleErrors errors if errors
|
Main.handleErrors errors if errors
|
||||||
|
|
||||||
Main.callbackNodes Thread, threads
|
Main.callbackNodes Thread, [thread]
|
||||||
Main.callbackNodesDB Post, posts, ->
|
Main.callbackNodesDB Post, posts, ->
|
||||||
$.event '4chanXInitFinished'
|
$.event '4chanXInitFinished'
|
||||||
|
|
||||||
@ -224,11 +216,19 @@ Main =
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
<% if (type === 'userscript') { %>
|
||||||
|
GMver = GM_info.version.split '.'
|
||||||
|
for v, i in "<%= meta.min.greasemonkey %>".split '.'
|
||||||
|
break if v < GMver[i]
|
||||||
|
continue if v is GMver[i]
|
||||||
|
new Notice 'warning', "Your version of Greasemonkey is outdated (v#{GM_info.version} instead of v<%= meta.min.greasemonkey %> minimum) and <%= meta.name %> may not operate correctly.", 30
|
||||||
|
break
|
||||||
|
<% } %>
|
||||||
|
|
||||||
try
|
try
|
||||||
localStorage.getItem '4chan-settings'
|
localStorage.getItem '4chan-settings'
|
||||||
catch err
|
catch err
|
||||||
new Notice 'warning', 'Cookies need to be enabled on 4chan for <%= meta.name %> to properly function.', 30
|
new Notice 'warning', 'Cookies need to be enabled on 4chan for <%= meta.name %> to operate properly.', 30
|
||||||
Main.disableReports = true
|
|
||||||
|
|
||||||
$.event '4chanXInitFinished'
|
$.event '4chanXInitFinished'
|
||||||
|
|
||||||
@ -326,18 +326,13 @@ Main =
|
|||||||
new Notice 'error', [div, logs], 30
|
new Notice 'error', [div, logs], 30
|
||||||
|
|
||||||
parseError: (data) ->
|
parseError: (data) ->
|
||||||
Main.logError data
|
c.error data.message, data.error.stack
|
||||||
message = $.el 'div',
|
message = $.el 'div',
|
||||||
textContent: data.message
|
textContent: data.message
|
||||||
error = $.el 'div',
|
error = $.el 'div',
|
||||||
textContent: data.error
|
textContent: data.error
|
||||||
[message, error]
|
[message, error]
|
||||||
|
|
||||||
errors: []
|
|
||||||
logError: (data) ->
|
|
||||||
c.error data.message, data.error.stack
|
|
||||||
Main.errors.push data
|
|
||||||
|
|
||||||
isThisPageLegit: ->
|
isThisPageLegit: ->
|
||||||
# 404 error page or similar.
|
# 404 error page or similar.
|
||||||
unless 'thisPageIsLegit' of Main
|
unless 'thisPageIsLegit' of Main
|
||||||
|
|||||||
@ -72,7 +72,7 @@ Settings =
|
|||||||
Settings.dialog = dialog = $.el 'div',
|
Settings.dialog = dialog = $.el 'div',
|
||||||
id: 'appchanx-settings'
|
id: 'appchanx-settings'
|
||||||
class: 'dialog'
|
class: 'dialog'
|
||||||
innerHTML: """<%= grunt.file.read('src/General/html/Settings/Settings.html').replace(/>\s+</g, '><').trim() %>"""
|
innerHTML: <%= importHTML('Settings/Settings') %>
|
||||||
|
|
||||||
Settings.overlay = overlay = $.el 'div',
|
Settings.overlay = overlay = $.el 'div',
|
||||||
id: 'overlay'
|
id: 'overlay'
|
||||||
@ -151,7 +151,7 @@ Settings =
|
|||||||
return
|
return
|
||||||
|
|
||||||
div = $.el 'div',
|
div = $.el 'div',
|
||||||
innerHTML: "<button></button><span class=description>: Clear manually-hidden threads and posts on all boards. Refresh the page to apply."
|
innerHTML: "<button></button><span class=description>: Clear manually-hidden threads and posts on all boards. Reload the page to apply."
|
||||||
button = $ 'button', div
|
button = $ 'button', div
|
||||||
hiddenNum = 0
|
hiddenNum = 0
|
||||||
$.get 'hiddenThreads', boards: {}, (item) ->
|
$.get 'hiddenThreads', boards: {}, (item) ->
|
||||||
@ -217,7 +217,7 @@ Settings =
|
|||||||
try
|
try
|
||||||
data = JSON.parse e.target.result
|
data = JSON.parse e.target.result
|
||||||
Settings.loadSettings data
|
Settings.loadSettings data
|
||||||
if confirm 'Import successful. Refresh now?'
|
if confirm 'Import successful. Reload now?'
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
catch err
|
catch err
|
||||||
output.textContent = 'Import failed due to an error.'
|
output.textContent = 'Import failed due to an error.'
|
||||||
@ -237,9 +237,7 @@ Settings =
|
|||||||
data
|
data
|
||||||
|
|
||||||
filter: (section) ->
|
filter: (section) ->
|
||||||
section.innerHTML = """
|
section.innerHTML = <%= importHTML('Settings/Filter-select') %>
|
||||||
<%= grunt.file.read('src/General/html/Settings/Filter-select.html').replace(/>\s+</g, '><').trim() %>
|
|
||||||
"""
|
|
||||||
select = $ 'select', section
|
select = $ 'select', section
|
||||||
$.on select, 'change', Settings.selectFilter
|
$.on select, 'change', Settings.selectFilter
|
||||||
Settings.selectFilter.call select
|
Settings.selectFilter.call select
|
||||||
@ -257,14 +255,10 @@ Settings =
|
|||||||
$.on ta, 'change', $.cb.value
|
$.on ta, 'change', $.cb.value
|
||||||
$.add div, ta
|
$.add div, ta
|
||||||
return
|
return
|
||||||
div.innerHTML = """
|
div.innerHTML = <%= importHTML('Settings/Filter-guide') %>
|
||||||
<%= grunt.file.read('src/General/html/Settings/Filter-guide.html').replace(/>\s+</g, '><').trim() %>
|
|
||||||
"""
|
|
||||||
|
|
||||||
sauce: (section) ->
|
sauce: (section) ->
|
||||||
section.innerHTML = """
|
section.innerHTML = <%= importHTML('Settings/Sauce') %>
|
||||||
<%= grunt.file.read('src/General/html/Settings/Sauce.html').replace(/>\s+</g, '><').trim() %>
|
|
||||||
"""
|
|
||||||
ta = $ 'textarea', section
|
ta = $ 'textarea', section
|
||||||
$.get 'sauces', Conf['sauces'], (item) ->
|
$.get 'sauces', Conf['sauces'], (item) ->
|
||||||
# XXX remove .replace func after 31-7-2013 (v1 transitioning)
|
# XXX remove .replace func after 31-7-2013 (v1 transitioning)
|
||||||
@ -283,7 +277,7 @@ Settings =
|
|||||||
$.on ta, 'change', $.cb.value
|
$.on ta, 'change', $.cb.value
|
||||||
|
|
||||||
advanced: (section) ->
|
advanced: (section) ->
|
||||||
section.innerHTML = """<%= grunt.file.read('src/General/html/Settings/Advanced.html').replace(/>\s+</g, '><').trim() %>"""
|
section.innerHTML = <%= importHTML('Settings/Advanced') %>
|
||||||
items = {}
|
items = {}
|
||||||
inputs = {}
|
inputs = {}
|
||||||
for name in ['boardnav', 'time', 'backlink', 'fileInfo', 'favicon', 'usercss']
|
for name in ['boardnav', 'time', 'backlink', 'fileInfo', 'favicon', 'usercss']
|
||||||
@ -406,7 +400,7 @@ Settings =
|
|||||||
data =
|
data =
|
||||||
isReply: true
|
isReply: true
|
||||||
file:
|
file:
|
||||||
URL: '//images.4chan.org/g/src/1334437723720.jpg'
|
URL: '//i.4cdn.org/g/src/1334437723720.jpg'
|
||||||
name: 'd9bb2efc98dd0df141a94399ff5880b7.jpg'
|
name: 'd9bb2efc98dd0df141a94399ff5880b7.jpg'
|
||||||
size: '276 KB'
|
size: '276 KB'
|
||||||
sizeInBytes: 276 * 1024
|
sizeInBytes: 276 * 1024
|
||||||
@ -435,10 +429,10 @@ Settings =
|
|||||||
|
|
||||||
usercss: ->
|
usercss: ->
|
||||||
CustomCSS.update()
|
CustomCSS.update()
|
||||||
|
|
||||||
keybinds: (section) ->
|
keybinds: (section) ->
|
||||||
section.innerHTML = """
|
section.innerHTML = <%= importHTML('Settings/Keybinds') %>
|
||||||
<%= grunt.file.read('src/General/html/Settings/Keybinds.html').replace(/>\s+</g, '><').trim() %>
|
|
||||||
"""
|
|
||||||
tbody = $ 'tbody', section
|
tbody = $ 'tbody', section
|
||||||
items = {}
|
items = {}
|
||||||
inputs = {}
|
inputs = {}
|
||||||
|
|||||||
@ -170,7 +170,7 @@ UI = do ->
|
|||||||
['0px', 'auto']
|
['0px', 'auto']
|
||||||
else
|
else
|
||||||
['auto', '0px']
|
['auto', '0px']
|
||||||
[left, right] = if eRect.right + sRect.width < cWidth
|
[left, right] = if eRect.right + sRect.width < cWidth - 150
|
||||||
['100%', 'auto']
|
['100%', 'auto']
|
||||||
else
|
else
|
||||||
['auto', '100%']
|
['auto', '100%']
|
||||||
|
|||||||
19
src/General/css/font-awesome.css
vendored
19
src/General/css/font-awesome.css
vendored
File diff suppressed because one or more lines are too long
@ -34,6 +34,9 @@
|
|||||||
background-color: #F2F2F2;
|
background-color: #F2F2F2;
|
||||||
color: #888;
|
color: #888;
|
||||||
}
|
}
|
||||||
|
.field::-webkit-search-decoration {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
.move {
|
.move {
|
||||||
cursor: move;
|
cursor: move;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -278,7 +281,7 @@ a {
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
.notification > .close {
|
.notification > .close {
|
||||||
padding: 6px;
|
padding: 7px;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 5px;
|
right: 5px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -329,7 +332,7 @@ a {
|
|||||||
}
|
}
|
||||||
#fourchanx-settings > nav a.close {
|
#fourchanx-settings > nav a.close {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
padding: 2px;
|
padding: 0 2px;
|
||||||
}
|
}
|
||||||
.section-container {
|
.section-container {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
@ -426,6 +429,37 @@ a {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Index */
|
||||||
|
:root.index-loading .navLinks,
|
||||||
|
:root.index-loading .board,
|
||||||
|
:root.index-loading .pagelist {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
#index-search {
|
||||||
|
padding-right: 1.5em;
|
||||||
|
width: 100px;
|
||||||
|
transition: color .25s, border-color .25s, width .25s;
|
||||||
|
}
|
||||||
|
#index-search:focus,
|
||||||
|
#index-search[data-searching] {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
#index-search-clear {
|
||||||
|
color: gray;
|
||||||
|
margin-left: -1.25em;
|
||||||
|
}
|
||||||
|
<% if (type === 'crx') { %>
|
||||||
|
/* ``::-webkit-*'' selectors break selector lists on Firefox. */
|
||||||
|
#index-search::-webkit-search-cancel-button,
|
||||||
|
<% } %>
|
||||||
|
#index-search:not([data-searching]) + #index-search-clear {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.summary {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Announcement Hiding */
|
/* Announcement Hiding */
|
||||||
:root.hide-announcement #globalMessage {
|
:root.hide-announcement #globalMessage {
|
||||||
display: none;
|
display: none;
|
||||||
@ -891,7 +925,7 @@ input#qr-filename:not(.edit) {
|
|||||||
opacity: .5;
|
opacity: .5;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
text-shadow: 0 1px 1px #000;
|
text-shadow: 0 0 2px #000;
|
||||||
-moz-transition: opacity .25s ease-in-out;
|
-moz-transition: opacity .25s ease-in-out;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
@ -923,8 +957,7 @@ input#qr-filename:not(.edit) {
|
|||||||
.remove {
|
.remove {
|
||||||
background: none;
|
background: none;
|
||||||
color: #e00;
|
color: #e00;
|
||||||
font-weight: 700;
|
padding: 1px;
|
||||||
padding: 3px;
|
|
||||||
}
|
}
|
||||||
a:only-of-type > .remove {
|
a:only-of-type > .remove {
|
||||||
display: none;
|
display: none;
|
||||||
@ -969,7 +1002,7 @@ a:only-of-type > .remove {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Menu */
|
/* Menu */
|
||||||
.menu-button {
|
.menu-button:not(.fa-bars) {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: relative;
|
position: relative;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -1023,7 +1056,7 @@ a:only-of-type > .remove {
|
|||||||
left: 100%;
|
left: 100%;
|
||||||
top: -1px;
|
top: -1px;
|
||||||
}
|
}
|
||||||
.focused .submenu {
|
.focused > .submenu {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
.imp-exp-result {
|
.imp-exp-result {
|
||||||
@ -1268,4 +1301,12 @@ a:only-of-type > .remove {
|
|||||||
:root.gal-hide-thumbnails:not(.gal-fit-height) .gal-name,
|
:root.gal-hide-thumbnails:not(.gal-fit-height) .gal-name,
|
||||||
:root.gal-hide-thumbnails:not(.gal-fit-height) .gal-count {
|
:root.gal-hide-thumbnails:not(.gal-fit-height) .gal-count {
|
||||||
right: 44px !important;
|
right: 44px !important;
|
||||||
|
}
|
||||||
|
@media screen and (resolution: 1dppx) {
|
||||||
|
.fa-bars {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
#shortcuts .fa-bars {
|
||||||
|
vertical-align: -1px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -38,7 +38,7 @@
|
|||||||
<span class='nameBlock#{capcodeClass}'>
|
<span class='nameBlock#{capcodeClass}'>
|
||||||
#{emailStart}
|
#{emailStart}
|
||||||
<span class=name>#{name or ''}</span>
|
<span class=name>#{name or ''}</span>
|
||||||
#{tripcode + capcodeStart + emailEnd + capcode + userID + flag + sticky + closed}
|
#{tripcode + capcodeStart + emailEnd + capcode + userID + flag}
|
||||||
</span>#{" "}
|
</span>#{" "}
|
||||||
<span class=dateTime data-utc=#{dateUTC}>#{date}</span>#{" "}
|
<span class=dateTime data-utc=#{dateUTC}>#{date}</span>#{" "}
|
||||||
<span class='postNum desktop'>
|
<span class='postNum desktop'>
|
||||||
@ -49,6 +49,7 @@
|
|||||||
else
|
else
|
||||||
"/#{boardID}/res/#{threadID}#q#{postID}"
|
"/#{boardID}/res/#{threadID}#q#{postID}"
|
||||||
}' title='Quote this post'>#{postID}</a>
|
}' title='Quote this post'>#{postID}</a>
|
||||||
|
#{pageIcon + sticky + closed + replyLink}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
4
src/General/html/Features/Index-navlinks.html
Normal file
4
src/General/html/Features/Index-navlinks.html
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
[<a href="./catalog">Catalog</a>]
|
||||||
|
[<time id="index-last-refresh" title="Last index refresh">...</time>]
|
||||||
|
<input type="search" id="index-search" class="field" placeholder="Search">
|
||||||
|
<a id="index-search-clear" class="fa fa-times-circle" href="javascript:;"></a>
|
||||||
14
src/General/html/Features/Index-pagelist.html
Normal file
14
src/General/html/Features/Index-pagelist.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<div class="prev">
|
||||||
|
<a>
|
||||||
|
<button disabled>Previous</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="pages"></div>
|
||||||
|
<div class="next">
|
||||||
|
<a>
|
||||||
|
<button disabled>Next</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="pages cataloglink">
|
||||||
|
<a href="./catalog">Catalog</a>
|
||||||
|
</div>
|
||||||
@ -23,7 +23,7 @@
|
|||||||
For example: <code>highlight;</code> or <code>highlight:wallpaper;</code>.
|
For example: <code>highlight;</code> or <code>highlight:wallpaper;</code>.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
Highlighted OPs will have their threads put on top of board pages by default.<br>
|
Highlighted OPs will have their threads put on top of the board index by default.<br>
|
||||||
For example: <code>top:yes;</code> or <code>top:no;</code>.
|
For example: <code>top:yes;</code> or <code>top:no;</code>.
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@ -63,7 +63,7 @@ $.ajax = do ->
|
|||||||
type or= form and 'post' or 'get'
|
type or= form and 'post' or 'get'
|
||||||
r.open type, url, !sync
|
r.open type, url, !sync
|
||||||
if whenModified
|
if whenModified
|
||||||
r.setRequestHeader 'If-Modified-Since', lastModified[url] or '0'
|
r.setRequestHeader 'If-Modified-Since', lastModified[url] if url of lastModified
|
||||||
$.on r, 'load', -> lastModified[url] = r.getResponseHeader 'Last-Modified'
|
$.on r, 'load', -> lastModified[url] = r.getResponseHeader 'Last-Modified'
|
||||||
$.extend r, options
|
$.extend r, options
|
||||||
$.extend r.upload, upCallbacks
|
$.extend r.upload, upCallbacks
|
||||||
@ -144,7 +144,7 @@ $.rm = do ->
|
|||||||
|
|
||||||
$.rmAll = (root) ->
|
$.rmAll = (root) ->
|
||||||
# jsperf.com/emptify-element
|
# jsperf.com/emptify-element
|
||||||
while node = root.firstChild
|
for node in [root.childNodes...]
|
||||||
# HTMLSelectElement.remove !== Element.remove
|
# HTMLSelectElement.remove !== Element.remove
|
||||||
root.removeChild node
|
root.removeChild node
|
||||||
return
|
return
|
||||||
@ -284,7 +284,7 @@ $.sync = do ->
|
|||||||
chrome.storage.onChanged.addListener (changes) ->
|
chrome.storage.onChanged.addListener (changes) ->
|
||||||
for key of changes
|
for key of changes
|
||||||
if cb = $.syncing[key]
|
if cb = $.syncing[key]
|
||||||
cb changes[key].newValue
|
cb changes[key].newValue, key
|
||||||
return
|
return
|
||||||
(key, cb) -> $.syncing[key] = cb
|
(key, cb) -> $.syncing[key] = cb
|
||||||
|
|
||||||
@ -367,9 +367,9 @@ $.set = do ->
|
|||||||
<% } else { %>
|
<% } else { %>
|
||||||
# http://wiki.greasespot.net/Main_Page
|
# http://wiki.greasespot.net/Main_Page
|
||||||
$.sync = do ->
|
$.sync = do ->
|
||||||
$.on window, 'storage', (e) ->
|
$.on window, 'storage', ({key, newValue}) ->
|
||||||
if cb = $.syncing[e.key]
|
if cb = $.syncing[key]
|
||||||
cb JSON.parse e.newValue
|
cb JSON.parse(newValue), key
|
||||||
(key, cb) -> $.syncing[g.NAMESPACE + key] = cb
|
(key, cb) -> $.syncing[g.NAMESPACE + key] = cb
|
||||||
|
|
||||||
$.delete = (keys) ->
|
$.delete = (keys) ->
|
||||||
|
|||||||
@ -73,8 +73,10 @@ class DataBoard
|
|||||||
@save()
|
@save()
|
||||||
|
|
||||||
ajaxClean: (boardID) ->
|
ajaxClean: (boardID) ->
|
||||||
$.cache "//api.4chan.org/#{boardID}/threads.json", (e) =>
|
$.cache "//a.4cdn.org/#{boardID}/threads.json", (e) =>
|
||||||
return if e.target.status isnt 200
|
if e.target.status isnt 200
|
||||||
|
@delete boardID if e.target.status is 404
|
||||||
|
return
|
||||||
board = @data.boards[boardID]
|
board = @data.boards[boardID]
|
||||||
threads = {}
|
threads = {}
|
||||||
for page in JSON.parse e.target.response
|
for page in JSON.parse e.target.response
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
class Notice
|
class Notice
|
||||||
constructor: (type, content, @timeout) ->
|
constructor: (type, content, @timeout) ->
|
||||||
@el = $.el 'div',
|
@el = $.el 'div',
|
||||||
innerHTML: '<a href=javascript:; class=close title=Close>✖</a><div class=message></div>'
|
innerHTML: '<a href=javascript:; class="close fa fa-times" title=Close></a><div class=message></div>'
|
||||||
@el.style.opacity = 0
|
@el.style.opacity = 0
|
||||||
@setType type
|
@setType type
|
||||||
$.on @el.firstElementChild, 'click', @close
|
$.on @el.firstElementChild, 'click', @close
|
||||||
@ -19,7 +19,7 @@ class Notice
|
|||||||
$.on d, 'visibilitychange', @add
|
$.on d, 'visibilitychange', @add
|
||||||
return
|
return
|
||||||
$.off d, 'visibilitychange', @add
|
$.off d, 'visibilitychange', @add
|
||||||
$.add $.id('notifications'), @el
|
$.add Header.noticesRoot, @el
|
||||||
@el.clientHeight # force reflow
|
@el.clientHeight # force reflow
|
||||||
@el.style.opacity = 1
|
@el.style.opacity = 1
|
||||||
setTimeout @close, @timeout * $.SECOND if @timeout
|
setTimeout @close, @timeout * $.SECOND if @timeout
|
||||||
|
|||||||
@ -6,7 +6,7 @@ Polyfill =
|
|||||||
@visibility()
|
@visibility()
|
||||||
<% } %>
|
<% } %>
|
||||||
notificationPermission: ->
|
notificationPermission: ->
|
||||||
return if !window.Notification or 'permission' of Notification
|
return if !window.Notification or 'permission' of Notification or !window.webkitNotifications
|
||||||
Object.defineProperty Notification, 'permission',
|
Object.defineProperty Notification, 'permission',
|
||||||
get: ->
|
get: ->
|
||||||
switch webkitNotifications.checkPermission()
|
switch webkitNotifications.checkPermission()
|
||||||
@ -27,7 +27,7 @@ Polyfill =
|
|||||||
cb new Blob [ui8a], type: 'image/png'
|
cb new Blob [ui8a], type: 'image/png'
|
||||||
visibility: ->
|
visibility: ->
|
||||||
# page visibility API
|
# page visibility API
|
||||||
return unless 'webkitHidden' of document
|
return if 'visibilityState' of d
|
||||||
Object.defineProperties HTMLDocument.prototype,
|
Object.defineProperties HTMLDocument.prototype,
|
||||||
visibilityState:
|
visibilityState:
|
||||||
get: -> @webkitVisibilityState
|
get: -> @webkitVisibilityState
|
||||||
|
|||||||
@ -55,7 +55,7 @@ class Post
|
|||||||
|
|
||||||
@parseComment()
|
@parseComment()
|
||||||
@parseQuotes()
|
@parseQuotes()
|
||||||
@parseFile(that)
|
@parseFile that
|
||||||
|
|
||||||
@clones = []
|
@clones = []
|
||||||
g.posts[@fullID] = thread.posts[@] = board.posts[@] = @
|
g.posts[@fullID] = thread.posts[@] = board.posts[@] = @
|
||||||
@ -135,7 +135,7 @@ class Post
|
|||||||
@file.thumbURL = if that.isArchived
|
@file.thumbURL = if that.isArchived
|
||||||
thumb.src
|
thumb.src
|
||||||
else
|
else
|
||||||
"#{location.protocol}//thumbs.4chan.org/#{@board}/thumb/#{@file.URL.match(/(\d+)\./)[1]}s.jpg"
|
"#{location.protocol}//t.4cdn.org/#{@board}/thumb/#{@file.URL.match(/(\d+)\./)[1]}s.jpg"
|
||||||
@file.name = $('span[title]', fileInfo).title
|
@file.name = $('span[title]', fileInfo).title
|
||||||
<% if (type === 'crx') { %>
|
<% if (type === 'crx') { %>
|
||||||
# replace %22 with quotes, see:
|
# replace %22 with quotes, see:
|
||||||
@ -202,6 +202,12 @@ class Post
|
|||||||
$.rmClass quotelink, 'deadlink'
|
$.rmClass quotelink, 'deadlink'
|
||||||
return
|
return
|
||||||
|
|
||||||
|
collect: ->
|
||||||
|
@kill()
|
||||||
|
delete g.posts[@fullID]
|
||||||
|
delete @thread.posts[@]
|
||||||
|
delete @board.posts[@]
|
||||||
|
|
||||||
addClone: (context) ->
|
addClone: (context) ->
|
||||||
new Clone @, context
|
new Clone @, context
|
||||||
|
|
||||||
|
|||||||
@ -3,11 +3,46 @@ class Thread
|
|||||||
toString: -> @ID
|
toString: -> @ID
|
||||||
|
|
||||||
constructor: (@ID, @board) ->
|
constructor: (@ID, @board) ->
|
||||||
@fullID = "#{@board}.#{@ID}"
|
@fullID = "#{@board}.#{@ID}"
|
||||||
@posts = {}
|
@posts = {}
|
||||||
|
@isSticky = false
|
||||||
|
@isClosed = false
|
||||||
|
@postLimit = false
|
||||||
|
@fileLimit = false
|
||||||
|
|
||||||
g.threads[@fullID] = board.threads[@] = @
|
g.threads[@fullID] = board.threads[@] = @
|
||||||
|
|
||||||
|
setPage: (pageNum) ->
|
||||||
|
icon = $ '.page-num', @OP.nodes.post
|
||||||
|
for key in ['title', 'textContent']
|
||||||
|
icon[key] = icon[key].replace /\d+/, pageNum
|
||||||
|
return
|
||||||
|
setStatus: (type, status) ->
|
||||||
|
name = "is#{type}"
|
||||||
|
return if @[name] is status
|
||||||
|
@[name] = status
|
||||||
|
return unless @OP
|
||||||
|
typeLC = type.toLowerCase()
|
||||||
|
unless status
|
||||||
|
$.rm $ ".#{typeLC}Icon", @OP.nodes.info
|
||||||
|
return
|
||||||
|
icon = $.el 'img',
|
||||||
|
src: "//s.4cdn.org/image/#{typeLC}#{if window.devicePixelRatio >= 2 then '@2x' else ''}.gif"
|
||||||
|
alt: type
|
||||||
|
title: type
|
||||||
|
className: "#{typeLC}Icon"
|
||||||
|
root = if type is 'Closed' and @isSticky
|
||||||
|
$ '.stickyIcon', @OP.nodes.info
|
||||||
|
else
|
||||||
|
$ '[title="Quote this post"]', @OP.nodes.info
|
||||||
|
$.after root, [$.tn(' '), icon]
|
||||||
|
|
||||||
kill: ->
|
kill: ->
|
||||||
@isDead = true
|
@isDead = true
|
||||||
@timeOfDeath = Date.now()
|
@timeOfDeath = Date.now()
|
||||||
|
|
||||||
|
collect: ->
|
||||||
|
for postID, post in @posts
|
||||||
|
post.collect()
|
||||||
|
delete g.threads[@fullID]
|
||||||
|
delete @board.threads[@]
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
"run_at": "document_start"
|
"run_at": "document_start"
|
||||||
}],
|
}],
|
||||||
"homepage_url": "<%= meta.page %>",
|
"homepage_url": "<%= meta.page %>",
|
||||||
"minimum_chrome_version": "27",
|
"minimum_chrome_version": "<%= meta.min.chrome %>",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"storage"
|
"storage"
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name <%= meta.name %>
|
// @name <%= meta.name %>
|
||||||
// @version <%= version %>
|
// @version <%= version %>
|
||||||
// @minGMVer 1.13
|
// @minGMVer <%= meta.min.greasemonkey %>
|
||||||
// @minFFVer 22
|
// @minFFVer <%= meta.min.firefox %>
|
||||||
// @namespace <%= meta.namespace %>
|
// @namespace <%= meta.namespace %>
|
||||||
// @description <%= description %>
|
// @description <%= description %>
|
||||||
// @license MIT; <%= meta.repo %>blob/<%= meta.mainBranch %>/LICENSE
|
// @license MIT; <%= meta.repo %>blob/<%= meta.mainBranch %>/LICENSE
|
||||||
|
|||||||
@ -6,7 +6,7 @@ Gallery =
|
|||||||
href: 'javascript:;'
|
href: 'javascript:;'
|
||||||
id: 'appchan-gal'
|
id: 'appchan-gal'
|
||||||
title: 'Gallery'
|
title: 'Gallery'
|
||||||
className: 'icon'
|
className: 'fa'
|
||||||
textContent: '\uf03e'
|
textContent: '\uf03e'
|
||||||
|
|
||||||
$.on el, 'click', @cb.toggle
|
$.on el, 'click', @cb.toggle
|
||||||
|
|||||||
@ -49,7 +49,7 @@ ImageExpand =
|
|||||||
continue unless file and file.isImage and doc.contains post.nodes.root
|
continue unless file and file.isImage and doc.contains post.nodes.root
|
||||||
if ImageExpand.on and
|
if ImageExpand.on and
|
||||||
(!Conf['Expand spoilers'] and file.isSpoiler or
|
(!Conf['Expand spoilers'] and file.isSpoiler or
|
||||||
Conf['Expand from here'] and file.thumb.getBoundingClientRect().top < 0)
|
Conf['Expand from here'] and Header.getTopOf(file.thumb) < 0)
|
||||||
continue
|
continue
|
||||||
$.queueTask func, post
|
$.queueTask func, post
|
||||||
return
|
return
|
||||||
@ -65,7 +65,7 @@ ImageExpand =
|
|||||||
# Scroll back to the thumbnail when contracting the image
|
# Scroll back to the thumbnail when contracting the image
|
||||||
# to avoid being left miles away from the relevant post.
|
# to avoid being left miles away from the relevant post.
|
||||||
{root} = post.nodes
|
{root} = post.nodes
|
||||||
rect = (if Conf['Advance on contract'] then do ->
|
{top, left} = (if Conf['Advance on contract'] then do ->
|
||||||
next = root
|
next = root
|
||||||
while next = $.x "following::div[contains(@class,'postContainer')][1]", next
|
while next = $.x "following::div[contains(@class,'postContainer')][1]", next
|
||||||
continue if $('.stub', next) or next.offsetHeight is 0
|
continue if $('.stub', next) or next.offsetHeight is 0
|
||||||
@ -75,13 +75,13 @@ ImageExpand =
|
|||||||
root
|
root
|
||||||
).getBoundingClientRect()
|
).getBoundingClientRect()
|
||||||
|
|
||||||
if rect.top < 0
|
if top < 0
|
||||||
y = rect.top
|
y = top
|
||||||
if Conf['Fixed Header'] and not Conf['Bottom Header']
|
if Conf['Fixed Header'] and not Conf['Bottom Header']
|
||||||
headRect = Header.bar.getBoundingClientRect()
|
headRect = Header.bar.getBoundingClientRect()
|
||||||
y -= headRect.top + headRect.height
|
y -= headRect.top + headRect.height
|
||||||
|
|
||||||
if rect.left < 0
|
if left < 0
|
||||||
x = -window.scrollX
|
x = -window.scrollX
|
||||||
window.scrollBy x, y if x or y
|
window.scrollBy x, y if x or y
|
||||||
ImageExpand.contract post
|
ImageExpand.contract post
|
||||||
@ -119,13 +119,12 @@ ImageExpand =
|
|||||||
$.addClass post.nodes.root, 'expanded-image'
|
$.addClass post.nodes.root, 'expanded-image'
|
||||||
$.rmClass post.file.thumb, 'expanding'
|
$.rmClass post.file.thumb, 'expanding'
|
||||||
return
|
return
|
||||||
prev = post.nodes.root.getBoundingClientRect()
|
{bottom} = post.nodes.root.getBoundingClientRect()
|
||||||
$.queueTask ->
|
$.queueTask ->
|
||||||
$.addClass post.nodes.root, 'expanded-image'
|
$.addClass post.nodes.root, 'expanded-image'
|
||||||
$.rmClass post.file.thumb, 'expanding'
|
$.rmClass post.file.thumb, 'expanding'
|
||||||
return unless prev.top + prev.height <= 0
|
return unless bottom <= 0
|
||||||
curr = post.nodes.root.getBoundingClientRect()
|
window.scrollBy 0, post.nodes.root.getBoundingClientRect().bottom - bottom
|
||||||
window.scrollBy 0, curr.height - prev.height + curr.top - prev.top
|
|
||||||
|
|
||||||
error: ->
|
error: ->
|
||||||
post = Get.postFromNode @
|
post = Get.postFromNode @
|
||||||
@ -140,7 +139,7 @@ ImageExpand =
|
|||||||
ImageExpand.contract post
|
ImageExpand.contract post
|
||||||
|
|
||||||
src = @src.split '/'
|
src = @src.split '/'
|
||||||
if src[2] is 'images.4chan.org'
|
if src[2] is 'i.4cdn.org'
|
||||||
URL = Redirect.to 'file',
|
URL = Redirect.to 'file',
|
||||||
boardID: src[3]
|
boardID: src[3]
|
||||||
filename: src[5]
|
filename: src[5]
|
||||||
@ -151,8 +150,8 @@ ImageExpand =
|
|||||||
return
|
return
|
||||||
|
|
||||||
timeoutID = setTimeout ImageExpand.expand, 10000, post
|
timeoutID = setTimeout ImageExpand.expand, 10000, post
|
||||||
# XXX CORS for images.4chan.org WHEN?
|
# XXX CORS for i.4cdn.org WHEN?
|
||||||
$.ajax "//api.4chan.org/#{post.board}/res/#{post.thread}.json", onload: ->
|
$.ajax "//a.4cdn.org/#{post.board}/res/#{post.thread}.json", onload: ->
|
||||||
return if @status isnt 200
|
return if @status isnt 200
|
||||||
for postObj in JSON.parse(@response).posts
|
for postObj in JSON.parse(@response).posts
|
||||||
break if postObj.no is post.ID
|
break if postObj.no is post.ID
|
||||||
|
|||||||
@ -27,7 +27,7 @@ ImageHover =
|
|||||||
post = g.posts[@dataset.fullID]
|
post = g.posts[@dataset.fullID]
|
||||||
|
|
||||||
src = @src.split '/'
|
src = @src.split '/'
|
||||||
if src[2] is 'images.4chan.org'
|
if src[2] is 'i.4cdn.org'
|
||||||
URL = Redirect.to 'file',
|
URL = Redirect.to 'file',
|
||||||
boardID: src[3]
|
boardID: src[3]
|
||||||
filename: src[5].replace /\?.+$/, ''
|
filename: src[5].replace /\?.+$/, ''
|
||||||
@ -38,8 +38,8 @@ ImageHover =
|
|||||||
return
|
return
|
||||||
|
|
||||||
timeoutID = setTimeout (=> @src = post.file.URL + '?' + Date.now()), 3000
|
timeoutID = setTimeout (=> @src = post.file.URL + '?' + Date.now()), 3000
|
||||||
# XXX CORS for images.4chan.org WHEN?
|
# XXX CORS for i.4cdn.org WHEN?
|
||||||
$.ajax "//api.4chan.org/#{post.board}/res/#{post.thread}.json", onload: ->
|
$.ajax "//a.4cdn.org/#{post.board}/res/#{post.thread}.json", onload: ->
|
||||||
return if @status isnt 200
|
return if @status isnt 200
|
||||||
for postObj in JSON.parse(@response).posts
|
for postObj in JSON.parse(@response).posts
|
||||||
break if postObj.no is post.ID
|
break if postObj.no is post.ID
|
||||||
|
|||||||
@ -346,6 +346,13 @@ Linkify =
|
|||||||
api: (uid) -> "//soundcloud.com/oembed?show_artwork=false&&maxwidth=500px&show_comments=false&format=json&url=https://www.soundcloud.com/#{uid}"
|
api: (uid) -> "//soundcloud.com/oembed?show_artwork=false&&maxwidth=500px&show_comments=false&format=json&url=https://www.soundcloud.com/#{uid}"
|
||||||
text: (_) -> _.title
|
text: (_) -> _.title
|
||||||
|
|
||||||
|
StrawPoll:
|
||||||
|
regExp: /strawpoll\.me\/(?:embed_\d+\/)?(\d+)/
|
||||||
|
style: 'border: 0; width: 600px; height: 406px;'
|
||||||
|
el: (a) ->
|
||||||
|
$.el 'iframe',
|
||||||
|
src: "http://strawpoll.me/embed_1/#{a.dataset.uid}"
|
||||||
|
|
||||||
TwitchTV:
|
TwitchTV:
|
||||||
regExp: /.*(?:twitch.tv\/)([^#\&\?]*).*/
|
regExp: /.*(?:twitch.tv\/)([^#\&\?]*).*/
|
||||||
style: "border: none; width: 640px; height: 360px;"
|
style: "border: none; width: 640px; height: 360px;"
|
||||||
|
|||||||
18
src/Meta/banner.js
Normal file
18
src/Meta/banner.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
/* <%= meta.name %> - Version <%= version %> - <%= grunt.template.today('yyyy-mm-dd') %>
|
||||||
|
* <%= meta.page %>
|
||||||
|
*
|
||||||
|
* Copyrights and License: <%= meta.repo %>blob/<%= meta.mainBranch %>/LICENSE
|
||||||
|
*
|
||||||
|
* Contributors:
|
||||||
|
* <%= meta.repo %>graphs/contributors
|
||||||
|
* Non-GitHub contributors:
|
||||||
|
* ferongr, xat-, Ongpot, thisisanon and Anonymous - favicon contributions
|
||||||
|
* e000 - cooldown sanity check
|
||||||
|
* Seiba - chrome quick reply focusing
|
||||||
|
* herpaderpderp - recaptcha fixes
|
||||||
|
* WakiMiko - recaptcha tab order http://userscripts.org/scripts/show/82657
|
||||||
|
*
|
||||||
|
* All the people who've taken the time to write bug reports and provide feedback.
|
||||||
|
*
|
||||||
|
* Thank you.
|
||||||
|
*/
|
||||||
@ -1,111 +1,97 @@
|
|||||||
ExpandThread =
|
ExpandThread =
|
||||||
init: ->
|
init: ->
|
||||||
return if g.VIEW isnt 'index' or !Conf['Thread Expansion']
|
return if g.VIEW isnt 'index' or !Conf['Thread Expansion']
|
||||||
|
@statuses = {}
|
||||||
|
$.on d, 'IndexRefresh', @onIndexRefresh
|
||||||
|
|
||||||
Thread.callbacks.push
|
setButton: (thread) ->
|
||||||
name: 'Thread Expansion'
|
return unless a = $.x 'following-sibling::a[contains(@class,"summary")][1]', thread.OP.nodes.root
|
||||||
cb: @node
|
a.textContent = ExpandThread.text '+', a.textContent.match(/\d+/g)...
|
||||||
|
|
||||||
node: ->
|
|
||||||
return unless span = $.x 'following-sibling::span[contains(@class,"summary")][1]', @OP.nodes.root
|
|
||||||
[posts, files] = span.textContent.match /\d+/g
|
|
||||||
a = $.el 'a',
|
|
||||||
textContent: ExpandThread.text '+', posts, files
|
|
||||||
className: 'summary'
|
|
||||||
href: 'javascript:;'
|
|
||||||
$.on a, 'click', ExpandThread.cbToggle
|
$.on a, 'click', ExpandThread.cbToggle
|
||||||
$.replace span, a
|
|
||||||
|
onIndexRefresh: ->
|
||||||
|
for threadID, status of ExpandThread.statuses
|
||||||
|
status.req?.abort()
|
||||||
|
delete ExpandThread.statuses[threadID]
|
||||||
|
for threadID, thread of g.BOARD.threads
|
||||||
|
ExpandThread.setButton thread
|
||||||
|
return
|
||||||
|
|
||||||
text: (status, posts, files) ->
|
text: (status, posts, files) ->
|
||||||
"#{status} #{posts} post#{if posts > 1 then 's' else ''}" +
|
"#{status} #{posts} post#{if posts > 1 then 's' else ''}" +
|
||||||
(if +files then " and #{files} image repl#{if files > 1 then 'ies' else 'y'}" else "") +
|
(if +files then " and #{files} image repl#{if files > 1 then 'ies' else 'y'}" else "") +
|
||||||
" #{if status is '-' then 'shown' else 'omitted'}."
|
" #{if status is '-' then 'shown' else 'omitted'}."
|
||||||
|
|
||||||
cbToggle: ->
|
cbToggle: (e) ->
|
||||||
|
return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or e.button isnt 0
|
||||||
|
e.preventDefault()
|
||||||
ExpandThread.toggle Get.threadFromNode @
|
ExpandThread.toggle Get.threadFromNode @
|
||||||
|
|
||||||
toggle: (thread) ->
|
toggle: (thread) ->
|
||||||
threadRoot = thread.OP.nodes.root.parentNode
|
threadRoot = thread.OP.nodes.root.parentNode
|
||||||
a = $ '.summary', threadRoot
|
return unless a = $ '.summary', threadRoot
|
||||||
|
if thread.ID of ExpandThread.statuses
|
||||||
switch thread.isExpanded
|
ExpandThread.contract thread, a, threadRoot
|
||||||
when false, undefined
|
else
|
||||||
for post in $$ '.thread > .postContainer', threadRoot
|
ExpandThread.expand thread, a, threadRoot
|
||||||
ExpandComment.expand Get.postFromRoot post
|
expand: (thread, a, threadRoot) ->
|
||||||
unless a
|
ExpandThread.statuses[thread] = status = {}
|
||||||
thread.isExpanded = true
|
a.textContent = ExpandThread.text '...', a.textContent.match(/\d+/g)...
|
||||||
return
|
status.req = $.cache "//a.4cdn.org/#{thread.board}/res/#{thread}.json", ->
|
||||||
thread.isExpanded = 'loading'
|
delete status.req
|
||||||
[posts, files] = a.textContent.match /\d+/g
|
ExpandThread.parse @, thread, a
|
||||||
a.textContent = ExpandThread.text '...', posts, files
|
contract: (thread, a, threadRoot) ->
|
||||||
$.cache "//api.4chan.org/#{thread.board}/res/#{thread}.json", ->
|
status = ExpandThread.statuses[thread]
|
||||||
ExpandThread.parse @, thread, a
|
delete ExpandThread.statuses[thread]
|
||||||
|
if status.req
|
||||||
when 'loading'
|
status.req.abort()
|
||||||
thread.isExpanded = false
|
a.textContent = ExpandThread.text '+', a.textContent.match(/\d+/g)... if a
|
||||||
return unless a
|
|
||||||
[posts, files] = a.textContent.match /\d+/g
|
|
||||||
a.textContent = ExpandThread.text '+', posts, files
|
|
||||||
|
|
||||||
when true
|
|
||||||
thread.isExpanded = false
|
|
||||||
#goddamit moot
|
|
||||||
num = if thread.isSticky
|
|
||||||
1
|
|
||||||
else switch g.BOARD.ID
|
|
||||||
# XXX boards config
|
|
||||||
when 'b', 'vg' then 3
|
|
||||||
when 't' then 1
|
|
||||||
else 5
|
|
||||||
posts = $$ ".thread > .replyContainer", threadRoot
|
|
||||||
for post in [thread.OP.nodes.root].concat posts[-num..]
|
|
||||||
ExpandComment.contract Get.postFromRoot post
|
|
||||||
return unless a
|
|
||||||
postsCount = 0
|
|
||||||
filesCount = 0
|
|
||||||
for reply in posts[...-num]
|
|
||||||
if Conf['Quote Inlining']
|
|
||||||
# rm clones
|
|
||||||
inlined.click() while inlined = $ '.inlined', reply
|
|
||||||
postsCount++
|
|
||||||
filesCount++ if 'file' of Get.postFromRoot reply
|
|
||||||
$.rm reply
|
|
||||||
a.textContent = ExpandThread.text '+', postsCount, filesCount
|
|
||||||
return
|
|
||||||
|
|
||||||
parse: (req, thread, a) ->
|
|
||||||
return if a.textContent[0] is '+'
|
|
||||||
unless [200, 304].contains req.status
|
|
||||||
a.textContent = "Error #{req.statusText} (#{req.status})"
|
|
||||||
$.off a, 'click', ExpandThread.cbToggle
|
|
||||||
return
|
return
|
||||||
|
|
||||||
thread.isExpanded = true
|
replies = $$ '.thread > .replyContainer', threadRoot
|
||||||
|
if Conf['Show Replies']
|
||||||
|
num = if thread.isSticky
|
||||||
|
1
|
||||||
|
else switch g.BOARD.ID
|
||||||
|
# XXX boards config
|
||||||
|
when 'b', 'vg' then 3
|
||||||
|
when 't' then 1
|
||||||
|
else 5
|
||||||
|
replies = replies[...-num]
|
||||||
|
postsCount = 0
|
||||||
|
filesCount = 0
|
||||||
|
for reply in replies
|
||||||
|
# rm clones
|
||||||
|
inlined.click() while inlined = $ '.inlined', reply if Conf['Quote Inlining']
|
||||||
|
postsCount++
|
||||||
|
filesCount++ if 'file' of Get.postFromRoot reply
|
||||||
|
$.rm reply
|
||||||
|
a.textContent = ExpandThread.text '+', postsCount, filesCount
|
||||||
|
parse: (req, thread, a) ->
|
||||||
|
if req.status not in [200, 304]
|
||||||
|
a.textContent = "Error #{req.statusText} (#{req.status})"
|
||||||
|
return
|
||||||
|
|
||||||
{posts} = JSON.parse req.response
|
data = JSON.parse(req.response).posts
|
||||||
if spoilerRange = posts.shift().custom_spoiler
|
Build.spoilerRange[thread.board] = data.shift().custom_spoiler
|
||||||
Build.spoilerRange[thread.board] = spoilerRange
|
|
||||||
|
|
||||||
postsObj = []
|
posts = []
|
||||||
postsRoot = []
|
postsRoot = []
|
||||||
filesCount = 0
|
filesCount = 0
|
||||||
for reply in posts
|
for postData in data
|
||||||
if post = thread.posts[reply.no]
|
if post = thread.posts[postData.no]
|
||||||
filesCount++ if 'file' of post
|
filesCount++ if 'file' of post
|
||||||
postsRoot.push post.nodes.root
|
postsRoot.push post.nodes.root
|
||||||
continue
|
continue
|
||||||
root = Build.postFromObject reply, thread.board.ID
|
root = Build.postFromObject postData, thread.board.ID
|
||||||
post = new Post root, thread, thread.board
|
post = new Post root, thread, thread.board
|
||||||
link = $ 'a[title="Highlight this post"]', root
|
|
||||||
link.href = "res/#{thread}#p#{post}"
|
|
||||||
link.nextSibling.href = "res/#{thread}#q#{post}"
|
|
||||||
filesCount++ if 'file' of post
|
filesCount++ if 'file' of post
|
||||||
postsObj.push post
|
posts.push post
|
||||||
postsRoot.push root
|
postsRoot.push root
|
||||||
Main.callbackNodes Post, postsObj
|
Main.callbackNodes Post, posts
|
||||||
$.after a, postsRoot
|
$.after a, postsRoot
|
||||||
|
|
||||||
postsCount = postsRoot.length
|
postsCount = postsRoot.length
|
||||||
a.textContent = ExpandThread.text '-', postsCount, filesCount
|
a.textContent = ExpandThread.text '-', postsCount, filesCount
|
||||||
|
|
||||||
Fourchan.parseThread thread.ID, 1, postsCount
|
Fourchan.parseThread thread.ID, 1, postsCount
|
||||||
|
|||||||
@ -6,8 +6,9 @@ Fourchan =
|
|||||||
if board is 'g'
|
if board is 'g'
|
||||||
$.globalEval """
|
$.globalEval """
|
||||||
window.addEventListener('prettyprint', function(e) {
|
window.addEventListener('prettyprint', function(e) {
|
||||||
var pre = e.detail;
|
window.dispatchEvent(new CustomEvent('prettyprint:cb', {
|
||||||
pre.innerHTML = prettyPrintOne(pre.innerHTML);
|
detail: prettyPrintOne(e.detail)
|
||||||
|
}));
|
||||||
}, false);
|
}, false);
|
||||||
"""
|
"""
|
||||||
Post.callbacks.push
|
Post.callbacks.push
|
||||||
@ -32,9 +33,11 @@ Fourchan =
|
|||||||
cb: @math
|
cb: @math
|
||||||
code: ->
|
code: ->
|
||||||
return if @isClone
|
return if @isClone
|
||||||
|
apply = (e) -> pre.innerHTML = e.detail
|
||||||
|
$.on window, 'prettyprint:cb', apply
|
||||||
for pre in $$ '.prettyprint:not(.prettyprinted)', @nodes.comment
|
for pre in $$ '.prettyprint:not(.prettyprinted)', @nodes.comment
|
||||||
$.event 'prettyprint', pre, window
|
$.event 'prettyprint', pre.innerHTML, window
|
||||||
$.addClass pre, 'prettyprinted'
|
$.off window, 'prettyprint:cb', apply
|
||||||
return
|
return
|
||||||
math: ->
|
math: ->
|
||||||
return if @isClone or !$ '.math', @nodes.comment
|
return if @isClone or !$ '.math', @nodes.comment
|
||||||
|
|||||||
@ -2,20 +2,25 @@ Keybinds =
|
|||||||
init: ->
|
init: ->
|
||||||
return if g.VIEW is 'catalog' or !Conf['Keybinds']
|
return if g.VIEW is 'catalog' or !Conf['Keybinds']
|
||||||
|
|
||||||
|
for hotkey of Conf.hotkeys
|
||||||
|
$.sync hotkey, Keybinds.sync
|
||||||
|
|
||||||
init = ->
|
init = ->
|
||||||
$.off d, '4chanXInitFinished', init
|
$.off d, '4chanXInitFinished', init
|
||||||
$.on d, 'keydown', Keybinds.keydown
|
$.on d, 'keydown', Keybinds.keydown
|
||||||
for node in $$ '[accesskey]'
|
for node in $$ '[accesskey]'
|
||||||
node.removeAttribute 'accesskey'
|
node.removeAttribute 'accesskey'
|
||||||
return
|
return
|
||||||
$.on d, '4chanXInitFinished', init
|
$.on d, '4chanXInitFinished', init
|
||||||
|
|
||||||
|
sync: (key, hotkey) ->
|
||||||
|
Conf[hotkey] = key
|
||||||
|
|
||||||
keydown: (e) ->
|
keydown: (e) ->
|
||||||
return unless key = Keybinds.keyCode e
|
return unless key = Keybinds.keyCode e
|
||||||
{target} = e
|
{target} = e
|
||||||
if ['INPUT', 'TEXTAREA'].contains target.nodeName
|
if ['INPUT', 'TEXTAREA'].contains target.nodeName
|
||||||
return unless /(Esc|Alt|Ctrl|Meta)/.test key
|
return unless /(Esc|Alt|Ctrl|Meta|Shift\+\w{2,})/.test key
|
||||||
|
|
||||||
threadRoot = Nav.getThread()
|
threadRoot = Nav.getThread()
|
||||||
if op = $ '.op', threadRoot
|
if op = $ '.op', threadRoot
|
||||||
thread = Get.postFromNode(op).thread
|
thread = Get.postFromNode(op).thread
|
||||||
@ -59,11 +64,15 @@ Keybinds =
|
|||||||
Keybinds.sage() if QR.nodes
|
Keybinds.sage() if QR.nodes
|
||||||
when Conf['Submit QR']
|
when Conf['Submit QR']
|
||||||
QR.submit() if QR.nodes and !QR.status()
|
QR.submit() if QR.nodes and !QR.status()
|
||||||
# Thread related
|
# Index/Thread related
|
||||||
|
when Conf['Update']
|
||||||
|
switch g.VIEW
|
||||||
|
when 'thread'
|
||||||
|
ThreadUpdater.update()
|
||||||
|
when 'index'
|
||||||
|
Index.update()
|
||||||
when Conf['Watch']
|
when Conf['Watch']
|
||||||
ThreadWatcher.toggle thread
|
ThreadWatcher.toggle thread
|
||||||
when Conf['Update']
|
|
||||||
ThreadUpdater.update()
|
|
||||||
# Images
|
# Images
|
||||||
when Conf['Expand image']
|
when Conf['Expand image']
|
||||||
Keybinds.img threadRoot
|
Keybinds.img threadRoot
|
||||||
@ -77,17 +86,20 @@ Keybinds =
|
|||||||
FappeTyme.cb.werk()
|
FappeTyme.cb.werk()
|
||||||
# Board Navigation
|
# Board Navigation
|
||||||
when Conf['Front page']
|
when Conf['Front page']
|
||||||
window.location = "/#{g.BOARD}/0#delform"
|
if g.VIEW is 'index'
|
||||||
|
Index.pageNav 0
|
||||||
|
else
|
||||||
|
window.location = "/#{g.BOARD}/"
|
||||||
when Conf['Open front page']
|
when Conf['Open front page']
|
||||||
$.open "/#{g.BOARD}/#delform"
|
$.open "/#{g.BOARD}/"
|
||||||
when Conf['Next page']
|
when Conf['Next page']
|
||||||
return if g.VIEW is 'thread'
|
return unless g.VIEW is 'index' and Conf['Index Mode'] is 'paged'
|
||||||
if form = $ '.next form'
|
$('.next button', Index.pagelist).click()
|
||||||
window.location = form.action
|
|
||||||
when Conf['Previous page']
|
when Conf['Previous page']
|
||||||
return if g.VIEW is 'thread'
|
return unless g.VIEW is 'index' and Conf['Index Mode'] is 'paged'
|
||||||
if form = $ '.prev form'
|
$('.prev button', Index.pagelist).click()
|
||||||
window.location = form.action
|
when Conf['Search form']
|
||||||
|
Index.searchInput.focus()
|
||||||
when Conf['Open catalog']
|
when Conf['Open catalog']
|
||||||
if Conf['External Catalog']
|
if Conf['External Catalog']
|
||||||
window.location = CatalogLinks.external(g.BOARD.ID)
|
window.location = CatalogLinks.external(g.BOARD.ID)
|
||||||
@ -201,43 +213,31 @@ Keybinds =
|
|||||||
location.href = url
|
location.href = url
|
||||||
|
|
||||||
hl: (delta, thread) ->
|
hl: (delta, thread) ->
|
||||||
|
postEl = $ '.reply.highlight', thread
|
||||||
|
|
||||||
unless delta
|
unless delta
|
||||||
if postEl = $ '.reply.highlight', thread
|
$.rmClass postEl, 'highlight' if postEl
|
||||||
$.rmClass postEl, 'highlight'
|
|
||||||
return
|
return
|
||||||
if Conf['Fixed Header'] and Conf['Bottom header']
|
|
||||||
topMargin = 0
|
if postEl
|
||||||
else
|
{height} = postEl.getBoundingClientRect()
|
||||||
headRect = Header.bar.getBoundingClientRect()
|
if Header.getTopOf(postEl) >= -height and Header.getBottomOf(postEl) >= -height # We're at least partially visible
|
||||||
topMargin = headRect.top + headRect.height
|
|
||||||
if postEl = $ '.reply.highlight', thread
|
|
||||||
$.rmClass postEl, 'highlight'
|
|
||||||
rect = postEl.getBoundingClientRect()
|
|
||||||
if rect.bottom >= topMargin and rect.top <= doc.clientHeight # We're at least partially visible
|
|
||||||
root = postEl.parentNode
|
root = postEl.parentNode
|
||||||
axe = if delta is +1
|
axe = if delta is +1
|
||||||
'following'
|
'following'
|
||||||
else
|
else
|
||||||
'preceding'
|
'preceding'
|
||||||
next = $.x "#{axe}-sibling::div[contains(@class,'replyContainer')][1]/child::div[contains(@class,'reply')]", root
|
return unless next = $.x "#{axe}-sibling::div[contains(@class,'replyContainer')][1]/child::div[contains(@class,'reply')]", root
|
||||||
unless next
|
Header.scrollToIfNeeded next, delta is +1
|
||||||
@focus postEl
|
|
||||||
return
|
|
||||||
return unless g.VIEW is 'thread' or $.x('ancestor::div[parent::div[@class="board"]]', next) is thread
|
|
||||||
rect = next.getBoundingClientRect()
|
|
||||||
if rect.top < 0 or rect.bottom > doc.clientHeight
|
|
||||||
if delta is -1
|
|
||||||
window.scrollBy 0, rect.top - topMargin
|
|
||||||
else
|
|
||||||
next.scrollIntoView false
|
|
||||||
@focus next
|
@focus next
|
||||||
|
$.rmClass postEl, 'highlight'
|
||||||
return
|
return
|
||||||
|
$.rmClass postEl, 'highlight'
|
||||||
|
|
||||||
replies = $$ '.reply', thread
|
replies = $$ '.reply', thread
|
||||||
replies.reverse() if delta is -1
|
replies.reverse() if delta is -1
|
||||||
for reply in replies
|
for reply in replies
|
||||||
rect = reply.getBoundingClientRect()
|
if delta is +1 and Header.getTopOf(reply) > 0 or delta is -1 and Header.getBottomOf(reply) > 0
|
||||||
if delta is +1 and rect.top >= topMargin or delta is -1 and rect.bottom <= doc.clientHeight
|
|
||||||
@focus reply
|
@focus reply
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@ -33,29 +33,24 @@ Nav =
|
|||||||
else
|
else
|
||||||
Nav.scroll +1
|
Nav.scroll +1
|
||||||
|
|
||||||
getThread: (full) ->
|
getThread: ->
|
||||||
if Conf['Bottom header'] or !Conf['Fixed Header']
|
for threadRoot in $$ '.thread'
|
||||||
topMargin = 0
|
thread = Get.threadFromRoot threadRoot
|
||||||
else
|
continue if thread.isHidden and !thread.stub
|
||||||
headRect = Header.bar.getBoundingClientRect()
|
if Header.getTopOf(threadRoot) >= -threadRoot.getBoundingClientRect().height # not scrolled past
|
||||||
topMargin = headRect.top + headRect.height
|
return threadRoot
|
||||||
threads = $$('.thread').filter (thread) ->
|
|
||||||
thread = Get.threadFromRoot thread
|
|
||||||
!(thread.isHidden and !thread.stub)
|
|
||||||
for thread, i in threads
|
|
||||||
rect = thread.getBoundingClientRect()
|
|
||||||
if rect.bottom > topMargin # not scrolled past
|
|
||||||
return if full then [threads, thread, i, rect, topMargin] else thread
|
|
||||||
return $ '.board'
|
return $ '.board'
|
||||||
|
|
||||||
scroll: (delta) ->
|
scroll: (delta) ->
|
||||||
[threads, thread, i, rect, topMargin] = Nav.getThread true
|
thread = Nav.getThread()
|
||||||
top = rect.top - topMargin
|
axe = if delta is +1
|
||||||
|
'following'
|
||||||
# unless we're not at the beginning of the current thread
|
else
|
||||||
# (and thus wanting to move to beginning)
|
'preceding'
|
||||||
# or we're above the first thread and don't want to skip it
|
if next = $.x "#{axe}-sibling::div[contains(@class,'thread') and not(@hidden)][1]", thread
|
||||||
if (delta is -1 and top > -5) or (delta is +1 and top < 5)
|
# Unless we're not at the beginning of the current thread,
|
||||||
top = threads[i + delta]?.getBoundingClientRect().top - topMargin
|
# and thus wanting to move to beginning,
|
||||||
|
# or we're above the first thread and don't want to skip it.
|
||||||
window.scrollBy 0, top
|
top = Header.getTopOf thread
|
||||||
|
thread = next if delta is +1 and top < 5 or delta is -1 and top > -5
|
||||||
|
Header.scrollTo thread
|
||||||
|
|||||||
@ -1,13 +1,17 @@
|
|||||||
RelativeDates =
|
RelativeDates =
|
||||||
INTERVAL: $.MINUTE / 2
|
INTERVAL: $.MINUTE / 2
|
||||||
init: ->
|
init: ->
|
||||||
return if g.VIEW is 'catalog' or !Conf['Relative Post Dates']
|
switch g.VIEW
|
||||||
|
when 'index'
|
||||||
# Flush when page becomes visible again or when the thread updates.
|
@flush()
|
||||||
$.on d, 'visibilitychange ThreadUpdate', @flush
|
$.on d, 'visibilitychange', @flush
|
||||||
|
return unless Conf['Relative Post Dates']
|
||||||
# Start the timeout.
|
when 'thread'
|
||||||
@flush()
|
return unless Conf['Relative Post Dates']
|
||||||
|
@flush()
|
||||||
|
$.on d, 'visibilitychange ThreadUpdate', @flush if g.VIEW is 'thread'
|
||||||
|
else
|
||||||
|
return
|
||||||
|
|
||||||
Post.callbacks.push
|
Post.callbacks.push
|
||||||
name: 'Relative Post Dates'
|
name: 'Relative Post Dates'
|
||||||
@ -21,7 +25,7 @@ RelativeDates =
|
|||||||
dateEl = @nodes.date
|
dateEl = @nodes.date
|
||||||
dateEl.title = dateEl.textContent
|
dateEl.title = dateEl.textContent
|
||||||
|
|
||||||
RelativeDates.setUpdate @
|
RelativeDates.update @
|
||||||
|
|
||||||
# diff is milliseconds from now.
|
# diff is milliseconds from now.
|
||||||
relative: (diff, now, date) ->
|
relative: (diff, now, date) ->
|
||||||
@ -71,37 +75,41 @@ RelativeDates =
|
|||||||
return if d.hidden
|
return if d.hidden
|
||||||
|
|
||||||
now = new Date()
|
now = new Date()
|
||||||
update now for update in RelativeDates.stale
|
RelativeDates.update data, now for data in RelativeDates.stale
|
||||||
RelativeDates.stale = []
|
RelativeDates.stale = []
|
||||||
|
|
||||||
# Reset automatic flush.
|
# Reset automatic flush.
|
||||||
clearTimeout RelativeDates.timeout
|
clearTimeout RelativeDates.timeout
|
||||||
RelativeDates.timeout = setTimeout RelativeDates.flush, RelativeDates.INTERVAL
|
RelativeDates.timeout = setTimeout RelativeDates.flush, RelativeDates.INTERVAL
|
||||||
|
|
||||||
# Create function `update()`, closed over post, that, when called
|
# `update()`, when called from `flush()`, updates the elements,
|
||||||
# from `flush()`, updates the elements, and re-calls `setOwnTimeout()` to
|
# and re-calls `setOwnTimeout()` to re-add `data` to the stale list later.
|
||||||
# re-add `update()` to the stale list later.
|
update: (data, now) ->
|
||||||
setUpdate: (post) ->
|
isPost = data instanceof Post
|
||||||
setOwnTimeout = (diff) ->
|
date = if isPost
|
||||||
delay = if diff < $.MINUTE
|
data.info.date
|
||||||
$.SECOND - (diff + $.SECOND / 2) % $.SECOND
|
else
|
||||||
else if diff < $.HOUR
|
new Date +data.dataset.utc
|
||||||
$.MINUTE - (diff + $.MINUTE / 2) % $.MINUTE
|
now or= new Date()
|
||||||
else if diff < $.DAY
|
diff = now - date
|
||||||
$.HOUR - (diff + $.HOUR / 2) % $.HOUR
|
relative = RelativeDates.relative diff, now, date
|
||||||
else
|
if isPost
|
||||||
$.DAY - (diff + $.DAY / 2) % $.DAY
|
for singlePost in [data].concat data.clones
|
||||||
setTimeout markStale, delay
|
|
||||||
|
|
||||||
update = (now) ->
|
|
||||||
{date} = post.info
|
|
||||||
diff = now - date
|
|
||||||
relative = RelativeDates.relative diff, now, date
|
|
||||||
for singlePost in [post].concat post.clones
|
|
||||||
singlePost.nodes.date.firstChild.textContent = relative
|
singlePost.nodes.date.firstChild.textContent = relative
|
||||||
setOwnTimeout diff
|
else
|
||||||
|
data.firstChild.textContent = relative
|
||||||
markStale = -> RelativeDates.stale.push update
|
RelativeDates.setOwnTimeout diff, data
|
||||||
|
setOwnTimeout: (diff, data) ->
|
||||||
# Kick off initial timeout.
|
delay = if diff < $.MINUTE
|
||||||
update new Date()
|
$.SECOND - (diff + $.SECOND / 2) % $.SECOND
|
||||||
|
else if diff < $.HOUR
|
||||||
|
$.MINUTE - (diff + $.MINUTE / 2) % $.MINUTE
|
||||||
|
else if diff < $.DAY
|
||||||
|
$.HOUR - (diff + $.HOUR / 2) % $.HOUR
|
||||||
|
else
|
||||||
|
$.DAY - (diff + $.DAY / 2) % $.DAY
|
||||||
|
setTimeout RelativeDates.markStale, delay, data
|
||||||
|
markStale: (data) ->
|
||||||
|
return if data in RelativeDates.stale # We can call RelativeDates.update() multiple times.
|
||||||
|
return if data instanceof Post and !g.posts[data.fullID] # collected post.
|
||||||
|
RelativeDates.stale.push data
|
||||||
|
|||||||
@ -59,5 +59,5 @@ Favicon =
|
|||||||
Favicon.unread = Favicon.unreadNSFW
|
Favicon.unread = Favicon.unreadNSFW
|
||||||
Favicon.unreadY = Favicon.unreadNSFWY
|
Favicon.unreadY = Favicon.unreadNSFWY
|
||||||
|
|
||||||
dead: 'data:image/png;base64,<%= grunt.file.read("src/General/img/favicons/dead.png", {encoding: "base64"}) %>'
|
dead: 'data:image/gif;base64,<%= grunt.file.read("src/General/img/favicons/dead.gif", {encoding: "base64"}) %>'
|
||||||
logo: 'data:image/png;base64,<%= grunt.file.read("src/General/img/icon128.png", {encoding: "base64"}) %>'
|
logo: 'data:image/png;base64,<%= grunt.file.read("src/General/img/icon128.png", {encoding: "base64"}) %>'
|
||||||
|
|||||||
@ -53,7 +53,7 @@ ThreadStats =
|
|||||||
$.addClass ThreadStats.pageCountEl, 'warning'
|
$.addClass ThreadStats.pageCountEl, 'warning'
|
||||||
return
|
return
|
||||||
setTimeout ThreadStats.fetchPage, 2 * $.MINUTE
|
setTimeout ThreadStats.fetchPage, 2 * $.MINUTE
|
||||||
$.ajax "//api.4chan.org/#{ThreadStats.thread.board}/threads.json", onload: ThreadStats.onThreadsLoad,
|
$.ajax "//a.4cdn.org/#{ThreadStats.thread.board}/threads.json", onload: ThreadStats.onThreadsLoad,
|
||||||
whenModified: true
|
whenModified: true
|
||||||
|
|
||||||
onThreadsLoad: ->
|
onThreadsLoad: ->
|
||||||
|
|||||||
@ -199,43 +199,32 @@ ThreadUpdater =
|
|||||||
ThreadUpdater.set 'timer', '...'
|
ThreadUpdater.set 'timer', '...'
|
||||||
else
|
else
|
||||||
ThreadUpdater.set 'timer', 'Update'
|
ThreadUpdater.set 'timer', 'Update'
|
||||||
ThreadUpdater.req.abort() if ThreadUpdater.req
|
ThreadUpdater.req?.abort()
|
||||||
url = "//api.4chan.org/#{ThreadUpdater.thread.board}/res/#{ThreadUpdater.thread}.json"
|
url = "//a.4cdn.org/#{ThreadUpdater.thread.board}/res/#{ThreadUpdater.thread}.json"
|
||||||
ThreadUpdater.req = $.ajax url, onloadend: ThreadUpdater.cb.load,
|
ThreadUpdater.req = $.ajax url, onloadend: ThreadUpdater.cb.load,
|
||||||
whenModified: true
|
whenModified: true
|
||||||
|
|
||||||
updateThreadStatus: (title, OP) ->
|
updateThreadStatus: (type, status) ->
|
||||||
titleLC = title.toLowerCase()
|
return unless hasChanged = ThreadUpdater.thread["is#{type}"] isnt status
|
||||||
return if ThreadUpdater.thread["is#{title}"] is !!OP[titleLC]
|
ThreadUpdater.thread.setStatus type, status
|
||||||
unless ThreadUpdater.thread["is#{title}"] = !!OP[titleLC]
|
change = if type is 'Sticky'
|
||||||
message = if title is 'Sticky'
|
if status
|
||||||
'The thread is not a sticky anymore.'
|
'now a sticky'
|
||||||
else
|
else
|
||||||
'The thread is not closed anymore.'
|
'not a sticky anymore'
|
||||||
new Notice 'info', message, 30
|
|
||||||
$.rm $ ".#{titleLC}Icon", ThreadUpdater.thread.OP.nodes.info
|
|
||||||
return
|
|
||||||
message = if title is 'Sticky'
|
|
||||||
'The thread is now a sticky.'
|
|
||||||
else
|
else
|
||||||
'The thread is now closed.'
|
if status
|
||||||
new Notice 'info', message, 30
|
'now closed'
|
||||||
icon = $.el 'img',
|
else
|
||||||
src: "//static.4chan.org/image/#{titleLC}.gif"
|
'not closed anymore'
|
||||||
alt: title
|
new Notice 'info', "The thread is #{change}.", 30
|
||||||
title: title
|
|
||||||
className: "#{titleLC}Icon"
|
|
||||||
root = $ '[title="Quote this post"]', ThreadUpdater.thread.OP.nodes.info
|
|
||||||
if title is 'Closed'
|
|
||||||
root = $('.stickyIcon', ThreadUpdater.thread.OP.nodes.info) or root
|
|
||||||
$.after root, [$.tn(' '), icon]
|
|
||||||
|
|
||||||
parse: (postObjects) ->
|
parse: (postObjects) ->
|
||||||
OP = postObjects[0]
|
OP = postObjects[0]
|
||||||
Build.spoilerRange[ThreadUpdater.thread.board] = OP.custom_spoiler
|
Build.spoilerRange[ThreadUpdater.thread.board] = OP.custom_spoiler
|
||||||
|
|
||||||
ThreadUpdater.updateThreadStatus 'Sticky', OP
|
ThreadUpdater.updateThreadStatus 'Sticky', !!OP.sticky
|
||||||
ThreadUpdater.updateThreadStatus 'Closed', OP
|
ThreadUpdater.updateThreadStatus 'Closed', !!OP.closed
|
||||||
ThreadUpdater.thread.postLimit = !!OP.bumplimit
|
ThreadUpdater.thread.postLimit = !!OP.bumplimit
|
||||||
ThreadUpdater.thread.fileLimit = !!OP.imagelimit
|
ThreadUpdater.thread.fileLimit = !!OP.imagelimit
|
||||||
|
|
||||||
@ -304,7 +293,7 @@ ThreadUpdater =
|
|||||||
if Conf['Bottom Scroll']
|
if Conf['Bottom Scroll']
|
||||||
window.scrollTo 0, d.body.clientHeight
|
window.scrollTo 0, d.body.clientHeight
|
||||||
else
|
else
|
||||||
Header.scrollToPost root if root
|
Header.scrollTo root if root
|
||||||
|
|
||||||
$.queueTask ->
|
$.queueTask ->
|
||||||
# Enable 4chan features.
|
# Enable 4chan features.
|
||||||
|
|||||||
@ -6,12 +6,19 @@ ThreadWatcher =
|
|||||||
@dialog = UI.dialog 'thread-watcher', 'top: 50px; left: 0px;', """<%=
|
@dialog = UI.dialog 'thread-watcher', 'top: 50px; left: 0px;', """<%=
|
||||||
grunt.file.read('src/General/html/Monitoring/ThreadWatcher.html').replace(/>\s+</g, '><').trim()
|
grunt.file.read('src/General/html/Monitoring/ThreadWatcher.html').replace(/>\s+</g, '><').trim()
|
||||||
%>"""
|
%>"""
|
||||||
|
|
||||||
@status = $ '#watcher-status', @dialog
|
@status = $ '#watcher-status', @dialog
|
||||||
@list = @dialog.lastElementChild
|
@list = @dialog.lastElementChild
|
||||||
|
|
||||||
$.on d, 'QRPostSuccessful', @cb.post
|
$.on d, 'QRPostSuccessful', @cb.post
|
||||||
$.on d, 'ThreadUpdate', @cb.threadUpdate if g.VIEW is 'thread'
|
$.on d, 'ThreadUpdate', @cb.threadUpdate if g.VIEW is 'thread'
|
||||||
|
|
||||||
$.on d, '4chanXInitFinished', @ready
|
$.on d, '4chanXInitFinished', @ready
|
||||||
|
switch g.VIEW
|
||||||
|
when 'index'
|
||||||
|
$.on d, 'IndexRefresh', @cb.onIndexRefresh
|
||||||
|
when 'thread'
|
||||||
|
$.on d, 'ThreadUpdate', @cb.onThreadRefresh
|
||||||
|
|
||||||
now = Date.now()
|
now = Date.now()
|
||||||
if (@db.data.lastChecked or 0) < now - 2 * $.HOUR
|
if (@db.data.lastChecked or 0) < now - 2 * $.HOUR
|
||||||
@ -79,7 +86,17 @@ ThreadWatcher =
|
|||||||
$.set 'AutoWatch', threadID
|
$.set 'AutoWatch', threadID
|
||||||
else if Conf['Auto Watch Reply']
|
else if Conf['Auto Watch Reply']
|
||||||
ThreadWatcher.add board.threads[threadID]
|
ThreadWatcher.add board.threads[threadID]
|
||||||
threadUpdate: (e) ->
|
onIndexRefresh: ->
|
||||||
|
{db} = ThreadWatcher
|
||||||
|
boardID = g.BOARD.ID
|
||||||
|
for threadID, data of db.data.boards[boardID] when not data.isDead and threadID not of g.BOARD.threads
|
||||||
|
if Conf['Auto Prune']
|
||||||
|
ThreadWatcher.db.delete {boardID, threadID}
|
||||||
|
else
|
||||||
|
data.isDead = true
|
||||||
|
ThreadWatcher.db.set {boardID, threadID, val: data}
|
||||||
|
ThreadWatcher.refresh()
|
||||||
|
onThreadRefresh: (e) ->
|
||||||
{thread} = e.detail
|
{thread} = e.detail
|
||||||
return unless e.detail[404] and ThreadWatcher.db.get {boardID: thread.board.ID, threadID: thread.ID}
|
return unless e.detail[404] and ThreadWatcher.db.get {boardID: thread.board.ID, threadID: thread.ID}
|
||||||
# Update 404 status.
|
# Update 404 status.
|
||||||
@ -98,7 +115,7 @@ ThreadWatcher =
|
|||||||
return if data.isDead
|
return if data.isDead
|
||||||
{fetchCount} = ThreadWatcher
|
{fetchCount} = ThreadWatcher
|
||||||
fetchCount.fetching++
|
fetchCount.fetching++
|
||||||
$.ajax "//api.4chan.org/#{boardID}/res/#{threadID}.json",
|
$.ajax "//a.4cdn.org/#{boardID}/res/#{threadID}.json",
|
||||||
onloadend: ->
|
onloadend: ->
|
||||||
fetchCount.fetched++
|
fetchCount.fetched++
|
||||||
if fetchCount.fetched is fetchCount.fetching
|
if fetchCount.fetched is fetchCount.fetching
|
||||||
@ -110,7 +127,7 @@ ThreadWatcher =
|
|||||||
ThreadWatcher.status.textContent = status
|
ThreadWatcher.status.textContent = status
|
||||||
return if @status isnt 404
|
return if @status isnt 404
|
||||||
if Conf['Auto Prune']
|
if Conf['Auto Prune']
|
||||||
ThreadWatcher.rm boardID, threadID
|
ThreadWatcher.db.delete {boardID, threadID}
|
||||||
else
|
else
|
||||||
data.isDead = true
|
data.isDead = true
|
||||||
ThreadWatcher.db.set {boardID, threadID, val: data}
|
ThreadWatcher.db.set {boardID, threadID, val: data}
|
||||||
@ -129,7 +146,7 @@ ThreadWatcher =
|
|||||||
|
|
||||||
makeLine: (boardID, threadID, data) ->
|
makeLine: (boardID, threadID, data) ->
|
||||||
x = $.el 'a',
|
x = $.el 'a',
|
||||||
textContent: '✖'
|
className: 'fa fa-times'
|
||||||
href: 'javascript:;'
|
href: 'javascript:;'
|
||||||
$.on x, 'click', ThreadWatcher.cb.rm
|
$.on x, 'click', ThreadWatcher.cb.rm
|
||||||
|
|
||||||
@ -271,6 +288,7 @@ ThreadWatcher =
|
|||||||
$.on entry.el, 'click', cb if cb
|
$.on entry.el, 'click', cb if cb
|
||||||
@refreshers.push refresh.bind entry if refresh
|
@refreshers.push refresh.bind entry if refresh
|
||||||
$.event 'AddMenuEntry', entry
|
$.event 'AddMenuEntry', entry
|
||||||
|
return
|
||||||
createSubEntry: (name, desc) ->
|
createSubEntry: (name, desc) ->
|
||||||
entry =
|
entry =
|
||||||
type: 'thread watcher'
|
type: 'thread watcher'
|
||||||
|
|||||||
@ -41,18 +41,16 @@ Unread =
|
|||||||
while root = $.x 'preceding-sibling::div[contains(@class,"replyContainer")][1]', post.nodes.root
|
while root = $.x 'preceding-sibling::div[contains(@class,"replyContainer")][1]', post.nodes.root
|
||||||
break unless (post = Get.postFromRoot root).isHidden
|
break unless (post = Get.postFromRoot root).isHidden
|
||||||
return unless root
|
return unless root
|
||||||
onload = -> root.scrollIntoView false if checkPosition root
|
down = true
|
||||||
else
|
else
|
||||||
# Scroll to the last read post.
|
# Scroll to the last read post.
|
||||||
posts = Object.keys Unread.thread.posts
|
posts = Object.keys Unread.thread.posts
|
||||||
{root} = Unread.thread.posts[posts[posts.length - 1]].nodes
|
{root} = Unread.thread.posts[posts[posts.length - 1]].nodes
|
||||||
onload = -> Header.scrollToPost root if checkPosition root
|
|
||||||
checkPosition = (target) ->
|
|
||||||
# Scroll to the target unless we scrolled past it.
|
|
||||||
target.getBoundingClientRect().bottom > doc.clientHeight
|
|
||||||
# Prevent the browser to scroll back to
|
# Prevent the browser to scroll back to
|
||||||
# the previous scroll location on page load.
|
# the previous scroll location on page load.
|
||||||
$.on window, 'load', onload
|
$.on window, 'load', ->
|
||||||
|
# Scroll to the target unless we scrolled past it.
|
||||||
|
Header.scrollTo root, down if Header.getBottomOf(root) < 0
|
||||||
|
|
||||||
sync: ->
|
sync: ->
|
||||||
lastReadPost = Unread.db.get
|
lastReadPost = Unread.db.get
|
||||||
@ -102,7 +100,7 @@ Unread =
|
|||||||
body: post.info.comment
|
body: post.info.comment
|
||||||
icon: Favicon.logo
|
icon: Favicon.logo
|
||||||
notif.onclick = ->
|
notif.onclick = ->
|
||||||
Header.scrollToPost post.nodes.root
|
Header.scrollToIfNeeded post.nodes.root, true
|
||||||
window.focus()
|
window.focus()
|
||||||
notif.onshow = ->
|
notif.onshow = ->
|
||||||
setTimeout ->
|
setTimeout ->
|
||||||
@ -137,7 +135,7 @@ Unread =
|
|||||||
i = 0
|
i = 0
|
||||||
|
|
||||||
while post = posts[i]
|
while post = posts[i]
|
||||||
if post.nodes.root.getBoundingClientRect().bottom < height # post is not completely read
|
if Header.getBottomOf(post.nodes.root) > -1 # post is not completely read
|
||||||
{ID} = post
|
{ID} = post
|
||||||
if Conf['Mark Quotes of You']
|
if Conf['Mark Quotes of You']
|
||||||
if post.info.yours
|
if post.info.yours
|
||||||
|
|||||||
@ -59,11 +59,15 @@ QR =
|
|||||||
$.on d, 'dragover', QR.dragOver
|
$.on d, 'dragover', QR.dragOver
|
||||||
$.on d, 'drop', QR.dropFile
|
$.on d, 'drop', QR.dropFile
|
||||||
$.on d, 'dragstart dragend', QR.drag
|
$.on d, 'dragstart dragend', QR.drag
|
||||||
$.on d, 'ThreadUpdate', ->
|
switch g.VIEW
|
||||||
if g.DEAD
|
when 'index'
|
||||||
QR.abort()
|
$.on d, 'IndexRefresh', QR.generatePostableThreadsList
|
||||||
else
|
when 'thread'
|
||||||
QR.status()
|
$.on d, 'ThreadUpdate', ->
|
||||||
|
if g.DEAD
|
||||||
|
QR.abort()
|
||||||
|
else
|
||||||
|
QR.status()
|
||||||
|
|
||||||
node: ->
|
node: ->
|
||||||
$.on $('a[title="Quote this post"]', @nodes.info), 'click', QR.quote
|
$.on $('a[title="Quote this post"]', @nodes.info), 'click', QR.quote
|
||||||
@ -278,7 +282,6 @@ QR =
|
|||||||
setTimers = (e) => QR.cooldown.types = e.detail
|
setTimers = (e) => QR.cooldown.types = e.detail
|
||||||
$.on window, 'cooldown:timers', setTimers
|
$.on window, 'cooldown:timers', setTimers
|
||||||
$.globalEval 'window.dispatchEvent(new CustomEvent("cooldown:timers", {detail: cooldowns}))'
|
$.globalEval 'window.dispatchEvent(new CustomEvent("cooldown:timers", {detail: cooldowns}))'
|
||||||
QR.cooldown.types or= {} # XXX tmp workaround until all pages and the catalogs get the cooldowns var.
|
|
||||||
$.off window, 'cooldown:timers', setTimers
|
$.off window, 'cooldown:timers', setTimers
|
||||||
for type of QR.cooldown.types
|
for type of QR.cooldown.types
|
||||||
QR.cooldown.types[type] = +QR.cooldown.types[type]
|
QR.cooldown.types[type] = +QR.cooldown.types[type]
|
||||||
@ -352,11 +355,6 @@ QR =
|
|||||||
QR.cooldown.unset start
|
QR.cooldown.unset start
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if 'timeout' of cooldown
|
|
||||||
# XXX tmp conversion from previous cooldowns
|
|
||||||
QR.cooldown.unset start
|
|
||||||
continue
|
|
||||||
|
|
||||||
if isReply is cooldown.isReply
|
if isReply is cooldown.isReply
|
||||||
# Only cooldowns relevant to this post can set the seconds variable:
|
# Only cooldowns relevant to this post can set the seconds variable:
|
||||||
# reply cooldown with a reply, thread cooldown with a thread
|
# reply cooldown with a reply, thread cooldown with a thread
|
||||||
@ -511,7 +509,7 @@ QR =
|
|||||||
className: 'qr-preview'
|
className: 'qr-preview'
|
||||||
draggable: true
|
draggable: true
|
||||||
href: 'javascript:;'
|
href: 'javascript:;'
|
||||||
innerHTML: '<a class=remove>✖</a><label hidden><input type=checkbox> Spoiler</label><span></span>'
|
innerHTML: '<a class="remove fa fa-times-circle" title=Remove></a><label hidden><input type=checkbox> Spoiler</label><span></span>'
|
||||||
|
|
||||||
@nodes =
|
@nodes =
|
||||||
el: el
|
el: el
|
||||||
@ -687,7 +685,7 @@ QR =
|
|||||||
# Resized pictures through canvases look like ass,
|
# Resized pictures through canvases look like ass,
|
||||||
# so we generate thumbnails `s` times bigger then expected
|
# so we generate thumbnails `s` times bigger then expected
|
||||||
# to avoid crappy resized quality.
|
# to avoid crappy resized quality.
|
||||||
s = 90*2
|
s = 90 * 2 * window.devicePixelRatio
|
||||||
s *= 3 if @file.type is 'image/gif' # let them animate
|
s *= 3 if @file.type is 'image/gif' # let them animate
|
||||||
{height, width} = img
|
{height, width} = img
|
||||||
if height < s or width < s
|
if height < s or width < s
|
||||||
@ -789,7 +787,7 @@ QR =
|
|||||||
|
|
||||||
imgContainer = $.el 'div',
|
imgContainer = $.el 'div',
|
||||||
className: 'captcha-img'
|
className: 'captcha-img'
|
||||||
title: 'Reload'
|
title: 'Reload reCAPTCHA'
|
||||||
innerHTML: '<img>'
|
innerHTML: '<img>'
|
||||||
input = $.el 'input',
|
input = $.el 'input',
|
||||||
className: 'captcha-input field'
|
className: 'captcha-input field'
|
||||||
@ -902,10 +900,28 @@ QR =
|
|||||||
return
|
return
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
|
generatePostableThreadsList: ->
|
||||||
|
return unless QR.nodes
|
||||||
|
list = QR.nodes.thread
|
||||||
|
options = [list.firstChild]
|
||||||
|
for thread of g.BOARD.threads
|
||||||
|
options.push $.el 'option',
|
||||||
|
value: thread
|
||||||
|
textContent: "Thread No.#{thread}"
|
||||||
|
val = list.value
|
||||||
|
$.rmAll list
|
||||||
|
$.add list, options
|
||||||
|
list.value = val
|
||||||
|
return unless list.value
|
||||||
|
# Fix the value if the option disappeared.
|
||||||
|
list.value = if g.VIEW is 'thread'
|
||||||
|
g.THREADID
|
||||||
|
else
|
||||||
|
'new'
|
||||||
|
|
||||||
dialog: ->
|
dialog: ->
|
||||||
QR.nodes = nodes =
|
QR.nodes = nodes =
|
||||||
el:
|
el: dialog = UI.dialog 'qr', 'top:0;right:0;', <%= importHTML('Features/QuickReply') %>
|
||||||
dialog = UI.dialog 'qr', 'top:0;right:0;', """<%= grunt.file.read('src/General/html/Features/QuickReply.html').replace(/>\s+</g, '><').trim() %>"""
|
|
||||||
|
|
||||||
nodes[key] = $ value, dialog for key, value of {
|
nodes[key] = $ value, dialog for key, value of {
|
||||||
move: '.move'
|
move: '.move'
|
||||||
@ -977,12 +993,6 @@ QR =
|
|||||||
nodes.flag.dataset.default = '0'
|
nodes.flag.dataset.default = '0'
|
||||||
$.add nodes.form, nodes.flag
|
$.add nodes.form, nodes.flag
|
||||||
|
|
||||||
# Make a list of threads.
|
|
||||||
for thread of g.BOARD.threads
|
|
||||||
$.add nodes.thread, $.el 'option',
|
|
||||||
value: thread
|
|
||||||
textContent: "Reply to #{thread}"
|
|
||||||
|
|
||||||
$.on nodes.filename.parentNode, 'click keydown', QR.openFileInput
|
$.on nodes.filename.parentNode, 'click keydown', QR.openFileInput
|
||||||
|
|
||||||
<% if (type === 'userscript') { %>
|
<% if (type === 'userscript') { %>
|
||||||
@ -1034,6 +1044,7 @@ QR =
|
|||||||
$.set 'QR Size', @style.cssText
|
$.set 'QR Size', @style.cssText
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
|
QR.generatePostableThreadsList()
|
||||||
QR.persona.init()
|
QR.persona.init()
|
||||||
new QR.post true
|
new QR.post true
|
||||||
QR.status()
|
QR.status()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user