mirror of
https://github.com/Eggbertx/gochan.git
synced 2025-08-19 08:26:23 -07:00
Switch to TypeScript for frontend
This commit is contained in:
parent
5e8804b9d2
commit
3d423e9623
45 changed files with 6395 additions and 8183 deletions
|
@ -1,5 +1,10 @@
|
|||
{
|
||||
"presets": ["@babel/preset-env"],
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env", {"targets": {"node": "current"}},
|
||||
"@babel/preset-typescript"
|
||||
]
|
||||
],
|
||||
"comments": false,
|
||||
"sourceMaps": true
|
||||
}
|
||||
|
|
1
frontend/.eslintignore
Normal file
1
frontend/.eslintignore
Normal file
|
@ -0,0 +1 @@
|
|||
/webpack.config.js
|
|
@ -1,41 +1,53 @@
|
|||
module.exports = {
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"env": {
|
||||
"browser": true,
|
||||
"jest": true,
|
||||
"node": true,
|
||||
"es6": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
],
|
||||
"ignorePatterns": ["**/legacy/**"],
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"experimentalObjectRestSpread": true,
|
||||
"jsx": true
|
||||
},
|
||||
"globals": {
|
||||
"styles": "readonly",
|
||||
"defaultStyle": "readonly",
|
||||
"webroot": "readonly",
|
||||
"serverTZ": "readonly"
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"no-unused-vars": ["warn", {
|
||||
"argsIgnorePattern": "^_"
|
||||
}],
|
||||
"semi": "warn",
|
||||
"no-constant-condition": "warn",
|
||||
"no-whitespace-before-property": "warn",
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"brace-style": ["error", "1tbs"],
|
||||
"array-bracket-spacing": ["error", "never"],
|
||||
"block-spacing": ["error", "always"],
|
||||
"func-call-spacing": ["error", "never"],
|
||||
"space-before-blocks": ["warn", "always"],
|
||||
"no-undef": "error",
|
||||
"keyword-spacing": ["warn", {
|
||||
"overrides": {
|
||||
"if": {"after": false},
|
||||
"for": {"after": false},
|
||||
"catch": {"after": false},
|
||||
"switch": {"after": false},
|
||||
"while": {"after": false}
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"rules": {
|
||||
"indent": ["error", "tab"],
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"quotes": ["error", "double", {
|
||||
"allowTemplateLiterals": true
|
||||
}],
|
||||
"semi": ["error", "always"],
|
||||
"no-var": ["error"],
|
||||
"brace-style": ["error"],
|
||||
"array-bracket-spacing": ["error", "never"],
|
||||
"block-spacing": ["error", "always"],
|
||||
"no-spaced-func": ["error"],
|
||||
"no-whitespace-before-property": ["error"],
|
||||
"space-before-blocks": ["error", "always"],
|
||||
"keyword-spacing": ["error", {
|
||||
"overrides": {
|
||||
"if": {"after": false},
|
||||
"for": {"after": false},
|
||||
"catch": {"after": false},
|
||||
"switch": {"after": false},
|
||||
"while": {"after": false}
|
||||
}
|
||||
}],
|
||||
"no-constant-condition": ["off"],
|
||||
"@typescript-eslint/no-explicit-any": ["off"],
|
||||
"@typescript-eslint/no-unused-vars": ["warn", {
|
||||
"argsIgnorePattern": "^_"
|
||||
}],
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
// this is here since parcel and babel don't seem to get along
|
||||
"extends": "@parcel/config-default",
|
||||
"transformers": {
|
||||
"*.js": [
|
||||
"@parcel/transformer-js"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,14 +1,16 @@
|
|||
import type {Config} from 'jest';
|
||||
import { defaults } from "jest-config"
|
||||
/*
|
||||
* For a detailed explanation regarding each configuration property, visit:
|
||||
* https://jestjs.io/docs/configuration
|
||||
*/
|
||||
|
||||
export default {
|
||||
const cfg: Config = {
|
||||
// All imported modules in your tests should be mocked automatically
|
||||
// automock: false,
|
||||
|
||||
// Stop running tests after `n` failures
|
||||
// bail: 0,
|
||||
bail: 1,
|
||||
|
||||
// The directory where Jest should store its cached dependency information
|
||||
// cacheDirectory: "/tmp/jest_rs",
|
||||
|
@ -70,16 +72,7 @@ export default {
|
|||
// "node_modules"
|
||||
// ],
|
||||
|
||||
// An array of file extensions your modules use
|
||||
// moduleFileExtensions: [
|
||||
// "js",
|
||||
// "jsx",
|
||||
// "ts",
|
||||
// "tsx",
|
||||
// "json",
|
||||
// "node"
|
||||
// ],
|
||||
|
||||
moduleFileExtensions: [...defaults.moduleFileExtensions, "mts"],
|
||||
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
|
||||
// moduleNameMapper: {},
|
||||
|
||||
|
@ -116,27 +109,6 @@ export default {
|
|||
// The root directory that Jest should scan for tests and modules within
|
||||
rootDir: "tests/",
|
||||
|
||||
// A list of paths to directories that Jest should use to search for files in
|
||||
// roots: [
|
||||
// "<rootDir>"
|
||||
// ],
|
||||
|
||||
// Allows you to use a custom runner instead of Jest's default test runner
|
||||
// runner: "jest-runner",
|
||||
|
||||
// The paths to modules that run some code to configure or set up the testing environment before each test
|
||||
// setupFiles: [],
|
||||
|
||||
// A list of paths to modules that run some code to configure or set up the testing framework before each test
|
||||
// setupFilesAfterEnv: [],
|
||||
|
||||
// The number of seconds after which a test is considered as slow and reported as such in the results.
|
||||
// slowTestThreshold: 5,
|
||||
|
||||
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
|
||||
// snapshotSerializers: [],
|
||||
|
||||
// The test environment that will be used for testing
|
||||
testEnvironment: "jsdom",
|
||||
|
||||
// Options that will be passed to the testEnvironment
|
||||
|
@ -172,7 +144,9 @@ export default {
|
|||
// timers: "real",
|
||||
|
||||
// A map from regular expressions to paths to transformers
|
||||
// transform: undefined,
|
||||
transform: {
|
||||
"^.+\\.ts?$": "ts-jest"
|
||||
},
|
||||
|
||||
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
||||
// transformIgnorePatterns: [
|
||||
|
@ -184,7 +158,7 @@ export default {
|
|||
// unmockedModulePathPatterns: undefined,
|
||||
|
||||
// Indicates whether each individual test should be reported during the run
|
||||
// verbose: undefined,
|
||||
verbose: true,
|
||||
|
||||
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
|
||||
// watchPathIgnorePatterns: [],
|
||||
|
@ -192,3 +166,4 @@ export default {
|
|||
// Whether to use watchman for file crawling
|
||||
// watchman: true,
|
||||
};
|
||||
export default cfg;
|
|
@ -1,23 +0,0 @@
|
|||
let noop = ()=>{};
|
||||
|
||||
/**
|
||||
* @param {JQuery<HTMLElement>} $elem
|
||||
*/
|
||||
export function updateUploadImage($elem, onLoad = noop) {
|
||||
if($elem.length == 0) return;
|
||||
$elem[0].onchange = function() {
|
||||
let img = new Image();
|
||||
img.src = URL.createObjectURL(this.files[0]);
|
||||
img.onload = onLoad;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getUploadFilename() {
|
||||
let elem = document.getElementById("imagefile");
|
||||
if(elem === null) return "";
|
||||
if(elem.files === undefined || elem.files.length < 1) return "";
|
||||
return elem.files[0].name;
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
import { getCookie, setCookie } from "./cookies";
|
||||
|
||||
|
||||
export function getStorageVal(key, defaultVal = "") {
|
||||
if(localStorage == undefined)
|
||||
return getCookie(key, defaultVal);
|
||||
let val = localStorage.getItem(key);
|
||||
if(val === null)
|
||||
return defaultVal;
|
||||
return val;
|
||||
}
|
||||
|
||||
export function getBooleanStorageVal(key, defaultVal = false) {
|
||||
let val = getStorageVal(key, defaultVal);
|
||||
return val == true || val == "true";
|
||||
}
|
||||
|
||||
export function getNumberStorageVal(key, defaultVal = 0) {
|
||||
return Number.parseFloat(getStorageVal(key, defaultVal));
|
||||
}
|
||||
|
||||
export function getJsonStorageVal(key, defaultVal) {
|
||||
let val = defaultVal;
|
||||
try {
|
||||
val = JSON.parse(getStorageVal(key, defaultVal));
|
||||
} catch(e) {
|
||||
val = defaultVal;
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
export function setStorageVal(key, val, isJSON = false) {
|
||||
let storeVal = isJSON?JSON.stringify(val):val;
|
||||
if(localStorage == undefined)
|
||||
setCookie(key, storeVal);
|
||||
else
|
||||
localStorage.setItem(key, storeVal);
|
||||
}
|
157
frontend/js/types/gochan.d.ts
vendored
157
frontend/js/types/gochan.d.ts
vendored
|
@ -1,157 +0,0 @@
|
|||
import "jquery";
|
||||
|
||||
declare interface GochanStyle {
|
||||
Name: string;
|
||||
Filename: string;
|
||||
}
|
||||
|
||||
// stored in /js/consts.json
|
||||
declare var styles: GochanStyle[];
|
||||
declare var defaultStyle: string;
|
||||
declare var serverTZ: number;
|
||||
interface Window {
|
||||
styles: GochanStyle[];
|
||||
defaultStyle: string;
|
||||
webroot: string;
|
||||
serverTZ: number;
|
||||
|
||||
openQR: () => void;
|
||||
closeQR: () => void;
|
||||
toTop: () => void;
|
||||
toBottom: () => void;
|
||||
}
|
||||
|
||||
// /boards.json
|
||||
declare interface BoardsJSON {
|
||||
boards: BoardJSON[];
|
||||
}
|
||||
|
||||
declare interface BoardCooldowns {
|
||||
threads: number;
|
||||
replies: number
|
||||
images: number;
|
||||
}
|
||||
|
||||
declare interface BoardJSON {
|
||||
pages: number;
|
||||
board: string;
|
||||
title: string;
|
||||
meta_description: string;
|
||||
max_filesize: number;
|
||||
max_pages: number;
|
||||
is_archived: boolean;
|
||||
bump_limit: number;
|
||||
image_limit: number;
|
||||
max_comment_chars: number;
|
||||
ws_board: boolean;
|
||||
cooldowns: BoardCooldowns
|
||||
per_page: number;
|
||||
}
|
||||
|
||||
// an array of these are in /boarddir/catalog.json
|
||||
declare interface CatalogBoard {
|
||||
page: number;
|
||||
threads: CatalogThread[];
|
||||
}
|
||||
|
||||
declare interface CatalogThread {
|
||||
replies: number;
|
||||
images: number;
|
||||
omitted_posts: number;
|
||||
omitted_images: number;
|
||||
sticky: number;
|
||||
locked: number;
|
||||
}
|
||||
|
||||
// /boarddir/res/#.json
|
||||
declare interface BoardThread {
|
||||
posts: ThreadPost[];
|
||||
}
|
||||
|
||||
declare interface ThreadPost {
|
||||
no: number;
|
||||
resto: number;
|
||||
name: string;
|
||||
trip: string;
|
||||
email: string;
|
||||
sub: string;
|
||||
com: string;
|
||||
tim: string;
|
||||
filename: string;
|
||||
md5: string;
|
||||
extension: string;
|
||||
fsize: number;
|
||||
w: number;
|
||||
h: number;
|
||||
tn_w: number;
|
||||
tn_h: number;
|
||||
capcode: string;
|
||||
time: string;
|
||||
last_modified: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* An object representing a staff member retreived by requesting /manage/staffinfo
|
||||
*/
|
||||
interface StaffInfo {
|
||||
/**
|
||||
* The staff member's ID in the database
|
||||
*/
|
||||
ID: number;
|
||||
/**
|
||||
* The staff member's username
|
||||
*/
|
||||
Username: string;
|
||||
/**
|
||||
* The staff member's rank.
|
||||
* 0 = not logged in.
|
||||
* 1 = janitor.
|
||||
* 2 = moderator.
|
||||
* 3 = administrator.
|
||||
*/
|
||||
Rank: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* An object representing a management action available to the current staff member
|
||||
*/
|
||||
interface StaffAction {
|
||||
/**
|
||||
* The GET key used when requesting /manage/<id>
|
||||
*/
|
||||
id?:string;
|
||||
/**
|
||||
* The title of the action, to be shown in the staff menu
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* The permission level required to access the action.
|
||||
* 0 = accessible by anyone.
|
||||
* 1 = user needs to be a janitor or higher.
|
||||
* 2 = user needs to be a moderator or higher.
|
||||
* 3 = user needs to be an administrator.
|
||||
*/
|
||||
perms: number;
|
||||
/**
|
||||
* The setting for how the request output is handled.
|
||||
* 0 = never JSON.
|
||||
* 1 = sometimes JSON depending on whether the `json` GET key is set to 1.
|
||||
* 2 = always JSON.
|
||||
*/
|
||||
jsonOutput: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of requesting /manage/actions
|
||||
*/
|
||||
declare var staffActions: StaffAction[];
|
||||
|
||||
/**
|
||||
* The menu shown when the Staff button on the top bar is clicked
|
||||
*/
|
||||
declare let $staffMenu: JQuery<HTMLElement>;
|
||||
|
||||
/**
|
||||
* Defaults to "/"
|
||||
*/
|
||||
declare let webroot:string;
|
13310
frontend/package-lock.json
generated
13310
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -2,23 +2,15 @@
|
|||
"name": "gochan.js",
|
||||
"version": "3.6.0",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"source": "js/gochan.js",
|
||||
"main": "../html/js/gochan.js",
|
||||
"targets": {
|
||||
"frontend": {
|
||||
"source": "js/gochan.js",
|
||||
"distDir": "../html/js/"
|
||||
}
|
||||
},
|
||||
"browserslist": "> 0.5%, last 2 versions, not dead",
|
||||
"main": "./ts/main.ts",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build-js": "parcel build --no-cache",
|
||||
"watch-js": "parcel watch --no-cache --no-hmr",
|
||||
"eslint": "eslint ./js/",
|
||||
"build-ts": "webpack --progress",
|
||||
"watch-ts": "webpack --progress -w",
|
||||
"eslint": "eslint ./ts/",
|
||||
"eslint-tests": "eslint ./tests/ --env node",
|
||||
"eslint-fix": "eslint --fix ./js/",
|
||||
"eslint-fix": "eslint --fix ./ts/",
|
||||
"tsc-check": "tsc --noEmit",
|
||||
"test": "jest --verbose",
|
||||
"build-sass": "sass --no-source-map sass:../html/css",
|
||||
"minify-sass": "sass --no-source-map sass:../html/css",
|
||||
|
@ -28,20 +20,27 @@
|
|||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"jquery": "^3.5.1",
|
||||
"jquery-ui": "^1.13.2"
|
||||
"jquery-ui": "^1.13.2",
|
||||
"path-browserify": "^1.0.1",
|
||||
"webstorage-polyfill": "^1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.16.11",
|
||||
"@jest/globals": "^27.4.6",
|
||||
"@babel/preset-env": "^7.21.5",
|
||||
"@babel/preset-typescript": "^7.21.5",
|
||||
"@jest/globals": "^29.5.0",
|
||||
"@types/jquery": "^3.3.38",
|
||||
"@types/jqueryui": "^1.12.16",
|
||||
"babel-jest": "^27.5.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.5",
|
||||
"eslint": "^8.18.0",
|
||||
"jest": "^27.4.7",
|
||||
"parcel": "^2.4.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"process": "^0.11.10",
|
||||
"jest": "^29.5.0",
|
||||
"jest-config": "^29.5.0",
|
||||
"jest-environment-jsdom": "^29.5.0",
|
||||
"sass": "^1.51.0",
|
||||
"yargs-parser": ">=5.0.0-security.0"
|
||||
"ts-jest": "^29.1.0",
|
||||
"ts-loader": "^9.4.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.0.4",
|
||||
"webpack": "^5.82.1",
|
||||
"webpack-cli": "^5.1.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,26 +1,25 @@
|
|||
/* global simpleHTML */
|
||||
'use strict';
|
||||
import {test, expect} from "@jest/globals";
|
||||
|
||||
import $ from "jquery";
|
||||
import "../js/vars";
|
||||
import "../ts/vars";
|
||||
import "./inittests";
|
||||
|
||||
import { applyBBCode, handleKeydown } from "../js/boardevents";
|
||||
import { applyBBCode, handleKeydown } from "../ts/boardevents";
|
||||
|
||||
document.documentElement.innerHTML = simpleHTML;
|
||||
|
||||
function doBBCode(keycode, text, start, end) {
|
||||
let $ta = $("<textarea/>");
|
||||
function doBBCode(keycode: number, text: string, start: number, end: number) {
|
||||
const $ta = $<HTMLTextAreaElement>("<textarea/>");
|
||||
$ta.text(text);
|
||||
let e = $.Event("keydown");
|
||||
const e = $.Event("keydown");
|
||||
e.ctrlKey = true;
|
||||
$ta[0].selectionStart = start;
|
||||
$ta[0].selectionEnd = end;
|
||||
e.keyCode = keycode;
|
||||
e.which = keycode;
|
||||
e.target = $ta[0];
|
||||
applyBBCode(e);
|
||||
$ta.first().trigger(e);
|
||||
applyBBCode(e as JQuery.KeyDownEvent);
|
||||
return $ta.text();
|
||||
}
|
||||
|
||||
|
@ -40,23 +39,23 @@ test("Tests BBCode events", () => {
|
|||
text = doBBCode(85, text, text.length, text.length);
|
||||
expect(text).toEqual("[?][s]strike[/s][/?][b]bold[/b][i]italics[/i][u][/u]");
|
||||
|
||||
let invalidKeyCode = doBBCode(0, text, 0, 1); // passes an invalid keycode to applyBBCode, no change
|
||||
const invalidKeyCode = doBBCode(0, text, 0, 1); // passes an invalid keycode to applyBBCode, no change
|
||||
expect(invalidKeyCode).toEqual(text);
|
||||
});
|
||||
|
||||
test("Tests proper form submission via JS", () => {
|
||||
let $form = $("form#postform");
|
||||
let text = doBBCode(83, "text", 0, 4);
|
||||
const $form = $("form#postform");
|
||||
const text = doBBCode(83, "text", 0, 4);
|
||||
$form.find("textarea#postmsg").text(text);
|
||||
let submitted = false;
|
||||
$form.on("submit", () => {
|
||||
submitted = true;
|
||||
return false;
|
||||
});
|
||||
let e = $.Event("keydown");
|
||||
const e = $.Event("keydown");
|
||||
e.ctrlKey = true;
|
||||
e.keyCode = 10;
|
||||
e.target = $form.find("textarea#postmsg")[0];
|
||||
handleKeydown(e);
|
||||
$form.find("textarea#postmsg").first().trigger(e);
|
||||
handleKeydown(e as JQuery.KeyDownEvent);
|
||||
expect(submitted).toBeTruthy();
|
||||
});
|
|
@ -1,37 +1,37 @@
|
|||
import { expect, test } from "@jest/globals";
|
||||
import { getBooleanCookie, getCookie, getNumberCookie, initCookies, setCookie } from "../js/cookies";
|
||||
import { getBooleanStorageVal, getJsonStorageVal, getNumberStorageVal, getStorageVal, setStorageVal } from "../js/storage";
|
||||
import { getBooleanCookie, getCookie, getNumberCookie, initCookies, setCookie } from "../ts/cookies";
|
||||
import { getBooleanStorageVal, getJsonStorageVal, getNumberStorageVal, getStorageVal, setStorageVal } from "../ts/storage";
|
||||
|
||||
global.webroot = "/";
|
||||
initCookies();
|
||||
|
||||
test("Test cookie types", () => {
|
||||
setCookie("name", "value", 100, "/");
|
||||
let value = getCookie("name");
|
||||
setCookie("name", "value", "100", "/");
|
||||
const value = getCookie("name");
|
||||
expect(value).toStrictEqual("value");
|
||||
|
||||
// test number storage
|
||||
setCookie("numberCookie", 32, 100, "/");
|
||||
let numberCookie = getNumberCookie("numberCookie");
|
||||
setCookie("numberCookie", "32", "100", "/");
|
||||
const numberCookie = getNumberCookie("numberCookie");
|
||||
expect(numberCookie).toStrictEqual(32);
|
||||
|
||||
setCookie("boolCookie", true, 100, "/");
|
||||
let boolCookie = getBooleanCookie("boolCookie");
|
||||
setCookie("boolCookie", "true", "100", "/");
|
||||
const boolCookie = getBooleanCookie("boolCookie");
|
||||
expect(boolCookie).toStrictEqual(true);
|
||||
|
||||
});
|
||||
|
||||
test("Test localStorage", () => {
|
||||
setStorageVal("name", "value");
|
||||
let value = getStorageVal("name");
|
||||
const value = getStorageVal("name");
|
||||
expect(value).toStrictEqual("value");
|
||||
|
||||
setStorageVal("numberVal", 33.2);
|
||||
let numberVal = getNumberStorageVal("numberVal");
|
||||
const numberVal = getNumberStorageVal("numberVal");
|
||||
expect(numberVal).toStrictEqual(33.2);
|
||||
|
||||
setStorageVal("boolVal", true);
|
||||
let boolVal = getBooleanStorageVal("boolVal");
|
||||
const boolVal = getBooleanStorageVal("boolVal");
|
||||
expect(boolVal).toStrictEqual(true);
|
||||
|
||||
setStorageVal("jsonVal", `{
|
||||
|
@ -39,7 +39,7 @@ test("Test localStorage", () => {
|
|||
"key2": 33,
|
||||
"aaa": [1,2,3]
|
||||
}`);
|
||||
let jsonVal = getJsonStorageVal("jsonVal");
|
||||
const jsonVal = getJsonStorageVal<{[k:string]:any}>("jsonVal", {});
|
||||
expect(jsonVal).toStrictEqual({
|
||||
"key1": "val1",
|
||||
"key2": 33,
|
|
@ -5,19 +5,19 @@ import $ from "jquery";
|
|||
* @param {string} type
|
||||
* @returns {number}
|
||||
*/
|
||||
function getCooldown(data, board, type) {
|
||||
function getCooldown(data: BoardsJSON, board: string, type: string) {
|
||||
for(const boardData of data.boards) {
|
||||
if(boardData.board != board) continue;
|
||||
return boardData.cooldowns[type];
|
||||
return (boardData.cooldowns as any)[type];
|
||||
}
|
||||
}
|
||||
|
||||
export async function getThreadCooldown(board) {
|
||||
export async function getThreadCooldown(board: string) {
|
||||
const boards = await $.getJSON(`${webroot}boards.json`);
|
||||
return getCooldown(boards, board, "threads");
|
||||
}
|
||||
|
||||
export async function getReplyCooldown(board) {
|
||||
export async function getReplyCooldown(board: string) {
|
||||
const boards = await $.getJSON(`${webroot}boards.json`);
|
||||
return getCooldown(boards, board, "replies");
|
||||
}
|
|
@ -2,20 +2,28 @@ import $ from "jquery";
|
|||
|
||||
/**
|
||||
* Returns true if the post has a lock icon without making a GET request
|
||||
* @param {JQuery<HTMLElement>} $elem the jQuery element of the post
|
||||
* @param $elem the jQuery element of the post
|
||||
*/
|
||||
export function isThreadLocked($elem) {
|
||||
export function isThreadLocked($elem: JQuery<HTMLElement>) {
|
||||
return $elem.find("span.status-icons img.locked-icon").length == 1;
|
||||
}
|
||||
|
||||
interface BoardLockJSON {
|
||||
board: string;
|
||||
thread: number;
|
||||
json: number;
|
||||
lock?: string;
|
||||
unlock?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a POST request to the server to lock or unlock a thread
|
||||
* @param {string} board The board dir of the thread to be (un)locked, e.g. "test2"
|
||||
* @param {number} op The post number of the top post in the thread
|
||||
* @param {boolean} lock If true, the thread will be locked, otherwise it will be unlocked
|
||||
* @param board The board dir of the thread to be (un)locked, e.g. "test2"
|
||||
* @param op The post number of the top post in the thread
|
||||
* @param lock If true, the thread will be locked, otherwise it will be unlocked
|
||||
*/
|
||||
export async function updateThreadLock(board, op, lock) {
|
||||
const data = {
|
||||
export async function updateThreadLock(board: string, op: number, lock: boolean) {
|
||||
const data: BoardLockJSON = {
|
||||
board: board,
|
||||
thread: op,
|
||||
json: 1
|
||||
|
@ -46,7 +54,7 @@ export async function updateThreadLock(board, op, lock) {
|
|||
$(`div#op${op} img.locked-icon`).remove();
|
||||
$lockOpt.text("Lock thread");
|
||||
}
|
||||
}).catch((data, status, xhr) => {
|
||||
}).catch((data: any, _status: any, xhr: any) => {
|
||||
if(data.responseJSON !== undefined && data.responseJSON.message !== undefined) {
|
||||
alert(`Error updating thread /${board}/${op} lock status: ${data.responseJSON.message}`);
|
||||
} else {
|
|
@ -4,7 +4,12 @@ import $ from "jquery";
|
|||
|
||||
import { currentBoard, currentThread } from "../postinfo";
|
||||
|
||||
const nullBoardsList = {
|
||||
interface BoardsList {
|
||||
boards: any[];
|
||||
currentBoard: string;
|
||||
}
|
||||
|
||||
const nullBoardsList: BoardsList = {
|
||||
boards: [],
|
||||
currentBoard: ""
|
||||
};
|
||||
|
@ -16,7 +21,7 @@ export async function getBoardList() {
|
|||
cache: false,
|
||||
dataType: "json",
|
||||
success: (d2 => {}),
|
||||
error: function(err, status, statusText) {
|
||||
error: function(_err, _status, statusText) {
|
||||
console.error("Error getting board list: " + statusText);
|
||||
return nullBoardsList;
|
||||
},
|
||||
|
@ -51,17 +56,17 @@ export async function getThread(board = "", thread = 0) {
|
|||
if(board != "")
|
||||
threadInfo.board = board;
|
||||
if(thread > 0)
|
||||
threadInfo.thread = thread;
|
||||
threadInfo.id = thread;
|
||||
|
||||
if(threadInfo.board === "") {
|
||||
return Promise.reject("not in a board");
|
||||
}
|
||||
if(threadInfo.thread < 1) {
|
||||
if(threadInfo.id < 1) {
|
||||
return Promise.reject("not in a thread");
|
||||
}
|
||||
|
||||
const data = await $.ajax({
|
||||
url: `${webroot}${threadInfo.board}/res/${threadInfo.thread}.json`,
|
||||
url: `${webroot}${threadInfo.board}/res/${threadInfo.id}.json`,
|
||||
cache: false,
|
||||
dataType: "json",
|
||||
error: function (err, status, statusText) {
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import $ from "jquery";
|
||||
|
||||
export async function getThreadJSON(threadID, board) {
|
||||
export async function getThreadJSON(threadID: number, board: string) {
|
||||
return $.ajax({
|
||||
url: `${webroot}${board}/res/${threadID}.json`,
|
||||
cache: false,
|
|
@ -2,7 +2,7 @@ import $ from "jquery";
|
|||
|
||||
import { openQR } from "./dom/qr";
|
||||
|
||||
export function handleKeydown(e) {
|
||||
export function handleKeydown(e: JQuery.KeyDownEvent) {
|
||||
let ta = e.target;
|
||||
let isPostMsg = ta.nodeName == "TEXTAREA" && ta.name == "postmsg";
|
||||
let inForm = ta.form != undefined;
|
||||
|
@ -13,7 +13,7 @@ export function handleKeydown(e) {
|
|||
}
|
||||
}
|
||||
|
||||
export function applyBBCode(e) {
|
||||
export function applyBBCode(e: JQuery.KeyDownEvent) {
|
||||
let tag = "";
|
||||
switch(e.keyCode) {
|
||||
case 10: // Enter key
|
|
@ -5,7 +5,7 @@ const YEAR_IN_MS = 365*24*60*60*1000;
|
|||
/**
|
||||
* @param {string} name
|
||||
*/
|
||||
export function getCookie(name, defaultVal = "") {
|
||||
export function getCookie(name: string, defaultVal = "") {
|
||||
let val = defaultVal;
|
||||
let cookieArr = document.cookie.split("; ");
|
||||
|
||||
|
@ -22,11 +22,11 @@ export function getCookie(name, defaultVal = "") {
|
|||
return val;
|
||||
}
|
||||
|
||||
export function getNumberCookie(name, defaultVal = "0") {
|
||||
export function getNumberCookie(name: string, defaultVal = "0") {
|
||||
return parseFloat(getCookie(name, defaultVal));
|
||||
}
|
||||
|
||||
export function getBooleanCookie(name, defaultVal = "true") {
|
||||
export function getBooleanCookie(name: string, defaultVal = "true") {
|
||||
return getCookie(name, defaultVal) == "true";
|
||||
}
|
||||
|
||||
|
@ -42,15 +42,10 @@ function randomPassword(len = 8) {
|
|||
|
||||
/**
|
||||
* Set a cookie
|
||||
* @param {string} name
|
||||
* @param {string} value
|
||||
* @param {string} expires
|
||||
*/
|
||||
export function setCookie(name, value, expires, root) {
|
||||
if(root === undefined || root === "")
|
||||
root = webroot;
|
||||
export function setCookie(name: string, value: string, expires = "", root = webroot) {
|
||||
let expiresStr = "";
|
||||
if(expires === undefined || expires == "") {
|
||||
if(expires == "") {
|
||||
expiresStr = ";expires=";
|
||||
let d = new Date();
|
||||
d.setTime(d.getTime() + YEAR_IN_MS);
|
||||
|
@ -59,7 +54,7 @@ export function setCookie(name, value, expires, root) {
|
|||
document.cookie = `${name}=${value}${expiresStr};path=${root};sameSite=strict`;
|
||||
}
|
||||
|
||||
$(() => {
|
||||
export function initCookies() {
|
||||
let pwCookie = getCookie("password");
|
||||
if(pwCookie == "") {
|
||||
pwCookie = randomPassword();
|
||||
|
@ -69,4 +64,6 @@ $(() => {
|
|||
$("input[name=postemail]").val(getCookie("email"));
|
||||
$("input[name=postpassword]").val(pwCookie);
|
||||
$("input[name=delete-password]").val(pwCookie);
|
||||
});
|
||||
}
|
||||
|
||||
$(initCookies);
|
|
@ -1,5 +1,12 @@
|
|||
import $ from "jquery";
|
||||
|
||||
interface BannerProps {
|
||||
src: string;
|
||||
alt: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export function setPageBanner() {
|
||||
const slashArr = location.pathname.split("/");
|
||||
const board = (slashArr.length >= 2)?slashArr[1]:"";
|
||||
|
@ -14,7 +21,7 @@ export function setPageBanner() {
|
|||
if(!data || data.Filename == undefined || data.Filename == "") {
|
||||
return; // no banners :(
|
||||
}
|
||||
const props = {
|
||||
const props: BannerProps = {
|
||||
src: `${webroot}static/banners/${data.Filename}`,
|
||||
alt: "Page banner"
|
||||
};
|
|
@ -2,24 +2,25 @@ import $ from "jquery";
|
|||
|
||||
const emptyFunc = () => {};
|
||||
|
||||
export function removeLightbox(...customs) {
|
||||
export function removeLightbox(...customs: any) {
|
||||
$(".lightbox, .lightbox-bg").remove();
|
||||
for(const custom of customs) {
|
||||
$(custom).remove();
|
||||
}
|
||||
}
|
||||
|
||||
export function showLightBox(title, innerHTML) {
|
||||
export function showLightBox(title: string, innerHTML: string) {
|
||||
$(document.body).prepend(
|
||||
`<div class="lightbox-bg"></div><div class="lightbox"><div class="lightbox-title">${title}<a href="javascript:;" class="lightbox-x">X</a><hr /></div>${innerHTML}</div>`
|
||||
);
|
||||
$("a.lightbox-x, .lightbox-bg").on("click", removeLightbox);
|
||||
}
|
||||
|
||||
function simpleLightbox(properties = {}, customCSS = {}, $elements = []) {
|
||||
|
||||
function simpleLightbox(properties: any = {}, customCSS: any = {}, $elements: any[] = []) {
|
||||
if(properties["class"] === undefined)
|
||||
properties["class"] = "lightbox";
|
||||
let defaultCSS = {
|
||||
let defaultCSS: {[key: string]: string} = {
|
||||
"display": "inline-block",
|
||||
"top": "50%",
|
||||
"left": "50%",
|
||||
|
@ -44,7 +45,7 @@ function simpleLightbox(properties = {}, customCSS = {}, $elements = []) {
|
|||
return $box;
|
||||
}
|
||||
|
||||
export function promptLightbox(defVal = "", isMasked = false, onOk = emptyFunc, title = "") {
|
||||
export function promptLightbox(defVal = "", isMasked = false, onOk: ($el:JQuery<HTMLElement>, val: any) => any = emptyFunc, title = "") {
|
||||
let $ok = $("<button/>").prop({
|
||||
"id": "okbutton"
|
||||
}).text("OK");
|
||||
|
@ -81,7 +82,7 @@ export function promptLightbox(defVal = "", isMasked = false, onOk = emptyFunc,
|
|||
return $lb;
|
||||
}
|
||||
|
||||
export function alertLightbox(msg = "", title = location.hostname, onOk = emptyFunc) {
|
||||
export function alertLightbox(msg = "", title = location.hostname, onOk: ($el: JQuery<HTMLElement>) => any = emptyFunc) {
|
||||
let $ok = $("<button/>").prop({
|
||||
"id": "okbutton"
|
||||
}).text("OK");
|
|
@ -10,7 +10,7 @@ import { updateThreadLock } from "../api/management";
|
|||
|
||||
const idRe = /^((reply)|(op))(\d+)/;
|
||||
|
||||
function editPost(id, _board) {
|
||||
function editPost(id: number, _board: string) {
|
||||
let cookiePass = getCookie("password");
|
||||
promptLightbox(cookiePass, true, (_jq, inputData) => {
|
||||
$("input[type=checkbox]").prop("checked", false);
|
||||
|
@ -20,7 +20,7 @@ function editPost(id, _board) {
|
|||
}, "Edit post");
|
||||
}
|
||||
|
||||
function moveThread(id, _board) {
|
||||
function moveThread(id: number, _board: string) {
|
||||
let cookiePass = getCookie("password");
|
||||
promptLightbox(cookiePass, true, (_jq, inputData) => {
|
||||
$("input[type=checkbox]").prop("checked", false);
|
||||
|
@ -30,17 +30,17 @@ function moveThread(id, _board) {
|
|||
}, "Move thread");
|
||||
}
|
||||
|
||||
function reportPost(id, board) {
|
||||
function reportPost(id: number, board: string) {
|
||||
promptLightbox("", false, ($lb, reason) => {
|
||||
if(reason == "" || reason === null) return;
|
||||
let xhrFields = {
|
||||
let xhrFields: {[k: string]: string} = {
|
||||
board: board,
|
||||
report_btn: "Report",
|
||||
reason: reason,
|
||||
json: "1"
|
||||
};
|
||||
xhrFields[`check${id}`] = "on";
|
||||
$.post(webroot + "util", xhrFields).fail(data => {
|
||||
$.post(webroot + "util", xhrFields).fail((data: any) => {
|
||||
let errStr = data.error;
|
||||
if(errStr === undefined)
|
||||
errStr = data.statusText;
|
||||
|
@ -51,11 +51,11 @@ function reportPost(id, board) {
|
|||
} else {
|
||||
alertLightbox("Report sent", "Success");
|
||||
}
|
||||
}, "json");
|
||||
}, "json" as any);
|
||||
}, "Report post");
|
||||
}
|
||||
|
||||
function deletePostFile(id) {
|
||||
function deletePostFile(id: number) {
|
||||
let $elem = $(`div#op${id}.op-post, div#reply${id}.reply`);
|
||||
if($elem.length === 0) return;
|
||||
$elem.find(".file-info,.upload-container").remove();
|
||||
|
@ -67,7 +67,7 @@ function deletePostFile(id) {
|
|||
$(document).trigger("deletePostFile", id);
|
||||
}
|
||||
|
||||
function deletePostElement(id) {
|
||||
function deletePostElement(id: number) {
|
||||
let $elem = $(`div#op${id}.op-post`);
|
||||
if($elem.length > 0) {
|
||||
$elem.parent().next().remove(); // also removes the <hr> element after
|
||||
|
@ -78,10 +78,10 @@ function deletePostElement(id) {
|
|||
}
|
||||
}
|
||||
|
||||
function deletePost(id, board, fileOnly) {
|
||||
function deletePost(id: number, board: string, fileOnly = false) {
|
||||
let cookiePass = getCookie("password");
|
||||
promptLightbox(cookiePass, true, ($lb, password) => {
|
||||
let xhrFields = {
|
||||
let xhrFields: {[k: string]: any} = {
|
||||
board: board,
|
||||
boardid: $("input[name=boardid]").val(),
|
||||
delete_btn: "Delete",
|
||||
|
@ -92,8 +92,8 @@ function deletePost(id, board, fileOnly) {
|
|||
if(fileOnly) {
|
||||
xhrFields.fileonly = "on";
|
||||
}
|
||||
$.post(webroot + "util", xhrFields).fail(data => {
|
||||
if(data !== "");
|
||||
$.post(webroot + "util", xhrFields).fail((data: any) => {
|
||||
if(data !== "")
|
||||
alertLightbox(`Delete failed: ${data.error}`, "Error");
|
||||
}).done(data => {
|
||||
if(data.error == undefined || data == "") {
|
||||
|
@ -112,14 +112,14 @@ function deletePost(id, board, fileOnly) {
|
|||
console.error(data);
|
||||
}
|
||||
}
|
||||
}, "json");
|
||||
}, "json" as any);
|
||||
}, "Password");
|
||||
}
|
||||
|
||||
function handleActions(action, postIDStr) {
|
||||
function handleActions(action: string, postIDStr: string) {
|
||||
let idArr = idRe.exec(postIDStr);
|
||||
if(!idArr) return;
|
||||
let postID = idArr[4];
|
||||
let postID = Number.parseInt(idArr[4]);
|
||||
let board = currentBoard();
|
||||
switch(action) {
|
||||
case "Watch thread":
|
||||
|
@ -154,7 +154,7 @@ function handleActions(action, postIDStr) {
|
|||
break;
|
||||
case "Delete thread":
|
||||
case "Delete post":
|
||||
deletePost(postID, board);
|
||||
deletePost(postID, board, false);
|
||||
break;
|
||||
// manage stuff
|
||||
case "Lock thread":
|
||||
|
@ -166,18 +166,18 @@ function handleActions(action, postIDStr) {
|
|||
updateThreadLock(board, postID, false);
|
||||
break;
|
||||
case "Posts from this IP":
|
||||
getPostInfo(postID).then(info => {
|
||||
getPostInfo(postID).then((info: any) => {
|
||||
window.open(`${webroot}manage/ipsearch?limit=100&ip=${info.ip}`);
|
||||
}).catch(reason => {
|
||||
}).catch((reason: JQuery.jqXHR) => {
|
||||
alertLightbox(`Failed getting post IP: ${reason.statusText}`, "Error");
|
||||
});
|
||||
break;
|
||||
case "Ban filename":
|
||||
case "Ban file checksum": {
|
||||
let banType = (action == "Ban filename")?"filename":"checksum";
|
||||
getPostInfo(postID).then(info => {
|
||||
getPostInfo(postID).then((info: any) => {
|
||||
return banFile(banType, info.originalFilename, info.checksum, `Added from post dropdown for post /${board}/${postID}`);
|
||||
}).then(result => {
|
||||
}).then((result: any) => {
|
||||
if(result.error !== undefined && result.error != "") {
|
||||
if(result.message !== undefined)
|
||||
alertLightbox(`Failed applying ${banType} ban: ${result.message}`, "Error");
|
||||
|
@ -186,7 +186,7 @@ function handleActions(action, postIDStr) {
|
|||
} else {
|
||||
alertLightbox(`Successfully applied ${banType} ban`, "Success");
|
||||
}
|
||||
}).catch((reason) => {
|
||||
}).catch((reason: any) => {
|
||||
let messageDetail = "";
|
||||
try {
|
||||
const responseJSON = JSON.parse(reason.responseText);
|
||||
|
@ -204,7 +204,7 @@ function handleActions(action, postIDStr) {
|
|||
}
|
||||
}
|
||||
|
||||
export function addPostDropdown($post) {
|
||||
export function addPostDropdown($post: JQuery<HTMLElement>) {
|
||||
if($post.find("select.post-actions").length > 0)
|
||||
return $post;
|
||||
let $postInfo = $post.find("label.post-info");
|
||||
|
@ -216,7 +216,7 @@ export function addPostDropdown($post) {
|
|||
class: "post-actions",
|
||||
id: postID
|
||||
}).append("<option disabled selected>Actions</option>");
|
||||
let idNum = idRe.exec(postID)[4];
|
||||
let idNum = Number.parseInt(idRe.exec(postID)[4]);
|
||||
if(isOP) {
|
||||
if(isThreadWatched(idNum, currentBoard())) {
|
||||
$ddownMenu.append("<option>Unwatch thread</option>");
|
||||
|
@ -233,7 +233,7 @@ export function addPostDropdown($post) {
|
|||
`<option>Delete ${threadPost}</option>`,
|
||||
).insertAfter($postInfo)
|
||||
.on("change", _e => {
|
||||
handleActions($ddownMenu.val(), postID);
|
||||
handleActions($ddownMenu.val() as string, postID);
|
||||
$ddownMenu.val("Actions");
|
||||
});
|
||||
if(hasUpload)
|
||||
|
@ -247,12 +247,12 @@ export function addPostDropdown($post) {
|
|||
|
||||
$(() => {
|
||||
$(document).on("watchThread", (_e, thread) => {
|
||||
$(`div#op${thread.id} select.post-actions > option`).each((i, el) => {
|
||||
$<HTMLOptionElement>(`div#op${thread.id} select.post-actions > option`).each((i, el) => {
|
||||
if(el.text == "Watch thread")
|
||||
el.text = "Unwatch thread";
|
||||
});
|
||||
}).on("unwatchThread", (_e, threadID) => {
|
||||
$(`div#op${threadID} select.post-actions > option`).each((i, el) => {
|
||||
$<HTMLOptionElement>(`div#op${threadID} select.post-actions > option`).each((i, el) => {
|
||||
if(el.text == "Unwatch thread")
|
||||
el.text = "Watch thread";
|
||||
});
|
|
@ -1,9 +1,3 @@
|
|||
/**
|
||||
* @typedef { import("../types/gochan").BoardThread } BoardThread
|
||||
* @typedef { import("../types/gochan").ThreadPost } ThreadPost
|
||||
*/
|
||||
|
||||
|
||||
import $ from "jquery";
|
||||
import { extname } from "path";
|
||||
import { formatDateString, formatFileSize } from "../formatting";
|
||||
|
@ -11,10 +5,8 @@ import { getThumbFilename } from "../postinfo";
|
|||
|
||||
/**
|
||||
* creates an element from the given post data
|
||||
* @param {ThreadPost} post
|
||||
* @param {string} boardDir
|
||||
*/
|
||||
export function createPostElement(post, boardDir, elementClass = "inlinepostprev") {
|
||||
export function createPostElement(post: ThreadPost, boardDir: string, elementClass = "inlinepostprev") {
|
||||
let $post = $("<div/>")
|
||||
.prop({
|
||||
id: `reply${post.no}`,
|
||||
|
@ -108,14 +100,8 @@ export function createPostElement(post, boardDir, elementClass = "inlinepostprev
|
|||
return $post;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {JQuery<HTMLElement>} elem
|
||||
*/
|
||||
export function shrinkOriginalFilenames(elem) {
|
||||
if(elem == undefined)
|
||||
elem = $(document.body);
|
||||
|
||||
elem.find("a.file-orig").each((i, el) => {
|
||||
export function shrinkOriginalFilenames(elem = $(document.body)) {
|
||||
elem.find<HTMLAnchorElement>("a.file-orig").each((i, el) => {
|
||||
let ext = extname(el.innerText);
|
||||
let noExt = el.innerText.slice(0,el.innerText.lastIndexOf("."));
|
||||
if(noExt.length > 16) {
|
|
@ -6,9 +6,9 @@ const emptyFunc = () => {};
|
|||
|
||||
/**
|
||||
* isPostVisible returns true if the post exists and is visible, otherwise false
|
||||
* @param {number} id the id of the post
|
||||
* @param id the id of the post
|
||||
*/
|
||||
export function isPostVisible(id) {
|
||||
export function isPostVisible(id: number) {
|
||||
let $post = $(`div#op${id}.op-post,div#reply${id}.reply`);
|
||||
if($post.length === 0)
|
||||
return false;
|
||||
|
@ -18,11 +18,11 @@ export function isPostVisible(id) {
|
|||
/**
|
||||
* setPostVisibility sets the visibility of the post with the given ID. It returns true if it finds
|
||||
* a post or thread with the given ID, otherwise false
|
||||
* @param {number} id the id of the post to be toggled
|
||||
* @param {boolean} visibility the visibility to be set
|
||||
* @param id the id of the post to be toggled
|
||||
* @param visibility the visibility to be set
|
||||
* @param onComplete called after the visibility is set
|
||||
*/
|
||||
export function setPostVisibility(id, visibility, onComplete = emptyFunc) {
|
||||
export function setPostVisibility(id: number|string, visibility: boolean, onComplete = emptyFunc) {
|
||||
let $post = $(`div#op${id}.op-post, div#reply${id}.reply`);
|
||||
|
||||
if($post.length === 0)
|
||||
|
@ -32,7 +32,7 @@ export function setPostVisibility(id, visibility, onComplete = emptyFunc) {
|
|||
let hiddenStorage = getStorageVal("hiddenposts", "").split(",");
|
||||
if(visibility) {
|
||||
$toSet.show(0, onComplete);
|
||||
$post.find("select.post-actions option").each((e, elem) => {
|
||||
$post.find<HTMLOptionElement>("select.post-actions option").each((e, elem) => {
|
||||
elem.text = elem.text.replace("Show", "Hide");
|
||||
});
|
||||
$backlink.text(id);
|
||||
|
@ -43,11 +43,11 @@ export function setPostVisibility(id, visibility, onComplete = emptyFunc) {
|
|||
setStorageVal("hiddenposts", newHidden.join(","));
|
||||
} else {
|
||||
$toSet.hide(0, onComplete);
|
||||
$post.find("select.post-actions option").each((e, elem) => {
|
||||
$post.find<HTMLOptionElement>("select.post-actions option").each((e, elem) => {
|
||||
elem.text = elem.text.replace("Hide", "Show");
|
||||
});
|
||||
$backlink.text(`${id} (hidden)`);
|
||||
if(hiddenStorage.indexOf(id) == -1) hiddenStorage.push(id);
|
||||
if(hiddenStorage.indexOf(id as string) == -1) hiddenStorage.push(id as string);
|
||||
setStorageVal("hiddenposts", hiddenStorage.join(","));
|
||||
}
|
||||
|
||||
|
@ -57,10 +57,10 @@ export function setPostVisibility(id, visibility, onComplete = emptyFunc) {
|
|||
/**
|
||||
* setThreadVisibility sets the visibility of the thread with the given ID, as well as its replies.
|
||||
* It returns true if it finds a thread with the given ID, otherwise false
|
||||
* @param {number} id the id of the thread to be hidden
|
||||
* @param {boolean} visibility the visibility to be set
|
||||
* @param id the id of the thread to be hidden
|
||||
* @param visibility the visibility to be set
|
||||
*/
|
||||
export function setThreadVisibility(opID, visibility) {
|
||||
export function setThreadVisibility(opID: number|string, visibility: boolean) {
|
||||
let $thread = $(`div#op${opID}.op-post`).parent(".thread");
|
||||
if($thread.length === 0) return false;
|
||||
return setPostVisibility(opID, visibility, () => {
|
|
@ -18,10 +18,7 @@ import { getReplyCooldown, getThreadCooldown } from "../api/cooldowns";
|
|||
import { getUploadFilename, updateUploadImage } from "./uploaddata";
|
||||
import { alertLightbox } from "./lightbox";
|
||||
|
||||
/**
|
||||
* @type {JQuery<HTMLElement>}
|
||||
*/
|
||||
export let $qr = null;
|
||||
export let $qr: JQuery<HTMLElement> = null;
|
||||
let threadCooldown = 0;
|
||||
let replyCooldown = 0;
|
||||
|
||||
|
@ -37,14 +34,14 @@ const qrTitleBar =
|
|||
|
||||
|
||||
function resetSubmitButtonText() {
|
||||
if(currentThread().thread < 1) {
|
||||
if(currentThread().id < 1) {
|
||||
setSubmitButtonText("New Thread");
|
||||
} else {
|
||||
setSubmitButtonText("Reply");
|
||||
}
|
||||
}
|
||||
|
||||
function setSubmitButtonText(text) {
|
||||
function setSubmitButtonText(text: string) {
|
||||
$qr.find("input[type=submit]").attr("value", text);
|
||||
}
|
||||
|
||||
|
@ -81,7 +78,7 @@ function qrUploadChange() {
|
|||
|
||||
function setButtonTimeout(prefix = "", cooldown = 5) {
|
||||
let currentSeconds = cooldown;
|
||||
let interval = 0;
|
||||
let interval: NodeJS.Timer;
|
||||
const timeoutCB = () => {
|
||||
if(currentSeconds == 0) {
|
||||
setSubmitButtonEnabled(true);
|
||||
|
@ -206,17 +203,17 @@ export function initQR() {
|
|||
openQR();
|
||||
updateUploadImage($qrbuttons.find("input#imagefile"), qrUploadChange);
|
||||
resetSubmitButtonText();
|
||||
if(currentThread().thread < 1) {
|
||||
if(currentThread().id < 1) {
|
||||
$("form#qrpostform").on("submit", function(_e) {
|
||||
copyCaptchaResponse($(this));
|
||||
});
|
||||
return;
|
||||
}
|
||||
$postform.on("submit", function(e) {
|
||||
let $form = $(this);
|
||||
let $form = $<HTMLFormElement>(this as HTMLFormElement);
|
||||
e.preventDefault();
|
||||
copyCaptchaResponse($form);
|
||||
let data = new FormData(this);
|
||||
let data = new FormData(this as HTMLFormElement);
|
||||
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
|
@ -231,7 +228,7 @@ export function initQR() {
|
|||
return;
|
||||
}
|
||||
clearQR();
|
||||
let cooldown = (currentThread().thread > 0)?replyCooldown:threadCooldown;
|
||||
let cooldown = (currentThread().id > 0)?replyCooldown:threadCooldown;
|
||||
setButtonTimeout("", cooldown);
|
||||
updateThread().then(clearQR).then(() => {
|
||||
let persist = getBooleanStorageVal("persistentqr", false);
|
||||
|
@ -247,7 +244,7 @@ export function initQR() {
|
|||
});
|
||||
}
|
||||
|
||||
function copyCaptchaResponse($copyToForm) {
|
||||
function copyCaptchaResponse($copyToForm: JQuery<HTMLElement>) {
|
||||
let $captchaResp = $("textarea[name=h-captcha-response]");
|
||||
if($captchaResp.length > 0) {
|
||||
$("<textarea/>").prop({
|
|
@ -2,24 +2,28 @@ import $ from "jquery";
|
|||
|
||||
import { getBooleanStorageVal } from "../storage";
|
||||
|
||||
/**
|
||||
* @type {JQuery<HTMLElement>}
|
||||
*/
|
||||
export let $topbar = null;
|
||||
export let $topbar:JQuery<HTMLElement> = null;
|
||||
export let topbarHeight = 32;
|
||||
|
||||
interface BeforeAfter {
|
||||
before?: any;
|
||||
after?: any;
|
||||
}
|
||||
/**
|
||||
* TopBarButton A button to be added to the right side of the top bar
|
||||
*/
|
||||
export class TopBarButton {
|
||||
title: string;
|
||||
buttonAction: ()=>any;
|
||||
button: JQuery<HTMLLinkElement>;
|
||||
/**
|
||||
* @param {string} title The text shown on the button
|
||||
* @param {()=>any} action The function executed when the button is clicked
|
||||
* @param title The text shown on the button
|
||||
* @param action The function executed when the button is clicked
|
||||
*/
|
||||
constructor(title, action = () => {}, beforeAfter = {}) {
|
||||
constructor(title: string, action: ()=>any = ()=>{}, beforeAfter:BeforeAfter = {}) {
|
||||
this.title = title;
|
||||
this.buttonAction = action;
|
||||
this.button = $("<a/>").prop({
|
||||
this.button = $<HTMLLinkElement>("<a/>").prop({
|
||||
"href": "javascript:;",
|
||||
"class": "dropdown-button",
|
||||
"id": title.toLowerCase()
|
17
frontend/ts/dom/uploaddata.ts
Normal file
17
frontend/ts/dom/uploaddata.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
let noop = ()=>{};
|
||||
|
||||
export function updateUploadImage($elem: JQuery<HTMLElement>, onLoad = noop) {
|
||||
if($elem.length == 0) return;
|
||||
$elem[0].onchange = function() {
|
||||
let img = new Image();
|
||||
img.src = URL.createObjectURL((this as any).files[0]);
|
||||
img.onload = onLoad;
|
||||
};
|
||||
}
|
||||
|
||||
export function getUploadFilename(): string {
|
||||
let elem = document.getElementById("imagefile") as HTMLInputElement;
|
||||
if(elem === null) return "";
|
||||
if(elem.files === undefined || elem.files.length < 1) return "";
|
||||
return elem.files[0].name;
|
||||
}
|
|
@ -1,17 +1,16 @@
|
|||
/**
|
||||
* Formats the timestamp strings from JSON into a more readable format
|
||||
* @param {string} dateStr timestamp string, assumed to be in ISO Date-Time format
|
||||
* @param dateStr timestamp string, assumed to be in ISO Date-Time format
|
||||
*/
|
||||
export function formatDateString(dateStr) {
|
||||
export function formatDateString(dateStr: string) {
|
||||
let date = new Date(dateStr);
|
||||
return date.toDateString() + ", " + date.toLocaleTimeString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the given number of bytes into an easier to read filesize
|
||||
* @param {number} size
|
||||
*/
|
||||
export function formatFileSize(size) {
|
||||
export function formatFileSize(size: number) {
|
||||
if(size < 1000) {
|
||||
return `${size} B`;
|
||||
} else if(size <= 100000) {
|
|
@ -1,8 +1,3 @@
|
|||
/**
|
||||
* @typedef { import("../types/gochan").StaffAction } StaffAction
|
||||
* @typedef { import("../types/gochan").StaffInfo } StaffInfo
|
||||
*/
|
||||
|
||||
import $ from 'jquery';
|
||||
|
||||
import { alertLightbox } from "../dom/lightbox";
|
||||
|
@ -11,10 +6,7 @@ import "./sections";
|
|||
import "./filebans";
|
||||
import { isThreadLocked } from '../api/management';
|
||||
|
||||
/**
|
||||
* @type {StaffInfo}
|
||||
*/
|
||||
const notAStaff = {
|
||||
const notAStaff: StaffInfo = {
|
||||
ID: 0,
|
||||
Username: "",
|
||||
Rank: 0
|
||||
|
@ -22,29 +14,22 @@ const notAStaff = {
|
|||
|
||||
const reportsTextRE = /^Reports( \(\d+\))?/;
|
||||
|
||||
/**
|
||||
* @type StaffAction[]
|
||||
*/
|
||||
export let staffActions = [];
|
||||
export let staffActions: StaffAction[] = [];
|
||||
export let staffInfo = notAStaff;
|
||||
let loginChecked = false;
|
||||
|
||||
/**
|
||||
* @type {JQuery<HTMLElement>}
|
||||
* The menu shown when the Staff button on the top bar is clicked
|
||||
*/
|
||||
let $staffMenu = null;
|
||||
/**
|
||||
* @type {TopBarButton}
|
||||
* A button that opens $staffMenu
|
||||
*/
|
||||
let $staffBtn = null;
|
||||
let $staffMenu: JQuery<HTMLElement> = null;
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} dropdown
|
||||
* @param {string} item
|
||||
* A button that opens $staffMenu
|
||||
*/
|
||||
function dropdownHasItem(dropdown, item) {
|
||||
let $staffBtn: TopBarButton = null;
|
||||
|
||||
|
||||
function dropdownHasItem(dropdown: any, item: string) {
|
||||
return [...dropdown.children].filter(v => v.text === item).length > 0;
|
||||
}
|
||||
|
||||
|
@ -80,8 +65,19 @@ function setupManagementEvents() {
|
|||
});
|
||||
}
|
||||
|
||||
export function banFile(banType, filename, checksum, staffNote = "") {
|
||||
let xhrFields = {
|
||||
interface BanFileJSON {
|
||||
bantype: string;
|
||||
board: string;
|
||||
json: number;
|
||||
staffnote: string;
|
||||
filename?: string;
|
||||
dofilenameban?: string;
|
||||
checksum?: string;
|
||||
dochecksumban?: string;
|
||||
}
|
||||
|
||||
export function banFile(banType: string, filename: string, checksum: string, staffNote = "") {
|
||||
const xhrFields: BanFileJSON = {
|
||||
bantype: banType,
|
||||
board: "",
|
||||
staffnote: staffNote,
|
||||
|
@ -150,7 +146,7 @@ export async function getStaffInfo() {
|
|||
dataType: "json"
|
||||
}).catch(() => {
|
||||
return notAStaff;
|
||||
}).then((info) => {
|
||||
}).then((info: any) => {
|
||||
if(info.error)
|
||||
return notAStaff;
|
||||
staffInfo = info;
|
||||
|
@ -158,7 +154,7 @@ export async function getStaffInfo() {
|
|||
});
|
||||
}
|
||||
|
||||
export async function getPostInfo(id) {
|
||||
export async function getPostInfo(id: number) {
|
||||
return $.ajax({
|
||||
method: "GET",
|
||||
url: `${webroot}manage/postinfo`,
|
||||
|
@ -189,26 +185,25 @@ export function banSelectedPost() {
|
|||
let postID = 0;
|
||||
for(let i = 0; i < checks.length; i++) {
|
||||
if(checks[i].id.indexOf("check") === 0) {
|
||||
postID = checks[i].id.replace("check", "");
|
||||
postID = Number.parseInt(checks[i].id.replace("check", ""));
|
||||
break;
|
||||
}
|
||||
}
|
||||
window.location = `${webroot}manage/bans?dir=${boardDir}&postid=${postID}`;
|
||||
window.location.pathname = `${webroot}manage/bans?dir=${boardDir}&postid=${postID}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function for creating a menu item
|
||||
* @param {StaffAction} action
|
||||
*/
|
||||
function menuItem(action, isCategory = false) {
|
||||
return isCategory ? $("<div/>").append($("<b/>").text(action)) : $("<div/>").append(
|
||||
function menuItem(action: StaffAction|string, isCategory = false) {
|
||||
return isCategory ? $("<div/>").append($("<b/>").text(action as string)) : $("<div/>").append(
|
||||
$("<a/>").prop({
|
||||
href: `${webroot}manage/${action.id}`
|
||||
}).text(action.title)
|
||||
href: `${webroot}manage/${(action as StaffAction).id}`
|
||||
}).text((action as StaffAction).title)
|
||||
);
|
||||
}
|
||||
|
||||
function getAction(id) {
|
||||
function getAction(id: string) {
|
||||
for(const action of staffActions) {
|
||||
if(action.id == id) {
|
||||
return action;
|
||||
|
@ -216,11 +211,7 @@ function getAction(id) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {StaffAction} action
|
||||
* @param {number} perms
|
||||
*/
|
||||
function filterAction(action, perms) {
|
||||
function filterAction(action: StaffAction, perms: number) {
|
||||
return action.title != "Logout"
|
||||
&& action.title != "Dashboard"
|
||||
&& action.jsonOutput < 2
|
||||
|
@ -230,7 +221,7 @@ function filterAction(action, perms) {
|
|||
/**
|
||||
* Creates a list of staff actions accessible to the user if they are logged in.
|
||||
* It is shown when the user clicks the Staff button
|
||||
* @param {StaffInfo} staff an object representing the staff's username and rank
|
||||
* @param staff an object representing the staff's username and rank
|
||||
*/
|
||||
export function createStaffMenu(staff = staffInfo) {
|
||||
let rank = staff.Rank;
|
||||
|
@ -278,7 +269,7 @@ function createStaffButton() {
|
|||
});
|
||||
}
|
||||
|
||||
function updateReports(reports) {
|
||||
function updateReports(reports: any[]) {
|
||||
// append " (#)" to the Reports link, replacing # with the number of reports
|
||||
$staffMenu.find("a").each((e, elem) => {
|
||||
if(elem.text.search(reportsTextRE) != 0) return;
|
|
@ -7,12 +7,9 @@ import "jquery-ui/ui/data";
|
|||
import "jquery-ui/ui/widgets/sortable";
|
||||
import { alertLightbox } from "../dom/lightbox";
|
||||
|
||||
/**
|
||||
* @type {JQuery<HTMLTableElement>}
|
||||
*/
|
||||
let $sectionsTable = null;
|
||||
let $sectionsTable: JQuery<HTMLTableElement> = null;
|
||||
let changesButtonAdded = false;
|
||||
let initialOrders = [];
|
||||
let initialOrders: string[] = [];
|
||||
|
||||
function applyOrderChanges() {
|
||||
let $sections = $sectionsTable.find("tr.sectionrow");
|
|
@ -8,7 +8,7 @@ function canNotify() {
|
|||
&& (typeof Notification !== "undefined");
|
||||
}
|
||||
|
||||
export function notify(title, body, img = noteIcon) {
|
||||
export function notify(title: string, body: string, img = noteIcon) {
|
||||
let n = new Notification(title, {
|
||||
body: body,
|
||||
image: img,
|
|
@ -1,4 +1,5 @@
|
|||
import $ from "jquery";
|
||||
import { WatchedThreadJSON } from "./watcher/watcher";
|
||||
|
||||
const opRE = /\/res\/(\d+)(p(\d)+)?.html$/;
|
||||
const threadRE = /^\d+/;
|
||||
|
@ -26,15 +27,16 @@ export function getPageThread() {
|
|||
page: 0
|
||||
};
|
||||
if(arr === null) return info;
|
||||
if(arr.length > 1) info.op = arr[1];
|
||||
if(arr.length > 3) info.page = arr[3];
|
||||
if(arr.board != "") info.boardID = $("form#postform input[name=boardid]").val() -1;
|
||||
|
||||
if(arr.length > 1) info.op = Number.parseInt(arr[1]);
|
||||
if(arr.length > 3) info.page = Number.parseInt(arr[3]);
|
||||
if(info.board != "") info.boardID = Number.parseInt($("form#postform input[name=boardid]").val() as string) -1;
|
||||
return info;
|
||||
}
|
||||
|
||||
export function currentThread() {
|
||||
export function currentThread(): WatchedThreadJSON {
|
||||
// returns the board and thread ID if we are viewing a thread
|
||||
let thread = {board: currentBoard(), thread: 0};
|
||||
let thread = {board: currentBoard(), id: 0};
|
||||
let pathname = location.pathname;
|
||||
if(typeof webroot == "string" && webroot != "/") {
|
||||
pathname = pathname.slice(webroot.length);
|
||||
|
@ -47,19 +49,18 @@ export function currentThread() {
|
|||
return thread;
|
||||
let reArr = threadRE.exec(splits[3]);
|
||||
if(reArr.length > 0)
|
||||
thread.thread = reArr[0];
|
||||
thread.id = Number.parseInt(reArr[0]);
|
||||
return thread;
|
||||
}
|
||||
|
||||
export function insideOP(elem) {
|
||||
export function insideOP(elem: any) {
|
||||
return $(elem).parents("div.op-post").length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the appropriate thumbnail filename for the given upload filename (replacing gif/webm with jpg, etc)
|
||||
* @param {string} filename
|
||||
*/
|
||||
export function getThumbFilename(filename) {
|
||||
export function getThumbFilename(filename: string) {
|
||||
let nameParts = /([^.]+)\.([^.]+)$/.exec(filename);
|
||||
if(nameParts === null) return filename;
|
||||
let name = nameParts[1] + "t";
|
|
@ -1,9 +1,3 @@
|
|||
/**
|
||||
* @typedef { import("./types/gochan").BoardThread } BoardThread
|
||||
* @typedef { import("./types/gochan").ThreadPost } ThreadPost
|
||||
*/
|
||||
|
||||
|
||||
import $ from "jquery";
|
||||
|
||||
import { alertLightbox } from "./dom/lightbox";
|
||||
|
@ -16,38 +10,36 @@ import { openQR } from "./dom/qr";
|
|||
|
||||
let doClickPreview = false;
|
||||
let doHoverPreview = false;
|
||||
let $hoverPreview = null;
|
||||
let $hoverPreview: JQuery<HTMLElement> = null;
|
||||
|
||||
const videoTestRE = /\.(mp4)|(webm)$/;
|
||||
const imageTestRE = /\.(gif)|(jfif)|(jpe?g)|(png)|(webp)$/;
|
||||
const postrefRE = /\/([^\s/]+)\/res\/(\d+)\.html(#(\d+))?/;
|
||||
|
||||
// data retrieved from /<board>/res/<thread>.json
|
||||
/** @type {BoardThread} */
|
||||
let currentThreadJSON = {
|
||||
let currentThreadJSON: BoardThread = {
|
||||
posts: []
|
||||
};
|
||||
|
||||
export function getUploadPostID(upload, container) {
|
||||
export function getUploadPostID(upload: any, container: any) {
|
||||
// if container, upload is div.upload-container
|
||||
// otherwise it's img or video
|
||||
let jqu = container? $(upload) : $(upload).parent();
|
||||
return insideOP(jqu) ? jqu.siblings().eq(4).text() : jqu.siblings().eq(3).text();
|
||||
}
|
||||
|
||||
export function updateThreadJSON() {
|
||||
export async function updateThreadJSON() {
|
||||
let thread = currentThread();
|
||||
if(thread.thread === 0) return; // not in a thread
|
||||
return getThreadJSON(thread.thread, thread.board).then((json) => {
|
||||
if(!(json.posts instanceof Array) || json.posts.length === 0)
|
||||
return;
|
||||
currentThreadJSON = json;
|
||||
});
|
||||
if(thread.id === 0) return; // not in a thread
|
||||
const json = await getThreadJSON(thread.id, thread.board);
|
||||
if (!(json.posts instanceof Array) || json.posts.length === 0)
|
||||
return;
|
||||
currentThreadJSON = json;
|
||||
}
|
||||
|
||||
function updateThreadHTML() {
|
||||
let thread = currentThread();
|
||||
if(thread.thread === 0) return; // not in a thread
|
||||
if(thread.id === 0) return; // not in a thread
|
||||
let numAdded = 0;
|
||||
for(const post of currentThreadJSON.posts) {
|
||||
let selector = "";
|
||||
|
@ -77,7 +69,7 @@ export function updateThread() {
|
|||
return updateThreadJSON().then(updateThreadHTML);
|
||||
}
|
||||
|
||||
function createPostPreview(e, $post, inline = true) {
|
||||
function createPostPreview(e: JQuery.MouseEventBase, $post: JQuery<HTMLElement>, inline = true) {
|
||||
let $preview = $post.clone();
|
||||
if(inline) $preview = addPostDropdown($post.clone());
|
||||
$preview
|
||||
|
@ -91,7 +83,7 @@ function createPostPreview(e, $post, inline = true) {
|
|||
return $preview;
|
||||
}
|
||||
|
||||
function previewMoveHandler(e) {
|
||||
function previewMoveHandler(e: JQuery.Event) {
|
||||
if($hoverPreview === null) return;
|
||||
$hoverPreview.css({position: "absolute"}).offset({
|
||||
top: e.pageY + 8,
|
||||
|
@ -99,7 +91,7 @@ function previewMoveHandler(e) {
|
|||
});
|
||||
}
|
||||
|
||||
function expandPost(e) {
|
||||
function expandPost(e: JQuery.MouseEventBase) {
|
||||
e.preventDefault();
|
||||
if($hoverPreview !== null) $hoverPreview.remove();
|
||||
let $next = $(e.target).next();
|
||||
|
@ -134,7 +126,7 @@ function expandPost(e) {
|
|||
}
|
||||
}
|
||||
|
||||
export function initPostPreviews($post = null) {
|
||||
export function initPostPreviews($post: JQuery<HTMLElement> = null) {
|
||||
if(getPageThread().board == "" && $post === null) return;
|
||||
doClickPreview = getBooleanStorageVal("enablepostclick", true);
|
||||
doHoverPreview = getBooleanStorageVal("enableposthover", false);
|
||||
|
@ -161,9 +153,9 @@ export function initPostPreviews($post = null) {
|
|||
/**
|
||||
* Sets thumbnails to expand when clicked. If a parent is provided, prepareThumbnails will only
|
||||
* be applied to that parent
|
||||
* @param {JQuery<HTMLElement>} $post the post (if set) to prepare the thumbnails for
|
||||
* @param $post the post (if set) to prepare the thumbnails for
|
||||
*/
|
||||
export function prepareThumbnails($parent = null) {
|
||||
export function prepareThumbnails($parent: JQuery<HTMLElement> = null) {
|
||||
const $container = $parent === null ? $("a.upload-container") : $parent.find("a");
|
||||
$container.on("click", function(e) {
|
||||
const $a = $(this);
|
||||
|
@ -221,15 +213,15 @@ function selectedText() {
|
|||
return window.getSelection().toString();
|
||||
}
|
||||
|
||||
export function quote(no) {
|
||||
export function quote(no: number) {
|
||||
if(getBooleanStorageVal("useqr", true)) {
|
||||
openQR();
|
||||
}
|
||||
let msgboxID = "postmsg";
|
||||
|
||||
let msgbox = document.getElementById("qr" + msgboxID);
|
||||
let msgbox = document.getElementById("qr" + msgboxID) as HTMLInputElement;
|
||||
if(msgbox === null)
|
||||
msgbox = document.getElementById(msgboxID);
|
||||
msgbox = document.getElementById(msgboxID) as HTMLInputElement;
|
||||
let selected = selectedText();
|
||||
let lines = selected.split("\n");
|
||||
|
|
@ -7,54 +7,55 @@ import { initPostPreviews } from "./postutil";
|
|||
import { closeQR, initQR } from "./dom/qr";
|
||||
import { initWatcher } from "./watcher/watcher";
|
||||
|
||||
/**
|
||||
* @type {TopBarButton}
|
||||
*/
|
||||
let $settingsButton = null;
|
||||
/**
|
||||
* @type {Map<string,Setting>}
|
||||
*/
|
||||
const settings = new Map();
|
||||
let $settingsButton: TopBarButton = null;
|
||||
|
||||
const settings: Map<string, Setting<boolean|number|string,HTMLElement>> = new Map();
|
||||
|
||||
class Setting {
|
||||
type ElementValue = string|number|string[];
|
||||
|
||||
class Setting<T = any, E extends HTMLElement = HTMLElement> {
|
||||
key: string;
|
||||
title: string;
|
||||
defaultVal: T;
|
||||
onSave: () => any;
|
||||
element: JQuery<E>
|
||||
/**
|
||||
* @param {string} key The name of the setting
|
||||
* @param {string} title text that gets shown in the Settings lightbox
|
||||
* @param {string} defaultVal the setting's default value
|
||||
* @param {string} onSave function that gets called when you save the settings
|
||||
* @param key The name of the setting
|
||||
* @param title text that gets shown in the Settings lightbox
|
||||
* @param defaultVal the setting's default value
|
||||
* @param onSave function that gets called when you save the settings
|
||||
*/
|
||||
constructor(key, title, defaultVal = "", onSave = () => {}) {
|
||||
constructor(key: string, title: string, defaultVal:T, onSave = () => {}) {
|
||||
this.key = key;
|
||||
this.title = title;
|
||||
this.defaultVal = defaultVal;
|
||||
this.onSave = onSave;
|
||||
this.element = null;
|
||||
}
|
||||
getElementValue() {
|
||||
if(this.element === null) return "";
|
||||
return this.element.val();
|
||||
getElementValue(): T {
|
||||
if(this.element === null) return this.defaultVal;
|
||||
return this.element.val() as T;
|
||||
}
|
||||
setElementValue(newVal) {
|
||||
setElementValue(newVal: T) {
|
||||
if(this.element === null) return;
|
||||
this.element.val(newVal);
|
||||
this.element.val(newVal as ElementValue);
|
||||
}
|
||||
getStorageValue() {
|
||||
return getStorageVal(this.key, this.defaultVal);
|
||||
getStorageValue(): T {
|
||||
return getStorageVal(this.key, this.defaultVal.toString()) as T;
|
||||
}
|
||||
setStorageValue(newVal) {
|
||||
setStorageValue(newVal: T) {
|
||||
setStorageVal(this.key, newVal);
|
||||
}
|
||||
createElement(selector = "<input/>", props = {}) {
|
||||
return $(selector).prop(props).prop({
|
||||
return $<E>(selector).prop(props).prop({
|
||||
id: this.key,
|
||||
name: this.key
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class TextSetting extends Setting {
|
||||
constructor(key, title, defaultVal = "", onSave = () => {}) {
|
||||
class TextSetting extends Setting<string, HTMLTextAreaElement> {
|
||||
constructor(key: string, title: string, defaultVal = "", onSave = () => {}) {
|
||||
super(key, title, defaultVal, onSave);
|
||||
this.element = this.createElement("<textarea/>");
|
||||
this.element.text(defaultVal);
|
||||
|
@ -68,19 +69,20 @@ class TextSetting extends Setting {
|
|||
}
|
||||
}
|
||||
|
||||
class DropdownSetting extends Setting {
|
||||
constructor(key, title, options = [], defaultVal = "", onSave = () => {}) {
|
||||
class DropdownSetting<T> extends Setting<ElementValue, HTMLSelectElement> {
|
||||
constructor(key: string, title: string, options:any[] = [], defaultVal: ElementValue, onSave = () => {}) {
|
||||
super(key, title, defaultVal, onSave);
|
||||
this.element = this.createElement("<select/>");
|
||||
for(const option of options) {
|
||||
$("<option/>").val(option.val).text(option.text).appendTo(this.element);
|
||||
let s: HTMLSelectElement
|
||||
$<HTMLSelectElement>("<option/>").val(option.val).text(option.text).appendTo(this.element);
|
||||
}
|
||||
this.element.val(this.getStorageValue());
|
||||
}
|
||||
}
|
||||
|
||||
class BooleanSetting extends Setting {
|
||||
constructor(key, title, defaultVal = false, onSave = () => {}) {
|
||||
class BooleanSetting extends Setting<boolean, HTMLInputElement> {
|
||||
constructor(key: string, title: string, defaultVal = false, onSave = () => {}) {
|
||||
super(key, title, defaultVal, onSave);
|
||||
this.element = this.createElement("<input/>", {
|
||||
type: "checkbox",
|
||||
|
@ -90,19 +92,24 @@ class BooleanSetting extends Setting {
|
|||
getElementValue() {
|
||||
return this.element.prop("checked");
|
||||
}
|
||||
setElementValue(newVal) {
|
||||
this.element.prop({checked: newVal});
|
||||
setElementValue(newVal: boolean) {
|
||||
this.element.prop({checked: newVal?"on":"off"});
|
||||
}
|
||||
getStorageValue() {
|
||||
let val = super.getStorageValue();
|
||||
return val == true || val == "true";
|
||||
return val == true;
|
||||
}
|
||||
}
|
||||
|
||||
class NumberSetting extends Setting {
|
||||
constructor(key, title, defaultVal = 0, minMax = {min: null, max: null}, onSave = () => {}) {
|
||||
interface MinMax {
|
||||
type?: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
}
|
||||
class NumberSetting extends Setting<number, HTMLInputElement> {
|
||||
constructor(key: string, title: string, defaultVal = 0, minMax: MinMax = {min: null, max: null}, onSave = () => {}) {
|
||||
super(key, title, defaultVal, onSave);
|
||||
let props = {
|
||||
let props: MinMax = {
|
||||
type: "number"
|
||||
};
|
||||
if(typeof minMax.min == "number" && !isNaN(minMax.min))
|
||||
|
@ -112,7 +119,7 @@ class NumberSetting extends Setting {
|
|||
this.element = this.createElement("<input />", props).val(this.getStorageValue());
|
||||
}
|
||||
getStorageValue() {
|
||||
let val = Number.parseFloat(super.getStorageValue());
|
||||
let val = Number.parseFloat(super.getStorageValue() as unknown as string);
|
||||
if(isNaN(val))
|
||||
val = this.defaultVal;
|
||||
return val;
|
||||
|
@ -166,10 +173,10 @@ $(() => {
|
|||
for(const style of styles) {
|
||||
styleOptions.push({text: style.Name, val: style.Filename});
|
||||
}
|
||||
settings.set("style", new DropdownSetting("style", "Style", styleOptions, defaultStyle, function() {
|
||||
settings.set("style", new DropdownSetting<string>("style", "Style", styleOptions, defaultStyle, function() {
|
||||
document.getElementById("theme").setAttribute("href",
|
||||
`${webroot}css/${this.getElementValue()}`);
|
||||
}));
|
||||
}) as Setting);
|
||||
settings.set("pintopbar", new BooleanSetting("pintopbar", "Pin top bar", true, initTopBar));
|
||||
settings.set("enableposthover", new BooleanSetting("enableposthover", "Preview post on hover", false, initPostPreviews));
|
||||
settings.set("enablepostclick", new BooleanSetting("enablepostclick", "Preview post on click", true, initPostPreviews));
|
38
frontend/ts/storage.ts
Normal file
38
frontend/ts/storage.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
import { getCookie, setCookie } from "./cookies";
|
||||
|
||||
|
||||
export function getStorageVal(key: string, defaultVal = "") {
|
||||
if(localStorage == undefined)
|
||||
return getCookie(key, defaultVal);
|
||||
let val = localStorage.getItem(key);
|
||||
if(val === null)
|
||||
return defaultVal;
|
||||
return val;
|
||||
}
|
||||
|
||||
export function getBooleanStorageVal(key: string, defaultVal = false) {
|
||||
let val = getStorageVal(key, defaultVal?"true":"false");
|
||||
return val == "true";
|
||||
}
|
||||
|
||||
export function getNumberStorageVal(key: string, defaultVal = 0) {
|
||||
return Number.parseFloat(getStorageVal(key, defaultVal.toString()));
|
||||
}
|
||||
|
||||
export function getJsonStorageVal<T>(key: string, defaultVal: T) {
|
||||
let val = defaultVal;
|
||||
try {
|
||||
val = JSON.parse(getStorageVal(key, defaultVal as string));
|
||||
} catch(e) {
|
||||
val = defaultVal;
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
export function setStorageVal(key: string, val: any, isJSON = false) {
|
||||
let storeVal = isJSON?JSON.stringify(val):val;
|
||||
if(localStorage == undefined)
|
||||
setCookie(key, storeVal);
|
||||
else
|
||||
localStorage.setItem(key, storeVal);
|
||||
}
|
164
frontend/ts/types/index.d.ts
vendored
Normal file
164
frontend/ts/types/index.d.ts
vendored
Normal file
|
@ -0,0 +1,164 @@
|
|||
import "jquery";
|
||||
|
||||
declare global {
|
||||
interface GochanStyle {
|
||||
Name: string;
|
||||
Filename: string;
|
||||
}
|
||||
|
||||
// stored in /js/consts.json
|
||||
var styles: GochanStyle[];
|
||||
var defaultStyle: string;
|
||||
var serverTZ: number;
|
||||
/**
|
||||
* Defaults to "/"
|
||||
*/
|
||||
var webroot: string;
|
||||
interface Window {
|
||||
$: JQueryStatic;
|
||||
jQuery: JQueryStatic;
|
||||
styles: GochanStyle[];
|
||||
defaultStyle: string;
|
||||
webroot: string;
|
||||
serverTZ: number;
|
||||
|
||||
openQR: () => void;
|
||||
closeQR: () => void;
|
||||
toTop: () => void;
|
||||
toBottom: () => void;
|
||||
quote: (no: number) => void;
|
||||
}
|
||||
|
||||
// /boards.json
|
||||
interface BoardsJSON {
|
||||
boards: BoardJSON[];
|
||||
}
|
||||
|
||||
interface BoardCooldowns {
|
||||
threads: number;
|
||||
replies: number
|
||||
images: number;
|
||||
}
|
||||
|
||||
interface BoardJSON {
|
||||
pages: number;
|
||||
board: string;
|
||||
title: string;
|
||||
meta_description: string;
|
||||
max_filesize: number;
|
||||
max_pages: number;
|
||||
is_archived: boolean;
|
||||
bump_limit: number;
|
||||
image_limit: number;
|
||||
max_comment_chars: number;
|
||||
ws_board: boolean;
|
||||
cooldowns: BoardCooldowns
|
||||
per_page: number;
|
||||
}
|
||||
|
||||
// an array of these are in /boarddir/catalog.json
|
||||
interface CatalogBoard {
|
||||
page: number;
|
||||
threads: CatalogThread[];
|
||||
}
|
||||
|
||||
interface CatalogThread {
|
||||
replies: number;
|
||||
images: number;
|
||||
omitted_posts: number;
|
||||
omitted_images: number;
|
||||
sticky: number;
|
||||
locked: number;
|
||||
}
|
||||
|
||||
// /boarddir/res/#.json
|
||||
interface BoardThread {
|
||||
posts: ThreadPost[];
|
||||
}
|
||||
|
||||
interface ThreadPost {
|
||||
no: number;
|
||||
resto: number;
|
||||
name: string;
|
||||
trip: string;
|
||||
email: string;
|
||||
sub: string;
|
||||
com: string;
|
||||
tim: string;
|
||||
filename: string;
|
||||
md5: string;
|
||||
extension: string;
|
||||
fsize: number;
|
||||
w: number;
|
||||
h: number;
|
||||
tn_w: number;
|
||||
tn_h: number;
|
||||
capcode: string;
|
||||
time: string;
|
||||
last_modified: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* An object representing a staff member retreived by requesting /manage/staffinfo
|
||||
*/
|
||||
interface StaffInfo {
|
||||
/**
|
||||
* The staff member's ID in the database
|
||||
*/
|
||||
ID: number;
|
||||
/**
|
||||
* The staff member's username
|
||||
*/
|
||||
Username: string;
|
||||
/**
|
||||
* The staff member's rank.
|
||||
* 0 = not logged in.
|
||||
* 1 = janitor.
|
||||
* 2 = moderator.
|
||||
* 3 = administrator.
|
||||
*/
|
||||
Rank: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* An object representing a management action available to the current staff member
|
||||
*/
|
||||
interface StaffAction {
|
||||
/**
|
||||
* The GET key used when requesting /manage/<id>
|
||||
*/
|
||||
id?:string;
|
||||
/**
|
||||
* The title of the action, to be shown in the staff menu
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* The permission level required to access the action.
|
||||
* 0 = accessible by anyone.
|
||||
* 1 = user needs to be a janitor or higher.
|
||||
* 2 = user needs to be a moderator or higher.
|
||||
* 3 = user needs to be an administrator.
|
||||
*/
|
||||
perms: number;
|
||||
/**
|
||||
* The setting for how the request output is handled.
|
||||
* 0 = never JSON.
|
||||
* 1 = sometimes JSON depending on whether the `json` GET key is set to 1.
|
||||
* 2 = always JSON.
|
||||
*/
|
||||
jsonOutput: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of requesting /manage/actions
|
||||
*/
|
||||
var staffActions: StaffAction[];
|
||||
|
||||
/**
|
||||
* The menu shown when the Staff button on the top bar is clicked
|
||||
*/
|
||||
let $staffMenu: JQuery<HTMLElement>;
|
||||
|
||||
// used for testing
|
||||
var simpleHTML: string;
|
||||
}
|
|
@ -1,9 +1,10 @@
|
|||
import jquery from "jquery";
|
||||
|
||||
export default (window.$ = window.jQuery = jquery);
|
||||
|
||||
// overwrite jQuery's deferred exception hook, because otherwise the sourcemap
|
||||
// is useless if AJAX is involved
|
||||
jquery.Deferred.exceptionHook = function(err) {
|
||||
jquery.Deferred.exceptionHook = function(err: any) {
|
||||
// throw err;
|
||||
return err;
|
||||
};
|
|
@ -3,15 +3,15 @@ import $ from "jquery";
|
|||
import { $topbar, TopBarButton } from "../dom/topbar";
|
||||
import { currentThread } from "../postinfo";
|
||||
import { getJsonStorageVal } from "../storage";
|
||||
import { unwatchThread } from "./watcher";
|
||||
import { WatchedThreadJSON, WatchedThreadsListJSON, unwatchThread } from "./watcher";
|
||||
import { downArrow } from "../vars";
|
||||
|
||||
let watcherBtn = null;
|
||||
/** @type {JQuery<HTMLElement>} */
|
||||
let $watcherMenu = null;
|
||||
let watcherBtn:TopBarButton = null;
|
||||
let $watcherMenu: JQuery<HTMLElement> = null;
|
||||
let numUpdatedThreads = 0; // incremented for each watched thread with new posts, added to the watcher button
|
||||
|
||||
function addThreadToMenu(thread) {
|
||||
|
||||
function addThreadToMenu(thread: WatchedThreadJSON) {
|
||||
if($watcherMenu.find(`div#thread${thread.id}.watcher-item`).length > 0) {
|
||||
// thread is already in menu, check for updates to it
|
||||
updateThreadInWatcherMenu(thread);
|
||||
|
@ -56,13 +56,13 @@ function addThreadToMenu(thread) {
|
|||
$watcherMenu.find("i#no-threads").remove();
|
||||
}
|
||||
|
||||
function removeThreadFromMenu(threadID) {
|
||||
function removeThreadFromMenu(threadID: number) {
|
||||
$watcherMenu.find(`div#thread${threadID}`).remove();
|
||||
if($watcherMenu.find("div.watcher-item").length == 0)
|
||||
$watcherMenu.append(`<i id="no-threads">no watched threads</i>`);
|
||||
}
|
||||
|
||||
function updateThreadInWatcherMenu(thread) {
|
||||
function updateThreadInWatcherMenu(thread: WatchedThreadJSON) {
|
||||
let currentPage = currentThread();
|
||||
|
||||
let $item = $watcherMenu.find(`div#thread${thread.op}`);
|
||||
|
@ -72,13 +72,13 @@ function updateThreadInWatcherMenu(thread) {
|
|||
id: "reply-counter"
|
||||
}).insertBefore($item.find(`a#unwatch${thread.op}`));
|
||||
|
||||
if(currentPage.board == thread.board && currentPage.thread == thread.op) {
|
||||
if(currentPage.board == thread.board && currentPage.id == thread.id) {
|
||||
// we're currently in the thread
|
||||
$replyCounter.text(` (Replies: ${thread.newNumPosts - 1}) `);
|
||||
} else {
|
||||
// we aren't currently in the thread, show a link to the first new thread
|
||||
$replyCounter.append(
|
||||
"(Replies: ", thread.newNumPosts - 1,", ",
|
||||
"(Replies: ", (thread.newNumPosts - 1).toString(),", ",
|
||||
$("<a/>").prop({
|
||||
id: "newposts",
|
||||
href: `${webroot}${thread.board}/res/${thread.op}.html#${thread.newPosts[0].no}`
|
||||
|
@ -117,7 +117,7 @@ $(() => {
|
|||
.on("beginNewPostsCheck", () => {
|
||||
numUpdatedThreads = 0;
|
||||
});
|
||||
let watched = getJsonStorageVal("watched", {});
|
||||
let watched = getJsonStorageVal<WatchedThreadsListJSON>("watched", {});
|
||||
let boards = Object.keys(watched);
|
||||
for(const board of boards) {
|
||||
for(const thread of watched[board]) {
|
|
@ -10,7 +10,7 @@ const subjectCuttoff = 24;
|
|||
let watcherInterval = -1;
|
||||
|
||||
export function updateWatchedThreads() {
|
||||
let watched = getJsonStorageVal("watched", {});
|
||||
let watched = getJsonStorageVal<any>("watched", {});
|
||||
let boards = Object.keys(watched);
|
||||
let currentPage = currentThread();
|
||||
for(const board of boards) {
|
||||
|
@ -26,7 +26,7 @@ export function updateWatchedThreads() {
|
|||
getThreadJSON(thread.id, board).then(data => {
|
||||
if(data.posts.length > thread.posts) {
|
||||
// watched thread has new posts, trigger a menu update
|
||||
if(currentPage.board == board && currentPage.thread == thread.id) {
|
||||
if(currentPage.board == board && currentPage.id == thread.id) {
|
||||
// we're currently in the thread, update the cookie
|
||||
watched[board][t].posts = data.posts.length;
|
||||
watched[board][t].latest = data.posts[data.posts.length - 1].no;
|
||||
|
@ -49,8 +49,25 @@ export function updateWatchedThreads() {
|
|||
}
|
||||
}
|
||||
|
||||
export function isThreadWatched(threadID, board) {
|
||||
let watched = getJsonStorageVal("watched", {});
|
||||
export interface WatchedThreadsListJSON {
|
||||
[board: string]: WatchedThreadJSON[]
|
||||
}
|
||||
|
||||
export interface WatchedThreadJSON {
|
||||
id: number;
|
||||
board?: string;
|
||||
posts?: number;
|
||||
op?: string;
|
||||
latest?: string;
|
||||
subject?: string;
|
||||
|
||||
newNumPosts?: number;
|
||||
err?: string;
|
||||
newPosts?: any[];
|
||||
}
|
||||
|
||||
export function isThreadWatched(threadID: number, board: string) {
|
||||
let watched = getJsonStorageVal<WatchedThreadsListJSON>("watched", {});
|
||||
let threads = watched[board];
|
||||
if(threads == undefined) return false;
|
||||
for(const thread of threads) {
|
||||
|
@ -59,13 +76,11 @@ export function isThreadWatched(threadID, board) {
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number|string} threadID
|
||||
* @param {string} board
|
||||
*/
|
||||
export function watchThread(threadID, board) {
|
||||
let watched = getJsonStorageVal("watched", {});
|
||||
threadID = parseInt(threadID);
|
||||
export function watchThread(threadID: string|number, board: string) {
|
||||
let watched = getJsonStorageVal<WatchedThreadsListJSON>("watched", {});
|
||||
if(typeof threadID == "string") {
|
||||
threadID = parseInt(threadID);
|
||||
}
|
||||
if(!(watched[board] instanceof Array))
|
||||
watched[board] = [];
|
||||
|
||||
|
@ -78,8 +93,8 @@ export function watchThread(threadID, board) {
|
|||
}
|
||||
getThreadJSON(threadID, board).then(data => {
|
||||
const op = data.posts[0];
|
||||
let threadObj = {
|
||||
id: threadID,
|
||||
let threadObj: WatchedThreadJSON = {
|
||||
id: threadID as number,
|
||||
board: board,
|
||||
posts: data.posts.length,
|
||||
op: op.name,
|
||||
|
@ -98,14 +113,14 @@ export function watchThread(threadID, board) {
|
|||
});
|
||||
}
|
||||
|
||||
export function unwatchThread(threadID, board) {
|
||||
let watched = getJsonStorageVal("watched", {});
|
||||
export function unwatchThread(threadID: number, board: string) {
|
||||
let watched = getJsonStorageVal<WatchedThreadsListJSON>("watched", {});
|
||||
if(!(watched[board] instanceof Array))
|
||||
return;
|
||||
for(const i in watched[board]) {
|
||||
if(watched[board][i].id == threadID) {
|
||||
console.log(threadID);
|
||||
watched[board].splice(i, 1);
|
||||
watched[board].splice(i as any, 1);
|
||||
setStorageVal("watched", watched, true);
|
||||
$(document).trigger("unwatchThread", threadID);
|
||||
return;
|
||||
|
@ -119,7 +134,9 @@ export function stopThreadWatcher() {
|
|||
|
||||
export function resetThreadWatcherInterval() {
|
||||
stopThreadWatcher();
|
||||
watcherInterval = setInterval(updateWatchedThreads, getNumberStorageVal("watcherseconds", 10) * 1000);
|
||||
(watcherInterval as unknown as NodeJS.Timer) = setInterval(
|
||||
updateWatchedThreads,
|
||||
getNumberStorageVal("watcherseconds", 10) * 1000);
|
||||
}
|
||||
|
||||
export function initWatcher() {
|
13
frontend/tsconfig.json
Normal file
13
frontend/tsconfig.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "../js/",
|
||||
"sourceMap": true,
|
||||
"noImplicitAny": true,
|
||||
"module": "es6",
|
||||
"target": "es6",
|
||||
"allowJs": true,
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true
|
||||
}
|
||||
}
|
24
frontend/webpack.config.js
Executable file
24
frontend/webpack.config.js
Executable file
|
@ -0,0 +1,24 @@
|
|||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
entry: './ts/gochan.ts',
|
||||
module: {
|
||||
rules: [{
|
||||
test: /\.tsx?$/,
|
||||
use: 'ts-loader',
|
||||
exclude: /node_modules/,
|
||||
}],
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js'],
|
||||
"fallback": {
|
||||
"path": require.resolve("path-browserify")
|
||||
}
|
||||
},
|
||||
output: {
|
||||
filename: "gochan.js",
|
||||
path: path.resolve(__dirname, '../html/js/'),
|
||||
},
|
||||
devtool: "source-map",
|
||||
mode: "production"
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue