Compare commits

..

2 Commits

Author SHA1 Message Date
Lalle
496b1ab9c8
revert some types due to error 2023-05-10 18:44:24 +02:00
Lalle
17fd343a2f
types and no strict 2023-05-10 18:30:08 +02:00
33 changed files with 3312 additions and 1972 deletions

View File

@ -1,5 +0,0 @@
# Current bugs
- posting a comment creates a new tab, and weird stuff happens
- threadwatcher spinner is constantly spinning

170
README.md
View File

@ -1,50 +1,162 @@
# 4chan XZ
# 4chan XT
**This repo is work in progress!** Use the build from the [repo this is forked from](https://github.com/ccd0/4chan-x) in the meantime.
PR to upstream: <https://github.com/ccd0/4chan-x/pull/3341>.
PR to upstream: https://github.com/ccd0/4chan-x/pull/3341.
## What is this?
This is a fork of [4chan X](github.com/ccd0/4chan-x) that aims to make it more modern, and easier to maintain.
## Why?
4chan X is a great extension, but it's a bit old. It uses a lot of old technologies, and it's a bit hard to maintain. This fork aims to make it easier to maintain, and more modern.
The 4chan XT project is a migration of 4chan X from coffeescript to TypeScript/JavaScript. It is named XT both as a continuation of eXTended, and a T for TypeScript. The goals of this project is to first get a working bundle from js/ts files, and then gradually convert js files to ts and add types as needed.
## TODO
- [x] add typescript
- find alternative for `<% if (`
- [x] made html templates jsx/txt functions
- this uses the typescript compiler to compile the jsx
- render code is in [src/globals/jsx.ts](./src/globals/jsx.ts)
- [x] binary files are included as base64 in the bundle step, they do need explicit imports
- [ ] \<% if (readJSON('/.tests_enabled')) { %\>, are these still used?
- build script
- [x] userscript
- [ ] .crx extension
- [x] crx directory that can be loaded as an unpacked extension is created
- [x] beta
- [x] noupdate
- [ ] run and debug
- [ ] port updates made to 4chan-X made since this was forked
- [] add types to all files
## Other notes
- [] make project use strict mode
- A lot of files have circular dependencies, but rollup can handle that
- but for some scripts that add to the same object I had to merge them, like Posting/QR and site/SW.yotsuba.js
- sometimes something might not be initialized before use, for example, `$.dict()` and `$.SECONDS`
- I moved these to a new file called helpers.ts, which shouldn't have dependencies itself, so it's also available
- tsconfig.json has `"checkJs": true,`, and a lot of js files report type errors when opened because of unknown properties on objects and reassigning variables with different types. These errors don't block the bundle at this moment.
- old files in the builds directory stay as reference until the new builds are functional, new files go in the builds/test directory
- old build scripts are also kept around for reference until the new build output is fully functional
- the es 2020 target was choses for optional chaining
- @violentmonkey/types was chosen over @types/greasemonkey because @types/greasemonkey only declares the GM object, and not GM\_ functions
- [] switch from fontawesome & pictures to svg icons.
## commits since this was forked
- [] add tests
<details>
<summary>Click to expand</summary>
- [] add vite for development
- [x] 944b04210c119aedf8da1a8bcabaca9b80312118 Update archive list.
- [x] 59ee8c57792d0f82491756a077e25f506fd62994 Desuarchive removes /gif/
- [x] 402679e33a06dfbe0dc39ceba5c24fed761b6a19 desuarchive removes /wsg/ files
- [x] 86071184aa39b3585f06c1a4e2921c411ad8cf10 archived.moe adds /pw/ search, tokyochronos has hosting issues
- [x] 8a6392b1cf721ddfae6d8f4e3ec2566f15755370 add Eientei
- [x] 451a06f54b878ce433b0775858affefc71927fc7 alice.al domain change
- [x] 2a8bf2adb0737ce7bb1e21f6b959e4c6e1de1bc7 Disable Javascript Whitelist on captcha iframe. #3292
- [x] e9c1529da7844a42a1b40458c2c77b77e23ca537 Make QR post more like original form post. #3330
- [x] d16062a8fac5c092c34310c7704ac3980494b6ef Merge remote-tracking branch '4chenz/master'
- [x] 8795b1c56dbdfb52a32ddb3ea80b549f0048dc7b Add Google Lens image search url
- [x] f3f03f5e79fb5f26c0fd4406b2ab6796851ea471 Replace Google image search link with Google Lens.
- [x] c68a8afbdf30e3cbb35f0834b364f20600151adf Switch Google image search back to old version, thanks to https://boards.4channel.org/g/thread/91737566#p91789527
- [x] aef984da1a6af4d0003b51e7f03bce252ac71dff Remove empty space from ads if they don't load. https://kissu.moe/b/res/7155#11052
- [x] 19268975ea2d49a753624315b0928f27496aac02 Update Randomize Filename to match current 4chan format. https://boards.4channel.org/g/thread/91737566#p91784238
- [x] 2a47dfd8ba724b17f5bc5f9214bea8ce8b469398 Catch errors due to "Restricted" selection. #2905
- [x] 27957c25af5d182adc38f1e67a098ab338631ccd Release 4chan X v1.14.22.2.
- [x] eb25d6e797a1673fd7cddb257fce04055383ec9b Update chrome-webstore-upload.
- [x] 14e67e9a958633e37b4e4a6293cfa3a921c1eab0 Release 4chan X v1.14.22.3.
- [x] 7295b21b73eb13ec53fdc61767ada341c2e13144 Avoid breaking sauce settings of people with links to original Google Images and Google Lens, provided they didn't already update to v1.14.22.3.
- [x] 71873cd7b22a565c2a41fa24f63f7504152683eb Recognize JPEG files with .jfif extensions as images for purposes of Image Hover etc.; also recognize .avif and .jxl files as images.
- [x] ea2462ecc47327c6f0c31348d95fd2b1b6447cb3 Release 4chan X v1.14.22.4.
- [] make it a chrome & firefox extension.
</details>
## Building the project
---
**Note:** THIS PROJECT IS FAR FROM BEING DONE. DO NOT USE IT FOR NON-DEVELOPMENT YET.
Original readme:
```npm
pnpm install
pnpm run build
```
![screenshot](https://ccd0.github.io/4chan-x/img/screenshot.png)
# 4chan X
4chan X is a script that adds various features to anonymous imageboards. It was originally developed for 4chan but has no affiliation with it.
or as i prefer
It was previously developed by [aeosynth](https://github.com/aeosynth/4chan-x), [Mayhem](https://github.com/MayhemYDG/4chan-x), [ihavenoface](https://github.com/ihavenoface/4chan-x), [Zixaphir](https://github.com/zixaphir/appchan-x), [Seaweed](https://github.com/seaweedchan/4chan-x), and [Spittie](https://github.com/Spittie/4chan-x), with contributions from many others.
```bash
make sneed
```
If you're looking for a maintained fork of OneeChan (a style script used in addition to 4chan X), try
https://github.com/KevinParnell/OneeChan.
## What might be added on
## Please note
**Uninstalling**: 4chan X disables the native extension, so if you uninstall 4chan X, you'll need to re-enable it. To do this, click the `[Settings]` link in the top right corner, uncheck "`Disable the native extension`" in the panel that appears, and click the "`Save Settings`" button. If you don't see a "`Save Settings`" button, it may be being hidden by your ad blocker.
- [] add React, or Vue
**Private browsing**: By default, 4chan X remembers your last read post in a thread and which posts were made by you, even if you are in private browsing / incognito mode. If you want to turn this off, uncheck the `Remember Last Read Post` and `Remember Your Posts` options in the settings panel. You can clear all 4chan browsing history saved by 4chan X by resetting your settings.
- [] add the auto-captcha solver to BTFO jannies
Use of the "Link Title" feature to fetch titles of Youtube links is subject to Youtube's [Terms of Service](https://www.youtube.com/t/terms) and [Privacy Policy](http://www.google.com/policies/privacy). For more details on what information is sent to Youtube and other sites, and how to turn it off if you don't want the feature, see 4chan X's [privacy documentation](https://github.com/ccd0/4chan-x/wiki/Privacy).
## Install
### Firefox
Install [Violentmonkey](https://addons.mozilla.org/en-US/firefox/addon/violentmonkey/), [Tampermonkey](https://addons.mozilla.org/en-US/firefox/addon/tampermonkey/), or [Greasemonkey](https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/) (issues since v4: [#2526](https://github.com/greasemonkey/greasemonkey/issues/2526), [#2576](https://github.com/greasemonkey/greasemonkey/issues/2574)), then **[click here to install 4chan X](https://www.4chan-x.net/builds/4chan-X.user.js)**.
Ports of Greasemonkey are available for [SeaMonkey](https://sourceforge.net/projects/gmport/) and [Pale Moon](https://github.com/janekptacijarabaci/greasemonkey/releases/latest).
### Chromium
**Userscript**: Install [Violentmonkey](https://chrome.google.com/webstore/detail/violent-monkey/jinjaccalgkegednnccohejagnlnfdag) or [Tampermonkey](https://tampermonkey.net/), then **[click here to install 4chan X](https://www.4chan-x.net/builds/4chan-X.user.js)**.
**Chrome extension**: 4chan X is also available as a standalone Chrome extension. The Chrome extension has the additional feature of being able to sync your settings and data with other devices via Chrome Sync. But there is an issue when the script updates: Whenever the Chrome extension is updated, until you hard refresh (F5) the tab, 4chan X is unable to save any data (such as posts marked as yours and settings changes). The userscript version above does not have this problem when 4chan X updates, only when Violentmonkey / Tampermonkey is updated. To install as a Chrome extension:
- **Chromium**, **Vivaldi**: **[Download 4chan X](https://www.4chan-x.net/builds/4chan-X.crx)**, then open `chrome://extensions` and drag the downloaded file onto the page. Alternatively, you can install 4chan X from the **[Chrome store](https://chrome.google.com/webstore/detail/ohnjgmpcibpbafdlkimncjhflgedgpam)**.
- **Opera**: **[Click to install 4chan X](https://www.4chan-x.net/builds/4chan-X.crx)**, then follow the prompts to activate it in your extension manager.
- **Chrome**: Install 4chan X from the **[Chrome store](https://chrome.google.com/webstore/detail/ohnjgmpcibpbafdlkimncjhflgedgpam)**.
Note: This version of 4chan X does not work with Opera 12. If you need Opera 12 support, try [loadletter's fork](https://github.com/loadletter/4chan-x) instead.
### Safari
Install the [Userscripts](https://itunes.apple.com/us/app/userscripts/id1463298887) extension. Enable it by pressing `⌘,`, navigating to the extensions pane and checking `Userscripts` checkbox. Now open the Userscripts editor by clicking on the `</>` button in the taskbar. Then click on the `+` button and select the `New Javascript` option. Replace the default text with the contents of the 4chan X **[script](https://www.4chan-x.net/builds/4chan-X.user.js)**. Finally save it by pressing `⌘s`.
### WebKitGTK+ / QtWebKit / QtWebEngine
Several minimal browsers have support for userscripts and can run 4chan X. Due to the lack of the cross-site GM_* API, and lack of support for userscripts in iframes, not all features will work. You may experience crashes when repeatedly solving the default image-based captchas. You can avoid this problem by enabling `Use Recaptcha v1` in your settings.
- **dwb**: Install the userscripts extension, then save the [script](https://www.4chan-x.net/builds/4chan-X.user.js) to the `$XDG_CONFIG_HOME/dwb/greasemonkey` or `$HOME/.config/dwb/greasemonkey` directory (creating it if necessary):
```
dwbem -N -i userscripts
wget -P ${XDG_CONFIG_HOME:-$HOME/.config}/dwb/greasemonkey https://www.4chan-x.net/builds/4chan-X.user.js
```
- **Midori**: Enable `User addons` in your preferences, under the Extensions tab. In the Privacy tab, check `Enable HTML5 local storage support`. Optionally, if you want 4chan X to be able to open new tabs when you start or reply to a thread, you will need to check `Allow scripts to open popups` under the Behavior tab. Then click the link to the [script](https://www.4chan-x.net/builds/4chan-X.user.js) to install it.
- **Luakit**: Navigate to the [script](https://www.4chan-x.net/builds/4chan-X.user.js), then type the command `:usi` to install it.
- **uzbl**: Install the script from https://github.com/singpolyma/singpolyma/blob/master/uzbl/data/scripts/userscript.sh, enable it in your config file, and then save [4chan X](https://www.4chan-x.net/builds/4chan-X.user.js) to `$XDG_DATA_HOME/uzbl/userscripts` (or `$HOME/.local/share/uzbl/userscripts`). The commands below assume you have run uzbl at least once to create its config file.
```
wget -P "${XDG_DATA_HOME:-$HOME/.local/share}/uzbl/scripts" https://raw.githubusercontent.com/singpolyma/singpolyma/master/uzbl/data/scripts/userscript.sh
chmod +x "${XDG_DATA_HOME:-$HOME/.local/share}/uzbl/scripts/userscript.sh"
echo '@on_event LOAD_COMMIT spawn @scripts_dir/userscript.sh document-start' >> "${XDG_CONFIG_HOME:-$HOME/.config}/uzbl/config"
echo '@on_event LOAD_FINISH spawn @scripts_dir/userscript.sh document-end' >> "${XDG_CONFIG_HOME:-$HOME/.config}/uzbl/config"
wget -P "${XDG_DATA_HOME:-$HOME/.local/share}/uzbl/userscripts" https://www.4chan-x.net/builds/4chan-X.user.js
```
- **qutebrowser**: Save the [script](https://www.4chan-x.net/builds/4chan-X.user.js) to the `$XDG_DATA_HOME/qutebrowser/greasemonkey` or `$HOME/.local/share/qutebrowser/greasemonkey` directory:
```
wget -P ${XDG_DATA_HOME:-$HOME/.local/share}/qutebrowser/greasemonkey https://www.4chan-x.net/builds/4chan-X.user.js
```
### MS Edge
Install [Tampermonkey](https://www.microsoft.com/en-us/store/p/tampermonkey/9nblggh5162s), then **[click here to install 4chan X](https://www.4chan-x.net/builds/4chan-X.user.js)**.
### Other browsers
4chan X can be used in some browsers that do not support userscripts using [a local proxy](https://github.com/ccd0/4chan-x-proxy). Not all features will work.
## Beta version
New features and non-urgent bugfixes are released on the beta channel for further testing before they are moved the stable version. Please [report](https://github.com/ccd0/4chan-x/issues?q=is%3Aopen+sort%3Aupdated-desc) any issues you find, and be sure to mention which version you're using. You should back up your settings regularly to prevent them from being lost due to bugs.
To install the **beta** version and get updates whenever there's a new **beta** version:
- [Install userscript](https://www.4chan-x.net/builds/4chan-X-beta.user.js) (use with Greasemonkey / Violentmonkey / Tampermonkey / JS Blocker / etc.)
- [Download Chrome extension](https://www.4chan-x.net/builds/4chan-X-beta.crx) (download and drag to `chrome://extensions`)
To install the current **beta** version but get updates from the **stable** channel (for example, if just you want a particular recent feature):
- [Install userscript](https://github.com/ccd0/4chan-x/raw/beta/builds/4chan-X.user.js)
- [Download Chrome extension](https://github.com/ccd0/4chan-x/raw/beta/builds/4chan-X.crx)
## Troubleshooting
If you encounter a bug, try the steps [here](https://github.com/ccd0/4chan-x/blob/master/CONTRIBUTING.md#reporting-bugs), then report it to the [issue tracker](https://github.com/ccd0/4chan-x/issues?q=is%3Aopen+sort%3Aupdated-desc). If the bug seems to be caused by a script update, you can install a old version from the [changelog](https://github.com/ccd0/4chan-x/blob/master/CHANGELOG.md).
## More information
- [Changelog](https://github.com/ccd0/4chan-x/blob/master/CHANGELOG.md)
- [Frequently Asked Questions](https://github.com/ccd0/4chan-x/wiki/Frequently-Asked-Questions)
- [Report Bugs](https://github.com/ccd0/4chan-x/issues)
- [Contributing](https://github.com/ccd0/4chan-x/blob/master/CONTRIBUTING.md)

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,11 @@
import { Conf, doc } from "../globals/globals"
import $ from "../platform/$"
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
const Anonymize = {
init() {
if (!Conf['Anonymize']) { return }

View File

@ -14,6 +14,14 @@ import QuoteYou from "../Quotelinks/QuoteYou"
import PostHiding from "./PostHiding"
import ThreadHiding from "./ThreadHiding"
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS205: Consider reworking code to avoid use of IIFEs
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
interface FilterObj {
isstring: boolean;
@ -32,6 +40,10 @@ type FilterType = "postID" | "name" | "uniqueID" | "tripcode" | "capcode" | "pas
| "flag" | "filename" | "dimensions" | "filesize" | "MD5"
const Filter = {
/**
* Uses a Map for string types, with the value to filter for as the key.
* This allows faster lookup than iterating over every filter.
*/
filters: new Map<FilterType, FilterObj[] | Map<string, FilterObj[]>>(),
init(this: typeof Filter) {
@ -283,7 +295,7 @@ const Filter = {
if (!(url = g.SITE.urls.catalogJSON?.(g.BOARD))) { return }
Filter.catalogData = dict()
$.ajax(url,
{ onloadend: Filter.catalogParse }, this)
{ onloadend: Filter.catalogParse })
return Callbacks.CatalogThreadNative.push({
name: 'Filter',
cb: this.catalogNode

View File

@ -21,7 +21,7 @@ const PostHiding = {
$.addClass(doc, "reply-hide")
}
this.db = new DataBoard('hiddenPosts', true, false)
this.db = new DataBoard('hiddenPosts')
return Callbacks.Post.push({
name: 'Reply Hiding',
cb: this.node

View File

@ -14,7 +14,7 @@ import { dict } from "../platform/helpers"
const ThreadHiding = {
init() {
if (!['index', 'catalog'].includes(g.VIEW) || (!Conf['Thread Hiding Buttons'] && !(Conf['Menu'] && Conf['Thread Hiding Link']) && !Conf['JSON Index'])) { return }
this.db = new DataBoard('hiddenThreads', true)
this.db = new DataBoard('hiddenThreads')
if (g.VIEW === 'catalog') { return this.catalogWatch() }
this.catalogSet(g.BOARD)
$.on(d, 'IndexRefreshInternal', this.onIndexRefresh)

View File

@ -1,43 +0,0 @@
selectRev: 0,
selectAll: false,
selectMode: false,
showHiddenThreads: false,
changed: false,
currentPage: 0,
currentSort: '',
navLinks: null,
selectSort: null,
selectSize: null,
lastLongOptions: null,
root: null,
button: null,
inputs: null,
pagelist: null,
search: '',
pageNum: 0,
req: null,
isReply: false,
isThread: false,
isClone: false,
enabled: false,
searchInput: null,
hideLabel: null,
lastLongInputs: null,
lastLongThresholds: null,
loaded: false,
liveThreadData: null,
pagesNum: 0,
ID: 0,
thread: null,
threadPosition: 0,
threadsNumPerPage: 0,
notice: null,
nodes: null,
nTimeout: 0,
replyData: null,
replyNodes: null,
sortedThreadIDs: null,
liveThreadDict: null,
parsedThreads: null,
liveThreadIDs: null,
initFinishedFired: false,

View File

@ -79,7 +79,7 @@ const ImageCommon = {
}
}
const threadJSON = g.SITE.urls.threadJSON?.(post, fileObj)
const threadJSON = g.SITE.urls.threadJSON?.(post)
if (!threadJSON) { return }
const parseJSON = function (isArchiveURL) {
let needle, postObj

View File

@ -8,45 +8,6 @@ import { SECOND } from "../platform/helpers"
import type { File } from "../types/globals"
import ImageCommon from "./ImageCommon"
import Volume from "./Volume"
interface File {
isVideo: boolean;
isExpanding: boolean;
isExpanded: boolean;
url: string;
dimensions: string | null;
}
interface ImageHover {
error(post: Post, file: File): () => void;
}
interface ImageCommon {
cache?: HTMLElement & { dataset: { fileID: string } };
popCache(): HTMLElement;
rewind(el: HTMLElement): void;
pause(el: HTMLElement): void;
pushCache(el: HTMLElement): void;
}
interface Volume {
setup(el: HTMLVideoElement): void;
}
interface UI {
hover(params: {
root: HTMLElement;
el: HTMLElement;
latestEvent: MouseEvent;
endEvents: string;
height: number;
width: number;
noRemove: boolean;
cb: () => void;
}): void;
}
const ImageHover = {
init() {
if (!['index', 'thread'].includes(g.VIEW)) { return }
@ -81,27 +42,23 @@ const ImageHover = {
return $.on(this.nodes.thumb, 'mouseover', hover)
},
mouseover(post: Post, file: File): (e: MouseEvent) => void {
return function (e: MouseEvent) {
let el: HTMLElement, height: number, width: number
if (!doc.contains(this)) {
return
}
mouseover(post: Post, file: File) {
return function (e) {
let el, height, width
if (!doc.contains(this)) { return }
const { isVideo } = file
if (file.isExpanding || file.isExpanded || g.SITE.isThumbExpanded?.(file)) {
return
}
if (file.isExpanding || file.isExpanded || g.SITE.isThumbExpanded?.(file)) { return }
const error = ImageHover.error(this.post, file)
if (ImageCommon.cache?.dataset.fileID === `${post.fullID}.${file.index}`) {
el = ImageCommon.popCache()
$.on(el, 'error', error)
} else {
el = $.el(isVideo ? 'video' : 'img', {
el = $.el((isVideo ? 'video' : 'img'), {
className: 'ihover',
style: {
maxWidth: '100%',
maxHeight: '100%',
},
maxHeight: '100%'
}
})
el.dataset.fileID = `${post.fullID}.${file.index}`
$.on(el, 'error', error)
@ -117,16 +74,14 @@ const ImageHover = {
if (isVideo) {
el.loop = true
el.controls = false
Volume.setup(el as HTMLVideoElement)
Volume.setup(el)
if (Conf['Autoplay']) {
(el as HTMLVideoElement).play()
if (this.nodeName === 'VIDEO') {
(this as HTMLVideoElement).currentTime = (el as HTMLVideoElement).currentTime
}
el.play()
if (this.nodeName === 'VIDEO') { this.currentTime = el.currentTime }
}
}
if (file.dimensions) {
[width, height] = Array.from(file.dimensions.split('x').map((x) => +x))
[width, height] = Array.from((file.dimensions.split('x').map((x) => +x)))
const maxWidth = doc.clientWidth
const maxHeight = doc.clientHeight - UI.hover.padding
const scale = Math.min(1, maxWidth / width, maxHeight / height)
@ -136,7 +91,7 @@ const ImageHover = {
el.style.maxHeight = `${height}px`
}
return UI.hover({
root: this as HTMLElement,
root: this,
el,
latestEvent: e,
endEvents: 'mouseout click',
@ -150,7 +105,7 @@ const ImageHover = {
$.rm(el)
el.removeAttribute('style')
return el.remove()
},
}
})
}
},

View File

@ -4,7 +4,12 @@ import $ from "../platform/$"
import { dict } from "../platform/helpers"
import QR from "../Posting/QR"
import Menu from "./Menu"
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
const DeleteLink = {
auto: [dict(), dict()],
@ -114,7 +119,8 @@ const DeleteLink = {
withCredentials: true,
onloadend() { return DeleteLink.load(link, post, fileOnly, this.response) },
form: $.formData(form)
})
}
)
},
load(link, post, fileOnly, resDoc) {

View File

@ -1,8 +1,13 @@
import { Conf, g } from "../globals/globals"
import { Conf,g } from "../globals/globals"
import ImageCommon from "../Images/ImageCommon"
import $ from "../platform/$"
import Menu from "./Menu"
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
const DownloadLink = {
init() {
if (!['index', 'thread'].includes(g.VIEW) || !Conf['Menu'] || !Conf['Download Link']) { return }
@ -19,9 +24,9 @@ const DownloadLink = {
return Menu.menu.addEntry({
el: a,
order: 100,
open({ file }) {
open({file}) {
if (!file) { return false }
a.href = file.url
a.href = file.url
a.download = file.name
return true
}

View File

@ -8,7 +8,6 @@ import { dict } from "../platform/helpers"
const Banner = {
db: DataBoard,
init() {
if (Conf['Custom Board Titles']) {
this.db = new DataBoard('customTitles', null, true)
@ -90,13 +89,18 @@ const Banner = {
return Banner.db.set({
boardID: g.BOARD.ID,
threadID: this.className,
title: this.textContent,
orig: Banner.original[this.className].textContent
}, true)
val: {
title: this.textContent,
orig: Banner.original[this.className].textContent
}
})
} else {
$.rmAll(this)
$.add(this, [...Array.from(Banner.original[this.className].cloneNode(true).childNodes)])
return Banner.db.delete({ boardID: g.BOARD.ID, threadID: this.className }, true)
return Banner.db.delete({
boardID: g.BOARD.ID,
threadID: this.className
})
}
}
},

View File

@ -33,7 +33,7 @@ const FileInfo = {
const oldInfo = $.el('span', {className: 'fileText-original'})
$.prepend(this.file.link.parentNode, oldInfo)
$.add(oldInfo, this.file.link)
$.add(oldInfo, [this.file.link.previousSibling, this.file.link, this.file.link.nextSibling])
const info = $.el('span', {className: 'file-info'})
FileInfo.format(Conf['fileInfo'], this, info)

View File

@ -15,12 +15,12 @@ const Flash = {
initReady() {
if ($.hasStorage) {
return $.global(function () { if (JSON.parse(localStorage['4chan-settings'] || '{}').disableAll) { return window.SWFEmbed.init() } }, 'SWFEmbed')
return $.global(function () { if (JSON.parse(localStorage['4chan-settings'] || '{}').disableAll) { return window.SWFEmbed.init() } })
} else {
if (g.VIEW === 'thread') {
$.global(() => window.Main.tid = location.pathname.split(/\/+/)[3], 'Main')
$.global(() => window.Main.tid = location.pathname.split(/\/+/)[3])
}
return $.global(() => window.SWFEmbed.init(), 'SWFEmbed')
return $.global(() => window.SWFEmbed.init())
}
}
}

View File

@ -6,7 +6,11 @@ import $ from "../platform/$"
import $$ from "../platform/$$"
import ExpandComment from "./ExpandComment"
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
const Fourchan = {
init() {
if ((g.SITE.software !== 'yotsuba') || !['index', 'thread', 'archive'].includes(g.VIEW)) { return }
@ -61,7 +65,7 @@ const Fourchan = {
}
}
}
, false), 'MathJax')
, false))
Callbacks.Post.push({
name: 'Parse [math] tags',
cb: Fourchan.math

View File

@ -1,5 +1,4 @@
import Callbacks from "../classes/Callbacks"
import Post from "../classes/Post"
import { g } from "../globals/globals"
import $ from "../platform/$"
@ -26,12 +25,12 @@ const IDHighlight = {
if (!this.isClone) { return IDHighlight.set(this) }
},
set(post: Post) {
set(post) {
const match = (post.info.uniqueID || post.info.capcode) === IDHighlight.uniqueID
return $[match ? 'addClass' : 'rmClass'](post.nodes.post, 'highlight')
},
click(post: Post) {
click(post) {
return function () {
const uniqueID = post.info.uniqueID || post.info.capcode
IDHighlight.uniqueID = IDHighlight.uniqueID === uniqueID ? null : uniqueID

View File

@ -6,7 +6,6 @@ import { Conf, g } from "../globals/globals"
import $ from "../platform/$"
const IDPostCount = {
thread: null as Thread,
init() {
if ((g.VIEW !== 'thread') || !Conf['Count Posts by ID']) { return }
Callbacks.Thread.push({

View File

@ -10,7 +10,6 @@ import CaptchaT from "./Captcha.t"
import QR from "./QR"
const Captcha = {
replace: CaptchaReplace,
cache: {
init() {
$.on(d, 'SaveCaptcha', e => {

View File

@ -80,12 +80,7 @@ const Quotify = {
// Don't (Dead) when quotifying in an archived post,
// and we know the post still exists.
a = $.el('a', {
href: g.SITE.Build.postURL({
siteID: boardID,
boardID,
threadID: post.thread.ID,
postID
}),
href: g.SITE.Build.postURL(boardID, post.thread.ID, postID),
className: 'quotelink',
textContent: quote
}
@ -93,17 +88,12 @@ const Quotify = {
} else {
// Replace the .deadlink span if we can redirect.
a = $.el('a', {
href: g.SITE.Build.postURL({
siteID: boardID,
boardID,
threadID: 0,
postID
}),
href: g.SITE.Build.postURL(boardID, post.thread.ID, postID),
className: 'quotelink deadlink',
textContent: quote
}
)
$.add(a, Post.deadMark.cloneNode(true) as Element)
$.add(a, Post.deadMark.cloneNode(true))
$.extend(a.dataset, { boardID, threadID: post.thread.ID, postID })
}
@ -118,7 +108,7 @@ const Quotify = {
textContent: quote
}
)
$.add(a, Post.deadMark.cloneNode(true) as Element)
$.add(a, Post.deadMark.cloneNode(true))
if (fetchable) {
// Make it function as a normal quote if we can fetch the post.
$.addClass(a, 'quotelink')
@ -130,7 +120,7 @@ const Quotify = {
if (!this.quotes.includes(quoteID)) { this.quotes.push(quoteID) }
if (!a) {
$.add(deadlink, Post.deadMark.cloneNode(true) as Element)
$.add(deadlink, Post.deadMark.cloneNode(true))
return
}
@ -148,11 +138,7 @@ const Quotify = {
$.before(deadlink, green)
$.add(green, deadlink)
}
return $.replace(deadlink, $.el('span', {
className: 'deadlink',
textContent: deadlink.textContent
}
))
return $.replace(deadlink, [...Array.from(deadlink.childNodes)])
}
}
export default Quotify

View File

@ -7,16 +7,9 @@ export default class CatalogThread {
ID: string | number
thread: Thread
board: Board
nodes: {
root: Post,
thumb: Element,
icons: Element,
postCount: Element,
fileCount: Element,
pageCount: Element,
replies: Element
}
nodes: { root: Post; thumb: HTMLElement; icons: any; postCount: number; fileCount: number; pageCount: number; replies: any }
toString() { return this.ID }
constructor(root: Post, thread: Thread) {
this.thread = thread
this.ID = this.thread.ID + ''

View File

@ -149,12 +149,12 @@ export default class Fetcher {
if (post.no !== this.postID) {
// Cached requests can be stale and must be rechecked.
if (isCached) {
const api = g.SITE.urls.threadJSON({ boardID: this.boardID, threadID: this.threadID }, true)
const api = g.SITE.urls.threadJSON({ boardID: this.boardID, threadID: this.threadID })
$.cleanCache(url => url === api)
const that = this
$.cache(api, function () {
return that.fetchedPost(this, false)
}, { force: true })
})
return
}

View File

@ -9,7 +9,7 @@ import Callbacks from "./Callbacks"
import type Thread from "./Thread"
export default class Post {
declare origin: Post
callbacksExecuted: boolean
declare root: HTMLElement
declare thread: Thread
declare board: Board
@ -33,7 +33,6 @@ export default class Post {
declare files: File[]
declare info: {
comment: string,
subject: string,
name: string,
email: string,
@ -58,7 +57,6 @@ export default class Post {
})()
normalizedOriginal: any
indexRefreshSeen: any
callbacksExecuted: boolean
toString() { return this.ID }

23
src/classes/ShimSet.ts Normal file
View File

@ -0,0 +1,23 @@
class ShimSet {
elements: Element
size: number
constructor() {
this.elements
this.size = 0
}
has(value) {
return value in this.elements
}
add(value) {
if (this.elements[value]) { return }
this.elements[value] = true
return this.size++
}
delete(value) {
if (!this.elements[value]) { return }
delete this.elements[value]
return this.size--
}
}
if (!('Set' in window)) { window.Set = ShimSet }

View File

@ -25,7 +25,7 @@ export default class Thread {
fileLimit: boolean
ipCount: number
json: JSON
catalogView: any
catalogView: Node
nodes: { root: Post }
constructor(ID: number | string, board: Board) {

View File

@ -42,15 +42,15 @@ import yotsuba from './yotsuba.css'
import yotsubaB from './yotsuba-b.css'
// <%
// var inc = require['style'];
// var faCSS = read('/node_modules/font-awesome/css/font-awesome.css');
// var faWebFont = readBase64('/node_modules/font-awesome/fonts/fontawesome-webfont.woff');
// var mainCSS = ['font-awesome', 'style', 'yotsuba', 'yotsuba-b', 'futaba', 'burichan', 'tomorrow', 'photon', 'spooky'].map(x => read(`${x}.css`)).join('');
// var inc = require['style'];
// var faCSS = read('/node_modules/font-awesome/css/font-awesome.css');
// var faWebFont = readBase64('/node_modules/font-awesome/fonts/fontawesome-webfont.woff');
// var mainCSS = ['font-awesome', 'style', 'yotsuba', 'yotsuba-b', 'futaba', 'burichan', 'tomorrow', 'photon', 'spooky'].map(x => read(`${x}.css`)).join('');
// var iconNames = files.filter(f => /^linkify\.[^.]+\.png$/.test(f));
// var icons = iconNames.map(readBase64);
// %>
const mainCSS = fontAwesome + style + yotsuba + yotsubaB + futaba + burichan + tomorrow + photon + spooky
const mainCSS = fontAwesome + style + yotsuba +yotsubaB+futaba+burichan+tomorrow + photon + spooky
const faIcons: { name: string, data: string }[] = [
{ name: "Audio", data: linkifyAudio },
{ name: "Bitchute", data: linkifyBitchute },
@ -83,11 +83,11 @@ const CSS = {
www,
sub: function (css: string) {
sub: function(css: string) {
const variables = {
site: g.SITE.selectors
}
return css.replace(/\$[\w\$]+/g, function (name) {
return css.replace(/\$[\w\$]+/g, function(name) {
const words = name.slice(1).split('$')
let sel = variables
for (let i = 0; i < words.length; i++) {

View File

@ -4,6 +4,7 @@
font-style: normal;
-webkit-font-smoothing: antialiased;
text-decoration: inherit;
speak: none;
display: inline-block;
font-size: 13px;
visibility: visible;

View File

@ -1,26 +1,37 @@
// == Reprocess Font Awesome CSS == //
export const fa = (css: string, font: string) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const [license, classes] = css.match(/(\/\*![^]*?\*\/)[\s\S]*?((\.fa-[^{]*{\s*content:[^}]*}\s*)+)/)!.slice(1)
return `${license}
@font-face {
export const fa = (css: string, font: string) => (
// Font Awesome CSS attribution and license
css.match(/\/\*\![^]*?\*\//)[0] + '\n' +
// Font Awesome web font
`@font-face {
font-family: FontAwesome;
src: url('data:application/font-woff;base64,${font}') format('woff');
font-weight: 400;
font-style: normal;
}
${classes.replace(/([,{;])\s+/g, '$1').replace(/,/g, ', ')}`
}
` +
// fa-[icon name] classes
css
.match(/(\.fa-[^{]*{\s*content:[^}]*}\s*)+/)[0]
.replace(/([,{;])\s+/g, '$1')
.replace(/,/g, ', ')
)
// == Create CSS for Link Title Favicons == //
export const icons = (data: { name: string, data: string }[]) => {
return `/* Link Title Favicons */\n${data.map(({ name, data }) => {
return `.linkify.${name}::before {
export const icons = (data: { name: string, data: string }[]) => (
'/* Link Title Favicons */\n' +
data.map(({ name, data }) =>
`.linkify.${name}::before {
content: "";
background: transparent url('data:image/png;base64,${data}') center left no-repeat!important;
padding-left: 18px;
}
`
}).join('')}`
}
).join('')
)

View File

@ -10,12 +10,10 @@ declare global {
wrappedJSObject: any
Tegaki: any
FCX: any
Parser: any
}
}
// interfaces might be incomplete
export interface BoardConfig {
code_tags: 1 | 0,
sjis_tags: string,
math_tags: string,
forced_anon: boolean,

View File

@ -17,9 +17,9 @@ import CrossOrigin from "./CrossOrigin"
import { debounce, dict, MINUTE, platform, SECOND } from "./helpers"
// not chainable
const $ = (selector: string, root: HTMLElement = document.body): Element | null => root.querySelector(selector)
const $ = (selector, root = document.body) => root.querySelector(selector)
$.id = (id: string): HTMLElement | null => document.getElementById(id)
$.id = id => d.getElementById(id)
type AjaxPageOptions = {
responseType?: string;
@ -62,100 +62,6 @@ $.setValue = function (key: string, value: string, cb) {
}
}
$.crxWarningShown = false
interface AjaxDetail {
url: string;
timeout: number;
responseType: XMLHttpRequestResponseType;
withCredentials: boolean;
type: string;
onprogress?: (e: ProgressEvent) => void;
form?: [string, string][];
headers?: Record<string, string>;
id: string;
}
$.ajaxPageInit = function (): void {
$.global(function (): void {
let r = new XMLHttpRequest()
window.FCX.requests = Object.create(null)
document.addEventListener('4chanXAjax', function (e: CustomEvent<AjaxDetail>): void {
let fd: FormData | null
const { url, timeout, responseType, withCredentials, type, onprogress, form, headers, id } = e.detail
window.FCX.requests[id] = (r = new XMLHttpRequest())
r.open(type, url, true)
const object = headers || {}
for (const key in object) {
const value = object[key]
r.setRequestHeader(key, value)
}
r.responseType = responseType === 'document' ? 'text' : responseType
r.timeout = timeout
r.withCredentials = withCredentials
if (onprogress) {
r.upload.onprogress = function (e: ProgressEvent) {
const { loaded, total } = e
const detail = { loaded, total, id }
return document.dispatchEvent(new CustomEvent('4chanXAjaxProgress', { bubbles: true, detail }))
}
}
r.onloadend = function (): void {
delete window.FCX.requests[id]
const { status, statusText, response } = this
const responseHeaderString = this.getAllResponseHeaders()
const detail = { status, statusText, response, responseHeaderString, id }
return document.dispatchEvent(new CustomEvent('4chanXAjaxLoadend', { bubbles: true, detail })) as any
}
// connection error or content blocker
r.onerror = function (): void {
if (!r.status) { return console.warn(`4chan X failed to load: ${url}`) }
}
if (form) {
fd = new FormData()
for (const entry of form) {
fd.append(entry[0], entry[1])
}
} else {
fd = null
}
return r.send(fd)
}, false)
return document.addEventListener('4chanXAbort', function (e: CustomEvent<{ id: string }>) {
const { id } = e.detail
if (window.FCX.requests[id]) {
window.FCX.requests[id].abort()
return delete window.FCX.requests[id]
}
}
, false)
}, '4chanXAjax')
$.on(d, '4chanXAjaxProgress', function (e: CustomEvent<{ id: string; loaded: number; total: number }>): void {
let req: XMLHttpRequest
if (!(req = Request[e.detail.id])) { return }
return req.upload.onprogress.call(req.upload, e.detail)
})
return $.on(d, '4chanXAjaxLoadend', function (e: CustomEvent<AjaxDetail & { status: number; statusText: string; response: string; responseHeaderString: string }>): void {
let req: XMLHttpRequest
if (!(req = Request[e.detail.id])) { return }
delete Request[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
(req.response, 'text/html')
}
return req.onloadend.call(req)
}
})
}
$.ajaxPage = function (url: string, options: AjaxPageOptions) {
const {
responseType = 'json',
@ -200,7 +106,7 @@ $.ready = function (fc: () => void) {
return $.on(d, 'DOMContentLoaded', cb)
}
$.formData = function (form: object | HTMLFormElement) {
$.formData = function (form) {
if (form instanceof HTMLFormElement) {
return new FormData(form)
}
@ -239,7 +145,7 @@ $.ajax = (function () {
pageXHR = XMLHttpRequest
}
const r = (function (url: string, options = dict(), cb?: (this: XMLHttpRequest, e: ProgressEvent) => void) {
const r = (function (url: string, options = dict(), cb: Callbacks) {
if (options.responseType == null) { options.responseType = 'json' }
if (!options.type) { options.type = (options.form && 'post') || 'get' }
url = url.replace(/^((?:https?:)?\/\/(?:\w+\.)?(?:4chan|4channel|4cdn)\.org)\/adv\//, '$1//adv/')
@ -286,7 +192,84 @@ $.ajax = (function () {
// # XXX https://bugs.chromium.org/p/chromium/issues/detail?id=920638
let requestID = 0
const requests = dict()
$.ajaxPageInit()
$.ajaxPageInit = function () {
$.global(function () {
window.FCX.requests = Object.create(null)
document.addEventListener('4chanXAjax', function (e: CustomEvent) {
let fd, r
const { url, timeout, responseType, withCredentials, type, onprogress, form, headers, id } = e.detail
window.FCX.requests[id] = (r = new XMLHttpRequest())
r.open(type, url, true)
const object = headers || {}
for (const key in object) {
const value = object[key]
r.setRequestHeader(key, value)
}
r.responseType = responseType === 'document' ? 'text' : responseType
r.timeout = timeout
r.withCredentials = withCredentials
if (onprogress) {
r.upload.onprogress = function (e) {
const { loaded, total } = e
const detail = { loaded, total, id }
return document.dispatchEvent(new CustomEvent('4chanXAjaxProgress', { bubbles: true, detail }))
}
}
r.onloadend = function () {
delete window.FCX.requests[id]
const { status, statusText, response } = this
const responseHeaderString = this.getAllResponseHeaders()
const detail = { status, statusText, response, responseHeaderString, id }
return document.dispatchEvent(new CustomEvent('4chanXAjaxLoadend', { bubbles: true, detail }))
}
// connection error or content blocker
r.onerror = function () {
if (!r.status) { return console.warn(`4chan X failed to load: ${url}`) }
}
if (form) {
fd = new FormData()
for (const entry of form) {
fd.append(entry[0], entry[1])
}
} else {
fd = null
}
return r.send(fd)
}
, false)
return document.addEventListener('4chanXAjaxAbort', function (e) {
let r
if (!(r = window.FCX.requests[e.detail.id])) { return }
return r.abort()
}
, false)
}, '4chanXAjax')
$.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: XMLHttpRequest
const { onloadend, timeout, responseType, withCredentials, type, onprogress, headers } = options
@ -307,7 +290,7 @@ $.ajax = (function () {
// With the `If-Modified-Since` header we only receive the HTTP headers and no body for 304 responses.
// This saves a lot of bandwidth and CPU time for both the users and the servers.
$.lastModified = dict()
$.whenModified = function (url: string, bucket: string, cb: Callbacks, options = {}) {
$.whenModified = function (url, bucket, cb, options = {}) {
let t: string
const { timeout, ajax } = options
const params = []
@ -331,7 +314,7 @@ $.whenModified = function (url: string, bucket: string, cb: Callbacks, options =
return r
}
$.cache = function (url: string, cb, options = {}) {
$.cache = function (url, cb, options = {}) {
const reqs = dict()
let req
const { ajax } = options
@ -357,7 +340,7 @@ $.cache = function (url: string, cb, options = {}) {
return reqs[url] = req
}
$.cleanCache = function (testf: (url: string) => boolean) {
$.cleanCache = function (testf) {
const reqs = dict()
for (const url in reqs) {
if (testf(url)) {
@ -367,9 +350,18 @@ $.cleanCache = function (testf: (url: string) => boolean) {
}
$.cb = function (cb: VoidCallback) {
if (cb) {
return cb()
$.cb = {
checked() {
if ($.hasOwn(Conf, this.name)) {
$.set(this.name, this.checked, this.type)
return Conf[this.name] = this.checked
}
},
value() {
if ($.hasOwn(Conf, this.name)) {
$.set(this.name, this.value.trim(), this.type)
return Conf[this.name] = this.value
}
}
}
@ -403,7 +395,7 @@ $.addStyle = function (css, id, test = 'head') {
return style
}
$.addCSP = function (policy: string, doc = d) {
$.addCSP = function (policy) {
const meta = $.el('meta', {
httpEquiv: 'Content-Security-Policy',
content: policy
@ -419,13 +411,13 @@ $.addCSP = function (policy: string, doc = d) {
}
}
$.x = function (path: string, root: Element) {
$.x = function (path, root) {
if (!root) { root = d.body }
// XPathResult.ANY_UNORDERED_NODE_TYPE === 8
return d.evaluate(path, root, null, 8, null).singleNodeValue
}
$.X = function (path: string, root: Element): XPathResult {
$.X = function (path, root) {
if (!root) { root = d.body }
// XPathResult.ORDERED_NODE_SNAPSHOT_TYPE === 7
return d.evaluate(path, root, null, 7, null)
@ -439,19 +431,20 @@ $.rmClass = function (el: Element, ...classNames: string[]) {
for (const className of classNames) { el.classList.remove(className) }
}
$.toggleClass = (el: Element, className: string) => el.classList.toggle(className)
$.toggleClass = (el, className) => el.classList.toggle(className)
$.hasClass = (el: Element, className: string) => el.classList.contains(className)
$.hasClass = (el, className) => el.classList.contains(className)
$.rm = (el: Element) => el?.remove()
$.rm = el => el?.remove()
$.rmAll = (root: HTMLElement, selector: string) => $(selector, root)?.remove()
$.rmAll = root => // https://gist.github.com/MayhemYDG/8646194
root.textContent = null
$.tn = s => d.createTextNode(s)
$.frag = () => d.createDocumentFragment()
$.nodes = function (nodes: Node | Node[]) {
$.nodes = function (nodes) {
if (!(nodes instanceof Array)) {
return nodes
}
@ -462,17 +455,17 @@ $.nodes = function (nodes: Node | Node[]) {
return frag
}
$.add = (parent: HTMLElement, el: Element) => parent.appendChild($.nodes(el))
$.add = (parent, el) => parent.appendChild($.nodes(el))
$.prepend = (parent: HTMLElement, el: Element) => parent.insertBefore($.nodes(el), parent.firstChild)
$.prepend = (parent, el) => parent.insertBefore($.nodes(el), parent.firstChild)
$.after = (root: HTMLElement, el: Element) => root.parentNode.insertBefore($.nodes(el), root.nextSibling)
$.after = (root, el) => root.parentNode.insertBefore($.nodes(el), root.nextSibling)
$.before = (root: HTMLElement, el: Element) => root.parentNode.insertBefore($.nodes(el), root)
$.before = (root, el) => root.parentNode.insertBefore($.nodes(el), root)
$.replace = (root: HTMLElement, el: Element) => root.parentNode.replaceChild($.nodes(el), root)
$.replace = (root, el) => root.parentNode.replaceChild($.nodes(el), root)
$.el = function (tag: string, properties: object, properties2?) {
$.el = function (tag, properties, properties2?) {
const el = d.createElement(tag)
if (properties) { $.extend(el, properties) }
if (properties2) { $.extend(el, properties2) }
@ -483,14 +476,12 @@ $.on = function (el, events, handler) {
for (const event of events.split(' ')) {
el.addEventListener(event, handler, false)
}
return handler
}
$.off = function (el: HTMLElement, events: string, handler: (e: Event) => void) {
$.off = function (el, events, handler) {
for (const event of events.split(' ')) {
el.removeEventListener(event, handler, false)
}
return handler
}
$.one = function (el, events, handler) {
@ -500,10 +491,8 @@ $.one = function (el, events, handler) {
}
return $.on(el, events, cb)
}
let cloneInto: (obj: object, win: Window) => object
$.event = function (event: string, detail: object, root = d) {
$.event = function (event: Event, detail: object, root = d) {
if (!globalThis.chrome?.extension) {
if ((detail != null) && (typeof cloneInto === 'function')) {
detail = cloneInto(detail, d.defaultView)
@ -557,12 +546,15 @@ if (!globalThis.chrome?.extension) {
$.debounce = function (wait, fn) {
let lastCall = 0
let timeout = null
let that = null
let args = null
const exec = function () {
lastCall = Date.now()
// eslint-disable-next-line prefer-rest-params
return fn.apply(this, ...arguments)
return fn.apply(that, args)
}
return function () {
args = arguments
that = this
if (lastCall < (Date.now() - wait)) {
return exec()
}
@ -647,8 +639,8 @@ $.unescape = function (text) {
return text.replace(/<[^>]*>/g, '').replace(/&(amp|#039|quot|lt|gt|#44);/g, c => ({ '&amp;': '&', '&#039;': "'", '&quot;': '"', '&lt;': '<', '&gt;': '>', '&#44;': ',' })[c])
}
$.isImage = (url: string) => /\.(jpe?g|png|gif|webp|bmp|ico|svg|tiff?)$/i.test(url)
$.isVideo = (url: string) => /\.(webm|mp4|ogv|flv|mov|mpe?g|3gp)$/i.test(url)
$.isImage = url => /\.(jpe?g|png|gif|webp|bmp|ico|svg|tiff?)$/i.test(url)
$.isVideo = url => /\.(webm|mp4|ogv|flv|mov|mpe?g|3gp)$/i.test(url)
$.engine = (function () {
if (/Edge\//.test(navigator.userAgent)) { return 'edge' }
@ -731,9 +723,9 @@ if (platform === 'crx') {
return false
}
$.get = $.oneItemSugar(function (data: DataBoard, cb) {
$.get = $.oneItemSugar(function (data, cb) {
if (!$.crxWorking()) { return }
const results = dict()
const results = {}
const get = function (area) {
let keys = Object.keys(data)
// XXX slow performance in Firefox
@ -893,7 +885,7 @@ if (platform === 'crx') {
})
}
$.get = $.oneItemSugar(function (items: any, cb) {
$.get = $.oneItemSugar(function (items, cb) {
const keys = Object.keys(items)
return Promise.all(keys.map((key) => GM.getValue(g.NAMESPACE + key))).then(function (values) {
for (let i = 0; i < values.length; i++) {
@ -906,7 +898,7 @@ if (platform === 'crx') {
})
})
$.set = $.oneItemSugar(function (items: any, cb: Callbacks) {
$.set = $.oneItemSugar(function (items, cb) {
$.securityCheck(items)
return Promise.all((() => {
const result = []
@ -948,14 +940,14 @@ if (platform === 'crx') {
if (typeof GM_deleteValue !== 'undefined' && GM_deleteValue !== null) {
$.oldValue = dict()
$.setValue = function (key: string, val: any) {
$.setValue = function (key, val) {
GM_setValue(key, val)
if (key in $.syncing) {
$.oldValue[key] = val
if ($.hasStorage) { return localStorage.setItem(key, val) } // for `storage` events
}
}
$.deleteValue = function (key: string) {
$.deleteValue = function (key) {
GM_deleteValue(key)
if (key in $.syncing) {
delete $.oldValue[key]
@ -965,11 +957,11 @@ if (platform === 'crx') {
if (!$.hasStorage) { $.cantSync = true }
} else if ($.hasStorage) {
$.oldValue = dict()
$.setValue = function (key: string, val: any) {
$.setValue = function (key, val) {
if (key in $.syncing) { $.oldValue[key] = val }
return localStorage.setItem(key, val)
}
$.deleteValue = function (key: string) {
$.deleteValue = function (key) {
if (key in $.syncing) { delete $.oldValue[key] }
return localStorage.removeItem(key)
}
@ -986,7 +978,7 @@ if (platform === 'crx') {
})
$.forceSync = function () {/* empty */ }
} else if ((typeof GM_deleteValue !== 'undefined' && GM_deleteValue !== null) || $.hasStorage) {
$.sync = function (key: string, cb: (newValue: any, key: string) => void) {
$.sync = function (key, cb) {
key = g.NAMESPACE + key
$.syncing[key] = cb
return $.oldValue[key] = $.getValue(key, cb)
@ -1008,7 +1000,10 @@ if (platform === 'crx') {
}
$.on(window, 'storage', onChange)
return $.forceSync = function (key: string, cb: (newValue: any, key: string) => void) {
return $.forceSync = function (key, cb) {
// Storage events don't work across origins
// e.g. http://boards.4chan.org and https://boards.4chan.org
// so force a check for changes to avoid lost data.
key = g.NAMESPACE + key
return onChange({ key, newValue: $.getValue(key, cb) })
}
@ -1046,7 +1041,7 @@ if (platform === 'crx') {
return cb(items)
}
$.set = $.oneItemSugar(function (items: any, cb: (items: any) => void) {
$.set = $.oneItemSugar(function (items, cb) {
$.securityCheck(items)
return $.queueTask(function () {
for (const key in items) {

View File

@ -6,9 +6,6 @@ import { dict } from "../platform/helpers"
const SWTinyboard = {
sfwBoards() {
return Conf['boardConfig'].config.sfw_boards
},
insertTags() {
const { config } = Conf['boardConfig']
const { markup_tags } = config
@ -74,6 +71,7 @@ const SWTinyboard = {
}
return false
},
ID: 'sw-tinyboard',
parseThreadMetadata(el: HTMLElement) {
const thread = dict()
const op = el.querySelector('.op')
@ -218,9 +216,6 @@ $\
},
Build: {
postURL({ siteID, boardID, threadID, postID }) {
return `${Conf['siteProperties'][siteID]?.root || `http://${siteID}/`}${boardID}/res/${threadID}.html#${postID}`
},
parseJSON(data, board) {
const o = this.parseJSON(data, board)
if (data.ext === 'deleted') {
@ -317,7 +312,7 @@ $\
if (!(info = infoNode.textContent.match(/\((.*,\s*)?([\d.]+ ?[KMG]?B).*\)/))) { return false }
const nameNode = $('.postfilename', text)
$.extend(file, {
name: nameNode ? (nameNode.textContent || '').trim() : '',
name: nameNode ? (nameNode.title || nameNode.textContent) : link.pathname.match(/[^/]*$/)[0],
size: info[2],
dimensions: info[0].match(/\d+x\d+/)?.[0]
})

View File

@ -24,20 +24,14 @@ const SWYotsuba = {
urls: {
thread({boardID, threadID}) { return `${location.protocol}//${BoardConfig.domain(boardID)}/${boardID}/thread/${threadID}` },
post({postID})
{ return `#p${postID}` },
index({boardID}) { return `${location.protocol}//${BoardConfig.domain(boardID)}/${boardID}/` },
catalog({boardID})
{ if (boardID === 'f') { return undefined } else { return `${location.protocol}//${BoardConfig.domain(boardID)}/${boardID}/catalog` } },
archive({boardID})
{ if (BoardConfig.isArchived(boardID)) { return `${location.protocol}//${BoardConfig.domain(boardID)}/${boardID}/archive` } else { return undefined } },
post({postID}) { return `#p${postID}` },
index({boardID}) { return `${location.protocol}//${BoardConfig.domain(boardID)}/${boardID}/` },
catalog({boardID}) { if (boardID === 'f') { return undefined } else { return `${location.protocol}//${BoardConfig.domain(boardID)}/${boardID}/catalog` } },
archive({boardID}) { if (BoardConfig.isArchived(boardID)) { return `${location.protocol}//${BoardConfig.domain(boardID)}/${boardID}/archive` } else { return undefined } },
threadJSON({boardID, threadID}) { return `${location.protocol}//a.4cdn.org/${boardID}/thread/${threadID}.json` },
threadsListJSON({boardID})
{ return `${location.protocol}//a.4cdn.org/${boardID}/threads.json` },
archiveListJSON({boardID})
{ if (BoardConfig.isArchived(boardID)) { return `${location.protocol}//a.4cdn.org/${boardID}/archive.json` } else { return '' } },
catalogJSON({boardID})
{ return `${location.protocol}//a.4cdn.org/${boardID}/catalog.json` },
threadsListJSON({boardID}) { return `${location.protocol}//a.4cdn.org/${boardID}/threads.json` },
archiveListJSON({boardID}) { if (BoardConfig.isArchived(boardID)) { return `${location.protocol}//a.4cdn.org/${boardID}/archive.json` } else { return '' } },
catalogJSON({boardID}) { return `${location.protocol}//a.4cdn.org/${boardID}/catalog.json` },
file({boardID}, filename) {
const hostname = boardID === 'f' ? ImageHost.flashHost() : ImageHost.host()
return `${location.protocol}//${hostname}/${boardID}/${filename}`
@ -306,19 +300,11 @@ $\
insertTags(bq) {
let node
const nodes = []
const {children} = bq
for (let i = 0; i < children.length; i++) {
if ((node = children[i]) && (node.nodeName === 'A') && (node.textContent === '>>')) {
nodes.push(node)
}
for (node of $$('s, .removed-spoiler', bq)) {
$.replace(node, [$.tn('[spoiler]'), ...Array.from(node.childNodes), $.tn('[/spoiler]')])
}
for (const node of nodes) {
const {href} = node
const {textContent} = node.nextSibling
const tag = $.el('a', {href, textContent})
$.after(node, tag)
$.rm(node)
for (node of $$('.prettyprint', bq)) {
$.replace(node, [$.tn('[code]'), ...Array.from(node.childNodes), $.tn('[/code]')])
}
},
@ -350,7 +336,7 @@ $\
},
transformBoardList() {
let node = null
let node
const nodes = []
const spacer = () => $.el('span', {className: 'spacer'})
const items = $.X('.//a|.//text()[not(ancestor::a)]', $(SWYotsuba.selectors.boardList))
@ -630,7 +616,7 @@ $\
thread(thread, data, withReplies) {
let root
if (root = thread.nodes.root) {
$.rmAll(root, thread.OP.nodes.root)
$.rmAll(root)
} else {
thread.nodes.root = (root = $.el('div', {
className: 'thread',
@ -686,7 +672,7 @@ $\
'div',
generateCatalogThreadHtml(thread, src, imgClass, data, postCount, fileCount, pageCount, staticPath, gifIcon)
)
$.before(thread.OP.nodes.info, container)
$.before(thread.OP.nodes.info, [...Array.from(container.childNodes)])
for (const br of $$('br', thread.OP.nodes.comment)) {
if (br.previousSibling && (br.previousSibling.nodeName === 'BR')) {
@ -699,7 +685,7 @@ $\
id: `t${thread}`
}
)
if (thread.OP.highlights) { $.addClass(root, 'highlight') }
if (thread.OP.highlights) { $.addClass(root, ...Array.from(thread.OP.highlights)) }
if (!thread.OP.file) { $.addClass(root, 'noFile') }
root.style.cssText = cssText || ''

View File

@ -1,6 +1,5 @@
declare const XPCNativeWrapper: any
export interface File {
MD5: string
name: string
isImage: boolean
isVideo: boolean