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