mirror of
https://github.com/LalleSX/4chan-XZ.git
synced 2026-01-30 09:48:12 +01:00
added a few types
This commit is contained in:
parent
cc4155500b
commit
329fc4dd14
@ -48,6 +48,7 @@
|
||||
"never"
|
||||
],
|
||||
"simple-import-sort/imports": "error",
|
||||
"simple-import-sort/exports": "error"
|
||||
"simple-import-sort/exports": "error",
|
||||
"no-cond-assign": "off"
|
||||
}
|
||||
}
|
||||
@ -1,22 +1,26 @@
|
||||
import BoardConfig from "../General/BoardConfig"
|
||||
import { d, g } from "../globals/globals"
|
||||
import Post from "./Post"
|
||||
import SimpleDict from "./SimpleDict"
|
||||
import Thread from "./Thread"
|
||||
|
||||
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
|
||||
*/
|
||||
export default class Board {
|
||||
ID: string
|
||||
boardID: number | string
|
||||
siteID: number
|
||||
threads: SimpleDict<Thread>
|
||||
posts: SimpleDict<Post>
|
||||
config: any
|
||||
toString() { return this.ID }
|
||||
|
||||
constructor(ID) {
|
||||
this.ID = ID
|
||||
this.boardID = this.ID
|
||||
this.siteID = g.SITE.ID
|
||||
this.siteID = g.SITE.ID
|
||||
this.threads = new SimpleDict()
|
||||
this.posts = new SimpleDict()
|
||||
this.config = BoardConfig.boards?.[this.ID] || {}
|
||||
this.posts = new SimpleDict()
|
||||
this.config = BoardConfig.boards?.[this.ID] || {}
|
||||
|
||||
g.boards[this] = this
|
||||
}
|
||||
@ -25,8 +29,8 @@ export default class Board {
|
||||
const c2 = (this.config || {}).cooldowns || {}
|
||||
const c = {
|
||||
thread: c2.threads || 0,
|
||||
reply: c2.replies || 0,
|
||||
image: c2.images || 0,
|
||||
reply: c2.replies || 0,
|
||||
image: c2.images || 0,
|
||||
thread_global: 300 // inter-board thread cooldown
|
||||
}
|
||||
// Pass users have reduced cooldowns.
|
||||
|
||||
@ -1,15 +1,17 @@
|
||||
import Main from "../main/Main"
|
||||
import Post from "./Post"
|
||||
|
||||
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS206: Consider reworking classes to avoid initClass
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
|
||||
*/
|
||||
export default class Callbacks {
|
||||
static Post: Callbacks
|
||||
static Thread: Callbacks
|
||||
static CatalogThread: Callbacks
|
||||
static CatalogThreadNative: Callbacks
|
||||
type: string
|
||||
keys: string[]
|
||||
static initClass() {
|
||||
this.Post = new Callbacks('Post')
|
||||
this.Thread = new Callbacks('Thread')
|
||||
this.Post = new Callbacks('Post')
|
||||
this.Thread = new Callbacks('Thread')
|
||||
this.CatalogThread = new Callbacks('Catalog Thread')
|
||||
this.CatalogThreadNative = new Callbacks('Catalog Thread')
|
||||
}
|
||||
@ -19,12 +21,12 @@ export default class Callbacks {
|
||||
this.keys = []
|
||||
}
|
||||
|
||||
push({name, cb}) {
|
||||
push({ name, cb }) {
|
||||
if (!this[name]) { this.keys.push(name) }
|
||||
return this[name] = cb
|
||||
}
|
||||
|
||||
execute(node, keys=this.keys, force=false) {
|
||||
execute(node, keys = this.keys, force = false) {
|
||||
let errors
|
||||
if (node.callbacksExecuted && !force) { return }
|
||||
node.callbacksExecuted = true
|
||||
|
||||
@ -1,21 +1,28 @@
|
||||
import $ from "../platform/$"
|
||||
import Board from "./Board"
|
||||
import Post from "./Post"
|
||||
import Thread from "./Thread"
|
||||
|
||||
export default class CatalogThread {
|
||||
ID: any
|
||||
thread: Thread
|
||||
board: any
|
||||
nodes: { root: Post; thumb: HTMLElement; icons: any; postCount: number; fileCount: number; pageCount: number; replies: any }
|
||||
toString() { return this.ID }
|
||||
|
||||
constructor(root, thread) {
|
||||
constructor(root: Post, thread: Thread) {
|
||||
this.thread = thread
|
||||
this.ID = this.thread.ID
|
||||
this.ID = this.thread.ID + ''
|
||||
this.board = this.thread.board
|
||||
const {post} = this.thread.OP.nodes
|
||||
const { post } = this.thread.OP.nodes
|
||||
this.nodes = {
|
||||
root,
|
||||
thumb: $('.catalog-thumb', post),
|
||||
icons: $('.catalog-icons', post),
|
||||
postCount: $('.post-count', post),
|
||||
fileCount: $('.file-count', post),
|
||||
pageCount: $('.page-count', post),
|
||||
replies: null
|
||||
thumb: $('.catalog-thumb', post),
|
||||
icons: $('.catalog-icons', post),
|
||||
postCount: $('.post-count', post),
|
||||
fileCount: $('.file-count', post),
|
||||
pageCount: $('.page-count', post),
|
||||
replies: null
|
||||
}
|
||||
this.thread.catalogView = this
|
||||
}
|
||||
|
||||
@ -4,6 +4,13 @@ import Board from "./Board"
|
||||
import Thread from "./Thread"
|
||||
|
||||
export default class CatalogThreadNative {
|
||||
ID: number | string
|
||||
nodes: { root: Thread; thumb: HTMLElement }
|
||||
siteID: string
|
||||
boardID: string
|
||||
board: Board | import("/home/victor/proj/4chan-XZ/src/globals/globals").Board
|
||||
threadID: number
|
||||
thread: Thread
|
||||
toString() { return this.ID }
|
||||
|
||||
constructor(root) {
|
||||
@ -11,7 +18,7 @@ export default class CatalogThreadNative {
|
||||
root,
|
||||
thumb: $(g.SITE.selectors.catalog.thumb, root)
|
||||
}
|
||||
this.siteID = g.SITE.ID
|
||||
this.siteID = g.SITE.ID
|
||||
this.boardID = this.nodes.thumb.parentNode.pathname.split(/\/+/)[1]
|
||||
this.board = g.boards[this.boardID] || new Board(this.boardID)
|
||||
this.ID = (this.threadID = +(root.dataset.id || root.id).match(/\d*$/)[0])
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import { g } from "../globals/globals"
|
||||
import $ from "../platform/$"
|
||||
import Callbacks from "./Callbacks"
|
||||
|
||||
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
|
||||
*/
|
||||
export default class Connection {
|
||||
constructor(target, origin, cb={}) {
|
||||
target: any
|
||||
origin: any
|
||||
cb: Callbacks
|
||||
constructor(target: Window, origin: string, cb: Callbacks) {
|
||||
this.send = this.send.bind(this)
|
||||
this.onMessage = this.onMessage.bind(this)
|
||||
this.target = target
|
||||
@ -28,7 +28,7 @@ export default class Connection {
|
||||
return this.targetWindow().postMessage(`${g.NAMESPACE}${JSON.stringify(data)}`, this.origin)
|
||||
}
|
||||
|
||||
onMessage(e) {
|
||||
onMessage(e: MessageEvent) {
|
||||
if ((e.source !== this.targetWindow()) ||
|
||||
(e.origin !== this.origin) ||
|
||||
(typeof e.data !== 'string') ||
|
||||
|
||||
@ -11,6 +11,11 @@ import { dict, HOUR } from "../platform/helpers"
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
|
||||
*/
|
||||
export default class DataBoard {
|
||||
static keys: string[]
|
||||
static changes: string[]
|
||||
key: string
|
||||
sync: VoidFunction
|
||||
data: any
|
||||
static initClass() {
|
||||
this.keys = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads', 'watcherLastModified', 'customTitles']
|
||||
|
||||
@ -38,8 +43,8 @@ export default class DataBoard {
|
||||
this.data = data
|
||||
if (this.data.boards) {
|
||||
let lastChecked;
|
||||
({boards, lastChecked} = this.data)
|
||||
this.data['4chan.org'] = {boards, lastChecked}
|
||||
({ boards, lastChecked } = this.data)
|
||||
this.data['4chan.org'] = { boards, lastChecked }
|
||||
delete this.data.boards
|
||||
delete this.data.lastChecked
|
||||
}
|
||||
@ -76,31 +81,31 @@ export default class DataBoard {
|
||||
})
|
||||
}
|
||||
|
||||
delete({siteID, boardID, threadID, postID}, cb) {
|
||||
delete({ siteID, boardID, threadID, postID }, cb) {
|
||||
if (!siteID) { siteID = g.SITE.ID }
|
||||
if (!this.data[siteID]) { return }
|
||||
return this.save(() => {
|
||||
if (postID) {
|
||||
if (!this.data[siteID].boards[boardID]?.[threadID]) { return }
|
||||
delete this.data[siteID].boards[boardID][threadID][postID]
|
||||
return this.deleteIfEmpty({siteID, boardID, threadID})
|
||||
return this.deleteIfEmpty({ siteID, boardID, threadID })
|
||||
} else if (threadID) {
|
||||
if (!this.data[siteID].boards[boardID]) { return }
|
||||
delete this.data[siteID].boards[boardID][threadID]
|
||||
return this.deleteIfEmpty({siteID, boardID})
|
||||
return this.deleteIfEmpty({ siteID, boardID })
|
||||
} else {
|
||||
return delete this.data[siteID].boards[boardID]
|
||||
}
|
||||
}
|
||||
, cb)
|
||||
, cb)
|
||||
}
|
||||
|
||||
deleteIfEmpty({siteID, boardID, threadID}) {
|
||||
deleteIfEmpty({ siteID, boardID, threadID }) {
|
||||
if (!this.data[siteID]) { return }
|
||||
if (threadID) {
|
||||
if (!Object.keys(this.data[siteID].boards[boardID][threadID]).length) {
|
||||
delete this.data[siteID].boards[boardID][threadID]
|
||||
return this.deleteIfEmpty({siteID, boardID})
|
||||
return this.deleteIfEmpty({ siteID, boardID })
|
||||
}
|
||||
} else if (!Object.keys(this.data[siteID].boards[boardID]).length) {
|
||||
return delete this.data[siteID].boards[boardID]
|
||||
@ -111,10 +116,10 @@ export default class DataBoard {
|
||||
return this.save(() => {
|
||||
return this.setUnsafe(data)
|
||||
}
|
||||
, cb)
|
||||
, cb)
|
||||
}
|
||||
|
||||
setUnsafe({siteID, boardID, threadID, postID, val}) {
|
||||
setUnsafe({ siteID, boardID, threadID, postID, val }) {
|
||||
if (!siteID) { siteID = g.SITE.ID }
|
||||
if (!this.data[siteID]) { this.data[siteID] = { boards: dict() } }
|
||||
if (postID !== undefined) {
|
||||
@ -127,7 +132,7 @@ export default class DataBoard {
|
||||
}
|
||||
}
|
||||
|
||||
extend({siteID, boardID, threadID, postID, val}, cb) {
|
||||
extend({ siteID, boardID, threadID, postID, val }, cb) {
|
||||
return this.save(() => {
|
||||
const oldVal = this.get({ siteID, boardID, threadID, postID, defaultValue: dict() })
|
||||
for (const key in val) {
|
||||
@ -138,18 +143,18 @@ export default class DataBoard {
|
||||
oldVal[key] = subVal
|
||||
}
|
||||
}
|
||||
return this.setUnsafe({siteID, boardID, threadID, postID, val: oldVal})
|
||||
return this.setUnsafe({ siteID, boardID, threadID, postID, val: oldVal })
|
||||
}
|
||||
, cb)
|
||||
, cb)
|
||||
}
|
||||
|
||||
setLastChecked(key='lastChecked') {
|
||||
setLastChecked(key = 'lastChecked') {
|
||||
return this.save(() => {
|
||||
return this.data[key] = Date.now()
|
||||
})
|
||||
}
|
||||
|
||||
get({siteID, boardID, threadID, postID, defaultValue}) {
|
||||
get({ siteID, boardID, threadID, postID, defaultValue }) {
|
||||
let board, val
|
||||
if (!siteID) { siteID = g.SITE.ID }
|
||||
if (board = this.data[siteID]?.boards[boardID]) {
|
||||
@ -169,7 +174,7 @@ export default class DataBoard {
|
||||
} else if (thread = board[threadID]) {
|
||||
val = (postID != null) ?
|
||||
thread[postID]
|
||||
:
|
||||
:
|
||||
thread
|
||||
}
|
||||
}
|
||||
@ -181,7 +186,7 @@ export default class DataBoard {
|
||||
const siteID = g.SITE.ID
|
||||
for (boardID in this.data[siteID].boards) {
|
||||
const val = this.data[siteID].boards[boardID]
|
||||
this.deleteIfEmpty({siteID, boardID})
|
||||
this.deleteIfEmpty({ siteID, boardID })
|
||||
}
|
||||
const now = Date.now()
|
||||
if (now - (2 * HOUR) >= ((middle = this.data[siteID].lastChecked || 0)) || middle > now) {
|
||||
@ -195,14 +200,14 @@ export default class DataBoard {
|
||||
ajaxClean(boardID) {
|
||||
const that = this
|
||||
const siteID = g.SITE.ID
|
||||
const threadsList = g.SITE.urls.threadsListJSON?.({siteID, boardID})
|
||||
const threadsList = g.SITE.urls.threadsListJSON?.({ siteID, boardID })
|
||||
if (!threadsList) { return }
|
||||
return $.cache(threadsList, function() {
|
||||
return $.cache(threadsList, function () {
|
||||
if (this.status !== 200) { return }
|
||||
const archiveList = g.SITE.urls.archiveListJSON?.({siteID, boardID})
|
||||
const archiveList = g.SITE.urls.archiveListJSON?.({ siteID, boardID })
|
||||
if (!archiveList) { return that.ajaxCleanParse(boardID, this.response) }
|
||||
const response1 = this.response
|
||||
return $.cache(archiveList, function() {
|
||||
return $.cache(archiveList, function () {
|
||||
if ((this.status !== 200) && (!!g.SITE.archivedBoardsKnown || (this.status !== 404))) { return }
|
||||
return that.ajaxCleanParse(boardID, response1, this.response)
|
||||
})
|
||||
@ -228,7 +233,7 @@ export default class DataBoard {
|
||||
}
|
||||
}
|
||||
this.data[siteID].boards[boardID] = threads
|
||||
this.deleteIfEmpty({siteID, boardID})
|
||||
this.deleteIfEmpty({ siteID, boardID })
|
||||
return $.set(this.key, this.data)
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import Redirect from "../Archive/Redirect"
|
||||
import Get from "../General/Get"
|
||||
import Index from "../General/Index"
|
||||
import { Conf, d,E, g } from "../globals/globals"
|
||||
import { Conf, d, E, g } from "../globals/globals"
|
||||
import ImageHost from "../Images/ImageHost"
|
||||
import Main from "../main/Main"
|
||||
import $ from "../platform/$"
|
||||
@ -21,30 +21,37 @@ import Thread from "./Thread"
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
|
||||
*/
|
||||
export default class Fetcher {
|
||||
archiveTags: { '\n': { innerHTML: string }; '[b]': { innerHTML: string }; '[/b]': { innerHTML: string }; '[spoiler]': { innerHTML: string }; '[/spoiler]': { innerHTML: string }; '[code]': { innerHTML: string }; '[/code]': { innerHTML: string }; '[moot]': { innerHTML: string }; '[/moot]': { innerHTML: string }; '[banned]': { innerHTML: string }; '[/banned]': { innerHTML: string }; '[fortune]'(text: string): { innerHTML: string }; '[/fortune]': { innerHTML: string }; '[i]': { innerHTML: string }; '[/i]': { innerHTML: string }; '[red]': { innerHTML: string }; '[/red]': { innerHTML: string }; '[green]': { innerHTML: string }; '[/green]': { innerHTML: string }; '[blue]': { innerHTML: string }; '[/blue]': { innerHTML: string } }
|
||||
boardID: string
|
||||
threadID: number
|
||||
postID: number
|
||||
root: HTMLElement
|
||||
quoter: any
|
||||
static flagCSS: any
|
||||
static initClass() {
|
||||
|
||||
|
||||
this.prototype.archiveTags = {
|
||||
'\n': {innerHTML: "<br>"},
|
||||
'[b]': {innerHTML: "<b>"},
|
||||
'[/b]': {innerHTML: "</b>"},
|
||||
'[spoiler]': {innerHTML: "<s>"},
|
||||
'[/spoiler]': {innerHTML: "</s>"},
|
||||
'[code]': {innerHTML: "<pre class=\"prettyprint\">"},
|
||||
'[/code]': {innerHTML: "</pre>"},
|
||||
'[moot]': {innerHTML: "<div style=\"padding:5px;margin-left:.5em;border-color:#faa;border:2px dashed rgba(255,0,0,.1);border-radius:2px\">"},
|
||||
'[/moot]': {innerHTML: "</div>"},
|
||||
'[banned]': {innerHTML: "<strong style=\"color: red;\">"},
|
||||
'[/banned]': {innerHTML: "</strong>"},
|
||||
'[fortune]'(text) { return {innerHTML: "<span class=\"fortune\" style=\"color:" + E(text.match(/#\w+|$/)[0]) + "\"><b>"} },
|
||||
'[/fortune]': {innerHTML: "</b></span>"},
|
||||
'[i]': {innerHTML: "<span class=\"mu-i\">"},
|
||||
'[/i]': {innerHTML: "</span>"},
|
||||
'[red]': {innerHTML: "<span class=\"mu-r\">"},
|
||||
'[/red]': {innerHTML: "</span>"},
|
||||
'[green]': {innerHTML: "<span class=\"mu-g\">"},
|
||||
'[/green]': {innerHTML: "</span>"},
|
||||
'[blue]': {innerHTML: "<span class=\"mu-b\">"},
|
||||
'[/blue]': {innerHTML: "</span>"}
|
||||
'\n': { innerHTML: "<br>" },
|
||||
'[b]': { innerHTML: "<b>" },
|
||||
'[/b]': { innerHTML: "</b>" },
|
||||
'[spoiler]': { innerHTML: "<s>" },
|
||||
'[/spoiler]': { innerHTML: "</s>" },
|
||||
'[code]': { innerHTML: "<pre class=\"prettyprint\">" },
|
||||
'[/code]': { innerHTML: "</pre>" },
|
||||
'[moot]': { innerHTML: "<div style=\"padding:5px;margin-left:.5em;border-color:#faa;border:2px dashed rgba(255,0,0,.1);border-radius:2px\">" },
|
||||
'[/moot]': { innerHTML: "</div>" },
|
||||
'[banned]': { innerHTML: "<strong style=\"color: red;\">" },
|
||||
'[/banned]': { innerHTML: "</strong>" },
|
||||
'[fortune]'(text) { return { innerHTML: "<span class=\"fortune\" style=\"color:" + E(text.match(/#\w+|$/)[0]) + "\"><b>" } },
|
||||
'[/fortune]': { innerHTML: "</b></span>" },
|
||||
'[i]': { innerHTML: "<span class=\"mu-i\">" },
|
||||
'[/i]': { innerHTML: "</span>" },
|
||||
'[red]': { innerHTML: "<span class=\"mu-r\">" },
|
||||
'[/red]': { innerHTML: "</span>" },
|
||||
'[green]': { innerHTML: "<span class=\"mu-g\">" },
|
||||
'[/green]': { innerHTML: "</span>" },
|
||||
'[blue]': { innerHTML: "<span class=\"mu-b\">" },
|
||||
'[/blue]': { innerHTML: "</span>" }
|
||||
}
|
||||
}
|
||||
constructor(boardID, threadID, postID, root, quoter) {
|
||||
@ -61,8 +68,8 @@ export default class Fetcher {
|
||||
|
||||
// 4chan X catalog data
|
||||
if ((post = Index.replyData?.[`${this.boardID}.${this.postID}`]) && (thread = g.threads.get(`${this.boardID}.${this.threadID}`))) {
|
||||
const board = g.boards[this.boardID]
|
||||
post = new Post(g.SITE.Build.postFromObject(post, this.boardID), thread, board, {isFetchedQuote: true})
|
||||
const board = g.boards[this.boardID]
|
||||
post = new Post(g.SITE.Build.postFromObject(post, this.boardID), thread, board, { isFetchedQuote: true })
|
||||
Main.callbackNodes('Post', [post])
|
||||
this.insert(post)
|
||||
return
|
||||
@ -71,7 +78,7 @@ export default class Fetcher {
|
||||
this.root.textContent = `Loading post No.${this.postID}...`
|
||||
if (this.threadID) {
|
||||
const that = this
|
||||
$.cache(g.SITE.urls.threadJSON({boardID: this.boardID, threadID: this.threadID}), function({isCached}) {
|
||||
$.cache(g.SITE.urls.threadJSON({ boardID: this.boardID, threadID: this.threadID }), function ({ isCached }) {
|
||||
return that.fetchedPost(this, isCached)
|
||||
})
|
||||
} else {
|
||||
@ -87,14 +94,14 @@ export default class Fetcher {
|
||||
Main.callbackNodes('Post', [clone])
|
||||
|
||||
// Get rid of the side arrows/stubs.
|
||||
const {nodes} = clone
|
||||
const { nodes } = clone
|
||||
$.rmAll(nodes.root)
|
||||
$.add(nodes.root, nodes.post)
|
||||
|
||||
// Indicate links to the containing post.
|
||||
const quotes = [...clone.nodes.quotelinks, ...clone.nodes.backlinks]
|
||||
for (const quote of quotes) {
|
||||
const {boardID, postID} = Get.postDataFromLink(quote)
|
||||
const { boardID, postID } = Get.postDataFromLink(quote)
|
||||
if ((postID === this.quoter.ID) && (boardID === this.quoter.board.ID)) {
|
||||
$.addClass(quote, 'forwardlink')
|
||||
}
|
||||
@ -125,7 +132,7 @@ export default class Fetcher {
|
||||
return
|
||||
}
|
||||
|
||||
const {status} = req
|
||||
const { status } = req
|
||||
if (status !== 200) {
|
||||
// The thread can die by the time we check a quote.
|
||||
if (status && this.archivedPost()) { return }
|
||||
@ -134,14 +141,14 @@ export default class Fetcher {
|
||||
this.root.textContent =
|
||||
status === 404 ?
|
||||
`Thread No.${this.threadID} 404'd.`
|
||||
: !status ?
|
||||
'Connection Error'
|
||||
:
|
||||
`Error ${req.statusText} (${req.status}).`
|
||||
: !status ?
|
||||
'Connection Error'
|
||||
:
|
||||
`Error ${req.statusText} (${req.status}).`
|
||||
return
|
||||
}
|
||||
|
||||
const {posts} = req.response
|
||||
const { posts } = req.response
|
||||
g.SITE.Build.spoilerRange[this.boardID] = posts[0].custom_spoiler
|
||||
for (post of posts) {
|
||||
if (post.no === this.postID) { break }
|
||||
@ -150,10 +157,10 @@ export default class Fetcher {
|
||||
if (post.no !== this.postID) {
|
||||
// Cached requests can be stale and must be rechecked.
|
||||
if (isCached) {
|
||||
const api = g.SITE.urls.threadJSON({boardID: this.boardID, threadID: this.threadID})
|
||||
const api = g.SITE.urls.threadJSON({ boardID: this.boardID, threadID: this.threadID })
|
||||
$.cleanCache(url => url === api)
|
||||
const that = this
|
||||
$.cache(api, function() {
|
||||
$.cache(api, function () {
|
||||
return that.fetchedPost(this, false)
|
||||
})
|
||||
return
|
||||
@ -171,7 +178,7 @@ export default class Fetcher {
|
||||
new Board(this.boardID)
|
||||
const thread = g.threads.get(`${this.boardID}.${this.threadID}`) ||
|
||||
new Thread(this.threadID, board)
|
||||
post = new Post(g.SITE.Build.postFromObject(post, this.boardID), thread, board, {isFetchedQuote: true})
|
||||
post = new Post(g.SITE.Build.postFromObject(post, this.boardID), thread, board, { isFetchedQuote: true })
|
||||
Main.callbackNodes('Post', [post])
|
||||
return this.insert(post)
|
||||
}
|
||||
@ -179,14 +186,14 @@ export default class Fetcher {
|
||||
archivedPost() {
|
||||
let url
|
||||
if (!Conf['Resurrect Quotes']) { return false }
|
||||
if (!(url = Redirect.to('post', {boardID: this.boardID, postID: this.postID}))) { return false }
|
||||
if (!(url = Redirect.to('post', { boardID: this.boardID, postID: this.postID }))) { return false }
|
||||
const archive = Redirect.data.post[this.boardID]
|
||||
const encryptionOK = /^https:\/\//.test(url) || (location.protocol === 'http:')
|
||||
if (encryptionOK || Conf['Exempt Archives from Encryption']) {
|
||||
const that = this
|
||||
CrossOrigin.cache(url, function() {
|
||||
CrossOrigin.cache(url, function () {
|
||||
if (!encryptionOK && this.response?.media) {
|
||||
const {media} = this.response
|
||||
const { media } = this.response
|
||||
for (const key in media) {
|
||||
// Image/thumbnail URLs loaded over HTTP can be modified in transit.
|
||||
// Require them to be from an HTTP host so that no referrer is sent to them from an HTTPS page.
|
||||
@ -237,42 +244,43 @@ export default class Fetcher {
|
||||
} else {
|
||||
const greentext = text[0] === '>'
|
||||
text = text.replace(/(\[\/?[a-z]+):lit(\])/g, '$1$2')
|
||||
text = text.split(/(>>(?:>\/[a-z\d]+\/)?\d+)/g).map((text2, j) =>
|
||||
{((j % 2) ? "<span class=\"deadlink\">" + E(text2) + "</span>" : E(text2))})
|
||||
text = {innerHTML: ((greentext) ? "<span class=\"quote\">" + E.cat(text) + "</span>" : E.cat(text))}
|
||||
text = text.split(/(>>(?:>\/[a-z\d]+\/)?\d+)/g).map((text2, j) => { ((j % 2) ? "<span class=\"deadlink\">" + E(text2) + "</span>" : E(text2)) })
|
||||
text = { innerHTML: ((greentext) ? "<span class=\"quote\">" + E.cat(text) + "</span>" : E.cat(text)) }
|
||||
result.push(text)
|
||||
}
|
||||
}
|
||||
return result
|
||||
})()
|
||||
comment = {innerHTML: E.cat(comment)}
|
||||
comment = { innerHTML: E.cat(comment) }
|
||||
|
||||
this.threadID = +data.thread_num
|
||||
const o = {
|
||||
ID: this.postID,
|
||||
ID: this.postID,
|
||||
threadID: this.threadID,
|
||||
boardID: this.boardID,
|
||||
isReply: this.postID !== this.threadID
|
||||
boardID: this.boardID,
|
||||
isReply: this.postID !== this.threadID
|
||||
}
|
||||
o.info = {
|
||||
subject: data.title,
|
||||
email: data.email,
|
||||
name: data.name || '',
|
||||
subject: data.title,
|
||||
email: data.email,
|
||||
name: data.name || '',
|
||||
tripcode: data.trip,
|
||||
capcode: (() => { switch (data.capcode) {
|
||||
// https://github.com/pleebe/FoolFuuka/blob/bf4224eed04637a4d0bd4411c2bf5f9945dfec0b/assets/themes/foolz/foolfuuka-theme-fuuka/src/Partial/Board.php#L77
|
||||
case 'M': return 'Mod'
|
||||
case 'A': return 'Admin'
|
||||
case 'D': return 'Developer'
|
||||
case 'V': return 'Verified'
|
||||
case 'F': return 'Founder'
|
||||
case 'G': return 'Manager'
|
||||
} })(),
|
||||
capcode: (() => {
|
||||
switch (data.capcode) {
|
||||
// https://github.com/pleebe/FoolFuuka/blob/bf4224eed04637a4d0bd4411c2bf5f9945dfec0b/assets/themes/foolz/foolfuuka-theme-fuuka/src/Partial/Board.php#L77
|
||||
case 'M': return 'Mod'
|
||||
case 'A': return 'Admin'
|
||||
case 'D': return 'Developer'
|
||||
case 'V': return 'Verified'
|
||||
case 'F': return 'Founder'
|
||||
case 'G': return 'Manager'
|
||||
}
|
||||
})(),
|
||||
uniqueID: data.poster_hash,
|
||||
flagCode: data.poster_country,
|
||||
flagCodeTroll: data.troll_country_code,
|
||||
flag: data.poster_country_name || data.troll_country_name,
|
||||
dateUTC: data.timestamp,
|
||||
flag: data.poster_country_name || data.troll_country_name,
|
||||
dateUTC: data.timestamp,
|
||||
dateText: data.fourchan_date,
|
||||
commentHTML: comment
|
||||
}
|
||||
@ -280,26 +288,26 @@ export default class Fetcher {
|
||||
if (data.media && !!+data.media.banned) {
|
||||
o.fileDeleted = true
|
||||
} else if (data.media?.media_filename) {
|
||||
let {thumb_link} = data.media
|
||||
let { thumb_link } = data.media
|
||||
// Fix URLs missing origin
|
||||
if (thumb_link?.[0] === '/') { thumb_link = url.split('/', 3).join('/') + thumb_link }
|
||||
if (!Redirect.securityCheck(thumb_link)) { thumb_link = '' }
|
||||
let media_link = Redirect.to('file', {boardID: this.boardID, filename: data.media.media_orig})
|
||||
let media_link = Redirect.to('file', { boardID: this.boardID, filename: data.media.media_orig })
|
||||
if (!Redirect.securityCheck(media_link)) { media_link = '' }
|
||||
o.file = {
|
||||
name: data.media.media_filename,
|
||||
url: media_link ||
|
||||
(this.boardID === 'f' ?
|
||||
`${location.protocol}//${ImageHost.flashHost()}/${this.boardID}/${encodeURIComponent(E(data.media.media_filename))}`
|
||||
:
|
||||
`${location.protocol}//${ImageHost.host()}/${this.boardID}/${data.media.media_orig}`),
|
||||
height: data.media.media_h,
|
||||
width: data.media.media_w,
|
||||
MD5: data.media.media_hash,
|
||||
size: $.bytesToString(data.media.media_size),
|
||||
thumbURL: thumb_link || `${location.protocol}//${ImageHost.thumbHost()}/${this.boardID}/${data.media.preview_orig}`,
|
||||
theight: data.media.preview_h,
|
||||
twidth: data.media.preview_w,
|
||||
name: data.media.media_filename,
|
||||
url: media_link ||
|
||||
(this.boardID === 'f' ?
|
||||
`${location.protocol}//${ImageHost.flashHost()}/${this.boardID}/${encodeURIComponent(E(data.media.media_filename))}`
|
||||
:
|
||||
`${location.protocol}//${ImageHost.host()}/${this.boardID}/${data.media.media_orig}`),
|
||||
height: data.media.media_h,
|
||||
width: data.media.media_w,
|
||||
MD5: data.media.media_hash,
|
||||
size: $.bytesToString(data.media.media_size),
|
||||
thumbURL: thumb_link || `${location.protocol}//${ImageHost.thumbHost()}/${this.boardID}/${data.media.preview_orig}`,
|
||||
theight: data.media.preview_h,
|
||||
twidth: data.media.preview_w,
|
||||
isSpoiler: data.media.spoiler === '1'
|
||||
}
|
||||
if (!/\.pdf$/.test(o.file.url)) { o.file.dimensions = `${o.file.width}x${o.file.height}` }
|
||||
@ -311,7 +319,7 @@ export default class Fetcher {
|
||||
new Board(this.boardID)
|
||||
const thread = g.threads.get(`${this.boardID}.${this.threadID}`) ||
|
||||
new Thread(this.threadID, board)
|
||||
post = new Post(g.SITE.Build.post(o), thread, board, {isFetchedQuote: true})
|
||||
post = new Post(g.SITE.Build.post(o), thread, board, { isFetchedQuote: true })
|
||||
post.kill()
|
||||
if (post.file) { post.file.thumbURL = o.file.thumbURL }
|
||||
Main.callbackNodes('Post', [post])
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import Get from "../General/Get"
|
||||
import { Conf,g } from "../globals/globals"
|
||||
import { Conf, g } from "../globals/globals"
|
||||
import ImageExpand from "../Images/ImageExpand"
|
||||
import $ from "../platform/$"
|
||||
import $$ from "../platform/$$"
|
||||
@ -8,41 +8,41 @@ import Callbacks from "./Callbacks"
|
||||
import type Thread from "./Thread"
|
||||
|
||||
export default class Post {
|
||||
declare root: HTMLElement
|
||||
declare thread: Thread
|
||||
declare board: Board
|
||||
declare ID: number
|
||||
declare postID: number
|
||||
declare threadID: number
|
||||
declare boardID: number | string
|
||||
declare siteID: number | string
|
||||
declare fullID: string
|
||||
declare context: Post
|
||||
declare isReply: boolean
|
||||
declare nodes: ReturnType<Post['parseNodes']>
|
||||
declare isDead: boolean
|
||||
declare isHidden: boolean
|
||||
declare clones: any[]
|
||||
declare isRebuilt?: boolean
|
||||
declare root: HTMLElement
|
||||
declare thread: Thread
|
||||
declare board: Board
|
||||
declare ID: number
|
||||
declare postID: number
|
||||
declare threadID: number
|
||||
declare boardID: number | string
|
||||
declare siteID: number | string
|
||||
declare fullID: string
|
||||
declare context: Post
|
||||
declare isReply: boolean
|
||||
declare nodes: ReturnType<Post['parseNodes']>
|
||||
declare isDead: boolean
|
||||
declare isHidden: boolean
|
||||
declare clones: any[]
|
||||
declare isRebuilt?: boolean
|
||||
declare isFetchedQuote: boolean
|
||||
declare isClone: boolean
|
||||
declare quotes: string[]
|
||||
declare file: ReturnType<Post['parseFile']>
|
||||
declare files: ReturnType<Post['parseFile']>[]
|
||||
declare isClone: boolean
|
||||
declare quotes: string[]
|
||||
declare file: ReturnType<Post['parseFile']>
|
||||
declare files: ReturnType<Post['parseFile']>[]
|
||||
|
||||
declare info: {
|
||||
subject: string | undefined,
|
||||
name: string | undefined,
|
||||
email: string | undefined,
|
||||
tripcode: string | undefined,
|
||||
uniqueID: string | undefined,
|
||||
capcode: string | undefined,
|
||||
pass: string | undefined,
|
||||
flagCode: string | undefined,
|
||||
subject: string | undefined,
|
||||
name: string | undefined,
|
||||
email: string | undefined,
|
||||
tripcode: string | undefined,
|
||||
uniqueID: string | undefined,
|
||||
capcode: string | undefined,
|
||||
pass: string | undefined,
|
||||
flagCode: string | undefined,
|
||||
flagCodeTroll: string | undefined,
|
||||
flag: string | undefined,
|
||||
date: Date | undefined,
|
||||
nameBlock: string,
|
||||
flag: string | undefined,
|
||||
date: Date | undefined,
|
||||
nameBlock: string,
|
||||
}
|
||||
|
||||
// because of a circular dependency $ might not be initialized, so we can't use $.el
|
||||
@ -57,7 +57,7 @@ export default class Post {
|
||||
|
||||
toString() { return this.ID }
|
||||
|
||||
constructor(root?: HTMLElement, thread?: Thread, board?: Board, flags={}) {
|
||||
constructor(root?: HTMLElement, thread?: Thread, board?: Board, flags = {}) {
|
||||
// <% if (readJSON('/.tests_enabled')) { %>
|
||||
// @normalizedOriginal = Test.normalize root
|
||||
// <% } %>
|
||||
@ -69,14 +69,14 @@ export default class Post {
|
||||
this.thread = thread
|
||||
this.board = board
|
||||
$.extend(this, flags)
|
||||
this.ID = +root.id.match(/\d*$/)[0]
|
||||
this.postID = this.ID
|
||||
this.ID = +root.id.match(/\d*$/)[0]
|
||||
this.postID = this.ID
|
||||
this.threadID = this.thread.ID
|
||||
this.boardID = this.board.ID
|
||||
this.siteID = g.SITE.ID
|
||||
this.fullID = `${this.board}.${this.ID}`
|
||||
this.context = this
|
||||
this.isReply = (this.ID !== this.threadID)
|
||||
this.boardID = this.board.ID
|
||||
this.siteID = g.SITE.ID
|
||||
this.fullID = `${this.board}.${this.ID}`
|
||||
this.context = this
|
||||
this.isReply = (this.ID !== this.threadID)
|
||||
|
||||
root.dataset.fullID = this.fullID
|
||||
|
||||
@ -100,17 +100,17 @@ export default class Post {
|
||||
const tripcode = this.nodes.tripcode?.textContent
|
||||
|
||||
this.info = {
|
||||
subject: this.nodes.subject?.textContent || undefined,
|
||||
subject: this.nodes.subject?.textContent || undefined,
|
||||
name,
|
||||
email: this.nodes.email ? decodeURIComponent(this.nodes.email.href.replace(/^mailto:/, '')) : undefined,
|
||||
email: this.nodes.email ? decodeURIComponent(this.nodes.email.href.replace(/^mailto:/, '')) : undefined,
|
||||
tripcode,
|
||||
uniqueID: this.nodes.uniqueID?.textContent,
|
||||
capcode: this.nodes.capcode?.textContent.replace('## ', ''),
|
||||
pass: this.nodes.pass?.title.match(/\d*$/)[0],
|
||||
flagCode: this.nodes.flag?.className.match(/flag-(\w+)/)?.[1].toUpperCase(),
|
||||
uniqueID: this.nodes.uniqueID?.textContent,
|
||||
capcode: this.nodes.capcode?.textContent.replace('## ', ''),
|
||||
pass: this.nodes.pass?.title.match(/\d*$/)[0],
|
||||
flagCode: this.nodes.flag?.className.match(/flag-(\w+)/)?.[1].toUpperCase(),
|
||||
flagCodeTroll: this.nodes.flag?.className.match(/bfl-(\w+)/)?.[1].toUpperCase(),
|
||||
flag: this.nodes.flag?.title,
|
||||
date: this.nodes.date ? g.SITE.parseDate(this.nodes.date) : undefined,
|
||||
flag: this.nodes.flag?.title,
|
||||
date: this.nodes.date ? g.SITE.parseDate(this.nodes.date) : undefined,
|
||||
nameBlock: Conf['Anonymize'] ? 'Anonymous' : `${name || ''} ${tripcode || ''}`.trim(),
|
||||
}
|
||||
|
||||
@ -121,7 +121,7 @@ export default class Post {
|
||||
this.parseQuotes()
|
||||
this.parseFiles()
|
||||
|
||||
this.isDead = false
|
||||
this.isDead = false
|
||||
this.isHidden = false
|
||||
|
||||
this.clones = []
|
||||
@ -149,31 +149,31 @@ export default class Post {
|
||||
const info: HTMLElement = $(s.infoRoot, post)
|
||||
|
||||
interface Node {
|
||||
root: HTMLElement,
|
||||
bottom: false | HTMLElement,
|
||||
post: HTMLElement,
|
||||
info: HTMLElement,
|
||||
comment: HTMLElement;
|
||||
quotelinks: HTMLAnchorElement[],
|
||||
root: HTMLElement,
|
||||
bottom: false | HTMLElement,
|
||||
post: HTMLElement,
|
||||
info: HTMLElement,
|
||||
comment: HTMLElement;
|
||||
quotelinks: HTMLAnchorElement[],
|
||||
archivelinks: HTMLAnchorElement[],
|
||||
embedlinks: HTMLAnchorElement[],
|
||||
backlinks: HTMLCollectionOf<HTMLAnchorElement>;
|
||||
embedlinks: HTMLAnchorElement[],
|
||||
backlinks: HTMLCollectionOf<HTMLAnchorElement>;
|
||||
uniqueIDRoot: any,
|
||||
uniqueID: any,
|
||||
uniqueID: any,
|
||||
}
|
||||
|
||||
const nodes: Node & Partial<Record<keyof Post['info'], HTMLElement>> = {
|
||||
root,
|
||||
bottom: this.isReply || !g.SITE.isOPContainerThread ? root : $(s.opBottom, root),
|
||||
bottom: this.isReply || !g.SITE.isOPContainerThread ? root : $(s.opBottom, root),
|
||||
post,
|
||||
info,
|
||||
comment: $(s.comment, post),
|
||||
comment: $(s.comment, post),
|
||||
quotelinks: [],
|
||||
archivelinks: [],
|
||||
embedlinks: [],
|
||||
backlinks: post.getElementsByClassName('backlink') as HTMLCollectionOf<HTMLAnchorElement>,
|
||||
embedlinks: [],
|
||||
backlinks: post.getElementsByClassName('backlink') as HTMLCollectionOf<HTMLAnchorElement>,
|
||||
uniqueIDRoot: undefined as any,
|
||||
uniqueID: undefined as any,
|
||||
uniqueID: undefined as any,
|
||||
}
|
||||
for (const key in s.info) {
|
||||
const selector = s.info[key]
|
||||
@ -293,13 +293,13 @@ export default class Post {
|
||||
|
||||
parseFile(fileRoot: HTMLElement) {
|
||||
interface File {
|
||||
text: string,
|
||||
link: HTMLAnchorElement,
|
||||
thumb: HTMLElement,
|
||||
thumbLink: HTMLElement,
|
||||
size: string,
|
||||
text: string,
|
||||
link: HTMLAnchorElement,
|
||||
thumb: HTMLElement,
|
||||
thumbLink: HTMLElement,
|
||||
size: string,
|
||||
sizeInBytes: number,
|
||||
isDead: boolean,
|
||||
isDead: boolean,
|
||||
}
|
||||
|
||||
const file: Partial<File> = { isDead: false }
|
||||
@ -313,20 +313,20 @@ export default class Post {
|
||||
if (!g.SITE.parseFile(this, file)) { return }
|
||||
|
||||
$.extend(file, {
|
||||
url: file.link.href,
|
||||
url: file.link.href,
|
||||
isImage: $.isImage(file.link.href),
|
||||
isVideo: $.isVideo(file.link.href)
|
||||
}
|
||||
)
|
||||
let size = +file.size.match(/[\d.]+/)[0]
|
||||
let unit = ['B', 'KB', 'MB', 'GB'].indexOf(file.size.match(/\w+$/)[0])
|
||||
let size = +file.size.match(/[\d.]+/)[0]
|
||||
let unit = ['B', 'KB', 'MB', 'GB'].indexOf(file.size.match(/\w+$/)[0])
|
||||
while (unit-- > 0) { size *= 1024 }
|
||||
file.sizeInBytes = size
|
||||
|
||||
return file as File
|
||||
}
|
||||
|
||||
kill(file, index=0) {
|
||||
kill(file, index = 0) {
|
||||
let strong
|
||||
if (file) {
|
||||
if (this.isDead || this.files[index].isDead) { return }
|
||||
@ -341,7 +341,7 @@ export default class Post {
|
||||
|
||||
if (!(strong = $('strong.warning', this.nodes.info))) {
|
||||
strong = $.el('strong',
|
||||
{className: 'warning'})
|
||||
{ className: 'warning' })
|
||||
$.after($('input', this.nodes.info), strong)
|
||||
}
|
||||
strong.textContent = file ? '[File deleted]' : '[Deleted]'
|
||||
|
||||
@ -4,6 +4,9 @@
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
|
||||
*/
|
||||
export default class RandomAccessList {
|
||||
length: number
|
||||
last: any
|
||||
first: any
|
||||
constructor(items) {
|
||||
this.length = 0
|
||||
if (items) { for (const item of items) { this.push(item) } }
|
||||
@ -11,10 +14,10 @@ export default class RandomAccessList {
|
||||
|
||||
push(data) {
|
||||
let item
|
||||
let {ID} = data
|
||||
let { ID } = data
|
||||
if (!ID) { ID = data.id }
|
||||
if (this[ID]) { return }
|
||||
const {last} = this
|
||||
const { last } = this
|
||||
this[ID] = (item = {
|
||||
prev: last,
|
||||
next: null,
|
||||
@ -24,7 +27,7 @@ export default class RandomAccessList {
|
||||
item.prev = last
|
||||
this.last = last ?
|
||||
(last.next = item)
|
||||
:
|
||||
:
|
||||
(this.first = item)
|
||||
return this.length++
|
||||
}
|
||||
@ -34,7 +37,7 @@ export default class RandomAccessList {
|
||||
|
||||
this.rmi(item)
|
||||
|
||||
const {prev} = root
|
||||
const { prev } = root
|
||||
root.prev = item
|
||||
item.next = root
|
||||
item.prev = prev
|
||||
@ -50,7 +53,7 @@ export default class RandomAccessList {
|
||||
|
||||
this.rmi(item)
|
||||
|
||||
const {next} = root
|
||||
const { next } = root
|
||||
root.next = item
|
||||
item.prev = root
|
||||
item.next = next
|
||||
@ -62,10 +65,10 @@ export default class RandomAccessList {
|
||||
}
|
||||
|
||||
prepend(item) {
|
||||
const {first} = this
|
||||
const { first } = this
|
||||
if ((item === first) || !this[item.ID]) { return }
|
||||
this.rmi(item)
|
||||
item.next = first
|
||||
item.next = first
|
||||
if (first) {
|
||||
first.prev = item
|
||||
} else {
|
||||
@ -97,7 +100,7 @@ export default class RandomAccessList {
|
||||
}
|
||||
|
||||
rmi(item) {
|
||||
const {prev, next} = item
|
||||
const { prev, next } = item
|
||||
if (prev) {
|
||||
prev.next = next
|
||||
} else {
|
||||
|
||||
@ -4,6 +4,8 @@
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
|
||||
*/
|
||||
class ShimSet {
|
||||
elements: any
|
||||
size: number
|
||||
constructor() {
|
||||
this.elements = $.dict()
|
||||
this.size = 0
|
||||
|
||||
@ -1,11 +1,5 @@
|
||||
import $ from "../platform/$"
|
||||
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS101: Remove unnecessary use of Array.from
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
|
||||
*/
|
||||
export default class SimpleDict<T> {
|
||||
keys: string[]
|
||||
|
||||
@ -13,14 +7,14 @@ export default class SimpleDict<T> {
|
||||
this.keys = []
|
||||
}
|
||||
|
||||
push(key, data: T) {
|
||||
push(key: string, data: T): T {
|
||||
key = `${key}`
|
||||
if (!this[key]) { this.keys.push(key) }
|
||||
return this[key] = data
|
||||
}
|
||||
|
||||
rm(key) {
|
||||
let i
|
||||
rm(key: string) {
|
||||
let i: number
|
||||
key = `${key}`
|
||||
if ((i = this.keys.indexOf(key)) !== -1) {
|
||||
this.keys.splice(i, 1)
|
||||
@ -28,11 +22,11 @@ export default class SimpleDict<T> {
|
||||
}
|
||||
}
|
||||
|
||||
forEach(fn) {
|
||||
forEach(fn: (value: T) => void): void {
|
||||
for (const key of [...Array.from(this.keys)]) { fn(this[key]) }
|
||||
}
|
||||
|
||||
get(key): T {
|
||||
get(key: string): T {
|
||||
if (key === 'keys') {
|
||||
return undefined
|
||||
} else {
|
||||
|
||||
@ -1,74 +1,92 @@
|
||||
import { g } from "../globals/globals"
|
||||
import $ from "../platform/$"
|
||||
import Board from "./Board"
|
||||
import Post from "./Post"
|
||||
import SimpleDict from "./SimpleDict"
|
||||
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
|
||||
*/
|
||||
|
||||
export default class Thread {
|
||||
ID: number
|
||||
OP: Post
|
||||
isArchived: boolean
|
||||
isClosed: boolean
|
||||
lastPost: number
|
||||
posts: SimpleDict<Post>
|
||||
board: Board
|
||||
threadID: number
|
||||
boardID: number | string
|
||||
siteID: number
|
||||
fullID: string
|
||||
isDead: boolean
|
||||
isHidden: boolean
|
||||
isSticky: boolean
|
||||
postLimit: boolean
|
||||
fileLimit: boolean
|
||||
ipCount: number
|
||||
json: JSON
|
||||
catalogView: Node
|
||||
nodes: { root: Post }
|
||||
toString() { return this.ID }
|
||||
|
||||
constructor(ID, board) {
|
||||
constructor(ID: number | string, board: Board) {
|
||||
this.board = board
|
||||
this.ID = +ID
|
||||
this.threadID = this.ID
|
||||
this.boardID = this.board.ID
|
||||
this.siteID = g.SITE.ID
|
||||
this.fullID = `${this.board}.${this.ID}`
|
||||
this.posts = new SimpleDict()
|
||||
this.isDead = false
|
||||
this.isHidden = false
|
||||
this.isSticky = false
|
||||
this.isClosed = false
|
||||
this.ID = +ID
|
||||
this.threadID = this.ID
|
||||
this.boardID = this.board.ID
|
||||
this.siteID = g.SITE.ID
|
||||
this.fullID = `${this.board}.${this.ID}`
|
||||
this.posts = new SimpleDict()
|
||||
this.isDead = false
|
||||
this.isHidden = false
|
||||
this.isSticky = false
|
||||
this.isClosed = false
|
||||
this.isArchived = false
|
||||
this.postLimit = false
|
||||
this.fileLimit = false
|
||||
this.lastPost = 0
|
||||
this.ipCount = undefined
|
||||
this.json = null
|
||||
this.postLimit = false
|
||||
this.fileLimit = false
|
||||
this.lastPost = 0
|
||||
this.ipCount = undefined
|
||||
this.json = null
|
||||
|
||||
this.OP = null
|
||||
this.catalogView = null
|
||||
|
||||
this.nodes =
|
||||
{root: null}
|
||||
{ root: null }
|
||||
|
||||
this.board.threads.push(this.ID, this)
|
||||
g.threads.push(this.fullID, this)
|
||||
}
|
||||
|
||||
setPage(pageNum) {
|
||||
let icon
|
||||
const {info, reply} = this.OP.nodes
|
||||
setPage(pageNum: number) {
|
||||
let icon: HTMLElement
|
||||
const { info, reply } = this.OP.nodes
|
||||
if (!(icon = $('.page-num', info))) {
|
||||
icon = $.el('span', {className: 'page-num'})
|
||||
icon = $.el('span', { className: 'page-num' })
|
||||
$.replace(reply.parentNode.previousSibling, [$.tn(' '), icon, $.tn(' ')])
|
||||
}
|
||||
icon.title = `This thread is on page ${pageNum} in the original index.`
|
||||
icon.title = `This thread is on page ${pageNum} in the original index.`
|
||||
icon.textContent = `[${pageNum}]`
|
||||
if (this.catalogView) { return this.catalogView.nodes.pageCount.textContent = pageNum }
|
||||
}
|
||||
|
||||
setCount(type, count, reachedLimit) {
|
||||
setCount(type: string, count: number, reachedLimit: boolean) {
|
||||
if (!this.catalogView) { return }
|
||||
const el = this.catalogView.nodes[`${type}Count`]
|
||||
el.textContent = count
|
||||
return (reachedLimit ? $.addClass : $.rmClass)(el, 'warning')
|
||||
}
|
||||
|
||||
setStatus(type, status) {
|
||||
setStatus(type: string, status: boolean) {
|
||||
const name = `is${type}`
|
||||
if (this[name] === status) { return }
|
||||
this[name] = status
|
||||
if (!this.OP) { return }
|
||||
this.setIcon('Sticky', this.isSticky)
|
||||
this.setIcon('Closed', this.isClosed && !this.isArchived)
|
||||
this.setIcon('Sticky', this.isSticky)
|
||||
this.setIcon('Closed', this.isClosed && !this.isArchived)
|
||||
return this.setIcon('Archived', this.isArchived)
|
||||
}
|
||||
|
||||
setIcon(type, status) {
|
||||
setIcon(type: string, status: boolean) {
|
||||
const typeLC = type.toLowerCase()
|
||||
let icon = $(`.${typeLC}Icon`, this.OP.nodes.info)
|
||||
if (!!icon === status) { return }
|
||||
@ -81,18 +99,17 @@ export default class Thread {
|
||||
}
|
||||
icon = $.el('img', {
|
||||
src: `${g.SITE.Build.staticPath}${typeLC}${g.SITE.Build.gifIcon}`,
|
||||
alt: type,
|
||||
alt: type,
|
||||
title: type,
|
||||
className: `${typeLC}Icon retina`
|
||||
}
|
||||
)
|
||||
}, g.BOARD.ID)
|
||||
if (g.BOARD.ID === 'f') {
|
||||
icon.style.cssText = 'height: 18px; width: 18px;'
|
||||
}
|
||||
|
||||
const root = (type !== 'Sticky') && this.isSticky ?
|
||||
$('.stickyIcon', this.OP.nodes.info)
|
||||
:
|
||||
:
|
||||
$('.page-num', this.OP.nodes.info) || this.OP.nodes.quote
|
||||
$.after(root, [$.tn(' '), icon])
|
||||
|
||||
@ -106,7 +123,7 @@ export default class Thread {
|
||||
|
||||
collect() {
|
||||
let n = 0
|
||||
this.posts.forEach(function(post) {
|
||||
this.posts.forEach(function (post) {
|
||||
if (post.clones.length) {
|
||||
return n++
|
||||
} else {
|
||||
|
||||
@ -18,10 +18,10 @@ const $ = (selector, root = document.body) => root.querySelector(selector)
|
||||
|
||||
$.id = id => d.getElementById(id)
|
||||
|
||||
$.ajaxPage = function(url, options) {
|
||||
$.ajaxPage = function (url, options) {
|
||||
if (options.responseType == null) { options.responseType = 'json' }
|
||||
if (!options.type) { options.type = (options.form && 'post') || 'get' }
|
||||
const {onloadend, timeout, responseType, withCredentials, type, onprogress, form, headers} = options
|
||||
const { onloadend, timeout, responseType, withCredentials, type, onprogress, form, headers } = options
|
||||
const r = new XMLHttpRequest()
|
||||
r.open(type, url, true)
|
||||
const object = headers || {}
|
||||
@ -29,26 +29,26 @@ $.ajaxPage = function(url, options) {
|
||||
const value = object[key]
|
||||
r.setRequestHeader(key, value)
|
||||
}
|
||||
$.extend(r, {onloadend, timeout, responseType, withCredentials})
|
||||
$.extend(r.upload, {onprogress})
|
||||
$.extend(r, { onloadend, timeout, responseType, withCredentials })
|
||||
$.extend(r.upload, { onprogress })
|
||||
// connection error or content blocker
|
||||
$.on(r, 'error', function() { if (!r.status) { return c.warn(`4chan X failed to load: ${url}`) } })
|
||||
$.on(r, 'error', function () { if (!r.status) { return c.warn(`4chan X failed to load: ${url}`) } })
|
||||
r.send(form)
|
||||
return r
|
||||
}
|
||||
$.ready = function(fc) {
|
||||
$.ready = function (fc) {
|
||||
if (d.readyState !== 'loading') {
|
||||
$.queueTask(fc)
|
||||
return
|
||||
}
|
||||
const cb = function() {
|
||||
const cb = function () {
|
||||
$.off(d, 'DOMContentLoaded', cb)
|
||||
return fc()
|
||||
}
|
||||
return $.on(d, 'DOMContentLoaded', cb)
|
||||
}
|
||||
|
||||
$.formData = function(form) {
|
||||
$.formData = function (form) {
|
||||
if (form instanceof HTMLFormElement) {
|
||||
return new FormData(form)
|
||||
}
|
||||
@ -66,7 +66,7 @@ $.formData = function(form) {
|
||||
return fd
|
||||
}
|
||||
|
||||
$.extend = function(object, properties) {
|
||||
$.extend = function (object, properties) {
|
||||
for (const key in properties) {
|
||||
const val = properties[key]
|
||||
object[key] = val
|
||||
@ -75,11 +75,11 @@ $.extend = function(object, properties) {
|
||||
|
||||
$.hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key)
|
||||
|
||||
$.getOwn = function(obj, key) {
|
||||
$.getOwn = function (obj, key) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key)) { return obj[key] } else { return undefined }
|
||||
}
|
||||
|
||||
$.ajax = (function() {
|
||||
$.ajax = (function () {
|
||||
let pageXHR
|
||||
// @ts-ignore
|
||||
if (window.wrappedJSObject && !XMLHttpRequest.wrappedJSObject) {
|
||||
@ -88,7 +88,7 @@ $.ajax = (function() {
|
||||
pageXHR = XMLHttpRequest
|
||||
}
|
||||
|
||||
const r = (function (url, options={}) {
|
||||
const r = (function (url, options = {}) {
|
||||
if (options.responseType == null) { options.responseType = 'json' }
|
||||
if (!options.type) { options.type = (options.form && 'post') || 'get' }
|
||||
// XXX https://forums.lanik.us/viewtopic.php?f=64&t=24173&p=78310
|
||||
@ -99,7 +99,7 @@ $.ajax = (function() {
|
||||
return $.ajaxPage(url, options)
|
||||
}
|
||||
}
|
||||
const {onloadend, timeout, responseType, withCredentials, type, onprogress, form, headers} = options
|
||||
const { onloadend, timeout, responseType, withCredentials, type, onprogress, form, headers } = options
|
||||
const r = new pageXHR()
|
||||
try {
|
||||
r.open(type, url, true)
|
||||
@ -108,10 +108,10 @@ $.ajax = (function() {
|
||||
const value = object[key]
|
||||
r.setRequestHeader(key, value)
|
||||
}
|
||||
$.extend(r, {onloadend, timeout, responseType, withCredentials})
|
||||
$.extend(r.upload, {onprogress})
|
||||
$.extend(r, { onloadend, timeout, responseType, withCredentials })
|
||||
$.extend(r.upload, { onprogress })
|
||||
// connection error or content blocker
|
||||
$.on(r, 'error', function() { if (!r.status) { return c.warn(`4chan X failed to load: ${url}`) } })
|
||||
$.on(r, 'error', function () { if (!r.status) { return c.warn(`4chan X failed to load: ${url}`) } })
|
||||
if (platform === 'crx') {
|
||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=920638
|
||||
$.on(r, 'load', () => {
|
||||
@ -125,7 +125,7 @@ $.ajax = (function() {
|
||||
// XXX Some content blockers in Firefox (e.g. Adblock Plus and NoScript) throw an exception instead of simulating a connection error.
|
||||
if (err.result !== 0x805e0006) { throw err }
|
||||
r.onloadend = onloadend
|
||||
$.queueTask($.event, 'error', null, r)
|
||||
$.queueTask($.event, 'error', null, r)
|
||||
$.queueTask($.event, 'loadend', null, r)
|
||||
}
|
||||
return r
|
||||
@ -138,13 +138,13 @@ $.ajax = (function() {
|
||||
let requestID = 0
|
||||
const requests = dict()
|
||||
|
||||
$.ajaxPageInit = function() {
|
||||
$.global(function() {
|
||||
$.ajaxPageInit = function () {
|
||||
$.global(function () {
|
||||
window.FCX.requests = Object.create(null)
|
||||
|
||||
document.addEventListener('4chanXAjax', function(e) {
|
||||
document.addEventListener('4chanXAjax', function (e) {
|
||||
let fd, r
|
||||
const {url, timeout, responseType, withCredentials, type, onprogress, form, headers, id} = e.detail
|
||||
const { url, timeout, responseType, withCredentials, type, onprogress, form, headers, id } = e.detail
|
||||
window.FCX.requests[id] = (r = new XMLHttpRequest())
|
||||
r.open(type, url, true)
|
||||
const object = headers || {}
|
||||
@ -156,21 +156,21 @@ $.ajax = (function() {
|
||||
r.timeout = timeout
|
||||
r.withCredentials = withCredentials
|
||||
if (onprogress) {
|
||||
r.upload.onprogress = function(e) {
|
||||
const {loaded, total} = e
|
||||
const detail = {loaded, total, id}
|
||||
return document.dispatchEvent(new CustomEvent('4chanXAjaxProgress', {bubbles: true, detail}))
|
||||
r.upload.onprogress = function (e) {
|
||||
const { loaded, total } = e
|
||||
const detail = { loaded, total, id }
|
||||
return document.dispatchEvent(new CustomEvent('4chanXAjaxProgress', { bubbles: true, detail }))
|
||||
}
|
||||
}
|
||||
r.onloadend = function() {
|
||||
r.onloadend = function () {
|
||||
delete window.FCX.requests[id]
|
||||
const {status, statusText, response} = this
|
||||
const { status, statusText, response } = this
|
||||
const responseHeaderString = this.getAllResponseHeaders()
|
||||
const detail = {status, statusText, response, responseHeaderString, id}
|
||||
return document.dispatchEvent(new CustomEvent('4chanXAjaxLoadend', {bubbles: true, detail}))
|
||||
const detail = { status, statusText, response, responseHeaderString, id }
|
||||
return document.dispatchEvent(new CustomEvent('4chanXAjaxLoadend', { bubbles: true, detail }))
|
||||
}
|
||||
// connection error or content blocker
|
||||
r.onerror = function() {
|
||||
r.onerror = function () {
|
||||
if (!r.status) { return console.warn(`4chan X failed to load: ${url}`) }
|
||||
}
|
||||
if (form) {
|
||||
@ -182,50 +182,50 @@ $.ajax = (function() {
|
||||
fd = null
|
||||
}
|
||||
return r.send(fd)
|
||||
}
|
||||
, false)
|
||||
|
||||
return document.addEventListener('4chanXAjaxAbort', function(e) {
|
||||
let r
|
||||
if (!(r = window.FCX.requests[e.detail.id])) { return }
|
||||
return r.abort()
|
||||
}
|
||||
, false)
|
||||
})
|
||||
|
||||
$.on(d, '4chanXAjaxProgress', function(e) {
|
||||
let req
|
||||
if (!(req = requests[e.detail.id])) { return }
|
||||
return req.upload.onprogress.call(req.upload, e.detail)
|
||||
})
|
||||
|
||||
return $.on(d, '4chanXAjaxLoadend', function(e) {
|
||||
let req
|
||||
if (!(req = requests[e.detail.id])) { return }
|
||||
delete requests[e.detail.id]
|
||||
if (e.detail.status) {
|
||||
for (const key of ['status', 'statusText', 'response', 'responseHeaderString']) {
|
||||
req[key] = e.detail[key]
|
||||
}
|
||||
if (req.responseType === 'document') {
|
||||
req.response = new DOMParser().parseFromString(e.detail.response, 'text/html')
|
||||
}
|
||||
}
|
||||
return req.onloadend()
|
||||
})
|
||||
}
|
||||
, false)
|
||||
|
||||
return $.ajaxPage = function(url, options={}) {
|
||||
return document.addEventListener('4chanXAjaxAbort', function (e) {
|
||||
let r
|
||||
if (!(r = window.FCX.requests[e.detail.id])) { return }
|
||||
return r.abort()
|
||||
}
|
||||
, false)
|
||||
})
|
||||
|
||||
$.on(d, '4chanXAjaxProgress', function (e) {
|
||||
let req
|
||||
if (!(req = requests[e.detail.id])) { return }
|
||||
return req.upload.onprogress.call(req.upload, e.detail)
|
||||
})
|
||||
|
||||
return $.on(d, '4chanXAjaxLoadend', function (e) {
|
||||
let req
|
||||
if (!(req = requests[e.detail.id])) { return }
|
||||
delete requests[e.detail.id]
|
||||
if (e.detail.status) {
|
||||
for (const key of ['status', 'statusText', 'response', 'responseHeaderString']) {
|
||||
req[key] = e.detail[key]
|
||||
}
|
||||
if (req.responseType === 'document') {
|
||||
req.response = new DOMParser().parseFromString(e.detail.response, 'text/html')
|
||||
}
|
||||
}
|
||||
return req.onloadend()
|
||||
})
|
||||
}
|
||||
|
||||
return $.ajaxPage = function (url, options = {}) {
|
||||
let req
|
||||
let {onloadend, timeout, responseType, withCredentials, type, onprogress, form, headers} = options
|
||||
const id = requestID++
|
||||
requests[id] = (req = new CrossOrigin.Request())
|
||||
$.extend(req, {responseType, onloadend})
|
||||
req.upload = {onprogress}
|
||||
req.abort = () => $.event('4chanXAjaxAbort', {id})
|
||||
if (form) { form = Array.from(form.entries()) }
|
||||
$.event('4chanXAjax', {url, timeout, responseType, withCredentials, type, onprogress: !!onprogress, form, headers, id})
|
||||
return req
|
||||
let { onloadend, timeout, responseType, withCredentials, type, onprogress, form, headers } = options
|
||||
const id = requestID++
|
||||
requests[id] = (req = new CrossOrigin.Request())
|
||||
$.extend(req, { responseType, onloadend })
|
||||
req.upload = { onprogress }
|
||||
req.abort = () => $.event('4chanXAjaxAbort', { id })
|
||||
if (form) { form = Array.from(form.entries()) }
|
||||
$.event('4chanXAjax', { url, timeout, responseType, withCredentials, type, onprogress: !!onprogress, form, headers, id })
|
||||
return req
|
||||
}
|
||||
}
|
||||
})()
|
||||
@ -234,9 +234,9 @@ $.ajax = (function() {
|
||||
// With the `If-Modified-Since` header we only receive the HTTP headers and no body for 304 responses.
|
||||
// This saves a lot of bandwidth and CPU time for both the users and the servers.
|
||||
$.lastModified = dict()
|
||||
$.whenModified = function(url, bucket, cb, options={}) {
|
||||
$.whenModified = function (url, bucket, cb, options = {}) {
|
||||
let t
|
||||
const {timeout, ajax} = options
|
||||
const { timeout, ajax } = options
|
||||
const params = []
|
||||
// XXX https://bugs.chromium.org/p/chromium/issues/detail?id=643659
|
||||
if ($.engine === 'blink') { params.push(`s=${bucket}`) }
|
||||
@ -258,33 +258,33 @@ $.whenModified = function(url, bucket, cb, options={}) {
|
||||
return r
|
||||
};
|
||||
|
||||
(function() {
|
||||
(function () {
|
||||
const reqs = dict()
|
||||
$.cache = function(url, cb, options={}) {
|
||||
$.cache = function (url, cb, options = {}) {
|
||||
let req
|
||||
const {ajax} = options
|
||||
const { ajax } = options
|
||||
if (req = reqs[url]) {
|
||||
if (req.callbacks) {
|
||||
req.callbacks.push(cb)
|
||||
} else {
|
||||
$.queueTask(() => cb.call(req, {isCached: true}))
|
||||
$.queueTask(() => cb.call(req, { isCached: true }))
|
||||
}
|
||||
return req
|
||||
}
|
||||
const onloadend = function() {
|
||||
const onloadend = function () {
|
||||
if (!this.status) {
|
||||
delete reqs[url]
|
||||
}
|
||||
for (cb of this.callbacks) {
|
||||
(cb => $.queueTask(() => cb.call(this, {isCached: false})))(cb)
|
||||
(cb => $.queueTask(() => cb.call(this, { isCached: false })))(cb)
|
||||
}
|
||||
return delete this.callbacks
|
||||
}
|
||||
req = (ajax || $.ajax)(url, {onloadend})
|
||||
req = (ajax || $.ajax)(url, { onloadend })
|
||||
req.callbacks = [cb]
|
||||
return reqs[url] = req
|
||||
}
|
||||
return $.cleanCache = function(testf) {
|
||||
return $.cleanCache = function (testf) {
|
||||
for (const url in reqs) {
|
||||
if (testf(url)) {
|
||||
delete reqs[url]
|
||||
@ -308,7 +308,7 @@ $.cb = {
|
||||
}
|
||||
}
|
||||
|
||||
$.asap = function(test, cb) {
|
||||
$.asap = function (test, cb) {
|
||||
if (test()) {
|
||||
return cb()
|
||||
} else {
|
||||
@ -316,32 +316,32 @@ $.asap = function(test, cb) {
|
||||
}
|
||||
}
|
||||
|
||||
$.onExists = function(root, selector, cb) {
|
||||
$.onExists = function (root, selector, cb) {
|
||||
let el
|
||||
if (el = $(selector, root)) {
|
||||
return cb(el)
|
||||
}
|
||||
var observer = new MutationObserver(function() {
|
||||
var observer = new MutationObserver(function () {
|
||||
if (el = $(selector, root)) {
|
||||
observer.disconnect()
|
||||
return cb(el)
|
||||
}
|
||||
})
|
||||
return observer.observe(root, {childList: true, subtree: true})
|
||||
return observer.observe(root, { childList: true, subtree: true })
|
||||
}
|
||||
|
||||
$.addStyle = function(css, id, test='head') {
|
||||
$.addStyle = function (css, id, test = 'head') {
|
||||
const style = $.el('style',
|
||||
{textContent: css})
|
||||
{ textContent: css })
|
||||
if (id != null) { style.id = id }
|
||||
$.onExists(doc, test, () => $.add(d.head, style))
|
||||
return style
|
||||
}
|
||||
|
||||
$.addCSP = function(policy) {
|
||||
$.addCSP = function (policy) {
|
||||
const meta = $.el('meta', {
|
||||
httpEquiv: 'Content-Security-Policy',
|
||||
content: policy
|
||||
content: policy
|
||||
}
|
||||
)
|
||||
if (d.head) {
|
||||
@ -354,23 +354,23 @@ $.addCSP = function(policy) {
|
||||
}
|
||||
}
|
||||
|
||||
$.x = function(path, root) {
|
||||
$.x = function (path, root) {
|
||||
if (!root) { root = d.body }
|
||||
// XPathResult.ANY_UNORDERED_NODE_TYPE === 8
|
||||
return d.evaluate(path, root, null, 8, null).singleNodeValue
|
||||
}
|
||||
|
||||
$.X = function(path, root) {
|
||||
$.X = function (path, root) {
|
||||
if (!root) { root = d.body }
|
||||
// XPathResult.ORDERED_NODE_SNAPSHOT_TYPE === 7
|
||||
return d.evaluate(path, root, null, 7, null)
|
||||
}
|
||||
|
||||
$.addClass = function(el, ...classNames) {
|
||||
$.addClass = function (el, ...classNames) {
|
||||
for (const className of classNames) { el.classList.add(className) }
|
||||
}
|
||||
|
||||
$.rmClass = function(el, ...classNames) {
|
||||
$.rmClass = function (el, ...classNames) {
|
||||
for (const className of classNames) { el.classList.remove(className) }
|
||||
}
|
||||
|
||||
@ -381,13 +381,13 @@ $.hasClass = (el, className) => el.classList.contains(className)
|
||||
$.rm = el => el?.remove()
|
||||
|
||||
$.rmAll = root => // https://gist.github.com/MayhemYDG/8646194
|
||||
root.textContent = null
|
||||
root.textContent = null
|
||||
|
||||
$.tn = s => d.createTextNode(s)
|
||||
|
||||
$.frag = () => d.createDocumentFragment()
|
||||
|
||||
$.nodes = function(nodes) {
|
||||
$.nodes = function (nodes) {
|
||||
if (!(nodes instanceof Array)) {
|
||||
return nodes
|
||||
}
|
||||
@ -408,55 +408,55 @@ $.before = (root, el) => root.parentNode.insertBefore($.nodes(el), root)
|
||||
|
||||
$.replace = (root, el) => root.parentNode.replaceChild($.nodes(el), root)
|
||||
|
||||
$.el = function(tag, properties, properties2) {
|
||||
$.el = function (tag, properties, properties2?) {
|
||||
const el = d.createElement(tag)
|
||||
if (properties) { $.extend(el, properties) }
|
||||
if (properties2) { $.extend(el, properties2) }
|
||||
return el
|
||||
}
|
||||
|
||||
$.on = function(el, events, handler) {
|
||||
$.on = function (el, events, handler) {
|
||||
for (const event of events.split(' ')) {
|
||||
el.addEventListener(event, handler, false)
|
||||
}
|
||||
}
|
||||
|
||||
$.off = function(el, events, handler) {
|
||||
$.off = function (el, events, handler) {
|
||||
for (const event of events.split(' ')) {
|
||||
el.removeEventListener(event, handler, false)
|
||||
}
|
||||
}
|
||||
|
||||
$.one = function(el, events, handler) {
|
||||
const cb = function(e) {
|
||||
$.one = function (el, events, handler) {
|
||||
const cb = function (e) {
|
||||
$.off(el, events, cb)
|
||||
return handler.call(this, e)
|
||||
}
|
||||
return $.on(el, events, cb)
|
||||
}
|
||||
|
||||
$.event = function(event, detail, root=d) {
|
||||
$.event = function (event, detail, root = d) {
|
||||
if (!globalThis.chrome?.extension) {
|
||||
if ((detail != null) && (typeof cloneInto === 'function')) {
|
||||
detail = cloneInto(detail, d.defaultView)
|
||||
}
|
||||
}
|
||||
return root.dispatchEvent(new CustomEvent(event, {bubbles: true, cancelable: true, detail}))
|
||||
return root.dispatchEvent(new CustomEvent(event, { bubbles: true, cancelable: true, detail }))
|
||||
}
|
||||
|
||||
if (platform === 'userscript') {
|
||||
// XXX Make $.event work in Pale Moon with GM 3.x (no cloneInto function).
|
||||
(function() {
|
||||
(function () {
|
||||
if (!/PaleMoon\//.test(navigator.userAgent) || (+GM_info?.version?.split('.')[0] < 2) || (typeof cloneInto !== 'undefined')) { return }
|
||||
|
||||
try {
|
||||
return new CustomEvent('x', {detail: {}})
|
||||
return new CustomEvent('x', { detail: {} })
|
||||
} catch (err) {
|
||||
const unsafeConstructors = {
|
||||
Object: unsafeWindow.Object,
|
||||
Array: unsafeWindow.Array
|
||||
Array: unsafeWindow.Array
|
||||
}
|
||||
const clone = function(obj) {
|
||||
const clone = function (obj) {
|
||||
let constructor
|
||||
if ((obj != null) && (typeof obj === 'object') && (constructor = unsafeConstructors[obj.constructor.name])) {
|
||||
const obj2 = new constructor()
|
||||
@ -466,36 +466,36 @@ if (platform === 'userscript') {
|
||||
return obj
|
||||
}
|
||||
}
|
||||
return $.event = (event, detail, root=d) => root.dispatchEvent(new CustomEvent(event, {bubbles: true, cancelable: true, detail: clone(detail)}))
|
||||
return $.event = (event, detail, root = d) => root.dispatchEvent(new CustomEvent(event, { bubbles: true, cancelable: true, detail: clone(detail) }))
|
||||
}
|
||||
})()
|
||||
}
|
||||
|
||||
$.modifiedClick = e => e.shiftKey || e.altKey || e.ctrlKey || e.metaKey || (e.button !== 0)
|
||||
|
||||
if (!globalThis.chrome?.extension) {
|
||||
$.open =
|
||||
if (!globalThis.chrome?.extension) {
|
||||
$.open =
|
||||
(GM?.openInTab != null) ?
|
||||
GM.openInTab
|
||||
: (typeof GM_openInTab !== 'undefined' && GM_openInTab !== null) ?
|
||||
GM_openInTab
|
||||
:
|
||||
: (typeof GM_openInTab !== 'undefined' && GM_openInTab !== null) ?
|
||||
GM_openInTab
|
||||
:
|
||||
url => window.open(url, '_blank')
|
||||
} else {
|
||||
$.open =
|
||||
url => window.open(url, '_blank')
|
||||
} else {
|
||||
$.open =
|
||||
url => window.open(url, '_blank')
|
||||
}
|
||||
}
|
||||
|
||||
$.debounce = function(wait, fn) {
|
||||
$.debounce = function (wait, fn) {
|
||||
let lastCall = 0
|
||||
let timeout = null
|
||||
let that = null
|
||||
let args = null
|
||||
const exec = function() {
|
||||
let timeout = null
|
||||
let that = null
|
||||
let args = null
|
||||
const exec = function () {
|
||||
lastCall = Date.now()
|
||||
return fn.apply(that, args)
|
||||
}
|
||||
return function() {
|
||||
return function () {
|
||||
args = arguments
|
||||
that = this
|
||||
if (lastCall < (Date.now() - wait)) {
|
||||
@ -508,10 +508,10 @@ $.debounce = function(wait, fn) {
|
||||
}
|
||||
}
|
||||
|
||||
$.queueTask = (function() {
|
||||
$.queueTask = (function () {
|
||||
// inspired by https://www.w3.org/Bugs/Public/show_bug.cgi?id=15007
|
||||
const taskQueue = []
|
||||
const execTask = function() {
|
||||
const execTask = function () {
|
||||
const task = taskQueue.shift()
|
||||
const func = task[0]
|
||||
const args = Array.prototype.slice.call(task, 1)
|
||||
@ -520,22 +520,22 @@ $.queueTask = (function() {
|
||||
if (window.MessageChannel) {
|
||||
const taskChannel = new MessageChannel()
|
||||
taskChannel.port1.onmessage = execTask
|
||||
return function() {
|
||||
return function () {
|
||||
taskQueue.push(arguments)
|
||||
return taskChannel.port2.postMessage(null)
|
||||
}
|
||||
} else { // XXX Firefox
|
||||
return function() {
|
||||
return function () {
|
||||
taskQueue.push(arguments)
|
||||
return setTimeout(execTask, 0)
|
||||
}
|
||||
}
|
||||
})()
|
||||
|
||||
$.global = function(fn, data) {
|
||||
$.global = function (fn, data) {
|
||||
if (doc) {
|
||||
const script = $.el('script',
|
||||
{textContent: `(${fn}).call(document.currentScript.dataset);`})
|
||||
{ textContent: `(${fn}).call(document.currentScript.dataset);` })
|
||||
if (data) { $.extend(script.dataset, data) }
|
||||
$.add((d.head || doc), script)
|
||||
$.rm(script)
|
||||
@ -544,12 +544,12 @@ $.global = function(fn, data) {
|
||||
// XXX dwb
|
||||
try {
|
||||
fn.call(data)
|
||||
} catch (error) {}
|
||||
} catch (error) { }
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
$.bytesToString = function(size) {
|
||||
$.bytesToString = function (size) {
|
||||
let unit = 0 // Bytes
|
||||
while (size >= 1024) {
|
||||
size /= 1024
|
||||
@ -561,7 +561,7 @@ $.bytesToString = function(size) {
|
||||
// Keep the size as a float if the size is greater than 2^20 B.
|
||||
// Round to hundredth.
|
||||
Math.round(size * 100) / 100
|
||||
:
|
||||
:
|
||||
// Round to an integer otherwise.
|
||||
Math.round(size)
|
||||
return `${size} ${['B', 'KB', 'MB', 'GB'][unit]}`
|
||||
@ -569,32 +569,32 @@ $.bytesToString = function(size) {
|
||||
|
||||
$.minmax = (value, min, max) => value < min ?
|
||||
min
|
||||
:
|
||||
:
|
||||
value > max ?
|
||||
max
|
||||
:
|
||||
:
|
||||
value
|
||||
|
||||
$.hasAudio = video => video.mozHasAudio || !!video.webkitAudioDecodedByteCount
|
||||
|
||||
$.luma = rgb => (rgb[0] * 0.299) + (rgb[1] * 0.587) + (rgb[2] * 0.114)
|
||||
|
||||
$.unescape = function(text) {
|
||||
$.unescape = function (text) {
|
||||
if (text == null) { return text }
|
||||
return text.replace(/<[^>]*>/g, '').replace(/&(amp|#039|quot|lt|gt|#44);/g, c => ({'&': '&', ''': "'", '"': '"', '<': '<', '>': '>', ',': ','})[c])
|
||||
return text.replace(/<[^>]*>/g, '').replace(/&(amp|#039|quot|lt|gt|#44);/g, c => ({ '&': '&', ''': "'", '"': '"', '<': '<', '>': '>', ',': ',' })[c])
|
||||
}
|
||||
|
||||
$.isImage = url => /\.(jpe?g|jfif|png|gif|bmp|webp|avif|jxl)$/i.test(url)
|
||||
$.isVideo = url => /\.(webm|mp4|ogv)$/i.test(url)
|
||||
|
||||
$.engine = (function() {
|
||||
$.engine = (function () {
|
||||
if (/Edge\//.test(navigator.userAgent)) { return 'edge' }
|
||||
if (/Chrome\//.test(navigator.userAgent)) { return 'blink' }
|
||||
if (/WebKit\//.test(navigator.userAgent)) { return 'webkit' }
|
||||
if (/Gecko\/|Goanna/.test(navigator.userAgent)) { return 'gecko' } // Goanna = Pale Moon 26+
|
||||
})()
|
||||
|
||||
$.hasStorage = (function() {
|
||||
$.hasStorage = (function () {
|
||||
try {
|
||||
if (localStorage.getItem(g.NAMESPACE + 'hasStorage') === 'true') { return true }
|
||||
localStorage.setItem(g.NAMESPACE + 'hasStorage', 'true')
|
||||
@ -604,13 +604,13 @@ $.hasStorage = (function() {
|
||||
}
|
||||
})()
|
||||
|
||||
$.item = function(key, val) {
|
||||
$.item = function (key, val) {
|
||||
const item = dict()
|
||||
item[key] = val
|
||||
return item
|
||||
}
|
||||
|
||||
$.oneItemSugar = fn => (function(key, val, cb) {
|
||||
$.oneItemSugar = fn => (function (key, val, cb) {
|
||||
if (typeof key === 'string') {
|
||||
return fn($.item(key, val), cb)
|
||||
} else {
|
||||
@ -620,7 +620,7 @@ $.oneItemSugar = fn => (function(key, val, cb) {
|
||||
|
||||
$.syncing = dict()
|
||||
|
||||
$.securityCheck = function(data) {
|
||||
$.securityCheck = function (data) {
|
||||
if (location.protocol !== 'https:') {
|
||||
return delete data['Redirect to HTTPS']
|
||||
}
|
||||
@ -630,10 +630,10 @@ if (platform === 'crx') {
|
||||
// https://developer.chrome.com/extensions/storage.html
|
||||
$.oldValue = {
|
||||
local: dict(),
|
||||
sync: dict()
|
||||
sync: dict()
|
||||
}
|
||||
|
||||
chrome.storage.onChanged.addListener(function(changes, area) {
|
||||
chrome.storage.onChanged.addListener(function (changes, area) {
|
||||
for (const key in changes) {
|
||||
const oldValue = $.oldValue.local[key] ?? $.oldValue.sync[key]
|
||||
$.oldValue[area][key] = dict.clone(changes[key].newValue)
|
||||
@ -645,17 +645,17 @@ if (platform === 'crx') {
|
||||
}
|
||||
})
|
||||
$.sync = (key, cb) => $.syncing[key] = cb
|
||||
$.forceSync = function() { }
|
||||
$.forceSync = function () { }
|
||||
|
||||
$.crxWorking = function() {
|
||||
$.crxWorking = function () {
|
||||
try {
|
||||
if (chrome.runtime.getManifest()) {
|
||||
return true
|
||||
}
|
||||
} catch (error) {}
|
||||
} catch (error) { }
|
||||
if (!$.crxWarningShown) {
|
||||
const msg = $.el('div',
|
||||
{innerHTML: '4chan X seems to have been updated. You will need to <a href="javascript:;">reload</a> the page.'})
|
||||
{ innerHTML: '4chan X seems to have been updated. You will need to <a href="javascript:;">reload</a> the page.' })
|
||||
$.on($('a', msg), 'click', () => location.reload())
|
||||
new Notice('warning', msg)
|
||||
$.crxWarningShown = true
|
||||
@ -663,16 +663,16 @@ if (platform === 'crx') {
|
||||
return false
|
||||
}
|
||||
|
||||
$.get = $.oneItemSugar(function(data, cb) {
|
||||
$.get = $.oneItemSugar(function (data, cb) {
|
||||
if (!$.crxWorking()) { return }
|
||||
const results = {}
|
||||
const get = function(area) {
|
||||
const get = function (area) {
|
||||
let keys = Object.keys(data)
|
||||
// XXX slow performance in Firefox
|
||||
if (($.engine === 'gecko') && (area === 'sync') && (keys.length > 3)) {
|
||||
keys = null
|
||||
}
|
||||
return chrome.storage[area].get(keys, function(result) {
|
||||
return chrome.storage[area].get(keys, function (result) {
|
||||
let key
|
||||
result = dict.clone(result)
|
||||
if (chrome.runtime.lastError) {
|
||||
@ -698,16 +698,16 @@ if (platform === 'crx') {
|
||||
return get('sync')
|
||||
});
|
||||
|
||||
(function() {
|
||||
(function () {
|
||||
const items = {
|
||||
local: dict(),
|
||||
sync: dict()
|
||||
sync: dict()
|
||||
}
|
||||
|
||||
const exceedsQuota = (key, value) => // bytes in UTF-8
|
||||
unescape(encodeURIComponent(JSON.stringify(key))).length + unescape(encodeURIComponent(JSON.stringify(value))).length > chrome.storage.sync.QUOTA_BYTES_PER_ITEM
|
||||
unescape(encodeURIComponent(JSON.stringify(key))).length + unescape(encodeURIComponent(JSON.stringify(value))).length > chrome.storage.sync.QUOTA_BYTES_PER_ITEM
|
||||
|
||||
$.delete = function(keys) {
|
||||
$.delete = function (keys) {
|
||||
if (!$.crxWorking()) { return }
|
||||
if (typeof keys === 'string') {
|
||||
keys = [keys]
|
||||
@ -721,11 +721,11 @@ if (platform === 'crx') {
|
||||
}
|
||||
|
||||
const timeout = {}
|
||||
const setArea = function(area, cb) {
|
||||
const setArea = function (area, cb) {
|
||||
const data = dict()
|
||||
$.extend(data, items[area])
|
||||
if (!Object.keys(data).length || (timeout[area] > Date.now())) { return }
|
||||
return chrome.storage[area].set(data, function() {
|
||||
return chrome.storage[area].set(data, function () {
|
||||
let err
|
||||
let key
|
||||
if (err = chrome.runtime.lastError) {
|
||||
@ -757,20 +757,20 @@ if (platform === 'crx') {
|
||||
|
||||
var setSync = debounce(SECOND, () => setArea('sync'))
|
||||
|
||||
$.set = $.oneItemSugar(function(data, cb) {
|
||||
$.set = $.oneItemSugar(function (data, cb) {
|
||||
if (!$.crxWorking()) { return }
|
||||
$.securityCheck(data)
|
||||
$.extend(items.local, data)
|
||||
return setArea('local', cb)
|
||||
})
|
||||
|
||||
return $.clear = function(cb) {
|
||||
return $.clear = function (cb) {
|
||||
if (!$.crxWorking()) { return }
|
||||
items.local = dict()
|
||||
items.sync = dict()
|
||||
items.sync = dict()
|
||||
let count = 2
|
||||
let err = null
|
||||
const done = function() {
|
||||
let err = null
|
||||
const done = function () {
|
||||
if (chrome.runtime.lastError) {
|
||||
c.error(chrome.runtime.lastError.message)
|
||||
}
|
||||
@ -804,19 +804,20 @@ if (platform === 'crx') {
|
||||
|
||||
$.sync = (key, cb) => $.syncing[key] = cb
|
||||
|
||||
$.forceSync = function() {}
|
||||
$.forceSync = function () { }
|
||||
|
||||
$.delete = function(keys, cb) {
|
||||
$.delete = function (keys, cb) {
|
||||
let key
|
||||
if (!(keys instanceof Array)) {
|
||||
keys = [keys]
|
||||
}
|
||||
return Promise.all((() => {
|
||||
const result = []
|
||||
for (key of keys) { result.push(GM.deleteValue(g.NAMESPACE + key))
|
||||
for (key of keys) {
|
||||
result.push(GM.deleteValue(g.NAMESPACE + key))
|
||||
}
|
||||
return result
|
||||
})()).then(function() {
|
||||
})()).then(function () {
|
||||
const items = dict()
|
||||
for (key of keys) { items[key] = undefined }
|
||||
$.syncChannel.postMessage(items)
|
||||
@ -824,9 +825,9 @@ if (platform === 'crx') {
|
||||
})
|
||||
}
|
||||
|
||||
$.get = $.oneItemSugar(function(items, cb) {
|
||||
$.get = $.oneItemSugar(function (items, cb) {
|
||||
const keys = Object.keys(items)
|
||||
return Promise.all(keys.map((key) => GM.getValue(g.NAMESPACE + key))).then(function(values) {
|
||||
return Promise.all(keys.map((key) => GM.getValue(g.NAMESPACE + key))).then(function (values) {
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
const val = values[i]
|
||||
if (val) {
|
||||
@ -837,7 +838,7 @@ if (platform === 'crx') {
|
||||
})
|
||||
})
|
||||
|
||||
$.set = $.oneItemSugar(function(items, cb) {
|
||||
$.set = $.oneItemSugar(function (items, cb) {
|
||||
$.securityCheck(items)
|
||||
return Promise.all((() => {
|
||||
const result = []
|
||||
@ -846,13 +847,13 @@ if (platform === 'crx') {
|
||||
result.push(GM.setValue(g.NAMESPACE + key, JSON.stringify(val)))
|
||||
}
|
||||
return result
|
||||
})()).then(function() {
|
||||
})()).then(function () {
|
||||
$.syncChannel.postMessage(items)
|
||||
return cb?.()
|
||||
})
|
||||
})
|
||||
|
||||
$.clear = cb => GM.listValues().then(keys => $.delete(keys.map(key => key.replace(g.NAMESPACE, '')), cb)).catch( () => $.delete(Object.keys(Conf).concat(['previousversion', 'QR Size', 'QR.persona']), cb))
|
||||
$.clear = cb => GM.listValues().then(keys => $.delete(keys.map(key => key.replace(g.NAMESPACE, '')), cb)).catch(() => $.delete(Object.keys(Conf).concat(['previousversion', 'QR Size', 'QR.persona']), cb))
|
||||
} else {
|
||||
|
||||
if (typeof GM_deleteValue === 'undefined' || GM_deleteValue === null) {
|
||||
@ -860,7 +861,7 @@ if (platform === 'crx') {
|
||||
}
|
||||
|
||||
if (typeof GM_deleteValue !== 'undefined' && GM_deleteValue !== null) {
|
||||
$.getValue = GM_getValue
|
||||
$.getValue = GM_getValue
|
||||
$.listValues = () => GM_listValues() // error when called if missing
|
||||
} else if ($.hasStorage) {
|
||||
$.getValue = key => localStorage.getItem(key)
|
||||
@ -874,23 +875,23 @@ if (platform === 'crx') {
|
||||
return result
|
||||
})()
|
||||
} else {
|
||||
$.getValue = function() {}
|
||||
$.getValue = function () { }
|
||||
$.listValues = () => []
|
||||
}
|
||||
|
||||
if (typeof GM_addValueChangeListener !== 'undefined' && GM_addValueChangeListener !== null) {
|
||||
$.setValue = GM_setValue
|
||||
$.setValue = GM_setValue
|
||||
$.deleteValue = GM_deleteValue
|
||||
} else if (typeof GM_deleteValue !== 'undefined' && GM_deleteValue !== null) {
|
||||
$.oldValue = dict()
|
||||
$.setValue = function(key, val) {
|
||||
$.setValue = function (key, val) {
|
||||
GM_setValue(key, val)
|
||||
if (key in $.syncing) {
|
||||
$.oldValue[key] = val
|
||||
$.oldValue[key] = val
|
||||
if ($.hasStorage) { return localStorage.setItem(key, val) } // for `storage` events
|
||||
}
|
||||
}
|
||||
$.deleteValue = function(key) {
|
||||
$.deleteValue = function (key) {
|
||||
GM_deleteValue(key)
|
||||
if (key in $.syncing) {
|
||||
delete $.oldValue[key]
|
||||
@ -900,37 +901,37 @@ if (platform === 'crx') {
|
||||
if (!$.hasStorage) { $.cantSync = true }
|
||||
} else if ($.hasStorage) {
|
||||
$.oldValue = dict()
|
||||
$.setValue = function(key, val) {
|
||||
if (key in $.syncing) { $.oldValue[key] = val }
|
||||
$.setValue = function (key, val) {
|
||||
if (key in $.syncing) { $.oldValue[key] = val }
|
||||
return localStorage.setItem(key, val)
|
||||
}
|
||||
$.deleteValue = function(key) {
|
||||
$.deleteValue = function (key) {
|
||||
if (key in $.syncing) { delete $.oldValue[key] }
|
||||
return localStorage.removeItem(key)
|
||||
}
|
||||
} else {
|
||||
$.setValue = function() {}
|
||||
$.deleteValue = function() {}
|
||||
$.setValue = function () { }
|
||||
$.deleteValue = function () { }
|
||||
$.cantSync = ($.cantSet = true)
|
||||
}
|
||||
|
||||
if (typeof GM_addValueChangeListener !== 'undefined' && GM_addValueChangeListener !== null) {
|
||||
$.sync = (key, cb) => $.syncing[key] = GM_addValueChangeListener(g.NAMESPACE + key, function(key2, oldValue, newValue, remote) {
|
||||
$.sync = (key, cb) => $.syncing[key] = GM_addValueChangeListener(g.NAMESPACE + key, function (key2, oldValue, newValue, remote) {
|
||||
if (remote) {
|
||||
if (newValue !== undefined) { newValue = dict.json(newValue) }
|
||||
return cb(newValue, key)
|
||||
}
|
||||
})
|
||||
$.forceSync = function() {}
|
||||
$.forceSync = function () { }
|
||||
} else if ((typeof GM_deleteValue !== 'undefined' && GM_deleteValue !== null) || $.hasStorage) {
|
||||
$.sync = function(key, cb) {
|
||||
$.sync = function (key, cb) {
|
||||
key = g.NAMESPACE + key
|
||||
$.syncing[key] = cb
|
||||
return $.oldValue[key] = $.getValue(key)
|
||||
};
|
||||
|
||||
(function() {
|
||||
const onChange = function({key, newValue}) {
|
||||
(function () {
|
||||
const onChange = function ({ key, newValue }) {
|
||||
let cb
|
||||
if (!(cb = $.syncing[key])) { return }
|
||||
if (newValue != null) {
|
||||
@ -945,20 +946,20 @@ if (platform === 'crx') {
|
||||
}
|
||||
$.on(window, 'storage', onChange)
|
||||
|
||||
return $.forceSync = function(key) {
|
||||
return $.forceSync = function (key) {
|
||||
// Storage events don't work across origins
|
||||
// e.g. http://boards.4chan.org and https://boards.4chan.org
|
||||
// so force a check for changes to avoid lost data.
|
||||
key = g.NAMESPACE + key
|
||||
return onChange({key, newValue: $.getValue(key)})
|
||||
return onChange({ key, newValue: $.getValue(key) })
|
||||
}
|
||||
})()
|
||||
} else {
|
||||
$.sync = function() {}
|
||||
$.forceSync = function() {}
|
||||
$.sync = function () { }
|
||||
$.forceSync = function () { }
|
||||
}
|
||||
|
||||
$.delete = function(keys) {
|
||||
$.delete = function (keys) {
|
||||
if (!(keys instanceof Array)) {
|
||||
keys = [keys]
|
||||
}
|
||||
@ -969,7 +970,7 @@ if (platform === 'crx') {
|
||||
|
||||
$.get = $.oneItemSugar((items, cb) => $.queueTask($.getSync, items, cb))
|
||||
|
||||
$.getSync = function(items, cb) {
|
||||
$.getSync = function (items, cb) {
|
||||
for (const key in items) {
|
||||
var val2
|
||||
if (val2 = $.getValue(g.NAMESPACE + key)) {
|
||||
@ -986,9 +987,9 @@ if (platform === 'crx') {
|
||||
return cb(items)
|
||||
}
|
||||
|
||||
$.set = $.oneItemSugar(function(items, cb) {
|
||||
$.set = $.oneItemSugar(function (items, cb) {
|
||||
$.securityCheck(items)
|
||||
return $.queueTask(function() {
|
||||
return $.queueTask(function () {
|
||||
for (const key in items) {
|
||||
const value = items[key]
|
||||
$.setValue(g.NAMESPACE + key, JSON.stringify(value))
|
||||
@ -997,14 +998,14 @@ if (platform === 'crx') {
|
||||
})
|
||||
})
|
||||
|
||||
$.clear = function(cb) {
|
||||
$.clear = function (cb) {
|
||||
// XXX https://github.com/greasemonkey/greasemonkey/issues/2033
|
||||
// Also support case where GM_listValues is not defined.
|
||||
$.delete(Object.keys(Conf))
|
||||
$.delete(['previousversion', 'QR Size', 'QR.persona'])
|
||||
try {
|
||||
$.delete($.listValues().map(key => key.replace(g.NAMESPACE, '')))
|
||||
} catch (error) {}
|
||||
} catch (error) { }
|
||||
return cb?.()
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,7 +50,7 @@ const SWTinyboard = {
|
||||
} else if (/^https?:/.test(root)) {
|
||||
properties.root = root
|
||||
}
|
||||
} catch (error) {}
|
||||
} catch (error) { }
|
||||
return properties
|
||||
}
|
||||
}
|
||||
@ -61,7 +61,7 @@ const SWTinyboard = {
|
||||
let reactUI
|
||||
if (reactUI = $.id('react-ui')) {
|
||||
const s = (this.selectors = Object.create(this.selectors))
|
||||
s.boardFor = {index: '.page-container'}
|
||||
s.boardFor = { index: '.page-container' }
|
||||
s.thread = 'div[id^="thread_"]'
|
||||
return Main.mounted(cb)
|
||||
} else {
|
||||
@ -70,32 +70,32 @@ const SWTinyboard = {
|
||||
},
|
||||
|
||||
urls: {
|
||||
thread({siteID, boardID, threadID}, isArchived) {
|
||||
thread({ siteID, boardID, threadID }, isArchived) {
|
||||
return `${Conf['siteProperties'][siteID]?.root || `http://${siteID}/`}${boardID}/${isArchived ? 'archive/' : ''}res/${threadID}.html`
|
||||
},
|
||||
post({postID}) { return `#${postID}` },
|
||||
index({siteID, boardID}) { return `${Conf['siteProperties'][siteID]?.root || `http://${siteID}/`}${boardID}/` },
|
||||
catalog({siteID, boardID}) { return `${Conf['siteProperties'][siteID]?.root || `http://${siteID}/`}${boardID}/catalog.html` },
|
||||
threadJSON({siteID, boardID, threadID}, isArchived) {
|
||||
post({ postID }) { return `#${postID}` },
|
||||
index({ siteID, boardID }) { return `${Conf['siteProperties'][siteID]?.root || `http://${siteID}/`}${boardID}/` },
|
||||
catalog({ siteID, boardID }) { return `${Conf['siteProperties'][siteID]?.root || `http://${siteID}/`}${boardID}/catalog.html` },
|
||||
threadJSON({ siteID, boardID, threadID }, isArchived) {
|
||||
const root = Conf['siteProperties'][siteID]?.root
|
||||
if (root) { return `${root}${boardID}/${isArchived ? 'archive/' : ''}res/${threadID}.json` } else { return '' }
|
||||
},
|
||||
archivedThreadJSON(thread) {
|
||||
return SWTinyboard.urls.threadJSON(thread, true)
|
||||
},
|
||||
threadsListJSON({siteID, boardID}) {
|
||||
threadsListJSON({ siteID, boardID }) {
|
||||
const root = Conf['siteProperties'][siteID]?.root
|
||||
if (root) { return `${root}${boardID}/threads.json` } else { return '' }
|
||||
},
|
||||
archiveListJSON({siteID, boardID}) {
|
||||
archiveListJSON({ siteID, boardID }) {
|
||||
const root = Conf['siteProperties'][siteID]?.root
|
||||
if (root) { return `${root}${boardID}/archive/archive.json` } else { return '' }
|
||||
},
|
||||
catalogJSON({siteID, boardID}) {
|
||||
catalogJSON({ siteID, boardID }) {
|
||||
const root = Conf['siteProperties'][siteID]?.root
|
||||
if (root) { return `${root}${boardID}/catalog.json` } else { return '' }
|
||||
},
|
||||
file({siteID, boardID}, filename) {
|
||||
file({ siteID, boardID }, filename) {
|
||||
return `${Conf['siteProperties'][siteID]?.root || `http://${siteID}/`}${boardID}/${filename}`
|
||||
},
|
||||
thumb(board, filename) {
|
||||
@ -104,55 +104,55 @@ const SWTinyboard = {
|
||||
},
|
||||
|
||||
selectors: {
|
||||
board: 'form[name="postcontrols"]',
|
||||
thread: 'input[name="board"] ~ div[id^="thread_"]',
|
||||
board: 'form[name="postcontrols"]',
|
||||
thread: 'input[name="board"] ~ div[id^="thread_"]',
|
||||
threadDivider: 'div[id^="thread_"] > hr:last-child',
|
||||
summary: '.omitted',
|
||||
summary: '.omitted',
|
||||
postContainer: 'div[id^="reply_"]:not(.hidden)', // postContainer is thread for OP
|
||||
opBottom: '.op',
|
||||
opBottom: '.op',
|
||||
replyOriginal: 'div[id^="reply_"]:not(.hidden)',
|
||||
infoRoot: '.intro',
|
||||
infoRoot: '.intro',
|
||||
info: {
|
||||
subject: '.subject',
|
||||
name: '.name',
|
||||
email: '.email',
|
||||
tripcode: '.trip',
|
||||
uniqueID: '.poster_id',
|
||||
capcode: '.capcode',
|
||||
flag: '.flag',
|
||||
date: 'time',
|
||||
subject: '.subject',
|
||||
name: '.name',
|
||||
email: '.email',
|
||||
tripcode: '.trip',
|
||||
uniqueID: '.poster_id',
|
||||
capcode: '.capcode',
|
||||
flag: '.flag',
|
||||
date: 'time',
|
||||
nameBlock: 'label',
|
||||
quote: 'a[href*="#q"]',
|
||||
reply: 'a[href*="/res/"]:not([href*="#"])'
|
||||
quote: 'a[href*="#q"]',
|
||||
reply: 'a[href*="/res/"]:not([href*="#"])'
|
||||
},
|
||||
icons: {
|
||||
isSticky: '.fa-thumb-tack',
|
||||
isClosed: '.fa-lock'
|
||||
isSticky: '.fa-thumb-tack',
|
||||
isClosed: '.fa-lock'
|
||||
},
|
||||
file: {
|
||||
text: '.fileinfo',
|
||||
link: '.fileinfo > a',
|
||||
text: '.fileinfo',
|
||||
link: '.fileinfo > a',
|
||||
thumb: 'a > .post-image'
|
||||
},
|
||||
thumbLink: '.file > a',
|
||||
multifile: '.files > .file',
|
||||
highlightable: {
|
||||
op: ' > .op',
|
||||
reply: '.reply',
|
||||
op: ' > .op',
|
||||
reply: '.reply',
|
||||
catalog: ' > .thread'
|
||||
},
|
||||
comment: '.body',
|
||||
spoiler: '.spoiler',
|
||||
comment: '.body',
|
||||
spoiler: '.spoiler',
|
||||
quotelink: 'a[onclick*="highlightReply("]',
|
||||
catalog: {
|
||||
board: '#Grid',
|
||||
board: '#Grid',
|
||||
thread: '.mix',
|
||||
thumb: '.thread-image'
|
||||
thumb: '.thread-image'
|
||||
},
|
||||
boardList: '.boardlist',
|
||||
boardListBottom: '.boardlist.bottom',
|
||||
styleSheet: '#stylesheet',
|
||||
psa: '.blotter',
|
||||
psa: '.blotter',
|
||||
nav: {
|
||||
prev: '.pages > form > [value=Previous]',
|
||||
next: '.pages > form > [value=Next]'
|
||||
@ -164,8 +164,8 @@ const SWTinyboard = {
|
||||
},
|
||||
|
||||
xpath: {
|
||||
thread: 'div[starts-with(@id,"thread_")]',
|
||||
postContainer: 'div[starts-with(@id,"reply_") or starts-with(@id,"thread_")]',
|
||||
thread: 'div[starts-with(@id,"thread_")]',
|
||||
postContainer: 'div[starts-with(@id,"reply_") or starts-with(@id,"thread_")]',
|
||||
replyContainer: 'div[starts-with(@id,"reply_")]'
|
||||
},
|
||||
|
||||
@ -222,7 +222,7 @@ $\
|
||||
},
|
||||
|
||||
bgColoredEl() {
|
||||
return $.el('div', {className: 'post reply'})
|
||||
return $.el('div', { className: 'post reply' })
|
||||
},
|
||||
|
||||
isFileURL(url) {
|
||||
@ -250,10 +250,10 @@ $\
|
||||
if (m = text.match(/(\s*ID:\s*)(\S+)/)) {
|
||||
let uniqueID
|
||||
nodes.info.normalize()
|
||||
let {nextSibling} = nodes.nameBlock
|
||||
let { nextSibling } = nodes.nameBlock
|
||||
nextSibling = nextSibling.splitText(m[1].length)
|
||||
nextSibling.splitText(m[2].length)
|
||||
nodes.uniqueID = (uniqueID = $.el('span', {className: 'poster_id'}))
|
||||
nodes.uniqueID = (uniqueID = $.el('span', { className: 'poster_id' }))
|
||||
$.replace(nextSibling, uniqueID)
|
||||
return $.add(uniqueID, nextSibling)
|
||||
}
|
||||
@ -269,19 +269,19 @@ $\
|
||||
|
||||
parseFile(post, file) {
|
||||
let info, infoNode
|
||||
const {text, link, thumb} = file
|
||||
const { text, link, thumb } = file
|
||||
if ($.x(`ancestor::${this.xpath.postContainer}[1]`, text) !== post.nodes.root) { return false } // file belongs to a reply
|
||||
if (!(infoNode = link.nextSibling?.textContent.includes('(') ? link.nextSibling : link.nextElementSibling)) { return false }
|
||||
if (!(info = infoNode.textContent.match(/\((.*,\s*)?([\d.]+ ?[KMG]?B).*\)/))) { return false }
|
||||
const nameNode = $('.postfilename', text)
|
||||
$.extend(file, {
|
||||
name: nameNode ? (nameNode.title || nameNode.textContent) : link.pathname.match(/[^/]*$/)[0],
|
||||
size: info[2],
|
||||
name: nameNode ? (nameNode.title || nameNode.textContent) : link.pathname.match(/[^/]*$/)[0],
|
||||
size: info[2],
|
||||
dimensions: info[0].match(/\d+x\d+/)?.[0]
|
||||
})
|
||||
if (thumb) {
|
||||
$.extend(file, {
|
||||
thumbURL: /\/static\//.test(thumb.src) && $.isImage(link.href) ? link.href : thumb.src,
|
||||
thumbURL: /\/static\//.test(thumb.src) && $.isImage(link.href) ? link.href : thumb.src,
|
||||
isSpoiler: /^Spoiler/i.test(info[1] || '') || (link.textContent === 'Spoiler Image')
|
||||
}
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user