mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-06-27 12:55:55 +00:00
More type progress.
This commit is contained in:
parent
fed7d3e103
commit
136a2491c8
42 changed files with 3451 additions and 2311 deletions
|
@ -25,7 +25,10 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@ffz/fontello-cli": "^1.0.4",
|
||||
"@types/crypto-js": "^4.2.1",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/safe-regex": "^1.1.6",
|
||||
"@types/vue-clickaway": "^2.2.4",
|
||||
"@types/webpack-env": "^1.18.4",
|
||||
"browserslist": "^4.21.10",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
|
@ -66,7 +69,7 @@
|
|||
"dependencies": {
|
||||
"@ffz/icu-msgparser": "^2.0.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"crypto-js": "^3.3.0",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dayjs": "^1.10.7",
|
||||
"denoflare-mqtt": "^0.0.2",
|
||||
"displacejs": "^1.4.1",
|
||||
|
@ -74,7 +77,7 @@
|
|||
"file-saver": "^2.0.5",
|
||||
"graphql": "^16.0.1",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"js-cookie": "^2.2.1",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jszip": "^3.7.1",
|
||||
"markdown-it": "^12.2.0",
|
||||
"markdown-it-link-attributes": "^3.0.0",
|
||||
|
|
41
pnpm-lock.yaml
generated
41
pnpm-lock.yaml
generated
|
@ -18,8 +18,8 @@ dependencies:
|
|||
specifier: ^2.11.8
|
||||
version: 2.11.8
|
||||
crypto-js:
|
||||
specifier: ^3.3.0
|
||||
version: 3.3.0
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
dayjs:
|
||||
specifier: ^1.10.7
|
||||
version: 1.10.7
|
||||
|
@ -42,8 +42,8 @@ dependencies:
|
|||
specifier: ^2.12.6
|
||||
version: 2.12.6(graphql@16.0.1)
|
||||
js-cookie:
|
||||
specifier: ^2.2.1
|
||||
version: 2.2.1
|
||||
specifier: ^3.0.5
|
||||
version: 3.0.5
|
||||
jszip:
|
||||
specifier: ^3.7.1
|
||||
version: 3.7.1
|
||||
|
@ -97,9 +97,18 @@ devDependencies:
|
|||
'@ffz/fontello-cli':
|
||||
specifier: ^1.0.4
|
||||
version: 1.0.4
|
||||
'@types/crypto-js':
|
||||
specifier: ^4.2.1
|
||||
version: 4.2.1
|
||||
'@types/js-cookie':
|
||||
specifier: ^3.0.6
|
||||
version: 3.0.6
|
||||
'@types/safe-regex':
|
||||
specifier: ^1.1.6
|
||||
version: 1.1.6
|
||||
'@types/vue-clickaway':
|
||||
specifier: ^2.2.4
|
||||
version: 2.2.4
|
||||
'@types/webpack-env':
|
||||
specifier: ^1.18.4
|
||||
version: 1.18.4
|
||||
|
@ -592,6 +601,10 @@ packages:
|
|||
'@types/node': 20.5.7
|
||||
dev: true
|
||||
|
||||
/@types/crypto-js@4.2.1:
|
||||
resolution: {integrity: sha512-FSPGd9+OcSok3RsM0UZ/9fcvMOXJ1ENE/ZbLfOPlBWj7BgXtEAM8VYfTtT760GiLbQIMoVozwVuisjvsVwqYWw==}
|
||||
dev: true
|
||||
|
||||
/@types/eslint-scope@3.7.4:
|
||||
resolution: {integrity: sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==}
|
||||
dependencies:
|
||||
|
@ -638,6 +651,10 @@ packages:
|
|||
'@types/node': 20.5.7
|
||||
dev: true
|
||||
|
||||
/@types/js-cookie@3.0.6:
|
||||
resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==}
|
||||
dev: true
|
||||
|
||||
/@types/json-schema@7.0.9:
|
||||
resolution: {integrity: sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==}
|
||||
dev: true
|
||||
|
@ -701,6 +718,12 @@ packages:
|
|||
'@types/node': 20.5.7
|
||||
dev: true
|
||||
|
||||
/@types/vue-clickaway@2.2.4:
|
||||
resolution: {integrity: sha512-Jy0dGNUrm/Fya1hY8bHM5lXJvZvlyU/rvgLEFVcjQkwNp2Z2IGNnRKS6ZH9orMDkUI7Qj0oyWp0b89VTErAS9Q==}
|
||||
dependencies:
|
||||
vue: 2.6.14
|
||||
dev: true
|
||||
|
||||
/@types/webpack-env@1.18.4:
|
||||
resolution: {integrity: sha512-I6e+9+HtWADAWeeJWDFQtdk4EVSAbj6Rtz4q8fJ7mSr1M0jzlFcs8/HZ+Xb5SHzVm1dxH7aUiI+A8kA8Gcrm0A==}
|
||||
dev: true
|
||||
|
@ -2173,8 +2196,8 @@ packages:
|
|||
which: 2.0.2
|
||||
dev: true
|
||||
|
||||
/crypto-js@3.3.0:
|
||||
resolution: {integrity: sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q==}
|
||||
/crypto-js@4.2.0:
|
||||
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
|
||||
dev: false
|
||||
|
||||
/css-loader@6.8.1(webpack@5.88.2):
|
||||
|
@ -3701,8 +3724,9 @@ packages:
|
|||
supports-color: 8.1.1
|
||||
dev: true
|
||||
|
||||
/js-cookie@2.2.1:
|
||||
resolution: {integrity: sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==}
|
||||
/js-cookie@3.0.5:
|
||||
resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
|
||||
engines: {node: '>=14'}
|
||||
dev: false
|
||||
|
||||
/js-tokens@3.0.2:
|
||||
|
@ -5652,7 +5676,6 @@ packages:
|
|||
|
||||
/vue@2.6.14:
|
||||
resolution: {integrity: sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ==}
|
||||
dev: false
|
||||
|
||||
/vuedraggable@2.24.3:
|
||||
resolution: {integrity: sha512-6/HDXi92GzB+Hcs9fC6PAAozK1RLt1ewPTLjK0anTYguXLAeySDmcnqE8IC0xa7shvSzRjQXq3/+dsZ7ETGF3g==}
|
||||
|
|
|
@ -18,7 +18,7 @@ import StagingSelector from './staging';
|
|||
import LoadTracker from './load_tracker';
|
||||
|
||||
import Site from './sites/clips';
|
||||
import Vue from 'utilities/vue';
|
||||
import VueModule from 'utilities/vue';
|
||||
|
||||
import Tooltips from 'src/modules/tooltips';
|
||||
import Chat from 'src/modules/chat';
|
||||
|
@ -64,7 +64,7 @@ class FrankerFaceZ extends Module {
|
|||
this.inject('site', Site);
|
||||
this.inject('addons', AddonManager);
|
||||
|
||||
this.register('vue', Vue);
|
||||
this.register('vue', VueModule);
|
||||
|
||||
// ========================================================================
|
||||
// Startup
|
||||
|
|
|
@ -5,13 +5,33 @@
|
|||
// ============================================================================
|
||||
|
||||
import {DEBUG, SERVER} from 'utilities/constants';
|
||||
import Module from 'utilities/module';
|
||||
import {has, deep_copy} from 'utilities/object';
|
||||
import Module, { GenericModule } from 'utilities/module';
|
||||
import {has, deep_copy, fetchJSON} from 'utilities/object';
|
||||
import { getBuster } from 'utilities/time';
|
||||
|
||||
import Cookie from 'js-cookie';
|
||||
import SHA1 from 'crypto-js/sha1';
|
||||
|
||||
import type SettingsManager from './settings';
|
||||
|
||||
declare module 'utilities/types' {
|
||||
interface ModuleMap {
|
||||
experiments: ExperimentManager;
|
||||
}
|
||||
interface ProviderTypeMap {
|
||||
'experiment-overrides': Record<string, unknown>
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__twilightSettings?: {
|
||||
experiments?: Record<string, TwitchExperimentData>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const OVERRIDE_COOKIE = 'experiment_overrides',
|
||||
COOKIE_OPTIONS = {
|
||||
expires: 7,
|
||||
|
@ -21,9 +41,68 @@ const OVERRIDE_COOKIE = 'experiment_overrides',
|
|||
|
||||
// We want to import this so that the file is included in the output.
|
||||
// We don't load using this because we might want a newer file from the
|
||||
// server.
|
||||
import EXPERIMENTS from './experiments.json'; // eslint-disable-line no-unused-vars
|
||||
// server. Because of our webpack settings, this is imported as a URL
|
||||
// and not an object.
|
||||
const EXPERIMENTS: string = require('./experiments.json');
|
||||
|
||||
// ============================================================================
|
||||
// Data Types
|
||||
// ============================================================================
|
||||
|
||||
export enum TwitchExperimentType {
|
||||
Unknown = 0,
|
||||
Device = 1,
|
||||
User = 2,
|
||||
Channel = 3
|
||||
};
|
||||
|
||||
export type ExperimentGroup = {
|
||||
value: unknown;
|
||||
weight: number;
|
||||
};
|
||||
|
||||
export type FFZExperimentData = {
|
||||
name: string;
|
||||
seed?: number;
|
||||
description: string;
|
||||
groups: ExperimentGroup[];
|
||||
}
|
||||
|
||||
export type TwitchExperimentData = {
|
||||
name: string;
|
||||
t: TwitchExperimentType;
|
||||
v: number;
|
||||
groups: ExperimentGroup[];
|
||||
};
|
||||
|
||||
export type ExperimentData = FFZExperimentData | TwitchExperimentData;
|
||||
|
||||
|
||||
export type OverrideCookie = {
|
||||
experiments: Record<string, string>;
|
||||
disabled: string[];
|
||||
};
|
||||
|
||||
|
||||
type ExperimentEvents = {
|
||||
':changed': [key: string, new_value: any, old_value: any];
|
||||
':twitch-changed': [key: string, new_value: string | null, old_value: string | null];
|
||||
[key: `:changed:${string}`]: [new_value: any, old_value: any];
|
||||
[key: `:twitch-changed:${string}`]: [new_value: string | null, old_value: string | null];
|
||||
}
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Helper Methods
|
||||
// ============================================================================
|
||||
|
||||
export function isTwitchExperiment(exp: ExperimentData): exp is TwitchExperimentData {
|
||||
return 't' in exp;
|
||||
}
|
||||
|
||||
export function isFFZExperiment(exp: ExperimentData): exp is FFZExperimentData {
|
||||
return 'description' in exp;
|
||||
}
|
||||
|
||||
function sortExperimentLog(a,b) {
|
||||
if ( a.rarity < b.rarity )
|
||||
|
@ -44,9 +123,24 @@ function sortExperimentLog(a,b) {
|
|||
// Experiment Manager
|
||||
// ============================================================================
|
||||
|
||||
export default class ExperimentManager extends Module {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
export default class ExperimentManager extends Module<'experiments', ExperimentEvents> {
|
||||
|
||||
// Dependencies
|
||||
settings: SettingsManager = null as any;
|
||||
|
||||
// State
|
||||
unique_id?: string;
|
||||
experiments: Record<string, FFZExperimentData>;
|
||||
|
||||
private cache: Map<string, unknown>;
|
||||
|
||||
|
||||
// Helpers
|
||||
Cookie: typeof Cookie;
|
||||
|
||||
|
||||
constructor(name?: string, parent?: GenericModule) {
|
||||
super(name, parent);
|
||||
|
||||
this.get = this.getAssignment;
|
||||
|
||||
|
@ -81,23 +175,23 @@ export default class ExperimentManager extends Module {
|
|||
|
||||
unique_id: () => this.unique_id,
|
||||
|
||||
ffz_data: () => deep_copy(this.experiments),
|
||||
ffz_data: () => deep_copy(this.experiments) ?? {},
|
||||
twitch_data: () => deep_copy(this.getTwitchExperiments()),
|
||||
|
||||
usingTwitchExperiment: key => this.usingTwitchExperiment(key),
|
||||
getTwitchAssignment: key => this.getTwitchAssignment(key),
|
||||
getTwitchType: type => this.getTwitchType(type),
|
||||
hasTwitchOverride: key => this.hasTwitchOverride(key),
|
||||
setTwitchOverride: (key, val) => this.setTwitchOverride(key, val),
|
||||
deleteTwitchOverride: key => this.deleteTwitchOverride(key),
|
||||
usingTwitchExperiment: (key: string) => this.usingTwitchExperiment(key),
|
||||
getTwitchAssignment: (key: string) => this.getTwitchAssignment(key),
|
||||
getTwitchType: (type: TwitchExperimentType) => this.getTwitchType(type),
|
||||
hasTwitchOverride: (key: string) => this.hasTwitchOverride(key),
|
||||
setTwitchOverride: (key: string, val: string) => this.setTwitchOverride(key, val),
|
||||
deleteTwitchOverride: (key: string) => this.deleteTwitchOverride(key),
|
||||
|
||||
getAssignment: key => this.getAssignment(key),
|
||||
hasOverride: key => this.hasOverride(key),
|
||||
setOverride: (key, val) => this.setOverride(key, val),
|
||||
deleteOverride: key => this.deleteOverride(key),
|
||||
getAssignment: (key: string) => this.getAssignment(key),
|
||||
hasOverride: (key: string) => this.hasOverride(key),
|
||||
setOverride: (key: string, val: any) => this.setOverride(key, val),
|
||||
deleteOverride: (key: string) => this.deleteOverride(key),
|
||||
|
||||
on: (...args) => this.on(...args),
|
||||
off: (...args) => this.off(...args)
|
||||
on: (...args: Parameters<typeof this.on>) => this.on(...args),
|
||||
off: (...args: Parameters<typeof this.off>) => this.off(...args)
|
||||
});
|
||||
|
||||
this.unique_id = Cookie.get('unique_id');
|
||||
|
@ -112,7 +206,7 @@ export default class ExperimentManager extends Module {
|
|||
if ( DEBUG )
|
||||
return false;
|
||||
|
||||
const ts = this.settings.provider.get('exp-lock', 0);
|
||||
const ts = this.settings.provider.get<number>('exp-lock', 0);
|
||||
if ( isNaN(ts) || ! isFinite(ts) )
|
||||
return true;
|
||||
|
||||
|
@ -129,14 +223,17 @@ export default class ExperimentManager extends Module {
|
|||
|
||||
|
||||
async loadExperiments() {
|
||||
let data;
|
||||
let data: Record<string, FFZExperimentData> | null;
|
||||
|
||||
try {
|
||||
data = await fetch(DEBUG ? EXPERIMENTS : `${SERVER}/script/experiments.json?_=${getBuster()}`).then(r =>
|
||||
r.ok ? r.json() : null);
|
||||
data = await fetchJSON(DEBUG
|
||||
? EXPERIMENTS
|
||||
: `${SERVER}/script/experiments.json?_=${getBuster()}`
|
||||
);
|
||||
|
||||
} catch(err) {
|
||||
this.log.warn('Unable to load experiment data.', err);
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! data )
|
||||
|
@ -153,8 +250,8 @@ export default class ExperimentManager extends Module {
|
|||
const new_val = this.getAssignment(key);
|
||||
if ( old_val !== new_val ) {
|
||||
changed++;
|
||||
this.emit(':changed', key, new_val);
|
||||
this.emit(`:changed:${key}`, new_val);
|
||||
this.emit(':changed', key, new_val, old_val);
|
||||
this.emit(`:changed:${key}`, new_val, old_val);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -162,21 +259,25 @@ export default class ExperimentManager extends Module {
|
|||
//this.emit(':loaded');
|
||||
}
|
||||
|
||||
|
||||
/** @internal */
|
||||
onEnable() {
|
||||
this.on('pubsub:command:reload_experiments', this.loadExperiments, this);
|
||||
this.on('pubsub:command:update_experiment', this.updateExperiment, this);
|
||||
}
|
||||
|
||||
|
||||
updateExperiment(key, data) {
|
||||
this.log.info(`Received updated data for experiment "${key}" via WebSocket.`, data);
|
||||
updateExperiment(key: string, data: FFZExperimentData | ExperimentGroup[]) {
|
||||
this.log.info(`Received updated data for experiment "${key}" via PubSub.`, data);
|
||||
|
||||
if ( Array.isArray(data) ) {
|
||||
if ( ! this.experiments[key] )
|
||||
return;
|
||||
|
||||
if ( data.groups )
|
||||
this.experiments[key] = data;
|
||||
else
|
||||
this.experiments[key].groups = data;
|
||||
|
||||
} else if ( data?.groups )
|
||||
this.experiments[key] = data;
|
||||
|
||||
this._rebuildKey(key);
|
||||
}
|
||||
|
||||
|
@ -261,7 +362,7 @@ export default class ExperimentManager extends Module {
|
|||
|
||||
// Twitch Experiments
|
||||
|
||||
getTwitchType(type) {
|
||||
getTwitchType(type: number) {
|
||||
const core = this.resolve('site')?.getCore?.();
|
||||
if ( core?.experiments?.getExperimentType )
|
||||
return core.experiments.getExperimentType(type);
|
||||
|
@ -275,10 +376,9 @@ export default class ExperimentManager extends Module {
|
|||
return type;
|
||||
}
|
||||
|
||||
getTwitchTypeByKey(key) {
|
||||
const core = this.resolve('site')?.getCore?.(),
|
||||
exps = core && core.experiments,
|
||||
exp = exps?.experiments?.[key];
|
||||
getTwitchTypeByKey(key: string) {
|
||||
const exps = this.getTwitchExperiments(),
|
||||
exp = exps?.[key];
|
||||
|
||||
if ( exp?.t )
|
||||
return this.getTwitchType(exp.t);
|
||||
|
@ -286,30 +386,67 @@ export default class ExperimentManager extends Module {
|
|||
return null;
|
||||
}
|
||||
|
||||
getTwitchExperiments() {
|
||||
getTwitchExperiments(): Record<string, TwitchExperimentData> {
|
||||
if ( window.__twilightSettings )
|
||||
return window.__twilightSettings.experiments;
|
||||
return window.__twilightSettings.experiments ?? {};
|
||||
|
||||
const core = this.resolve('site')?.getCore?.();
|
||||
return core && core.experiments.experiments;
|
||||
return core && core.experiments.experiments || {};
|
||||
}
|
||||
|
||||
|
||||
usingTwitchExperiment(key) {
|
||||
usingTwitchExperiment(key: string) {
|
||||
const core = this.resolve('site')?.getCore?.();
|
||||
return core && has(core.experiments.assignments, key)
|
||||
}
|
||||
|
||||
|
||||
setTwitchOverride(key, value = null) {
|
||||
const overrides = Cookie.getJSON(OVERRIDE_COOKIE) || {};
|
||||
const experiments = overrides.experiments = overrides.experiments || {};
|
||||
const disabled = overrides.disabled = overrides.disabled || [];
|
||||
private _getOverrideCookie() {
|
||||
const raw = Cookie.get(OVERRIDE_COOKIE);
|
||||
let out: OverrideCookie;
|
||||
|
||||
try {
|
||||
out = raw ? JSON.parse(raw) : {};
|
||||
} catch(err) {
|
||||
out = {} as OverrideCookie;
|
||||
}
|
||||
|
||||
if ( ! out.experiments )
|
||||
out.experiments = {};
|
||||
|
||||
if ( ! out.disabled )
|
||||
out.disabled = [];
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
private _saveOverrideCookie(value?: OverrideCookie) {
|
||||
if ( value ) {
|
||||
if ((! value.experiments || ! Object.keys(value.experiments).length) &&
|
||||
(! value.disabled || ! value.disabled.length)
|
||||
)
|
||||
value = undefined;
|
||||
}
|
||||
|
||||
if ( value )
|
||||
Cookie.set(OVERRIDE_COOKIE, JSON.stringify(value), COOKIE_OPTIONS);
|
||||
else
|
||||
Cookie.remove(OVERRIDE_COOKIE, COOKIE_OPTIONS);
|
||||
}
|
||||
|
||||
|
||||
setTwitchOverride(key: string, value: string) {
|
||||
const overrides = this._getOverrideCookie(),
|
||||
experiments = overrides.experiments,
|
||||
disabled = overrides.disabled;
|
||||
|
||||
experiments[key] = value;
|
||||
|
||||
const idx = disabled.indexOf(key);
|
||||
if (idx != -1)
|
||||
disabled.remove(idx);
|
||||
Cookie.set(OVERRIDE_COOKIE, overrides, COOKIE_OPTIONS);
|
||||
disabled.splice(idx, 1);
|
||||
|
||||
this._saveOverrideCookie(overrides);
|
||||
|
||||
const core = this.resolve('site')?.getCore?.();
|
||||
if ( core )
|
||||
|
@ -318,15 +455,17 @@ export default class ExperimentManager extends Module {
|
|||
this._rebuildTwitchKey(key, true, value);
|
||||
}
|
||||
|
||||
deleteTwitchOverride(key) {
|
||||
const overrides = Cookie.getJSON(OVERRIDE_COOKIE),
|
||||
experiments = overrides?.experiments;
|
||||
if ( ! experiments || ! has(experiments, key) )
|
||||
deleteTwitchOverride(key: string) {
|
||||
const overrides = this._getOverrideCookie(),
|
||||
experiments = overrides.experiments;
|
||||
|
||||
if ( ! has(experiments, key) )
|
||||
return;
|
||||
|
||||
const old_val = experiments[key];
|
||||
delete experiments[key];
|
||||
Cookie.set(OVERRIDE_COOKIE, overrides, COOKIE_OPTIONS);
|
||||
|
||||
this._saveOverrideCookie(overrides);
|
||||
|
||||
const core = this.resolve('site')?.getCore?.();
|
||||
if ( core )
|
||||
|
@ -335,13 +474,14 @@ export default class ExperimentManager extends Module {
|
|||
this._rebuildTwitchKey(key, false, old_val);
|
||||
}
|
||||
|
||||
hasTwitchOverride(key) { // eslint-disable-line class-methods-use-this
|
||||
const overrides = Cookie.getJSON(OVERRIDE_COOKIE),
|
||||
experiments = overrides?.experiments;
|
||||
return experiments && has(experiments, key);
|
||||
hasTwitchOverride(key: string) { // eslint-disable-line class-methods-use-this
|
||||
const overrides = this._getOverrideCookie(),
|
||||
experiments = overrides.experiments;
|
||||
|
||||
return has(experiments, key);
|
||||
}
|
||||
|
||||
getTwitchAssignment(key, channel = null) {
|
||||
getTwitchAssignment(key: string, channel: string | null = null) {
|
||||
const core = this.resolve('site')?.getCore?.(),
|
||||
exps = core && core.experiments;
|
||||
|
||||
|
@ -355,22 +495,47 @@ export default class ExperimentManager extends Module {
|
|||
this.log.warn('Error attempting to initialize Twitch experiments tracker.', err);
|
||||
}
|
||||
|
||||
if ( channel || this.getTwitchType(exps.experiments[key]?.t) === 'channel_id' )
|
||||
return exps.getAssignmentById(key, {channel: channel ?? this.settings.get('context.channel')});
|
||||
|
||||
if ( exps.overrides && exps.overrides[key] )
|
||||
return exps.overrides[key];
|
||||
|
||||
else if ( exps.assignments && exps.assignments[key] )
|
||||
const exp_data = exps.experiments[key],
|
||||
type = this.getTwitchType(exp_data?.t ?? 0);
|
||||
|
||||
// channel_id experiments always use getAssignmentById
|
||||
if ( type === 'channel_id' ) {
|
||||
return exps.getAssignmentById(key, {
|
||||
bucketing: {
|
||||
type: 1,
|
||||
value: channel ?? this.settings.get('context.channelID')
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Otherwise, just use the default assignment?
|
||||
if ( exps.assignments?.[key] )
|
||||
return exps.assignments[key];
|
||||
|
||||
// If there is no default assignment, we should try to figure out
|
||||
// what assignment they *would* get.
|
||||
|
||||
if ( type === 'device_id' )
|
||||
return exps.selectTreatment(key, exp_data, this.unique_id);
|
||||
|
||||
else if ( type === 'user_id' )
|
||||
// Technically, some experiments are expecting to get the user's
|
||||
// login rather than user ID. But we don't care that much if an
|
||||
// inactive legacy experiment is shown wrong. Meh.
|
||||
return exps.selectTreatment(key, exp_data, this.resolve('site')?.getUser?.()?.id);
|
||||
|
||||
// We don't know what kind of experiment this is.
|
||||
// Give up!
|
||||
return null;
|
||||
}
|
||||
|
||||
getTwitchKeyFromName(name) {
|
||||
getTwitchKeyFromName(name: string) {
|
||||
const experiments = this.getTwitchExperiments();
|
||||
if ( ! experiments )
|
||||
return undefined;
|
||||
return;
|
||||
|
||||
name = name.toLowerCase();
|
||||
for(const key in experiments)
|
||||
|
@ -381,30 +546,37 @@ export default class ExperimentManager extends Module {
|
|||
}
|
||||
}
|
||||
|
||||
getTwitchAssignmentByName(name, channel = null) {
|
||||
return this.getTwitchAssignment(this.getTwitchKeyFromName(name), channel);
|
||||
getTwitchAssignmentByName(name: string, channel: string | null = null) {
|
||||
const key = this.getTwitchKeyFromName(name);
|
||||
if ( ! key )
|
||||
return null;
|
||||
return this.getTwitchAssignment(key, channel);
|
||||
}
|
||||
|
||||
_rebuildTwitchKey(key, is_set, new_val) {
|
||||
_rebuildTwitchKey(
|
||||
key: string,
|
||||
is_set: boolean,
|
||||
new_val: string | null
|
||||
) {
|
||||
const core = this.resolve('site')?.getCore?.(),
|
||||
exps = core.experiments,
|
||||
|
||||
old_val = has(exps.assignments, key) ?
|
||||
exps.assignments[key] :
|
||||
undefined;
|
||||
exps.assignments[key] as string :
|
||||
null;
|
||||
|
||||
if ( old_val !== new_val ) {
|
||||
const value = is_set ? new_val : old_val;
|
||||
this.emit(':twitch-changed', key, value);
|
||||
this.emit(`:twitch-changed:${key}`, value);
|
||||
this.emit(':twitch-changed', key, value, old_val);
|
||||
this.emit(`:twitch-changed:${key}`, value, old_val);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// FFZ Experiments
|
||||
|
||||
setOverride(key, value = null) {
|
||||
const overrides = this.settings.provider.get('experiment-overrides') || {};
|
||||
setOverride(key: string, value: unknown = null) {
|
||||
const overrides = this.settings.provider.get('experiment-overrides', {});
|
||||
overrides[key] = value;
|
||||
|
||||
this.settings.provider.set('experiment-overrides', overrides);
|
||||
|
@ -412,25 +584,30 @@ export default class ExperimentManager extends Module {
|
|||
this._rebuildKey(key);
|
||||
}
|
||||
|
||||
deleteOverride(key) {
|
||||
deleteOverride(key: string) {
|
||||
const overrides = this.settings.provider.get('experiment-overrides');
|
||||
if ( ! overrides || ! has(overrides, key) )
|
||||
return;
|
||||
|
||||
delete overrides[key];
|
||||
this.settings.provider.set('experiment-overrides', overrides);
|
||||
if ( Object.keys(overrides).length )
|
||||
this.settings.provider.set('experiment-overrides', overrides);
|
||||
else
|
||||
this.settings.provider.delete('experiment-overrides');
|
||||
|
||||
this._rebuildKey(key);
|
||||
}
|
||||
|
||||
hasOverride(key) {
|
||||
hasOverride(key: string) {
|
||||
const overrides = this.settings.provider.get('experiment-overrides');
|
||||
return overrides && has(overrides, key);
|
||||
return overrides ? has(overrides, key): false;
|
||||
}
|
||||
|
||||
getAssignment(key) {
|
||||
get: <T>(key: string) => T | null;
|
||||
|
||||
getAssignment<T>(key: string): T | null {
|
||||
if ( this.cache.has(key) )
|
||||
return this.cache.get(key);
|
||||
return this.cache.get(key) as T;
|
||||
|
||||
const experiment = this.experiments[key];
|
||||
if ( ! experiment ) {
|
||||
|
@ -440,14 +617,14 @@ export default class ExperimentManager extends Module {
|
|||
|
||||
const overrides = this.settings.provider.get('experiment-overrides'),
|
||||
out = overrides && has(overrides, key) ?
|
||||
overrides[key] :
|
||||
ExperimentManager.selectGroup(key, experiment, this.unique_id);
|
||||
overrides[key] as T :
|
||||
ExperimentManager.selectGroup<T>(key, experiment, this.unique_id ?? '');
|
||||
|
||||
this.cache.set(key, out);
|
||||
return out;
|
||||
}
|
||||
|
||||
_rebuildKey(key) {
|
||||
_rebuildKey(key: string) {
|
||||
if ( ! this.cache.has(key) )
|
||||
return;
|
||||
|
||||
|
@ -456,13 +633,17 @@ export default class ExperimentManager extends Module {
|
|||
const new_val = this.getAssignment(key);
|
||||
|
||||
if ( new_val !== old_val ) {
|
||||
this.emit(':changed', key, new_val);
|
||||
this.emit(`:changed:${key}`, new_val);
|
||||
this.emit(':changed', key, new_val, old_val);
|
||||
this.emit(`:changed:${key}`, new_val, old_val);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static selectGroup(key, experiment, unique_id) {
|
||||
static selectGroup<T>(
|
||||
key: string,
|
||||
experiment: FFZExperimentData,
|
||||
unique_id: string
|
||||
): T | null {
|
||||
const seed = key + unique_id + (experiment.seed || ''),
|
||||
total = experiment.groups.reduce((a,b) => a + b.weight, 0);
|
||||
|
||||
|
@ -471,9 +652,9 @@ export default class ExperimentManager extends Module {
|
|||
for(const group of experiment.groups) {
|
||||
value -= group.weight / total;
|
||||
if ( value <= 0 )
|
||||
return group.value;
|
||||
return group.value as T;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,6 +7,20 @@
|
|||
import Module, { GenericModule } from 'utilities/module';
|
||||
import type SettingsManager from './settings';
|
||||
|
||||
|
||||
declare module 'utilities/types' {
|
||||
interface ModuleEventMap {
|
||||
load_tracker: LoadEvents;
|
||||
}
|
||||
interface ModuleMap {
|
||||
load_tracker: LoadTracker;
|
||||
}
|
||||
interface SettingsTypeMap {
|
||||
'chat.update-when-loaded': boolean;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
type PendingLoadData = {
|
||||
pending: Set<string>;
|
||||
timers: Record<string, ReturnType<typeof setTimeout> | null>;
|
||||
|
|
|
@ -16,7 +16,7 @@ import TranslationManager from './i18n';
|
|||
import SocketClient from './socket';
|
||||
import PubSubClient from './pubsub';
|
||||
import Site from 'site';
|
||||
import Vue from 'utilities/vue';
|
||||
import VueModule from 'utilities/vue';
|
||||
import StagingSelector from './staging';
|
||||
import LoadTracker from './load_tracker';
|
||||
|
||||
|
@ -138,7 +138,7 @@ class FrankerFaceZ extends Module {
|
|||
this.inject('site', Site);
|
||||
this.inject('addons', AddonManager);
|
||||
|
||||
this.register('vue', Vue);
|
||||
this.register('vue', VueModule);
|
||||
|
||||
|
||||
// ========================================================================
|
||||
|
|
|
@ -1554,4 +1554,4 @@ export function fixBadgeData(badge) {
|
|||
}
|
||||
|
||||
return badge;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -633,7 +633,7 @@ export default {
|
|||
// TODO: Update timestamps for pinned chat?
|
||||
}
|
||||
|
||||
this.chat.resolve('site.subpump').inject(item.topic, item.data);
|
||||
this.chat.resolve('site.subpump').simulateMessage(item.topic, item.data);
|
||||
}
|
||||
|
||||
if ( item.chat ) {
|
||||
|
@ -731,4 +731,4 @@ export default {
|
|||
|
||||
}
|
||||
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
@ -173,7 +173,7 @@
|
|||
@change="onTwitchChange($event)"
|
||||
>
|
||||
<option
|
||||
v-if="exp.in_use === false"
|
||||
v-if="exp.value === null"
|
||||
:selected="exp.default"
|
||||
>
|
||||
{{ t('setting.experiments.unset', 'unset') }}
|
||||
|
@ -436,4 +436,4 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
@ -165,7 +165,7 @@
|
|||
<figure class="ffz-i-discord tw-font-size-3" />
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
<!--a
|
||||
:data-title="t('home.twitter', 'Twitter')"
|
||||
class="tw-flex-grow-1 tw-button ffz-tooltip ffz--twitter-button tw-mg-r-1"
|
||||
href="https://twitter.com/frankerfacez"
|
||||
|
@ -175,7 +175,7 @@
|
|||
<span class="tw-button__icon tw-pd-05">
|
||||
<figure class="ffz-i-twitter tw-font-size-3" />
|
||||
</span>
|
||||
</a>
|
||||
</a-->
|
||||
<a
|
||||
:data-title="t('home.github', 'GitHub')"
|
||||
class="tw-flex-grow-1 tw-button ffz-tooltip ffz--github-button"
|
||||
|
@ -189,7 +189,12 @@
|
|||
</a>
|
||||
</div>
|
||||
|
||||
<template v-if="not_extension">
|
||||
<rich-feed
|
||||
url="https://bsky.app/FFZ_SPECIAL_FEED::stendec.dev"
|
||||
:context="context"
|
||||
/>
|
||||
|
||||
<!--template v-if="not_extension">
|
||||
<a
|
||||
:data-theme="theme"
|
||||
class="twitter-timeline"
|
||||
|
@ -198,7 +203,7 @@
|
|||
>
|
||||
{{ t('home.tweets', 'Tweets by FrankerFaceZ') }}
|
||||
</a>
|
||||
</template>
|
||||
</template-->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -221,7 +226,7 @@ export default {
|
|||
addons: null,
|
||||
new_addons: null,
|
||||
unseen: this.item.getUnseen(),
|
||||
not_extension: ! EXTENSION
|
||||
//not_extension: ! EXTENSION
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -243,7 +248,7 @@ export default {
|
|||
ffz.off('addons:data-loaded', this.updateAddons, this);
|
||||
},
|
||||
|
||||
mounted() {
|
||||
/*mounted() {
|
||||
let el;
|
||||
if ( this.not_extension )
|
||||
document.head.appendChild(el = e('script', {
|
||||
|
@ -253,7 +258,7 @@ export default {
|
|||
src: 'https://platform.twitter.com/widgets.js',
|
||||
onLoad: () => el.remove()
|
||||
}));
|
||||
},
|
||||
},*/
|
||||
|
||||
methods: {
|
||||
updateUnseen() {
|
||||
|
@ -308,4 +313,4 @@ export default {
|
|||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
88
src/modules/main_menu/components/rich-feed.vue
Normal file
88
src/modules/main_menu/components/rich-feed.vue
Normal file
|
@ -0,0 +1,88 @@
|
|||
<template>
|
||||
<div v-if="feed">
|
||||
<chat-rich
|
||||
v-for="entry in feed"
|
||||
:data="entry"
|
||||
class="tw-mg-b-1"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { maybe_call } from 'utilities/object';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
'chat-rich': async () => {
|
||||
const stuff = await import(/* webpackChunkName: "chat" */ 'src/modules/chat/components');
|
||||
return stuff.default('./chat-rich.vue').default;
|
||||
}
|
||||
},
|
||||
|
||||
props: ['context', 'url'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
error: null,
|
||||
feed: null
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.loadFromURL();
|
||||
},
|
||||
|
||||
methods: {
|
||||
async loadFromURL() {
|
||||
if ( this.loading )
|
||||
return;
|
||||
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
this.feed = null;
|
||||
|
||||
const chat = this.context.getFFZ().resolve('chat'),
|
||||
url = this.url;
|
||||
|
||||
if ( ! url ) {
|
||||
this.loading = false;
|
||||
this.error = null;
|
||||
this.feed = [];
|
||||
return;
|
||||
}
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = await chat.get_link_info(url, false, false);
|
||||
} catch(err) {
|
||||
this.loading = false;
|
||||
this.error = err;
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! data?.v ) {
|
||||
this.error = 'Invalid response.';
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! data.feed )
|
||||
data = {feed: [data]};
|
||||
|
||||
this.feed = data.feed.map(entry => {
|
||||
entry.allow_media = true;
|
||||
entry.allow_unsafe = false;
|
||||
|
||||
return {
|
||||
getData: () => entry
|
||||
}
|
||||
});
|
||||
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
|
@ -34,6 +34,20 @@ declare global {
|
|||
}
|
||||
}
|
||||
|
||||
declare module 'utilities/types' {
|
||||
interface ModuleMap {
|
||||
metadata: Metadata
|
||||
}
|
||||
interface SettingsTypeMap {
|
||||
'metadata.clip-download': boolean;
|
||||
'metadata.clip-download.force': boolean;
|
||||
'metadata.player-stats': boolean;
|
||||
'metadata.uptime': number;
|
||||
'metadata.stream-delay-warning': number;
|
||||
'metadata.viewers': boolean;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export type MetadataState = {
|
||||
/** Whether or not the metadata is being rendered onto the player directly. */
|
||||
|
@ -482,10 +496,17 @@ export default class Metadata extends Module {
|
|||
icon: 'ffz-i-download',
|
||||
|
||||
click(src) {
|
||||
const title = this.settings.get('context.title');
|
||||
const title = this.settings.get('context.title') || 'Untitled';
|
||||
const name = title.replace(/[\\/:"*?<>|]+/, '_') + '.mp4';
|
||||
|
||||
const link = createElement('a', {target: '_blank', download: name, href: src, style: {display: 'none'}});
|
||||
const link = createElement('a', {
|
||||
target: '_blank',
|
||||
download: name,
|
||||
href: src,
|
||||
style: {
|
||||
display: 'none'
|
||||
}
|
||||
});
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
|
|
@ -20,6 +20,16 @@ declare global {
|
|||
}
|
||||
}
|
||||
|
||||
declare module 'utilities/types' {
|
||||
interface ModuleEventMap {
|
||||
tooltips: TooltipEvents;
|
||||
}
|
||||
interface ModuleMap {
|
||||
tooltips: TooltipProvider;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export type TooltipEvents = {
|
||||
/**
|
||||
* When this event is emitted, the tooltip provider will attempt to remove
|
||||
|
|
|
@ -8,6 +8,10 @@ import {EventEmitter} from 'utilities/events';
|
|||
import {has, get as getter, array_equals, set_equals, map_equals, deep_equals} from 'utilities/object';
|
||||
|
||||
import * as DEFINITIONS from './typehandlers';
|
||||
import type { AllSettingsKeys, ContextData, SettingMetadata, SettingType, SettingDefinition, SettingsKeys } from './types';
|
||||
import type SettingsManager from '.';
|
||||
import type SettingsProfile from './profile';
|
||||
import type { SettingsTypeMap } from 'utilities/types';
|
||||
|
||||
/**
|
||||
* Perform a basic check of a setting's requirements to see if they changed.
|
||||
|
@ -16,7 +20,11 @@ import * as DEFINITIONS from './typehandlers';
|
|||
* @param {Map} old_cache
|
||||
* @returns Whether or not they changed.
|
||||
*/
|
||||
function compare_requirements(definition, cache, old_cache) {
|
||||
function compare_requirements(
|
||||
definition: SettingDefinition<any>,
|
||||
cache: Map<string, unknown>,
|
||||
old_cache: Map<string, unknown>
|
||||
) {
|
||||
if ( ! definition || ! Array.isArray(definition.requires) )
|
||||
return false;
|
||||
|
||||
|
@ -47,14 +55,44 @@ function compare_requirements(definition, cache, old_cache) {
|
|||
}
|
||||
|
||||
|
||||
export type SettingsContextEvents = {
|
||||
[K in keyof SettingsTypeMap as `changed:${K}`]: [value: SettingsTypeMap[K], old_value: SettingsTypeMap[K]];
|
||||
} & {
|
||||
[K in keyof SettingsTypeMap as `uses_changed:${K}`]: [uses: number[] | null, old_uses: number[] | null];
|
||||
} & {
|
||||
changed: [key: SettingsKeys, value: any, old_value: any];
|
||||
uses_changed: [key: SettingsKeys, uses: number[] | null, old_uses: number[] | null];
|
||||
|
||||
context_changed: [];
|
||||
profiles_changed: [];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The SettingsContext class provides a context through which to read
|
||||
* settings values in addition to emitting events when settings values
|
||||
* are changed.
|
||||
* @extends EventEmitter
|
||||
*/
|
||||
export default class SettingsContext extends EventEmitter {
|
||||
constructor(manager, context) {
|
||||
export default class SettingsContext extends EventEmitter<SettingsContextEvents> {
|
||||
|
||||
parent: SettingsContext | null;
|
||||
manager: SettingsManager;
|
||||
|
||||
order: number[];
|
||||
|
||||
/** @internal */
|
||||
_context: ContextData;
|
||||
private __context: ContextData = null as any;
|
||||
|
||||
private __profiles: SettingsProfile[];
|
||||
|
||||
private __cache: Map<SettingsKeys, unknown>;
|
||||
private __meta: Map<SettingsKeys, SettingMetadata>;
|
||||
|
||||
private __ls_listening: boolean;
|
||||
private __ls_wanted: Map<string, Set<string>>;
|
||||
|
||||
constructor(manager: SettingsContext | SettingsManager, context?: ContextData) {
|
||||
super();
|
||||
|
||||
if ( manager instanceof SettingsContext ) {
|
||||
|
@ -68,7 +106,7 @@ export default class SettingsContext extends EventEmitter {
|
|||
this.manager = manager;
|
||||
}
|
||||
|
||||
this.manager.__contexts.push(this);
|
||||
(this.manager as any).__contexts.push(this);
|
||||
this._context = context || {};
|
||||
|
||||
/*this._context_objects = new Set;
|
||||
|
@ -93,7 +131,7 @@ export default class SettingsContext extends EventEmitter {
|
|||
for(const profile of this.__profiles)
|
||||
profile.off('changed', this._onChanged, this);
|
||||
|
||||
const contexts = this.manager.__contexts,
|
||||
const contexts = (this.manager as any).__contexts,
|
||||
idx = contexts.indexOf(this);
|
||||
|
||||
if ( idx !== -1 )
|
||||
|
@ -106,26 +144,26 @@ export default class SettingsContext extends EventEmitter {
|
|||
// ========================================================================
|
||||
|
||||
_watchLS() {
|
||||
if ( this.__ls_watched )
|
||||
if ( this.__ls_listening )
|
||||
return;
|
||||
|
||||
this.__ls_watched = true;
|
||||
this.__ls_listening = true;
|
||||
this.manager.on(':ls-update', this._onLSUpdate, this);
|
||||
}
|
||||
|
||||
_unwatchLS() {
|
||||
if ( ! this.__ls_watched )
|
||||
if ( ! this.__ls_listening )
|
||||
return;
|
||||
|
||||
this.__ls_watched = false;
|
||||
this.__ls_listening = false;
|
||||
this.manager.off(':ls-update', this._onLSUpdate, this);
|
||||
}
|
||||
|
||||
_onLSUpdate(key) {
|
||||
_onLSUpdate(key: string) {
|
||||
const keys = this.__ls_wanted.get(`ls.${key}`);
|
||||
if ( keys )
|
||||
for(const key of keys)
|
||||
this._update(key, key, []);
|
||||
this._update(key as SettingsKeys, key as SettingsKeys, []);
|
||||
}
|
||||
|
||||
|
||||
|
@ -147,8 +185,8 @@ export default class SettingsContext extends EventEmitter {
|
|||
|
||||
|
||||
selectProfiles() {
|
||||
const new_profiles = [],
|
||||
order = this.order = [];
|
||||
const new_profiles: SettingsProfile[] = [],
|
||||
order: number[] = this.order = [];
|
||||
|
||||
if ( ! this.manager.disable_profiles ) {
|
||||
for(const profile of this.manager.__profiles)
|
||||
|
@ -171,13 +209,13 @@ export default class SettingsContext extends EventEmitter {
|
|||
|
||||
for(const profile of new_profiles)
|
||||
if ( ! this.__profiles.includes(profile) ) {
|
||||
profile.on('changed', this._onChanged, this);
|
||||
profile.on('changed', this._onChanged as any, this);
|
||||
changed_ids.add(profile.id);
|
||||
}
|
||||
|
||||
this.__profiles = new_profiles;
|
||||
this.emit('profiles_changed');
|
||||
this.rebuildCache(changed_ids);
|
||||
this.rebuildCache(/*changed_ids*/);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -203,7 +241,7 @@ export default class SettingsContext extends EventEmitter {
|
|||
const definition = this.manager.definitions.get(key);
|
||||
let changed = false;
|
||||
|
||||
if ( definition && definition.equals ) {
|
||||
if ( ! Array.isArray(definition) && definition?.equals ) {
|
||||
if ( definition.equals === 'requirements' )
|
||||
changed = compare_requirements(definition, this.__cache, old_cache);
|
||||
else if ( typeof definition.equals === 'function' )
|
||||
|
@ -224,7 +262,7 @@ export default class SettingsContext extends EventEmitter {
|
|||
|
||||
if ( changed ) {
|
||||
this.emit('changed', key, new_value, old_value);
|
||||
this.emit(`changed:${key}`, new_value, old_value);
|
||||
this.emit(`changed:${key}`, new_value, old_value as any);
|
||||
}
|
||||
|
||||
if ( ! array_equals(new_uses, old_uses) ) {
|
||||
|
@ -239,12 +277,12 @@ export default class SettingsContext extends EventEmitter {
|
|||
// Context Control
|
||||
// ========================================================================
|
||||
|
||||
context(context) {
|
||||
context(context: ContextData) {
|
||||
return new SettingsContext(this, context);
|
||||
}
|
||||
|
||||
|
||||
updateContext(context) {
|
||||
updateContext(context: ContextData) {
|
||||
let changed = false;
|
||||
|
||||
for(const key in context)
|
||||
|
@ -258,7 +296,7 @@ export default class SettingsContext extends EventEmitter {
|
|||
// This can catch a recursive structure error.
|
||||
}
|
||||
|
||||
this._context[key] = val;
|
||||
this._context[key] = val as any;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
|
@ -325,8 +363,8 @@ export default class SettingsContext extends EventEmitter {
|
|||
}*/
|
||||
|
||||
|
||||
setContext(context) {
|
||||
this._context_objects = new Set;
|
||||
setContext(context: ContextData) {
|
||||
//this._context_objects = new Set;
|
||||
this._context = {};
|
||||
this.updateContext(context);
|
||||
}
|
||||
|
@ -336,11 +374,14 @@ export default class SettingsContext extends EventEmitter {
|
|||
// Data Access
|
||||
// ========================================================================
|
||||
|
||||
_onChanged(key) {
|
||||
_onChanged(key: SettingsKeys) {
|
||||
this._update(key, key, []);
|
||||
}
|
||||
|
||||
_update(key, initial, visited) {
|
||||
_update<
|
||||
K extends SettingsKeys,
|
||||
TValue = SettingType<K>
|
||||
>(key: K, initial: SettingsKeys, visited: SettingsKeys[]) {
|
||||
if ( ! this.__cache.has(key) )
|
||||
return;
|
||||
|
||||
|
@ -349,7 +390,7 @@ export default class SettingsContext extends EventEmitter {
|
|||
|
||||
visited.push(key);
|
||||
|
||||
const old_value = this.__cache.get(key),
|
||||
const old_value = this.__cache.get(key) as TValue | undefined,
|
||||
old_meta = this.__meta.get(key),
|
||||
new_value = this._get(key, key, []),
|
||||
new_meta = this.__meta.get(key),
|
||||
|
@ -359,38 +400,41 @@ export default class SettingsContext extends EventEmitter {
|
|||
|
||||
if ( ! array_equals(new_uses, old_uses) ) {
|
||||
this.emit('uses_changed', key, new_uses, old_uses);
|
||||
this.emit(`uses_changed:${key}`, new_uses, old_uses);
|
||||
this.emit(`uses_changed:${key}` as any, new_uses, old_uses);
|
||||
}
|
||||
|
||||
if ( old_value === new_value )
|
||||
return;
|
||||
|
||||
this.emit('changed', key, new_value, old_value);
|
||||
this.emit(`changed:${key}`, new_value, old_value);
|
||||
this.emit(`changed:${key}` as any, new_value, old_value);
|
||||
|
||||
const definition = this.manager.definitions.get(key);
|
||||
if ( definition && definition.required_by )
|
||||
if ( ! Array.isArray(definition) && definition?.required_by )
|
||||
for(const req_key of definition.required_by)
|
||||
if ( ! req_key.startsWith('context.') && ! req_key.startsWith('ls.') )
|
||||
this._update(req_key, initial, Array.from(visited));
|
||||
this._update(req_key as SettingsKeys, initial, Array.from(visited));
|
||||
}
|
||||
|
||||
|
||||
_get(key, initial, visited) {
|
||||
_get<
|
||||
K extends SettingsKeys,
|
||||
TValue = SettingType<K>
|
||||
>(key: K, initial: SettingsKeys, visited: SettingsKeys[]): TValue {
|
||||
if ( visited.includes(key) )
|
||||
throw new Error(`cyclic dependency when resolving setting "${initial}"`);
|
||||
|
||||
visited.push(key);
|
||||
|
||||
const definition = this.manager.definitions.get(key),
|
||||
raw_type = definition && definition.type,
|
||||
const definition = this.manager.definitions.get(key);
|
||||
const raw_type = ! Array.isArray(definition) && definition?.type,
|
||||
type = raw_type ? DEFINITIONS[raw_type] : DEFINITIONS.basic;
|
||||
|
||||
if ( ! type )
|
||||
throw new Error(`non-existent setting type "${raw_type}"`);
|
||||
|
||||
const raw_value = this._getRaw(key, type),
|
||||
meta = {
|
||||
meta: SettingMetadata = {
|
||||
uses: raw_value ? raw_value[1] : null
|
||||
};
|
||||
|
||||
|
@ -421,8 +465,8 @@ export default class SettingsContext extends EventEmitter {
|
|||
|
||||
keys.add(key);
|
||||
|
||||
} else if ( ! req_key.startsWith('context.') && ! this.__cache.has(req_key) )
|
||||
this._get(req_key, initial, Array.from(visited));
|
||||
} else if ( ! req_key.startsWith('context.') && ! this.__cache.has(req_key as SettingsKeys) )
|
||||
this._get(req_key as SettingsKeys, initial, Array.from(visited));
|
||||
|
||||
if ( definition.process )
|
||||
value = definition.process(this, value, meta);
|
||||
|
@ -440,70 +484,84 @@ export default class SettingsContext extends EventEmitter {
|
|||
}
|
||||
|
||||
|
||||
hasProfile(profile) {
|
||||
if ( typeof profile === 'number' )
|
||||
hasProfile(profile: number | SettingsProfile) {
|
||||
if ( typeof profile === 'number' ) {
|
||||
for(const prof of this.__profiles)
|
||||
if ( prof.id === profile )
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.__profiles.includes(profile);
|
||||
}
|
||||
|
||||
|
||||
_getRaw(key, type) {
|
||||
_getRaw(key: SettingsKeys, type) {
|
||||
if ( ! type )
|
||||
throw new Error(`non-existent type for ${key}`)
|
||||
|
||||
return type.get(key, this.profiles(), this.manager.definitions.get(key), this.manager.log, this);
|
||||
return type.get(
|
||||
key,
|
||||
this.profiles(),
|
||||
this.manager.definitions.get(key),
|
||||
this.manager.log,
|
||||
this
|
||||
);
|
||||
}
|
||||
/* for(const profile of this.__profiles)
|
||||
if ( profile.has(key) )
|
||||
return [profile.get(key), profile]
|
||||
}*/
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Data Access
|
||||
// ========================================================================
|
||||
|
||||
update(key) {
|
||||
update(key: SettingsKeys) {
|
||||
this._update(key, key, []);
|
||||
}
|
||||
|
||||
get(key) {
|
||||
get<
|
||||
K extends AllSettingsKeys,
|
||||
TValue = SettingType<K>
|
||||
>(key: K): TValue {
|
||||
if ( key.startsWith('ls.') )
|
||||
return this.manager.getLS(key.slice(3));
|
||||
return this.manager.getLS(key.slice(3)) as TValue;
|
||||
|
||||
if ( key.startsWith('context.') )
|
||||
//return this.__context[key.slice(8)];
|
||||
return getter(key.slice(8), this.__context);
|
||||
|
||||
if ( this.__cache.has(key) )
|
||||
return this.__cache.get(key);
|
||||
if ( this.__cache.has(key as SettingsKeys) )
|
||||
return this.__cache.get(key as SettingsKeys) as TValue;
|
||||
|
||||
return this._get(key, key, []);
|
||||
return this._get(key as SettingsKeys, key as SettingsKeys, []);
|
||||
}
|
||||
|
||||
getChanges(key, fn, ctx) {
|
||||
getChanges<
|
||||
K extends SettingsKeys,
|
||||
TValue = SettingsTypeMap[K]
|
||||
>(key: K, fn: (value: TValue, old_value: TValue | undefined) => void, ctx?: any) {
|
||||
this.onChange(key, fn, ctx);
|
||||
fn.call(ctx, this.get(key));
|
||||
fn.call(ctx, this.get(key), undefined as TValue);
|
||||
}
|
||||
|
||||
onChange(key, fn, ctx) {
|
||||
this.on(`changed:${key}`, fn, ctx);
|
||||
onChange<
|
||||
K extends SettingsKeys,
|
||||
TValue = SettingsTypeMap[K]
|
||||
>(key: K, fn: (value: TValue, old_value: TValue) => void, ctx?: any) {
|
||||
this.on(`changed:${key}`, fn as any, ctx);
|
||||
}
|
||||
|
||||
|
||||
uses(key) {
|
||||
uses(key: AllSettingsKeys) {
|
||||
if ( key.startsWith('ls.') )
|
||||
return null;
|
||||
|
||||
if ( key.startsWith('context.') )
|
||||
return null;
|
||||
|
||||
if ( ! this.__meta.has(key) )
|
||||
this._get(key, key, []);
|
||||
if ( ! this.__meta.has(key as SettingsKeys) )
|
||||
this._get(key as SettingsKeys, key as SettingsKeys, []);
|
||||
|
||||
return this.__meta.get(key).uses;
|
||||
return this.__meta.get(key as SettingsKeys)?.uses ?? null;
|
||||
}
|
||||
}
|
|
@ -4,29 +4,31 @@
|
|||
// Settings System
|
||||
// ============================================================================
|
||||
|
||||
import { DEBUG } from 'utilities/constants';
|
||||
import Module, { GenericModule, buildAddonProxy } from 'utilities/module';
|
||||
import {deep_equals, has, debounce, deep_copy} from 'utilities/object';
|
||||
import {parse as parse_path} from 'utilities/path-parser';
|
||||
import {PathNode, parse as parse_path} from 'utilities/path-parser';
|
||||
|
||||
import SettingsProfile from './profile';
|
||||
import SettingsContext from './context';
|
||||
//import MigrationManager from './migration';
|
||||
|
||||
import * as PROCESSORS from './processors';
|
||||
import * as VALIDATORS from './validators';
|
||||
import * as FILTERS from './filters';
|
||||
import * as CLEARABLES from './clearables';
|
||||
import type { SettingsProfileMetadata, ContextData, ExportedFullDump, SettingsClearable, SettingsDefinition, SettingsProcessor, SettingsUiDefinition, SettingsValidator } from './types';
|
||||
import type { FilterType } from '../utilities/filtering';
|
||||
|
||||
import type { SettingsProfileMetadata, ContextData, ExportedFullDump, SettingsClearable, SettingDefinition, SettingProcessor, SettingUiDefinition, SettingValidator, SettingType, ExportedBlobMetadata, SettingsKeys, AllSettingsKeys, ConcreteLocalStorageData } from './types';
|
||||
import type { FilterType } from 'utilities/filtering';
|
||||
import { AdvancedSettingsProvider, IndexedDBProvider, LocalStorageProvider, Providers, type SettingsProvider } from './providers';
|
||||
import type { AddonInfo } from '../utilities/types';
|
||||
import type { AddonInfo, SettingsTypeMap } from 'utilities/types';
|
||||
|
||||
export {parse as parse_path} from 'utilities/path-parser';
|
||||
|
||||
|
||||
function postMessage(target: Window, msg) {
|
||||
// TODO: Special types for msg.
|
||||
function postMessage(target: MessageEventSource, msg: any) {
|
||||
try {
|
||||
target.postMessage(msg, '*');
|
||||
(target as Window).postMessage(msg, '*');
|
||||
return true;
|
||||
} catch(err) {
|
||||
return false;
|
||||
|
@ -36,14 +38,33 @@ function postMessage(target: Window, msg) {
|
|||
export const NO_SYNC_KEYS = ['session'];
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Registration
|
||||
// ============================================================================
|
||||
|
||||
declare module 'utilities/types' {
|
||||
interface ModuleEventMap {
|
||||
settings: SettingsEvents;
|
||||
}
|
||||
interface ModuleMap {
|
||||
settings: SettingsManager;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Events
|
||||
// ============================================================================
|
||||
|
||||
// TODO: Check settings keys for better typing on events.
|
||||
|
||||
export type SettingsEvents = {
|
||||
[key: `:changed:${string}`]: [value: any, old_value: any];
|
||||
[key: `:uses_changed:${string}`]: [uses: number[], old_uses: number[]];
|
||||
[K in keyof SettingsTypeMap as `:changed:${K}`]: [value: SettingsTypeMap[K], old_value: SettingsTypeMap[K]];
|
||||
} & {
|
||||
[key: `:uses_changed:${string}`]: [uses: number[] | null, old_uses: number[] | null];
|
||||
|
||||
':added-definition': [key: string, definition: SettingsDefinition<any>];
|
||||
':removed-definition': [key: string, definition: SettingsDefinition<any>];
|
||||
':added-definition': [key: SettingsKeys, definition: SettingDefinition<any>];
|
||||
':removed-definition': [key: SettingsKeys, definition: SettingDefinition<any>];
|
||||
|
||||
':quota-exceeded': [];
|
||||
':change-provider': [];
|
||||
|
@ -81,29 +102,35 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
|||
// Storage of Things
|
||||
clearables: Record<string, SettingsClearable>;
|
||||
filters: Record<string, FilterType<any, ContextData>>;
|
||||
processors: Record<string, SettingsProcessor<any>>;
|
||||
processors: Record<string, SettingProcessor<any>>;
|
||||
providers: Record<string, typeof SettingsProvider>;
|
||||
validators: Record<string, SettingsValidator<any>>;
|
||||
validators: Record<string, SettingValidator<any>>;
|
||||
|
||||
// Storage of Settings
|
||||
ui_structures: Map<string, SettingsUiDefinition<any>>;
|
||||
definitions: Map<string, SettingsDefinition<any> | string[]>;
|
||||
ui_structures: Map<string, SettingDefinition<any>>;
|
||||
definitions: Map<string, SettingDefinition<any> | string[]>;
|
||||
|
||||
// Storage of State
|
||||
provider: SettingsProvider | null = null;
|
||||
// The provider *can* technically be null but it won't ever be in practice.
|
||||
// So we don't set the type to null to avoid making annoying checks everywhere.
|
||||
provider: SettingsProvider = null as any;
|
||||
main_context: SettingsContext;
|
||||
|
||||
private _context_proxies: Set<MessageEventSource>;
|
||||
|
||||
private _update_timer?: ReturnType<typeof setTimeout> | null;
|
||||
private _time_timer?: ReturnType<typeof setTimeout> | null;
|
||||
private _time_next?: number | null;
|
||||
|
||||
private _active_provider: string = 'local';
|
||||
private _idb: IndexedDBProvider | null = null;
|
||||
|
||||
private _provider_waiter?: Promise<SettingsProvider> | null;
|
||||
private _provider_resolve?: ((input: SettingsProvider) => void) | null;
|
||||
|
||||
private __contexts: SettingsContext[];
|
||||
private __profiles: SettingsProfile[];
|
||||
/** @internal */
|
||||
__contexts: SettingsContext[];
|
||||
/** @internal */
|
||||
__profiles: SettingsProfile[];
|
||||
private __profile_ids: Record<number, SettingsProfile | null>;
|
||||
|
||||
/**
|
||||
|
@ -123,8 +150,6 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
|||
this.providers[key] = provider;
|
||||
}
|
||||
|
||||
const test = this.resolve('load_tracker');
|
||||
|
||||
// This cannot be modified at a future time, as providers NEED
|
||||
// to be ready very early in FFZ intitialization. Seal it.
|
||||
Object.seal(this.providers);
|
||||
|
@ -204,11 +229,11 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
|||
// Also create the main context as early as possible.
|
||||
this.main_context = new SettingsContext(this);
|
||||
|
||||
this.main_context.on('changed', (key, new_value, old_value) => {
|
||||
this.main_context.on('changed', (key: keyof SettingsTypeMap, new_value, old_value) => {
|
||||
this.emit(`:changed:${key}`, new_value, old_value);
|
||||
});
|
||||
|
||||
this.main_context.on('uses_changed', (key, new_uses, old_uses) => {
|
||||
this.main_context.on('uses_changed', (key: keyof SettingsTypeMap, new_uses, old_uses) => {
|
||||
this.emit(`:uses_changed:${key}`, new_uses, old_uses);
|
||||
});
|
||||
|
||||
|
@ -218,7 +243,7 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
|||
window.addEventListener('message', event => {
|
||||
const type = event.data?.ffz_type;
|
||||
|
||||
if ( type === 'request-context' ) {
|
||||
if ( type === 'request-context' && event.source ) {
|
||||
this._context_proxies.add(event.source);
|
||||
this._updateContextProxies(event.source);
|
||||
}
|
||||
|
@ -234,7 +259,7 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
|||
this.enable();
|
||||
}
|
||||
|
||||
_updateContextProxies(proxy) {
|
||||
_updateContextProxies(proxy?: MessageEventSource) {
|
||||
if ( ! proxy && ! this._context_proxies.size )
|
||||
return;
|
||||
|
||||
|
@ -266,7 +291,7 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
|||
|
||||
generateLog() {
|
||||
const out = [];
|
||||
for(const [key, value] of this.main_context.__cache.entries())
|
||||
for(const [key, value] of (this.main_context as any).__cache.entries())
|
||||
out.push(`${key}: ${JSON.stringify(value)}`);
|
||||
|
||||
return out.join('\n');
|
||||
|
@ -390,6 +415,9 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
|||
if ( ! this.__ls_timer )
|
||||
this.__ls_timer = setTimeout(this._updateLS, 0);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
private _hookLS() {
|
||||
|
@ -532,7 +560,7 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
|||
out.file('settings.json', JSON.stringify(settings));
|
||||
|
||||
// Blob Settings
|
||||
const metadata = {};
|
||||
const metadata: Record<string, ExportedBlobMetadata> = {};
|
||||
|
||||
if ( this.provider instanceof AdvancedSettingsProvider && this.provider.supportsBlobs ) {
|
||||
const keys = await this.provider.blobKeys();
|
||||
|
@ -542,7 +570,9 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
|||
if ( ! blob )
|
||||
continue;
|
||||
|
||||
const md = {key};
|
||||
const md: ExportedBlobMetadata = {
|
||||
key
|
||||
};
|
||||
|
||||
if ( blob instanceof File ) {
|
||||
md.type = 'file';
|
||||
|
@ -664,9 +694,6 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
|||
|
||||
if ( this.providers[wanted] ) {
|
||||
const provider = new (this.providers[wanted] as any)(this) as SettingsProvider;
|
||||
if ( wanted === 'idb' )
|
||||
this._idb = provider as IndexedDBProvider;
|
||||
|
||||
this._active_provider = wanted;
|
||||
return provider;
|
||||
}
|
||||
|
@ -721,7 +748,7 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
|||
return;
|
||||
|
||||
const old_provider = this.provider;
|
||||
this.provider = null;
|
||||
this.provider = null as any;
|
||||
|
||||
// Let all other tabs know what's up.
|
||||
old_provider.broadcastTransfer();
|
||||
|
@ -1101,15 +1128,38 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
|||
// Context Helpers
|
||||
// ========================================================================
|
||||
|
||||
context(env) { return this.main_context.context(env) }
|
||||
get(key) { return this.main_context.get(key); }
|
||||
getChanges(key, fn, ctx) { return this.main_context.getChanges(key, fn, ctx); }
|
||||
onChange(key, fn, ctx) { return this.main_context.onChange(key, fn, ctx); }
|
||||
uses(key: string) { return this.main_context.uses(key) }
|
||||
update(key: string) { return this.main_context.update(key) }
|
||||
context(env: ContextData) { return this.main_context.context(env) }
|
||||
|
||||
updateContext(context: Partial<ContextData>) { return this.main_context.updateContext(context) }
|
||||
setContext(context: Partial<ContextData>) { return this.main_context.setContext(context) }
|
||||
get<
|
||||
K extends AllSettingsKeys,
|
||||
TValue = SettingType<K>
|
||||
>(
|
||||
key: K
|
||||
): TValue { return this.main_context.get(key); }
|
||||
|
||||
getChanges<
|
||||
K extends SettingsKeys,
|
||||
TValue = SettingType<K>
|
||||
>(
|
||||
key: K,
|
||||
fn: (val: TValue) => void,
|
||||
ctx?: any
|
||||
) { return this.main_context.getChanges(key, fn, ctx); }
|
||||
|
||||
onChange<
|
||||
K extends SettingsKeys,
|
||||
TValue = SettingType<K>
|
||||
>(
|
||||
key: K,
|
||||
fn: (val: TValue) => void,
|
||||
ctx?: any
|
||||
) { return this.main_context.onChange(key, fn, ctx); }
|
||||
|
||||
uses<K extends AllSettingsKeys>(key: K) { return this.main_context.uses(key) }
|
||||
update<K extends SettingsKeys>(key: K) { return this.main_context.update(key) }
|
||||
|
||||
updateContext(context: ContextData) { return this.main_context.updateContext(context) }
|
||||
setContext(context: ContextData) { return this.main_context.setContext(context) }
|
||||
|
||||
|
||||
// ========================================================================
|
||||
|
@ -1123,11 +1173,18 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
|||
const overrides: Record<string, any> = {},
|
||||
is_dev = DEBUG || addon?.dev;
|
||||
|
||||
overrides.add = <T,>(key: string, definition: SettingsDefinition<T>) => {
|
||||
overrides.add = <
|
||||
K extends SettingsKeys,
|
||||
TValue = SettingType<K>,
|
||||
>(key: K, definition: SettingDefinition<TValue>) => {
|
||||
return this.add(key, definition, addon_id);
|
||||
};
|
||||
|
||||
overrides.addUI = <T,>(key: string, definition: SettingsUiDefinition<T>) => {
|
||||
// TODO: Update addUI here too
|
||||
overrides.addUI = <
|
||||
K extends string,
|
||||
TValue = K extends SettingsKeys ? SettingType<K> : unknown,
|
||||
>(key: K, definition: SettingUiDefinition<TValue>) => {
|
||||
return this.addUI(key, definition, addon_id);
|
||||
};
|
||||
|
||||
|
@ -1142,7 +1199,10 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
|||
// Definitions
|
||||
// ========================================================================
|
||||
|
||||
add<T>(key: string, definition: SettingsDefinition<T>, source?: string) {
|
||||
add<
|
||||
K extends SettingsKeys,
|
||||
TValue = SettingType<K>
|
||||
>(key: K, definition: SettingDefinition<TValue>, source?: string) {
|
||||
|
||||
const old_definition = this.definitions.get(key),
|
||||
required_by = (Array.isArray(old_definition)
|
||||
|
@ -1179,8 +1239,10 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
|||
if ( ! ui.key && ui.title )
|
||||
ui.key = ui.title.toSnakeCase();
|
||||
|
||||
if ( (ui.component === 'setting-select-box' || ui.component === 'setting-combo-box') && Array.isArray(ui.data) && ! ui.no_i18n
|
||||
&& key !== 'ffzap.core.highlight_sound' ) { // TODO: Remove workaround.
|
||||
if ( (ui.component === 'setting-select-box' ||
|
||||
ui.component === 'setting-combo-box') &&
|
||||
Array.isArray(ui.data) && ! ui.no_i18n
|
||||
) {
|
||||
const i18n_base = `${ui.i18n_key || `setting.entry.${key}`}.values`;
|
||||
for(const value of ui.data) {
|
||||
if ( value.i18n_key === undefined && value.value !== undefined )
|
||||
|
@ -1190,7 +1252,7 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
|||
}
|
||||
|
||||
if ( definition.changed )
|
||||
this.on(`:changed:${key}`, definition.changed);
|
||||
this.on(`:changed:${key}` as any, definition.changed);
|
||||
|
||||
this.definitions.set(key, definition);
|
||||
|
||||
|
@ -1220,36 +1282,39 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
|||
if ( Array.isArray(definition.requires) )
|
||||
for(const req_key of definition.requires) {
|
||||
let req = this.definitions.get(req_key);
|
||||
if ( req.required_by )
|
||||
req = req.required_by;
|
||||
if ( Array.isArray(req) ) {
|
||||
const idx = req.indexOf(key);
|
||||
if ( idx !== -1 )
|
||||
req.splice(idx, 1);
|
||||
}
|
||||
|
||||
} else if ( req?.required_by )
|
||||
req = req.required_by;
|
||||
}
|
||||
|
||||
if ( definition.changed )
|
||||
this.off(`:changed:${key}`, definition.changed);
|
||||
this.off(`:changed:${key}` as any, definition.changed);
|
||||
|
||||
this.emit(':removed-definition', key, definition);
|
||||
this.emit(':removed-definition', key as any, definition);
|
||||
}
|
||||
|
||||
|
||||
addUI(key, definition, source) {
|
||||
if ( typeof key === 'object' ) {
|
||||
for(const k in key)
|
||||
if ( has(key, k) )
|
||||
this.add(k, key[k]);
|
||||
return;
|
||||
}
|
||||
// TODO: Update this because addUI doesn't use keys or types
|
||||
addUI<
|
||||
K extends string,
|
||||
TValue = K extends SettingsKeys ? SettingType<K> : unknown
|
||||
>(key: K, definition: Partial<SettingUiDefinition<TValue>>, source?: string) {
|
||||
|
||||
if ( ! definition.ui )
|
||||
definition = {ui: definition};
|
||||
let def: SettingDefinition<TValue>;
|
||||
if ( (definition as any).ui )
|
||||
def = (definition as any);
|
||||
else
|
||||
def = {
|
||||
ui: definition as SettingUiDefinition<TValue>
|
||||
} as SettingDefinition<TValue>;
|
||||
|
||||
definition.__source = source;
|
||||
def.__source = source;
|
||||
|
||||
const ui = definition.ui;
|
||||
const ui = def.ui as SettingUiDefinition<TValue>;
|
||||
ui.path_tokens = ui.path_tokens ?
|
||||
format_path_tokens(ui.path_tokens) :
|
||||
ui.path ?
|
||||
|
@ -1260,12 +1325,12 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
|||
ui.key = ui.title.toSnakeCase();
|
||||
|
||||
const old_definition = this.ui_structures.get(key);
|
||||
this.ui_structures.set(key, definition);
|
||||
this.ui_structures.set(key, def);
|
||||
|
||||
// Do not re-emit `added-definition` when re-adding an existing
|
||||
// setting. Prevents the settings UI from goofing up.
|
||||
if ( ! old_definition )
|
||||
this.emit(':added-definition', key, definition);
|
||||
this.emit(':added-definition', key as any, def);
|
||||
}
|
||||
|
||||
|
||||
|
@ -1288,7 +1353,7 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
|||
}
|
||||
|
||||
|
||||
addProcessor(key: string | Record<string, SettingsProcessor<any>>, processor?: SettingsProcessor<any>) {
|
||||
addProcessor(key: string | Record<string, SettingProcessor<any>>, processor?: SettingProcessor<any>) {
|
||||
if ( typeof key === 'object' ) {
|
||||
for(const [k, value] of Object.entries(key))
|
||||
this.addProcessor(k, value);
|
||||
|
@ -1300,7 +1365,7 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
|||
this.processors[key] = processor;
|
||||
}
|
||||
|
||||
getProcessor<T>(key: string): SettingsProcessor<T> | null {
|
||||
getProcessor<T>(key: string): SettingProcessor<T> | null {
|
||||
return this.processors[key] ?? null;
|
||||
}
|
||||
|
||||
|
@ -1308,7 +1373,7 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
|||
return deep_copy(this.processors);
|
||||
}
|
||||
|
||||
addValidator(key: string | Record<string, SettingsValidator<any>>, validator?: SettingsValidator<any>) {
|
||||
addValidator(key: string | Record<string, SettingValidator<any>>, validator?: SettingValidator<any>) {
|
||||
if ( typeof key === 'object' ) {
|
||||
for(const [k, value] of Object.entries(key))
|
||||
this.addValidator(k, value);
|
||||
|
@ -1320,7 +1385,7 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
|||
this.validators[key] = validator;
|
||||
}
|
||||
|
||||
getValidator<T>(key: string): SettingsValidator<T> | null {
|
||||
getValidator<T>(key: string): SettingValidator<T> | null {
|
||||
return this.validators[key] ?? null;
|
||||
}
|
||||
|
||||
|
@ -1330,14 +1395,14 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
|||
}
|
||||
|
||||
|
||||
export function format_path_tokens(tokens) {
|
||||
export function format_path_tokens(tokens: (string | PathNode)[]) {
|
||||
for(let i=0, l = tokens.length; i < l; i++) {
|
||||
const token = tokens[i];
|
||||
if ( typeof token === 'string' ) {
|
||||
tokens[i] = {
|
||||
key: token.toSnakeCase(),
|
||||
title: token
|
||||
}
|
||||
};
|
||||
|
||||
continue;
|
||||
}
|
||||
|
@ -1346,5 +1411,5 @@ export function format_path_tokens(tokens) {
|
|||
token.key = token.title.toSnakeCase();
|
||||
}
|
||||
|
||||
return tokens;
|
||||
return tokens as PathNode[];
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
'use strict';
|
||||
|
||||
import type { SettingsDefinition, SettingsProcessor, SettingsUiDefinition } from "./types";
|
||||
import type { SettingDefinition, SettingProcessor, SettingUiDefinition } from "./types";
|
||||
|
||||
const BAD = Symbol('BAD');
|
||||
type BadType = typeof BAD;
|
||||
|
@ -8,7 +8,7 @@ type BadType = typeof BAD;
|
|||
function do_number(
|
||||
input: number | BadType,
|
||||
default_value: number,
|
||||
definition: SettingsUiDefinition<number>
|
||||
definition: SettingUiDefinition<number>
|
||||
) {
|
||||
if ( typeof input !== 'number' || isNaN(input) || ! isFinite(input) )
|
||||
input = BAD;
|
||||
|
@ -38,7 +38,7 @@ function do_number(
|
|||
return input === BAD ? default_value : input;
|
||||
}
|
||||
|
||||
export const to_int: SettingsProcessor<number> = (
|
||||
export const to_int: SettingProcessor<number> = (
|
||||
value,
|
||||
default_value,
|
||||
definition
|
||||
|
@ -51,7 +51,7 @@ export const to_int: SettingsProcessor<number> = (
|
|||
return do_number(value as number, default_value, definition);
|
||||
}
|
||||
|
||||
export const to_float: SettingsProcessor<number> = (
|
||||
export const to_float: SettingProcessor<number> = (
|
||||
value: unknown,
|
||||
default_value,
|
||||
definition
|
||||
|
|
|
@ -10,7 +10,7 @@ import { isValidBlob, deserializeBlob, serializeBlob, BlobLike, SerializedBlobLi
|
|||
import {EventEmitter} from 'utilities/events';
|
||||
import {has, once} from 'utilities/object';
|
||||
import type SettingsManager from '.';
|
||||
import type { OptionalArray, OptionalPromise } from '../utilities/types';
|
||||
import type { OptionalArray, OptionalPromise, ProviderTypeMap } from '../utilities/types';
|
||||
|
||||
const DB_VERSION = 1,
|
||||
NOT_WWW_TWITCH = window.location.host !== 'www.twitch.tv',
|
||||
|
@ -96,10 +96,23 @@ export abstract class SettingsProvider extends EventEmitter<ProviderEvents> {
|
|||
|
||||
abstract flush(): OptionalPromise<void>;
|
||||
|
||||
abstract get<T>(key: string, default_value: T): T;
|
||||
abstract get<T>(key: string): T | null;
|
||||
abstract get<K extends keyof ProviderTypeMap>(
|
||||
key: K,
|
||||
default_value: ProviderTypeMap[K]
|
||||
): ProviderTypeMap[K];
|
||||
abstract get<K extends keyof ProviderTypeMap>(
|
||||
key: K
|
||||
): ProviderTypeMap[K] | null;
|
||||
abstract get<T>(
|
||||
key: Exclude<string, keyof ProviderTypeMap>,
|
||||
default_value: T
|
||||
): T;
|
||||
abstract get<T>(
|
||||
key: Exclude<string, keyof ProviderTypeMap>
|
||||
): T | null;
|
||||
|
||||
abstract set(key: string, value: any): void;
|
||||
abstract set<K extends keyof ProviderTypeMap>(key: K, value: ProviderTypeMap[K]): void;
|
||||
abstract set<K extends string>(key: Exclude<K, keyof ProviderTypeMap>, value: unknown): void;
|
||||
abstract delete(key: string): void;
|
||||
abstract clear(): void;
|
||||
|
||||
|
@ -301,8 +314,10 @@ export class LocalStorageProvider extends SettingsProvider {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
get<T>(key: string, default_value?: T): T {
|
||||
get<T>(
|
||||
key: string,
|
||||
default_value?: T
|
||||
): T {
|
||||
return this._cached.has(key)
|
||||
? this._cached.get(key)
|
||||
: default_value;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import type SettingsManager from ".";
|
||||
import type { FilterData } from "../utilities/filtering";
|
||||
import type { OptionalPromise, OptionallyCallable, RecursivePartial } from "../utilities/types";
|
||||
import type { PathNode } from "../utilities/path-parser";
|
||||
import type { ExtractSegments, ExtractType, JoinKeyPaths, ObjectKeyPaths, OptionalPromise, OptionallyCallable, RecursivePartial, SettingsTypeMap } from "../utilities/types";
|
||||
import type SettingsContext from "./context";
|
||||
import type { SettingsProvider } from "./providers";
|
||||
|
||||
|
@ -24,7 +25,7 @@ export type SettingsClearable = {
|
|||
|
||||
// Context
|
||||
|
||||
export type ContextData = RecursivePartial<{
|
||||
export interface ConcreteContextData {
|
||||
addonDev: boolean;
|
||||
|
||||
category: string;
|
||||
|
@ -62,17 +63,52 @@ export type ContextData = RecursivePartial<{
|
|||
theme: number;
|
||||
};
|
||||
|
||||
}>;
|
||||
};
|
||||
|
||||
export type ContextData = RecursivePartial<ConcreteContextData>;
|
||||
|
||||
export interface ConcreteLocalStorageData {
|
||||
test: number;
|
||||
}
|
||||
|
||||
export type LocalStorageData = Partial<ConcreteLocalStorageData>;
|
||||
|
||||
export type SettingsContextKeys = JoinKeyPaths<'context', ObjectKeyPaths<ConcreteContextData>>;
|
||||
export type SettingsLocalStorageKeys = JoinKeyPaths<'ls', ObjectKeyPaths<ConcreteLocalStorageData>> | JoinKeyPaths<'ls.raw', ObjectKeyPaths<ConcreteLocalStorageData>>;
|
||||
export type SettingsKeys = keyof SettingsTypeMap;
|
||||
export type AllSettingsKeys = SettingsKeys | SettingsContextKeys | SettingsLocalStorageKeys;
|
||||
|
||||
export type SettingType<K extends AllSettingsKeys> =
|
||||
K extends `context.${infer Rest}`
|
||||
? ExtractType<ConcreteContextData, ExtractSegments<Rest>> | undefined
|
||||
:
|
||||
K extends `ls.raw.${infer _}`
|
||||
? string | undefined
|
||||
:
|
||||
K extends `ls.${infer Rest}`
|
||||
? Rest extends keyof LocalStorageData
|
||||
? LocalStorageData[Rest]
|
||||
: unknown
|
||||
:
|
||||
K extends keyof SettingsTypeMap
|
||||
? SettingsTypeMap[K]
|
||||
:
|
||||
unknown;
|
||||
|
||||
export type SettingMetadata = {
|
||||
uses: number[];
|
||||
};
|
||||
|
||||
// Definitions
|
||||
|
||||
export type SettingsDefinition<T> = {
|
||||
export type SettingDefinition<T> = {
|
||||
|
||||
default: T,
|
||||
type?: string;
|
||||
|
||||
process?(this: SettingsManager, ctx: SettingsContext, val: T): T;
|
||||
equals?: 'requirements' | ((new_value: T, old_value: T | undefined, cache: Map<SettingsKeys, unknown>, old_cache: Map<SettingsKeys, unknown>) => boolean);
|
||||
|
||||
process?(ctx: SettingsContext, val: T, meta: SettingMetadata): T;
|
||||
|
||||
// Dependencies
|
||||
required_by?: string[];
|
||||
|
@ -82,17 +118,26 @@ export type SettingsDefinition<T> = {
|
|||
__source?: string | null;
|
||||
|
||||
// UI Stuff
|
||||
ui?: SettingsUiDefinition<T>;
|
||||
ui?: SettingUiDefinition<T>;
|
||||
|
||||
// Reactivity
|
||||
changed?: () => void;
|
||||
|
||||
};
|
||||
|
||||
export type SettingsUiDefinition<T> = {
|
||||
export type SettingUiDefinition<T> = {
|
||||
i18n_key?: string;
|
||||
key: string;
|
||||
path: string;
|
||||
path_tokens?: PathNode[];
|
||||
|
||||
component: string;
|
||||
|
||||
no_i18n?: boolean;
|
||||
|
||||
// TODO: Handle this better.
|
||||
data: any;
|
||||
|
||||
process?: string;
|
||||
|
||||
/**
|
||||
|
@ -128,6 +173,15 @@ export type ExportedFullDump = {
|
|||
};
|
||||
|
||||
|
||||
export type ExportedBlobMetadata = {
|
||||
key: string;
|
||||
type?: string;
|
||||
name?: string;
|
||||
modified?: number;
|
||||
mime?: string;
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Profiles
|
||||
|
||||
|
@ -153,16 +207,16 @@ export type SettingsProfileMetadata = {
|
|||
|
||||
// Processors
|
||||
|
||||
export type SettingsProcessor<T> = (
|
||||
export type SettingProcessor<T> = (
|
||||
input: unknown,
|
||||
default_value: T,
|
||||
definition: SettingsUiDefinition<T>
|
||||
definition: SettingUiDefinition<T>
|
||||
) => T;
|
||||
|
||||
|
||||
// Validators
|
||||
|
||||
export type SettingsValidator<T> = (
|
||||
export type SettingValidator<T> = (
|
||||
value: T,
|
||||
definition: SettingsUiDefinition<T>
|
||||
definition: SettingUiDefinition<T>
|
||||
) => boolean;
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
'use strict';
|
||||
|
||||
import type { SettingsUiDefinition, SettingsValidator } from "./types";
|
||||
import type { SettingUiDefinition, SettingValidator } from "./types";
|
||||
|
||||
|
||||
function do_number(value: any, definition: SettingsUiDefinition<number>) {
|
||||
function do_number(value: any, definition: SettingUiDefinition<number>) {
|
||||
if ( typeof value !== 'number' || isNaN(value) || ! isFinite(value) )
|
||||
return false;
|
||||
|
||||
|
@ -29,7 +29,7 @@ function do_number(value: any, definition: SettingsUiDefinition<number>) {
|
|||
return true;
|
||||
}
|
||||
|
||||
export const process_to_int: SettingsValidator<number> = (
|
||||
export const process_to_int: SettingValidator<number> = (
|
||||
value,
|
||||
definition
|
||||
) => {
|
||||
|
@ -41,7 +41,7 @@ export const process_to_int: SettingsValidator<number> = (
|
|||
return do_number(value, definition);
|
||||
}
|
||||
|
||||
export const process_to_float: SettingsValidator<number> = (
|
||||
export const process_to_float: SettingValidator<number> = (
|
||||
value,
|
||||
definition
|
||||
) => {
|
||||
|
|
|
@ -257,51 +257,6 @@ export default class Channel extends Module {
|
|||
}
|
||||
}
|
||||
|
||||
/*setHost(channel_id, channel_login, target_id, target_login) {
|
||||
const topic = `stream-chat-room-v1.${channel_id}`;
|
||||
|
||||
this.subpump.inject(topic, {
|
||||
type: 'host_target_change',
|
||||
data: {
|
||||
channel_id,
|
||||
channel_login,
|
||||
target_channel_id: target_id || null,
|
||||
target_channel_login: target_login || null,
|
||||
previous_target_channel_id: null,
|
||||
num_viewers: 0
|
||||
}
|
||||
});
|
||||
|
||||
this.subpump.inject(topic, {
|
||||
type: 'host_target_change_v2',
|
||||
data: {
|
||||
channel_id,
|
||||
channel_login,
|
||||
target_channel_id: target_id || null,
|
||||
target_channel_login: target_login || null,
|
||||
previous_target_channel_id: null,
|
||||
num_viewers: 0
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
onPubSub(event) {
|
||||
if ( event.prefix !== 'stream-chat-room-v1' || this.settings.get('channel.hosting.enable') )
|
||||
return;
|
||||
|
||||
const type = event.message.type;
|
||||
if ( type === 'host_target_change' || type === 'host_target_change_v2' ) {
|
||||
this.log.info('Nulling Host Target Change', type);
|
||||
event.message.data.target_channel_id = null;
|
||||
event.message.data.target_channel_login = null;
|
||||
event.message.data.previous_target_channel_id = null;
|
||||
event.message.data.num_viewers = 0;
|
||||
event.markChanged();
|
||||
}
|
||||
}*/
|
||||
|
||||
|
||||
updateSubscription(id, login) {
|
||||
if ( this._subbed_login === login && this._subbed_id === id )
|
||||
return;
|
||||
|
@ -725,4 +680,4 @@ export default class Channel extends Module {
|
|||
err ? pair[1](err) : pair[0](id);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,64 @@
|
|||
'use strict';
|
||||
|
||||
import type SettingsManager from 'root/src/settings';
|
||||
import type { FineWrapper } from 'root/src/utilities/compat/fine';
|
||||
import type Fine from 'root/src/utilities/compat/fine';
|
||||
import type { ReactStateNode } from 'root/src/utilities/compat/react-types';
|
||||
|
||||
// ============================================================================
|
||||
// Loadable Stuff
|
||||
// ============================================================================
|
||||
|
||||
import Module from 'utilities/module';
|
||||
import Module, { GenericModule } from 'utilities/module';
|
||||
import type { AnyFunction } from 'utilities/types';
|
||||
import type Twilight from '..';
|
||||
|
||||
declare module 'utilities/types' {
|
||||
interface ModuleEventMap {
|
||||
|
||||
}
|
||||
interface ModuleMap {
|
||||
'site.loadable': Loadable
|
||||
}
|
||||
interface SettingsTypeMap {
|
||||
'chat.hype.show-pinned': boolean;
|
||||
'layout.turbo-cta': boolean;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
type LoadableNode = ReactStateNode<{
|
||||
component: string;
|
||||
loader: any;
|
||||
}, {
|
||||
Component?: AnyFunction;
|
||||
}>;
|
||||
|
||||
type ErrorBoundaryNode = ReactStateNode<{
|
||||
name: string;
|
||||
onError: any;
|
||||
children: any;
|
||||
}> & {
|
||||
onErrorBoundaryTestEmit: any
|
||||
}
|
||||
|
||||
|
||||
export default class Loadable extends Module {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
// Dependencies
|
||||
settings: SettingsManager = null as any;
|
||||
site: Twilight = null as any;
|
||||
fine: Fine = null as any;
|
||||
|
||||
// State
|
||||
overrides: Map<string, boolean>;
|
||||
|
||||
// Fine
|
||||
ErrorBoundaryComponent: FineWrapper<ErrorBoundaryNode>;
|
||||
LoadableComponent: FineWrapper<LoadableNode>;
|
||||
|
||||
constructor(name?: string, parent?: GenericModule) {
|
||||
super(name, parent);
|
||||
|
||||
this.should_enable = true;
|
||||
|
||||
|
@ -19,12 +68,18 @@ export default class Loadable extends Module {
|
|||
|
||||
this.LoadableComponent = this.fine.define(
|
||||
'loadable-component',
|
||||
n => n.props?.component && n.props.loader
|
||||
n =>
|
||||
(n as LoadableNode).props?.component &&
|
||||
(n as LoadableNode).props.loader
|
||||
);
|
||||
|
||||
this.ErrorBoundaryComponent = this.fine.define(
|
||||
'error-boundary-component',
|
||||
n => n.props?.name && n.props?.onError && n.props?.children && n.onErrorBoundaryTestEmit
|
||||
n =>
|
||||
(n as ErrorBoundaryNode).props?.name &&
|
||||
(n as ErrorBoundaryNode).props?.onError &&
|
||||
(n as ErrorBoundaryNode).props?.children &&
|
||||
(n as ErrorBoundaryNode).onErrorBoundaryTestEmit
|
||||
);
|
||||
|
||||
this.overrides = new Map();
|
||||
|
@ -44,9 +99,10 @@ export default class Loadable extends Module {
|
|||
this.log.debug('Found Error Boundary component wrapper.');
|
||||
|
||||
const t = this,
|
||||
old_render = cls.prototype.render;
|
||||
proto = cls.prototype as ErrorBoundaryNode,
|
||||
old_render = proto.render;
|
||||
|
||||
cls.prototype.render = function() {
|
||||
proto.render = function() {
|
||||
try {
|
||||
const type = this.props.name;
|
||||
if ( t.overrides.has(type) && ! t.shouldRender(type) )
|
||||
|
@ -66,32 +122,33 @@ export default class Loadable extends Module {
|
|||
this.log.debug('Found Loadable component wrapper.');
|
||||
|
||||
const t = this,
|
||||
old_render = cls.prototype.render;
|
||||
proto = cls.prototype,
|
||||
old_render = proto.render;
|
||||
|
||||
cls.prototype.render = function() {
|
||||
proto.render = function() {
|
||||
try {
|
||||
const type = this.props.component;
|
||||
if ( t.overrides.has(type) ) {
|
||||
if ( t.overrides.has(type) && this.state ) {
|
||||
let cmp = this.state.Component;
|
||||
if ( typeof cmp === 'function' && ! cmp.ffzWrapped ) {
|
||||
if ( typeof cmp === 'function' && ! (cmp as any).ffzWrapped ) {
|
||||
const React = t.site.getReact(),
|
||||
createElement = React && React.createElement;
|
||||
|
||||
if ( createElement ) {
|
||||
if ( ! cmp.ffzWrapper ) {
|
||||
if ( ! (cmp as any).ffzWrapper ) {
|
||||
const th = this;
|
||||
function FFZWrapper(props, state) {
|
||||
if ( t.shouldRender(th.props.component, props, state) )
|
||||
function FFZWrapper(props: any) {
|
||||
if ( t.shouldRender(th.props.component) )
|
||||
return createElement(cmp, props);
|
||||
return null;
|
||||
}
|
||||
|
||||
FFZWrapper.ffzWrapped = true;
|
||||
FFZWrapper.displayName = `FFZWrapper(${this.props.component})`;
|
||||
cmp.ffzWrapper = FFZWrapper;
|
||||
(cmp as any).ffzWrapper = FFZWrapper;
|
||||
}
|
||||
|
||||
this.state.Component = cmp.ffzWrapper;
|
||||
this.state.Component = (cmp as any).ffzWrapper;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -107,7 +164,7 @@ export default class Loadable extends Module {
|
|||
});
|
||||
}
|
||||
|
||||
toggle(cmp, state = null) {
|
||||
toggle(cmp: string, state: boolean | null = null) {
|
||||
const existing = this.overrides.get(cmp) ?? true;
|
||||
|
||||
if ( state == null )
|
||||
|
@ -121,22 +178,22 @@ export default class Loadable extends Module {
|
|||
}
|
||||
}
|
||||
|
||||
update(cmp) {
|
||||
update(cmp: string) {
|
||||
for(const inst of this.LoadableComponent.instances) {
|
||||
const type = inst?.props?.component;
|
||||
const type = inst.props?.component;
|
||||
if ( type && type === cmp )
|
||||
inst.forceUpdate();
|
||||
}
|
||||
|
||||
for(const inst of this.ErrorBoundaryComponent.instances) {
|
||||
const name = inst?.props?.name;
|
||||
const name = inst.props?.name;
|
||||
if ( name && name === cmp )
|
||||
inst.forceUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
shouldRender(cmp, props) {
|
||||
shouldRender(cmp: string) {
|
||||
return this.overrides.get(cmp) ?? true;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -4,12 +4,50 @@
|
|||
// Sub Button
|
||||
// ============================================================================
|
||||
|
||||
import Module from 'utilities/module';
|
||||
import Module, { GenericModule } from 'utilities/module';
|
||||
import {createElement} from 'utilities/dom';
|
||||
import type SettingsManager from 'src/settings';
|
||||
import type TranslationManager from 'src/i18n';
|
||||
import type Fine from 'utilities/compat/fine';
|
||||
import type { FineWrapper } from 'utilities/compat/fine';
|
||||
import type { ReactStateNode } from 'root/src/utilities/compat/react-types';
|
||||
|
||||
declare module 'utilities/types' {
|
||||
interface ModuleMap {
|
||||
'site.sub_button': SubButton;
|
||||
}
|
||||
interface SettingsTypeMap {
|
||||
'layout.swap-sidebars': unknown;
|
||||
'sub-button.prime-notice': boolean;
|
||||
}
|
||||
}
|
||||
|
||||
type SubButtonNode = ReactStateNode<{
|
||||
data?: {
|
||||
user?: {
|
||||
self?: {
|
||||
canPrimeSubscribe: boolean;
|
||||
subscriptionBenefit: unknown;
|
||||
}
|
||||
}
|
||||
}
|
||||
}> & {
|
||||
handleSubMenuAction: any;
|
||||
openSubModal: any;
|
||||
};
|
||||
|
||||
export default class SubButton extends Module {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
// Dependencies
|
||||
i18n: TranslationManager = null as any;
|
||||
fine: Fine = null as any;
|
||||
settings: SettingsManager = null as any;
|
||||
|
||||
// Stuff
|
||||
SubButton: FineWrapper<SubButtonNode>;
|
||||
|
||||
constructor(name?: string, parent?: GenericModule) {
|
||||
super(name, parent);
|
||||
|
||||
this.should_enable = true;
|
||||
|
||||
|
@ -32,39 +70,20 @@ export default class SubButton extends Module {
|
|||
|
||||
this.SubButton = this.fine.define(
|
||||
'sub-button',
|
||||
n => n.handleSubMenuAction && n.openSubModal,
|
||||
n =>
|
||||
(n as SubButtonNode).handleSubMenuAction &&
|
||||
(n as SubButtonNode).openSubModal,
|
||||
['user', 'user-home', 'user-video', 'user-clip', 'video', 'user-videos', 'user-clips', 'user-collections', 'user-events', 'user-followers', 'user-following']
|
||||
);
|
||||
}
|
||||
|
||||
onEnable() {
|
||||
this.settings.on(':changed:layout.swap-sidebars', () => this.SubButton.forceUpdate())
|
||||
this.settings.on(':changed:layout.swap-sidebars', () =>
|
||||
this.SubButton.forceUpdate());
|
||||
|
||||
this.SubButton.ready((cls, instances) => {
|
||||
const t = this,
|
||||
old_render = cls.prototype.render;
|
||||
|
||||
cls.prototype.render = function() {
|
||||
try {
|
||||
const old_direction = this.props.balloonDirection;
|
||||
if ( old_direction !== undefined ) {
|
||||
const should_be_left = t.settings.get('layout.swap-sidebars'),
|
||||
is_left = old_direction.includes('--left');
|
||||
|
||||
if ( should_be_left && ! is_left )
|
||||
this.props.balloonDirection = old_direction.replace('--right', '--left');
|
||||
else if ( ! should_be_left && is_left )
|
||||
this.props.balloonDirection = old_direction.replace('--left', '--right');
|
||||
}
|
||||
} catch(err) { /* no-op */ }
|
||||
|
||||
return old_render.call(this);
|
||||
}
|
||||
|
||||
for(const inst of instances)
|
||||
this.updateSubButton(inst);
|
||||
|
||||
this.SubButton.forceUpdate();
|
||||
});
|
||||
|
||||
this.SubButton.on('mount', this.updateSubButton, this);
|
||||
|
@ -72,9 +91,9 @@ export default class SubButton extends Module {
|
|||
}
|
||||
|
||||
|
||||
updateSubButton(inst) {
|
||||
const container = this.fine.getChildNode(inst),
|
||||
btn = container && container.querySelector('button[data-a-target="subscribe-button"]');
|
||||
updateSubButton(inst: SubButtonNode) {
|
||||
const container = this.fine.getChildNode<HTMLElement>(inst),
|
||||
btn = container?.querySelector('button[data-a-target="subscribe-button"]');
|
||||
if ( ! btn )
|
||||
return;
|
||||
|
||||
|
@ -102,4 +121,4 @@ export default class SubButton extends Module {
|
|||
post.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,682 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
export function hue2rgb(p, q, t) {
|
||||
if ( t < 0 ) t += 1;
|
||||
if ( t > 1 ) t -= 1;
|
||||
if ( t < 1/6 )
|
||||
return p + (q-p) * 6 * t;
|
||||
if ( t < 1/2 )
|
||||
return q;
|
||||
if ( t < 2/3 )
|
||||
return p + (q-p) * (2/3 - t) * 6;
|
||||
return p;
|
||||
}
|
||||
|
||||
export function bit2linear(channel) {
|
||||
// http://www.brucelindbloom.com/Eqn_RGB_to_XYZ.html
|
||||
// This converts rgb 8bit to rgb linear, lazy because the other algorithm is really really dumb
|
||||
//return Math.pow(channel, 2.2);
|
||||
|
||||
// CSS Colors Level 4 says 0.03928, Bruce Lindbloom who cared to write all algos says 0.04045, used bruce because whynawt
|
||||
return (channel <= 0.04045) ? channel / 12.92 : Math.pow((channel + 0.055) / 1.055, 2.4);
|
||||
}
|
||||
|
||||
export function linear2bit(channel) {
|
||||
// Using lazy conversion in the other direction as well
|
||||
//return Math.pow(channel, 1/2.2);
|
||||
|
||||
// I'm honestly not sure about 0.0031308, I've only seen it referenced on Bruce Lindbloom's site
|
||||
return (channel <= 0.0031308) ? channel * 12.92 : Math.pow(1.055 * channel, 1/2.4) - 0.055;
|
||||
}
|
||||
|
||||
|
||||
export const Color = {};
|
||||
|
||||
Color._canvas = null;
|
||||
Color._context = null;
|
||||
|
||||
Color.CVDMatrix = {
|
||||
protanope: [ // reds are greatly reduced (1% men)
|
||||
0.0, 2.02344, -2.52581,
|
||||
0.0, 1.0, 0.0,
|
||||
0.0, 0.0, 1.0
|
||||
],
|
||||
deuteranope: [ // greens are greatly reduced (1% men)
|
||||
1.0, 0.0, 0.0,
|
||||
0.494207, 0.0, 1.24827,
|
||||
0.0, 0.0, 1.0
|
||||
],
|
||||
tritanope: [ // blues are greatly reduced (0.003% population)
|
||||
1.0, 0.0, 0.0,
|
||||
0.0, 1.0, 0.0,
|
||||
-0.395913, 0.801109, 0.0
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
const RGBAColor = Color.RGBA = function(r, g, b, a) {
|
||||
this.r = r||0; this.g = g||0; this.b = b||0; this.a = a||0;
|
||||
};
|
||||
|
||||
const HSVAColor = Color.HSVA = function(h, s, v, a) {
|
||||
this.h = h||0; this.s = s||0; this.v = v||0; this.a = a||0;
|
||||
};
|
||||
|
||||
const HSLAColor = Color.HSLA = function(h, s, l, a) {
|
||||
this.h = h||0; this.s = s||0; this.l = l||0; this.a = a||0;
|
||||
};
|
||||
|
||||
const XYZAColor = Color.XYZA = function(x, y, z, a) {
|
||||
this.x = x||0; this.y = y||0; this.z = z||0; this.a = a||0;
|
||||
};
|
||||
|
||||
const LUVAColor = Color.LUVA = function(l, u, v, a) {
|
||||
this.l = l||0; this.u = u||0; this.v = v||0; this.a = a||0;
|
||||
};
|
||||
|
||||
|
||||
// RGBA Colors
|
||||
|
||||
RGBAColor.prototype.eq = function(rgb) {
|
||||
return rgb.r === this.r && rgb.g === this.g && rgb.b === this.b && rgb.a === this.a;
|
||||
}
|
||||
|
||||
RGBAColor.fromName = function(name) {
|
||||
let context = Color._context;
|
||||
if ( ! context ) {
|
||||
const canvas = Color._canvas = document.createElement('canvas');
|
||||
context = Color._context = canvas.getContext('2d');
|
||||
}
|
||||
|
||||
context.clearRect(0,0,1,1);
|
||||
context.fillStyle = name;
|
||||
context.fillRect(0,0,1,1);
|
||||
const data = context.getImageData(0,0,1,1);
|
||||
|
||||
if ( ! data || ! data.data || data.data.length !== 4 )
|
||||
return null;
|
||||
|
||||
return new RGBAColor(data.data[0], data.data[1], data.data[2], data.data[3] / 255);
|
||||
}
|
||||
|
||||
RGBAColor.fromCSS = function(rgb) {
|
||||
if ( ! rgb )
|
||||
return null;
|
||||
|
||||
rgb = rgb.trim();
|
||||
|
||||
if ( rgb.charAt(0) === '#' )
|
||||
return RGBAColor.fromHex(rgb);
|
||||
|
||||
const match = /rgba?\( *(\d+%?) *, *(\d+%?) *, *(\d+%?) *(?:, *([\d.]+))?\)/i.exec(rgb);
|
||||
if ( match ) {
|
||||
let r = match[1],
|
||||
g = match[2],
|
||||
b = match[3],
|
||||
a = match[4];
|
||||
|
||||
if ( r.charAt(r.length-1) === '%' )
|
||||
r = 255 * (parseInt(r,10) / 100);
|
||||
else
|
||||
r = parseInt(r,10);
|
||||
|
||||
if ( g.charAt(g.length-1) === '%' )
|
||||
g = 255 * (parseInt(g,10) / 100);
|
||||
else
|
||||
g = parseInt(g,10);
|
||||
|
||||
if ( b.charAt(b.length-1) === '%' )
|
||||
b = 255 * (parseInt(b,10) / 100);
|
||||
else
|
||||
b = parseInt(b,10);
|
||||
|
||||
if ( a )
|
||||
if ( a.charAt(a.length-1) === '%' )
|
||||
a = parseInt(a,10) / 100;
|
||||
else
|
||||
a = parseFloat(a);
|
||||
else
|
||||
a = 1;
|
||||
|
||||
return new RGBAColor(
|
||||
Math.min(Math.max(0, r), 255),
|
||||
Math.min(Math.max(0, g), 255),
|
||||
Math.min(Math.max(0, b), 255),
|
||||
Math.min(Math.max(0, a), 1)
|
||||
);
|
||||
}
|
||||
|
||||
return RGBAColor.fromName(rgb);
|
||||
}
|
||||
|
||||
RGBAColor.fromHex = function(code, alpha = 1) {
|
||||
if ( code.charAt(0) === '#' )
|
||||
code = code.slice(1);
|
||||
|
||||
if ( code.length === 3 )
|
||||
code = `${code[0]}${code[0]}${code[1]}${code[1]}${code[2]}${code[2]}`;
|
||||
|
||||
else if ( code.length === 4 )
|
||||
code = `${code[0]}${code[0]}${code[1]}${code[1]}${code[2]}${code[2]}${code[3]}${code[3]}`;
|
||||
|
||||
if ( code.length === 8 ) {
|
||||
alpha = parseInt(code.slice(6), 16) / 255;
|
||||
code = code.slice(0, 6);
|
||||
|
||||
} else if ( code.length !== 6 )
|
||||
throw new Error('invalid hex code');
|
||||
|
||||
const raw = parseInt(code, 16);
|
||||
return new RGBAColor(
|
||||
(raw >> 16), // Red
|
||||
(raw >> 8 & 0x00FF), // Green
|
||||
(raw & 0x0000FF), // Blue,
|
||||
alpha // Alpha
|
||||
);
|
||||
}
|
||||
|
||||
RGBAColor.fromHSVA = function(h, s, v, a) {
|
||||
let r, g, b;
|
||||
|
||||
const i = Math.floor(h * 6),
|
||||
f = h * 6 - i,
|
||||
p = v * (1 - s),
|
||||
q = v * (1 - f * s),
|
||||
t = v * (1 - (1 - f) * s);
|
||||
|
||||
switch(i % 6) {
|
||||
case 0: r = v; g = t; b = p; break;
|
||||
case 1: r = q; g = v; b = p; break;
|
||||
case 2: r = p; g = v; b = t; break;
|
||||
case 3: r = p; g = q; b = v; break;
|
||||
case 4: r = t; g = p; b = v; break;
|
||||
case 5: r = v; g = p; b = q;
|
||||
}
|
||||
|
||||
return new RGBAColor(
|
||||
Math.round(Math.min(Math.max(0, r*255), 255)),
|
||||
Math.round(Math.min(Math.max(0, g*255), 255)),
|
||||
Math.round(Math.min(Math.max(0, b*255), 255)),
|
||||
a === undefined ? 1 : a
|
||||
);
|
||||
}
|
||||
|
||||
RGBAColor.fromXYZA = function(x, y, z, a) {
|
||||
const R = 3.240479 * x - 1.537150 * y - 0.498535 * z,
|
||||
G = -0.969256 * x + 1.875992 * y + 0.041556 * z,
|
||||
B = 0.055648 * x - 0.204043 * y + 1.057311 * z;
|
||||
|
||||
// Make sure we end up in a real color space
|
||||
return new RGBAColor(
|
||||
Math.max(0, Math.min(255, 255 * linear2bit(R))),
|
||||
Math.max(0, Math.min(255, 255 * linear2bit(G))),
|
||||
Math.max(0, Math.min(255, 255 * linear2bit(B))),
|
||||
a === undefined ? 1 : a
|
||||
);
|
||||
}
|
||||
|
||||
RGBAColor.fromHSLA = function(h, s, l, a) {
|
||||
if ( s === 0 ) {
|
||||
const v = Math.round(Math.min(Math.max(0, 255*l), 255));
|
||||
return new RGBAColor(v, v, v, a === undefined ? 1 : a);
|
||||
}
|
||||
|
||||
const q = l < 0.5 ? l * (1 + s) : l + s - l * s,
|
||||
p = 2 * l - q;
|
||||
|
||||
return new RGBAColor(
|
||||
Math.round(Math.min(Math.max(0, 255 * hue2rgb(p, q, h + 1/3)), 255)),
|
||||
Math.round(Math.min(Math.max(0, 255 * hue2rgb(p, q, h)), 255)),
|
||||
Math.round(Math.min(Math.max(0, 255 * hue2rgb(p, q, h - 1/3)), 255)),
|
||||
a === undefined ? 1 : a
|
||||
);
|
||||
}
|
||||
|
||||
RGBAColor.prototype.toRGBA = function() { return this; }
|
||||
RGBAColor.prototype.toHSVA = function() { return HSVAColor.fromRGBA(this.r, this.g, this.b, this.a); }
|
||||
RGBAColor.prototype.toHSLA = function() { return HSLAColor.fromRGBA(this.r, this.g, this.b, this.a); }
|
||||
RGBAColor.prototype.toCSS = function() { return `rgb${this.a !== 1 ? 'a' : ''}(${Math.round(this.r)},${Math.round(this.g)},${Math.round(this.b)}${this.a !== 1 ? `,${this.a}` : ''})`; }
|
||||
RGBAColor.prototype.toXYZA = function() { return XYZAColor.fromRGBA(this.r, this.g, this.b, this.a); }
|
||||
RGBAColor.prototype.toLUVA = function() { return this.toXYZA().toLUVA(); }
|
||||
|
||||
RGBAColor.prototype.toHex = function() {
|
||||
const rgb = this.b | (this.g << 8) | (this.r << 16);
|
||||
return `#${(0x1000000 + rgb).toString(16).slice(1)}`;
|
||||
}
|
||||
|
||||
|
||||
RGBAColor.prototype.get_Y = function() {
|
||||
return ((0.299 * this.r) + ( 0.587 * this.g) + ( 0.114 * this.b)) / 255;
|
||||
}
|
||||
|
||||
|
||||
RGBAColor.prototype.luminance = function() {
|
||||
const r = bit2linear(this.r / 255),
|
||||
g = bit2linear(this.g / 255),
|
||||
b = bit2linear(this.b / 255);
|
||||
|
||||
return (0.2126 * r) + (0.7152 * g) + (0.0722 * b);
|
||||
}
|
||||
|
||||
|
||||
RGBAColor.prototype.brighten = function(amount) {
|
||||
amount = typeof amount === `number` ? amount : 1;
|
||||
amount = Math.round(255 * (amount / 100));
|
||||
|
||||
return new RGBAColor(
|
||||
Math.max(0, Math.min(255, this.r + amount)),
|
||||
Math.max(0, Math.min(255, this.g + amount)),
|
||||
Math.max(0, Math.min(255, this.b + amount)),
|
||||
this.a
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
RGBAColor.prototype.daltonize = function(type) {
|
||||
let cvd;
|
||||
if ( typeof type === 'string' ) {
|
||||
if ( Color.CVDMatrix.hasOwnProperty(type) )
|
||||
cvd = Color.CVDMatrix[type];
|
||||
else
|
||||
throw new Error('Invalid CVD matrix');
|
||||
} else
|
||||
cvd = type;
|
||||
|
||||
const cvd_a = cvd[0], cvd_b = cvd[1], cvd_c = cvd[2],
|
||||
cvd_d = cvd[3], cvd_e = cvd[4], cvd_f = cvd[5],
|
||||
cvd_g = cvd[6], cvd_h = cvd[7], cvd_i = cvd[8];
|
||||
|
||||
//let L, M, S, l, m, s, R, G, B, RR, GG, BB;
|
||||
|
||||
// RGB to LMS matrix conversion
|
||||
const L = (17.8824 * this.r) + (43.5161 * this.g) + (4.11935 * this.b),
|
||||
M = (3.45565 * this.r) + (27.1554 * this.g) + (3.86714 * this.b),
|
||||
S = (0.0299566 * this.r) + (0.184309 * this.g) + (1.46709 * this.b);
|
||||
|
||||
// Simulate color blindness
|
||||
const l = (cvd_a * L) + (cvd_b * M) + (cvd_c * S),
|
||||
m = (cvd_d * L) + (cvd_e * M) + (cvd_f * S),
|
||||
s = (cvd_g * L) + (cvd_h * M) + (cvd_i * S);
|
||||
|
||||
// LMS to RGB matrix conversion
|
||||
let R = (0.0809444479 * l) + (-0.130504409 * m) + (0.116721066 * s),
|
||||
G = (-0.0102485335 * l) + (0.0540193266 * m) + (-0.113614708 * s),
|
||||
B = (-0.000365296938 * l) + (-0.00412161469 * m) + (0.693511405 * s);
|
||||
|
||||
// Isolate invisible colors to color vision deficiency (calculate error matrix)
|
||||
R = this.r - R;
|
||||
G = this.g - G;
|
||||
B = this.b - B;
|
||||
|
||||
// Shift colors towards visible spectrum (apply error modifications)
|
||||
const RR = (0.0 * R) + (0.0 * G) + (0.0 * B),
|
||||
GG = (0.7 * R) + (1.0 * G) + (0.0 * B),
|
||||
BB = (0.7 * R) + (0.0 * G) + (1.0 * B);
|
||||
|
||||
// Add compensation to original values
|
||||
R = Math.min(Math.max(0, RR + this.r), 255);
|
||||
G = Math.min(Math.max(0, GG + this.g), 255);
|
||||
B = Math.min(Math.max(0, BB + this.b), 255);
|
||||
|
||||
return new RGBAColor(R, G, B, this.a);
|
||||
}
|
||||
|
||||
RGBAColor.prototype._r = function(r) { return new RGBAColor(r, this.g, this.b, this.a); }
|
||||
RGBAColor.prototype._g = function(g) { return new RGBAColor(this.r, g, this.b, this.a); }
|
||||
RGBAColor.prototype._b = function(b) { return new RGBAColor(this.r, this.g, b, this.a); }
|
||||
RGBAColor.prototype._a = function(a) { return new RGBAColor(this.r, this.g, this.b, a); }
|
||||
|
||||
|
||||
// HSL Colors
|
||||
|
||||
HSLAColor.prototype.eq = function(hsl) {
|
||||
return hsl.h === this.h && hsl.s === this.s && hsl.l === this.l && hsl.a === this.a;
|
||||
}
|
||||
|
||||
HSLAColor.fromRGBA = function(r, g, b, a) {
|
||||
r /= 255; g /= 255; b /= 255;
|
||||
|
||||
const max = Math.max(r,g,b),
|
||||
min = Math.min(r,g,b),
|
||||
|
||||
l = Math.min(Math.max(0, (max+min) / 2), 1),
|
||||
d = Math.min(Math.max(0, max - min), 1);
|
||||
|
||||
let h, s;
|
||||
|
||||
if ( d === 0 )
|
||||
h = s = 0;
|
||||
else {
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
switch(max) {
|
||||
case r:
|
||||
h = (g - b) / d + (g < b ? 6 : 0);
|
||||
break;
|
||||
case g:
|
||||
h = (b - r) / d + 2;
|
||||
break;
|
||||
case b:
|
||||
h = (r - g) / d + 4;
|
||||
}
|
||||
h /= 6;
|
||||
}
|
||||
|
||||
return new HSLAColor(h, s, l, a === undefined ? 1 : a);
|
||||
}
|
||||
|
||||
HSLAColor.prototype.targetLuminance = function (target) {
|
||||
let s = this.s,
|
||||
min = 0,
|
||||
max = 1;
|
||||
|
||||
s *= Math.pow(this.l > 0.5 ? -this.l : this.l - 1, 7) + 1;
|
||||
|
||||
let d = (max - min) / 2,
|
||||
mid = min + d;
|
||||
|
||||
for (; d > 1/65536; d /= 2, mid = min + d) {
|
||||
const luminance = RGBAColor.fromHSLA(this.h, s, mid, 1).luminance()
|
||||
if (luminance > target) {
|
||||
max = mid;
|
||||
} else {
|
||||
min = mid;
|
||||
}
|
||||
}
|
||||
|
||||
return new HSLAColor(this.h, s, mid, this.a);
|
||||
}
|
||||
|
||||
HSLAColor.prototype.toRGBA = function() { return RGBAColor.fromHSLA(this.h, this.s, this.l, this.a); }
|
||||
HSLAColor.prototype.toCSS = function() { return `hsl${this.a !== 1 ? 'a' : ''}(${Math.round(this.h*360)},${Math.round(this.s*100)}%,${Math.round(this.l*100)}%${this.a !== 1 ? `,${this.a}` : ''})`; }
|
||||
HSLAColor.prototype.toHSLA = function() { return this; }
|
||||
HSLAColor.prototype.toHSVA = function() { return this.toRGBA().toHSVA(); }
|
||||
HSLAColor.prototype.toXYZA = function() { return this.toRGBA().toXYZA(); }
|
||||
HSLAColor.prototype.toLUVA = function() { return this.toRGBA().toLUVA(); }
|
||||
|
||||
|
||||
HSLAColor.prototype._h = function(h) { return new HSLAColor(h, this.s, this.l, this.a); }
|
||||
HSLAColor.prototype._s = function(s) { return new HSLAColor(this.h, s, this.l, this.a); }
|
||||
HSLAColor.prototype._l = function(l) { return new HSLAColor(this.h, this.s, l, this.a); }
|
||||
HSLAColor.prototype._a = function(a) { return new HSLAColor(this.h, this.s, this.l, a); }
|
||||
|
||||
|
||||
// HSV Colors
|
||||
|
||||
HSVAColor.prototype.eq = function(hsv) { return hsv.h === this.h && hsv.s === this.s && hsv.v === this.v && hsv.a === this.a; }
|
||||
|
||||
HSVAColor.fromRGBA = function(r, g, b, a) {
|
||||
r /= 255; g /= 255; b /= 255;
|
||||
|
||||
const max = Math.max(r, g, b),
|
||||
min = Math.min(r, g, b),
|
||||
d = Math.min(Math.max(0, max - min), 1),
|
||||
|
||||
s = max === 0 ? 0 : d / max,
|
||||
v = max;
|
||||
|
||||
let h;
|
||||
|
||||
if ( d === 0 )
|
||||
h = 0;
|
||||
else {
|
||||
switch(max) {
|
||||
case r:
|
||||
h = (g - b) / d + (g < b ? 6 : 0);
|
||||
break;
|
||||
case g:
|
||||
h = (b - r) / d + 2;
|
||||
break;
|
||||
case b:
|
||||
h = (r - g) / d + 4;
|
||||
}
|
||||
h /= 6;
|
||||
}
|
||||
|
||||
return new HSVAColor(h, s, v, a === undefined ? 1 : a);
|
||||
}
|
||||
|
||||
|
||||
HSVAColor.prototype.toRGBA = function() { return RGBAColor.fromHSVA(this.h, this.s, this.v, this.a); }
|
||||
HSVAColor.prototype.toHSVA = function() { return this; }
|
||||
HSVAColor.prototype.toHSLA = function() { return this.toRGBA().toHSLA(); }
|
||||
HSVAColor.prototype.toXYZA = function() { return this.toRGBA().toXYZA(); }
|
||||
HSVAColor.prototype.toLUVA = function() { return this.toRGBA().toLUVA(); }
|
||||
|
||||
|
||||
HSVAColor.prototype._h = function(h) { return new HSVAColor(h, this.s, this.v, this.a); }
|
||||
HSVAColor.prototype._s = function(s) { return new HSVAColor(this.h, s, this.v, this.a); }
|
||||
HSVAColor.prototype._v = function(v) { return new HSVAColor(this.h, this.s, v, this.a); }
|
||||
HSVAColor.prototype._a = function(a) { return new HSVAColor(this.h, this.s, this.v, a); }
|
||||
|
||||
|
||||
// XYZ Colors
|
||||
|
||||
XYZAColor.prototype.eq = function(xyz) { return xyz.x === this.x && xyz.y === this.y && xyz.z === this.z; }
|
||||
|
||||
XYZAColor.fromRGBA = function(r, g, b, a) {
|
||||
const R = bit2linear(r / 255),
|
||||
G = bit2linear(g / 255),
|
||||
B = bit2linear(b / 255);
|
||||
|
||||
return new XYZAColor(
|
||||
0.412453 * R + 0.357580 * G + 0.180423 * B,
|
||||
0.212671 * R + 0.715160 * G + 0.072169 * B,
|
||||
0.019334 * R + 0.119193 * G + 0.950227 * B,
|
||||
a === undefined ? 1 : a
|
||||
);
|
||||
}
|
||||
|
||||
XYZAColor.fromLUVA = function(l, u, v, alpha) {
|
||||
const deltaGammaFactor = 1 / (XYZAColor.WHITE.x + 15 * XYZAColor.WHITE.y + 3 * XYZAColor.WHITE.z),
|
||||
uDeltaGamma = 4 * XYZAColor.WHITE.x * deltaGammaFactor,
|
||||
vDeltagamma = 9 * XYZAColor.WHITE.y * deltaGammaFactor;
|
||||
|
||||
// XYZAColor.EPSILON * XYZAColor.KAPPA = 8
|
||||
const Y = (l > 8) ? Math.pow((l + 16) / 116, 3) : l / XYZAColor.KAPPA,
|
||||
a = 1/3 * (((52 * l) / (u + 13 * l * uDeltaGamma)) - 1),
|
||||
b = -5 * Y,
|
||||
c = -1/3,
|
||||
d = Y * (((39 * l) / (v + 13 * l * vDeltagamma)) - 5),
|
||||
|
||||
X = (d - b) / (a - c),
|
||||
Z = X * a + b;
|
||||
|
||||
return new XYZAColor(X, Y, Z, alpha === undefined ? 1 : alpha);
|
||||
}
|
||||
|
||||
|
||||
XYZAColor.prototype.toRGBA = function() { return RGBAColor.fromXYZA(this.x, this.y, this.z, this.a); }
|
||||
XYZAColor.prototype.toLUVA = function() { return LUVAColor.fromXYZA(this.x, this.y, this.z, this.a); }
|
||||
XYZAColor.prototype.toHSLA = function() { return this.toRGBA().toHSLA(); }
|
||||
XYZAColor.prototype.toHSVA = function() { return this.toRGBA().toHSVA(); }
|
||||
XYZAColor.prototype.toXYZA = function() { return this; }
|
||||
|
||||
|
||||
XYZAColor.prototype._x = function(x) { return new XYZAColor(x, this.y, this.z, this.a); }
|
||||
XYZAColor.prototype._y = function(y) { return new XYZAColor(this.x, y, this.z, this.a); }
|
||||
XYZAColor.prototype._z = function(z) { return new XYZAColor(this.x, this.y, z, this.a); }
|
||||
XYZAColor.prototype._a = function(a) { return new XYZAColor(this.x, this.y, this.z, a); }
|
||||
|
||||
|
||||
// LUV Colors
|
||||
|
||||
XYZAColor.EPSILON = Math.pow(6 / 29, 3);
|
||||
XYZAColor.KAPPA = Math.pow(29 / 3, 3);
|
||||
XYZAColor.WHITE = (new RGBAColor(255, 255, 255, 1)).toXYZA();
|
||||
|
||||
|
||||
LUVAColor.prototype.eq = function(luv) { return luv.l === this.l && luv.u === this.u && luv.v === this.v; }
|
||||
|
||||
LUVAColor.fromXYZA = function(X, Y, Z, a) {
|
||||
const deltaGammaFactor = 1 / (XYZAColor.WHITE.x + 15 * XYZAColor.WHITE.y + 3 * XYZAColor.WHITE.z),
|
||||
uDeltaGamma = 4 * XYZAColor.WHITE.x * deltaGammaFactor,
|
||||
vDeltagamma = 9 * XYZAColor.WHITE.y * deltaGammaFactor,
|
||||
|
||||
yGamma = Y / XYZAColor.WHITE.y;
|
||||
|
||||
let deltaDivider = (X + 15 * Y + 3 * Z);
|
||||
if (deltaDivider === 0) {
|
||||
deltaDivider = 1;
|
||||
}
|
||||
|
||||
const deltaFactor = 1 / deltaDivider,
|
||||
|
||||
uDelta = 4 * X * deltaFactor,
|
||||
vDelta = 9 * Y * deltaFactor,
|
||||
|
||||
L = (yGamma > XYZAColor.EPSILON) ? 116 * Math.pow(yGamma, 1/3) - 16 : XYZAColor.KAPPA * yGamma,
|
||||
u = 13 * L * (uDelta - uDeltaGamma),
|
||||
v = 13 * L * (vDelta - vDeltagamma);
|
||||
|
||||
return new LUVAColor(L, u, v, a === undefined ? 1 : a);
|
||||
}
|
||||
|
||||
|
||||
LUVAColor.prototype.toXYZA = function() { return XYZAColor.fromLUVA(this.l, this.u, this.v, this.a); }
|
||||
LUVAColor.prototype.toRGBA = function() { return this.toXYZA().toRGBA(); }
|
||||
LUVAColor.prototype.toHSLA = function() { return this.toXYZA().toHSLA(); }
|
||||
LUVAColor.prototype.toHSVA = function() { return this.toXYZA().toHSVA(); }
|
||||
LUVAColor.prototype.toLUVA = function() { return this; }
|
||||
|
||||
|
||||
LUVAColor.prototype._l = function(l) { return new LUVAColor(l, this.u, this.v, this.a); }
|
||||
LUVAColor.prototype._u = function(u) { return new LUVAColor(this.l, u, this.v, this.a); }
|
||||
LUVAColor.prototype._v = function(v) { return new LUVAColor(this.l, this.u, v, this.a); }
|
||||
LUVAColor.prototype._a = function(a) { return new LUVAColor(this.l, this.u, this.v, a); }
|
||||
|
||||
|
||||
|
||||
export class ColorAdjuster {
|
||||
|
||||
/*
|
||||
private _base: string;
|
||||
private _contrast: number;
|
||||
private _mode: number;
|
||||
|
||||
private _dark: boolean = false;
|
||||
private _cache: Map<string, string> = new Map;
|
||||
|
||||
private _luv: number = 0;
|
||||
private _luma: number = 0;
|
||||
*/
|
||||
|
||||
constructor(base = '#232323', mode = 0, contrast = 4.5) {
|
||||
this._contrast = contrast;
|
||||
this._base = base;
|
||||
this._mode = mode;
|
||||
|
||||
this.rebuildContrast();
|
||||
}
|
||||
|
||||
get contrast() { return this._contrast }
|
||||
set contrast(val) { this._contrast = val; this.rebuildContrast() }
|
||||
|
||||
get base() { return this._base }
|
||||
set base(val) { this._base = val; this.rebuildContrast() }
|
||||
|
||||
get dark() { return this._dark }
|
||||
|
||||
get mode() { return this._mode }
|
||||
set mode(val) { this._mode = val; this.rebuildContrast() }
|
||||
|
||||
|
||||
rebuildContrast() {
|
||||
this._cache = new Map;
|
||||
|
||||
const base = RGBAColor.fromCSS(this._base),
|
||||
lum = base.luminance();
|
||||
|
||||
const dark = this._dark = lum < 0.5;
|
||||
|
||||
if ( dark ) {
|
||||
this._luv = new XYZAColor(
|
||||
0,
|
||||
(this._contrast * (base.toXYZA().y + 0.05) - 0.05),
|
||||
0,
|
||||
1
|
||||
).toLUVA().l;
|
||||
|
||||
this._luma = this._contrast * (base.luminance() + 0.05) - 0.05;
|
||||
|
||||
} else {
|
||||
this._luv = new XYZAColor(
|
||||
0,
|
||||
(base.toXYZA().y + 0.05) / this._contrast - 0.05,
|
||||
0,
|
||||
1
|
||||
).toLUVA().l;
|
||||
|
||||
this._luma = (base.luminance() + 0.05) / this._contrast - 0.05;
|
||||
}
|
||||
}
|
||||
|
||||
process(color, throw_errors = false) {
|
||||
if ( this._mode === -1 )
|
||||
return '';
|
||||
else if ( this._mode === 0 )
|
||||
return color;
|
||||
|
||||
if ( color instanceof RGBAColor )
|
||||
color = color.toCSS();
|
||||
|
||||
if ( ! color )
|
||||
return null;
|
||||
|
||||
if ( this._cache.has(color) )
|
||||
return this._cache.get(color);
|
||||
|
||||
let rgb;
|
||||
|
||||
try {
|
||||
rgb = RGBAColor.fromCSS(color);
|
||||
} catch(err) {
|
||||
if ( throw_errors )
|
||||
throw err;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( this._mode === 1 ) {
|
||||
// HSL Luma
|
||||
const luma = rgb.luminance();
|
||||
|
||||
if ( this._dark ? luma < this._luma : luma > this._luma )
|
||||
rgb = rgb.toHSLA().targetLuminance(this._luma).toRGBA();
|
||||
|
||||
} else if ( this._mode === 2 ) {
|
||||
// LUV
|
||||
const luv = rgb.toLUVA();
|
||||
if ( this._dark ? luv.l < this._luv : luv.l > this._luv )
|
||||
rgb = luv._l(this._luv).toRGBA();
|
||||
|
||||
} else if ( this._mode === 3 ) {
|
||||
// HSL Loop (aka BTTV Style)
|
||||
if ( this._dark )
|
||||
while ( rgb.get_Y() < 0.5 ) {
|
||||
const hsl = rgb.toHSLA();
|
||||
rgb = hsl._l(Math.min(Math.max(0, 0.1 + 0.9 * hsl.l), 1)).toRGBA();
|
||||
}
|
||||
|
||||
else
|
||||
while ( rgb.get_Y() >= 0.5 ) {
|
||||
const hsl = rgb.toHSLA();
|
||||
rgb = hsl._l(Math.min(Math.max(0, 0.9 * hsl.l), 1)).toRGBA();
|
||||
}
|
||||
|
||||
} else if ( this._mode === 4 ) {
|
||||
// RGB Loop
|
||||
let i = 0;
|
||||
if ( this._dark )
|
||||
while ( rgb.luminance() < 0.15 && i++ < 127 )
|
||||
rgb = rgb.brighten();
|
||||
|
||||
else
|
||||
while ( rgb.luminance() > 0.3 && i++ < 127 )
|
||||
rgb = rgb.brighten(-1);
|
||||
}
|
||||
|
||||
const out = rgb.toCSS();
|
||||
this._cache.set(color, out);
|
||||
return out;
|
||||
}
|
||||
}
|
895
src/utilities/color.ts
Normal file
895
src/utilities/color.ts
Normal file
|
@ -0,0 +1,895 @@
|
|||
'use strict';
|
||||
|
||||
export type CVDMatrix = [number, number, number, number, number, number, number, number, number];
|
||||
|
||||
export function hue2rgb(p: number, q: number, t: number) {
|
||||
if ( t < 0 ) t += 1;
|
||||
if ( t > 1 ) t -= 1;
|
||||
if ( t < 1/6 )
|
||||
return p + (q-p) * 6 * t;
|
||||
if ( t < 1/2 )
|
||||
return q;
|
||||
if ( t < 2/3 )
|
||||
return p + (q-p) * (2/3 - t) * 6;
|
||||
return p;
|
||||
}
|
||||
|
||||
export function bit2linear(channel: number) {
|
||||
// http://www.brucelindbloom.com/Eqn_RGB_to_XYZ.html
|
||||
// This converts rgb 8bit to rgb linear, lazy because the other algorithm is really really dumb
|
||||
//return Math.pow(channel, 2.2);
|
||||
|
||||
// CSS Colors Level 4 says 0.03928, Bruce Lindbloom who cared to write all algos says 0.04045, used bruce because whynawt
|
||||
return (channel <= 0.04045) ? channel / 12.92 : Math.pow((channel + 0.055) / 1.055, 2.4);
|
||||
}
|
||||
|
||||
export function linear2bit(channel: number) {
|
||||
// Using lazy conversion in the other direction as well
|
||||
//return Math.pow(channel, 1/2.2);
|
||||
|
||||
// I'm honestly not sure about 0.0031308, I've only seen it referenced on Bruce Lindbloom's site
|
||||
return (channel <= 0.0031308) ? channel * 12.92 : Math.pow(1.055 * channel, 1/2.4) - 0.055;
|
||||
}
|
||||
|
||||
|
||||
export interface BaseColor {
|
||||
eq(other: BaseColor | null | undefined, ignoreAlpha: boolean): boolean;
|
||||
|
||||
toCSS(): string;
|
||||
toHex(): string;
|
||||
|
||||
toRGBA(): RGBAColor;
|
||||
toHSVA(): HSVAColor;
|
||||
toHSLA(): HSLAColor;
|
||||
toXYZA(): XYZAColor;
|
||||
toLUVA(): LUVAColor;
|
||||
}
|
||||
|
||||
|
||||
class RGBAColor implements BaseColor {
|
||||
|
||||
readonly r: number;
|
||||
readonly g: number;
|
||||
readonly b: number;
|
||||
readonly a: number;
|
||||
|
||||
constructor(r: number, g: number, b: number, a?: number) {
|
||||
this.r = r || 0;
|
||||
this.g = g || 0;
|
||||
this.b = b || 0;
|
||||
this.a = a || 0;
|
||||
}
|
||||
|
||||
eq(other?: BaseColor | null, ignoreAlpha = false): boolean {
|
||||
if ( other instanceof RGBAColor )
|
||||
return this.r === other.r && this.g === other.g && this.b === other.b && (ignoreAlpha || this.a === other.a);
|
||||
return other ? this.eq(other.toRGBA(), ignoreAlpha) : false;
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Updates
|
||||
// ========================================================================
|
||||
|
||||
_r(r: number) { return new RGBAColor(r, this.g, this.b, this.a); }
|
||||
_g(g: number) { return new RGBAColor(this.r, g, this.b, this.a); }
|
||||
_b(b: number) { return new RGBAColor(this.r, this.g, b, this.a); }
|
||||
_a(a: number) { return new RGBAColor(this.r, this.g, this.b, a); }
|
||||
|
||||
// ========================================================================
|
||||
// Conversion: to RGBA
|
||||
// ========================================================================
|
||||
|
||||
static fromName(name: string) {
|
||||
const ctx = Color.getContext();
|
||||
ctx.clearRect(0, 0, 1, 1);
|
||||
ctx.fillStyle = name;
|
||||
ctx.fillRect(0, 0, 1, 1);
|
||||
const data = ctx.getImageData(0, 0, 1, 1);
|
||||
if ( data?.data?.length !== 4 )
|
||||
return null;
|
||||
|
||||
return new RGBAColor(data.data[0], data.data[1], data.data[2], data.data[3]);
|
||||
}
|
||||
|
||||
static fromCSS(input: string) {
|
||||
input = input && input.trim();
|
||||
if ( ! input?.length )
|
||||
return null;
|
||||
|
||||
if ( input.charAt(0) === '#' )
|
||||
return RGBAColor.fromHex(input);
|
||||
|
||||
// fillStyle can handle rgba() inputs
|
||||
/*const match = /rgba?\( *(\d+%?) *, *(\d+%?) *, *(\d+%?) *(?:, *([\d.]+))?\)/i.exec(input);
|
||||
if ( match ) {
|
||||
let r: number, g: number, b: number, a: number;
|
||||
let rS = match[1],
|
||||
gS = match[2],
|
||||
bS = match[3],
|
||||
aS = match[4];
|
||||
|
||||
if ( rS.charAt(rS.length-1) === '%' )
|
||||
r = 255 * (parseInt(rS,10) / 100);
|
||||
else
|
||||
r = parseInt(rS,10);
|
||||
|
||||
if ( gS.charAt(gS.length-1) === '%' )
|
||||
g = 255 * (parseInt(gS,10) / 100);
|
||||
else
|
||||
g = parseInt(gS,10);
|
||||
|
||||
if ( bS.charAt(bS.length-1) === '%' )
|
||||
b = 255 * (parseInt(bS,10) / 100);
|
||||
else
|
||||
b = parseInt(bS,10);
|
||||
|
||||
if ( aS )
|
||||
if ( aS.charAt(aS.length-1) === '%' )
|
||||
a = parseInt(aS,10) / 100;
|
||||
else
|
||||
a = parseFloat(aS);
|
||||
else
|
||||
a = 1;
|
||||
|
||||
return new RGBAColorA(
|
||||
Math.min(Math.max(0, r), 255),
|
||||
Math.min(Math.max(0, g), 255),
|
||||
Math.min(Math.max(0, b), 255),
|
||||
Math.min(Math.max(0, a), 1)
|
||||
);
|
||||
}*/
|
||||
|
||||
return RGBAColor.fromName(input);
|
||||
}
|
||||
|
||||
static fromHex(input: string) {
|
||||
if ( input.charAt(0) === '#' )
|
||||
input = input.slice(1);
|
||||
|
||||
let raw: number;
|
||||
let alpha: number = 255;
|
||||
|
||||
if ( input.length === 4 ) {
|
||||
alpha = parseInt(input[3], 16) * 17;
|
||||
input = input.slice(0, 3);
|
||||
} else if ( input.length === 8 ) {
|
||||
alpha = parseInt(input.slice(6), 16);
|
||||
input = input.slice(0, 6);
|
||||
}
|
||||
|
||||
if ( input.length === 3 )
|
||||
raw =
|
||||
((parseInt(input[0], 16) * 17) << 16) +
|
||||
((parseInt(input[1], 16) * 17) << 8) +
|
||||
parseInt(input[2], 16) * 17;
|
||||
|
||||
else
|
||||
raw = parseInt(input, 16);
|
||||
|
||||
return new RGBAColor(
|
||||
(raw >> 16), // Red
|
||||
(raw >> 8 & 0x00FF), // Green
|
||||
(raw & 0xFF), // Blue
|
||||
alpha / 255 // Alpha (scaled from 0 to 1)
|
||||
);
|
||||
}
|
||||
|
||||
static fromHSVA(h: number, s: number, v: number, a?: number) {
|
||||
let r: number, g: number, b: number;
|
||||
|
||||
const i = Math.floor(h * 6),
|
||||
f = h * 6 - i,
|
||||
p = v * (1 - s),
|
||||
q = v * (1 - f * s),
|
||||
t = v * (1 - (1 - f) * s);
|
||||
|
||||
switch(i % 6) {
|
||||
case 0: r = v; g = t; b = p; break;
|
||||
case 1: r = q; g = v; b = p; break;
|
||||
case 2: r = p; g = v; b = t; break;
|
||||
case 3: r = p; g = q; b = v; break;
|
||||
case 4: r = t; g = p; b = v; break;
|
||||
default: // case 5:
|
||||
r = v; g = p; b = q;
|
||||
}
|
||||
|
||||
return new RGBAColor(
|
||||
Math.round(Math.min(Math.max(0, r*255), 255)),
|
||||
Math.round(Math.min(Math.max(0, g*255), 255)),
|
||||
Math.round(Math.min(Math.max(0, b*255), 255)),
|
||||
a === undefined ? 1 : a
|
||||
);
|
||||
}
|
||||
|
||||
static fromHSLA(h: number, s: number, l: number, a?: number) {
|
||||
if ( s === 0 ) {
|
||||
const v = Math.round(Math.min(Math.max(0, 255*l), 255));
|
||||
return new RGBAColor(v, v, v, a === undefined ? 1 : a);
|
||||
}
|
||||
|
||||
const q = l < 0.5 ? l * (1 + s) : l + s - l * s,
|
||||
p = 2 * l - q;
|
||||
|
||||
return new RGBAColor(
|
||||
Math.round(Math.min(Math.max(0, 255 * hue2rgb(p, q, h + 1/3)), 255)),
|
||||
Math.round(Math.min(Math.max(0, 255 * hue2rgb(p, q, h)), 255)),
|
||||
Math.round(Math.min(Math.max(0, 255 * hue2rgb(p, q, h - 1/3)), 255)),
|
||||
a === undefined ? 1 : a
|
||||
);
|
||||
}
|
||||
|
||||
static fromXYZA(x: number, y: number, z: number, a?: number) {
|
||||
const R = 3.240479 * x - 1.537150 * y - 0.498535 * z,
|
||||
G = -0.969256 * x + 1.875992 * y + 0.041556 * z,
|
||||
B = 0.055648 * x - 0.204043 * y + 1.057311 * z;
|
||||
|
||||
// Make sure we end up in a real color space
|
||||
return new RGBAColor(
|
||||
Math.max(0, Math.min(255, 255 * linear2bit(R))),
|
||||
Math.max(0, Math.min(255, 255 * linear2bit(G))),
|
||||
Math.max(0, Math.min(255, 255 * linear2bit(B))),
|
||||
a === undefined ? 1 : a
|
||||
);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Conversion: from RGBA
|
||||
// ========================================================================
|
||||
|
||||
// CSS
|
||||
toCSS() {
|
||||
if ( this.a !== 1 )
|
||||
return `rgba(${this.r},${this.g},${this.b},${this.a})`;
|
||||
return this.toHex();
|
||||
}
|
||||
|
||||
toHex() {
|
||||
const value = (this.r << 16) + (this.g << 8) + this.b;
|
||||
return `#${value.toString(16).padStart(6, '0')}`;
|
||||
}
|
||||
|
||||
// Color Spaces
|
||||
toRGBA() { return this; }
|
||||
toHSVA() { return HSVAColor.fromRGBA(this.r, this.g, this.b, this.a); }
|
||||
toHSLA() { return HSLAColor.fromRGBA(this.r, this.g, this.b, this.a); }
|
||||
toXYZA() { return XYZAColor.fromRGBA(this.r, this.g, this.b, this.a); }
|
||||
toLUVA() { return this.toXYZA().toLUVA(); }
|
||||
|
||||
// ========================================================================
|
||||
// Processing
|
||||
// ========================================================================
|
||||
|
||||
get_Y() {
|
||||
return ((0.299 * this.r) + ( 0.587 * this.g) + ( 0.114 * this.b)) / 255;
|
||||
}
|
||||
|
||||
luminance() {
|
||||
const r = bit2linear(this.r / 255),
|
||||
g = bit2linear(this.g / 255),
|
||||
b = bit2linear(this.b / 255);
|
||||
|
||||
return (0.2126 * r) + (0.7152 * g) + (0.0722 * b);
|
||||
}
|
||||
|
||||
/** @deprecated This is a horrible function. */
|
||||
brighten(amount?: number) {
|
||||
amount = typeof amount === `number` ? amount : 1;
|
||||
amount = Math.round(255 * (amount / 100));
|
||||
|
||||
return new RGBAColor(
|
||||
Math.max(0, Math.min(255, this.r + amount)),
|
||||
Math.max(0, Math.min(255, this.g + amount)),
|
||||
Math.max(0, Math.min(255, this.b + amount)),
|
||||
this.a
|
||||
);
|
||||
}
|
||||
|
||||
daltonize(type: string | CVDMatrix) {
|
||||
let cvd: CVDMatrix;
|
||||
if ( typeof type === 'string' ) {
|
||||
if ( Color.CVDMatrix.hasOwnProperty(type) )
|
||||
cvd = Color.CVDMatrix[type];
|
||||
else
|
||||
throw new Error('Invalid CVD matrix');
|
||||
} else
|
||||
cvd = type;
|
||||
|
||||
const cvd_a = cvd[0], cvd_b = cvd[1], cvd_c = cvd[2],
|
||||
cvd_d = cvd[3], cvd_e = cvd[4], cvd_f = cvd[5],
|
||||
cvd_g = cvd[6], cvd_h = cvd[7], cvd_i = cvd[8];
|
||||
|
||||
//let L, M, S, l, m, s, R, G, B, RR, GG, BB;
|
||||
|
||||
// RGB to LMS matrix conversion
|
||||
const L = (17.8824 * this.r) + (43.5161 * this.g) + (4.11935 * this.b),
|
||||
M = (3.45565 * this.r) + (27.1554 * this.g) + (3.86714 * this.b),
|
||||
S = (0.0299566 * this.r) + (0.184309 * this.g) + (1.46709 * this.b);
|
||||
|
||||
// Simulate color blindness
|
||||
const l = (cvd_a * L) + (cvd_b * M) + (cvd_c * S),
|
||||
m = (cvd_d * L) + (cvd_e * M) + (cvd_f * S),
|
||||
s = (cvd_g * L) + (cvd_h * M) + (cvd_i * S);
|
||||
|
||||
// LMS to RGB matrix conversion
|
||||
let R = (0.0809444479 * l) + (-0.130504409 * m) + (0.116721066 * s),
|
||||
G = (-0.0102485335 * l) + (0.0540193266 * m) + (-0.113614708 * s),
|
||||
B = (-0.000365296938 * l) + (-0.00412161469 * m) + (0.693511405 * s);
|
||||
|
||||
// Isolate invisible colors to color vision deficiency (calculate error matrix)
|
||||
R = this.r - R;
|
||||
G = this.g - G;
|
||||
B = this.b - B;
|
||||
|
||||
// Shift colors towards visible spectrum (apply error modifications)
|
||||
const RR = (0.0 * R) + (0.0 * G) + (0.0 * B),
|
||||
GG = (0.7 * R) + (1.0 * G) + (0.0 * B),
|
||||
BB = (0.7 * R) + (0.0 * G) + (1.0 * B);
|
||||
|
||||
// Add compensation to original values
|
||||
R = Math.min(Math.max(0, RR + this.r), 255);
|
||||
G = Math.min(Math.max(0, GG + this.g), 255);
|
||||
B = Math.min(Math.max(0, BB + this.b), 255);
|
||||
|
||||
return new RGBAColor(R, G, B, this.a);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
class HSVAColor implements BaseColor {
|
||||
|
||||
readonly h: number;
|
||||
readonly s: number;
|
||||
readonly v: number;
|
||||
readonly a: number;
|
||||
|
||||
constructor(h: number, s: number, v: number, a?: number) {
|
||||
this.h = h || 0;
|
||||
this.s = s || 0;
|
||||
this.v = v || 0;
|
||||
this.a = a || 0;
|
||||
}
|
||||
|
||||
eq(other?: BaseColor | null, ignoreAlpha = false): boolean {
|
||||
if ( other instanceof HSVAColor )
|
||||
return this.h === other.h && this.s === other.s && this.v === other.v && (ignoreAlpha || this.a === other.a);
|
||||
return other ? this.eq(other.toHSVA(), ignoreAlpha) : false;
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Updates
|
||||
// ========================================================================
|
||||
|
||||
_h(h: number) { return new HSVAColor(h, this.s, this.v, this.a); }
|
||||
_s(s: number) { return new HSVAColor(this.h, s, this.v, this.a); }
|
||||
_v(v: number) { return new HSVAColor(this.h, this.s, v, this.a); }
|
||||
_a(a: number) { return new HSVAColor(this.h, this.s, this.v, a); }
|
||||
|
||||
// ========================================================================
|
||||
// Conversion: to HSVA
|
||||
// ========================================================================
|
||||
|
||||
static fromRGBA(r: number, g: number, b: number, a?: number) {
|
||||
r /= 255; g /= 255; b /= 255;
|
||||
|
||||
const max = Math.max(r, g, b),
|
||||
min = Math.min(r, g, b),
|
||||
d = Math.min(Math.max(0, max - min), 1),
|
||||
|
||||
s = max === 0 ? 0 : d / max,
|
||||
v = max;
|
||||
|
||||
let h;
|
||||
|
||||
if ( d === 0 )
|
||||
h = 0;
|
||||
else {
|
||||
switch(max) {
|
||||
case r:
|
||||
h = (g - b) / d + (g < b ? 6 : 0);
|
||||
break;
|
||||
case g:
|
||||
h = (b - r) / d + 2;
|
||||
break;
|
||||
default: // case b:
|
||||
h = (r - g) / d + 4;
|
||||
}
|
||||
h /= 6;
|
||||
}
|
||||
|
||||
return new HSVAColor(
|
||||
h,
|
||||
s,
|
||||
v,
|
||||
a === undefined ? 1 : a
|
||||
);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Conversion: from HSVA
|
||||
// ========================================================================
|
||||
|
||||
toCSS() { return this.toRGBA().toCSS(); }
|
||||
toHex() { return this.toRGBA().toHex(); }
|
||||
|
||||
toRGBA() { return RGBAColor.fromHSVA(this.h, this.s, this.v, this.a); }
|
||||
toHSVA() { return this; }
|
||||
toHSLA() { return this.toRGBA().toHSLA(); }
|
||||
toXYZA() { return this.toRGBA().toXYZA(); }
|
||||
toLUVA() { return this.toRGBA().toLUVA(); }
|
||||
|
||||
}
|
||||
|
||||
|
||||
class HSLAColor implements BaseColor {
|
||||
|
||||
readonly h: number;
|
||||
readonly s: number;
|
||||
readonly l: number;
|
||||
readonly a: number;
|
||||
|
||||
constructor(h: number, s: number, l: number, a?: number) {
|
||||
this.h = h || 0;
|
||||
this.s = s || 0;
|
||||
this.l = l || 0;
|
||||
this.a = a || 0;
|
||||
}
|
||||
|
||||
eq(other?: BaseColor | null, ignoreAlpha = false): boolean {
|
||||
if ( other instanceof HSLAColor )
|
||||
return this.h === other.h && this.s === other.s && this.l === other.l && (ignoreAlpha || this.a === other.a);
|
||||
return other ? this.eq(other.toHSLA(), ignoreAlpha) : false;
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Updates
|
||||
// ========================================================================
|
||||
|
||||
_h(h: number) { return new HSLAColor(h, this.s, this.l, this.a); }
|
||||
_s(s: number) { return new HSLAColor(this.h, s, this.l, this.a); }
|
||||
_l(l: number) { return new HSLAColor(this.h, this.s, l, this.a); }
|
||||
_a(a: number) { return new HSLAColor(this.h, this.s, this.l, a); }
|
||||
|
||||
// ========================================================================
|
||||
// Conversion: to HSLA
|
||||
// ========================================================================
|
||||
|
||||
static fromRGBA(r: number, g: number, b: number, a?: number) {
|
||||
r /= 255; g /= 255; b /= 255;
|
||||
|
||||
const max = Math.max(r,g,b),
|
||||
min = Math.min(r,g,b),
|
||||
|
||||
l = Math.min(Math.max(0, (max+min) / 2), 1),
|
||||
d = Math.min(Math.max(0, max - min), 1);
|
||||
|
||||
let h, s;
|
||||
|
||||
if ( d === 0 )
|
||||
h = s = 0;
|
||||
else {
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
switch(max) {
|
||||
case r:
|
||||
h = (g - b) / d + (g < b ? 6 : 0);
|
||||
break;
|
||||
case g:
|
||||
h = (b - r) / d + 2;
|
||||
break;
|
||||
default: //case b:
|
||||
h = (r - g) / d + 4;
|
||||
}
|
||||
h /= 6;
|
||||
}
|
||||
|
||||
return new HSLAColor(h, s, l, a === undefined ? 1 : a);
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Conversion: from HSLA
|
||||
// ========================================================================
|
||||
|
||||
toCSS() {
|
||||
const a = this.a;
|
||||
return `hsl${a !== 1 ? 'a' : ''}(${Math.round(this.h*360)},${Math.round(this.s*100)}%,${Math.round(this.l*100)}%${a !== 1 ? `,${this.a}` : ''})`;
|
||||
}
|
||||
|
||||
toHex() { return this.toRGBA().toHex(); }
|
||||
|
||||
toRGBA() { return RGBAColor.fromHSLA(this.h, this.s, this.l, this.a); }
|
||||
toHSLA() { return this; }
|
||||
toHSVA() { return this.toRGBA().toHSVA(); }
|
||||
toXYZA() { return this.toRGBA().toXYZA(); }
|
||||
toLUVA() { return this.toRGBA().toLUVA(); }
|
||||
|
||||
// ========================================================================
|
||||
// Processing
|
||||
// ========================================================================
|
||||
|
||||
targetLuminance(target: number) {
|
||||
let s = this.s,
|
||||
min = 0,
|
||||
max = 1;
|
||||
|
||||
s *= Math.pow(this.l > 0.5 ? -this.l : this.l - 1, 7) + 1;
|
||||
|
||||
let d = (max - min) / 2,
|
||||
mid = min + d;
|
||||
|
||||
for (; d > 1/65536; d /= 2, mid = min + d) {
|
||||
const luminance = RGBAColor.fromHSLA(this.h, s, mid, 1).luminance();
|
||||
if (luminance > target) {
|
||||
max = mid;
|
||||
} else {
|
||||
min = mid;
|
||||
}
|
||||
}
|
||||
|
||||
return new HSLAColor(this.h, s, mid, this.a);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
class XYZAColor implements BaseColor {
|
||||
|
||||
readonly x: number;
|
||||
readonly y: number;
|
||||
readonly z: number;
|
||||
readonly a: number;
|
||||
|
||||
constructor(x: number, y: number, z: number, a?: number) {
|
||||
this.x = x || 0;
|
||||
this.y = y || 0;
|
||||
this.z = z || 0;
|
||||
this.a = a || 0;
|
||||
}
|
||||
|
||||
eq(other?: BaseColor, ignoreAlpha = false): boolean {
|
||||
if ( other instanceof XYZAColor )
|
||||
return this.x === other.x && this.y === other.y && this.z === other.z && (ignoreAlpha || this.a === other.a);
|
||||
return other ? this.eq(other.toXYZA(), ignoreAlpha) : false;
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Updates
|
||||
// ========================================================================
|
||||
|
||||
_x(x: number) { return new XYZAColor(x, this.y, this.z, this.a); }
|
||||
_y(y: number) { return new XYZAColor(this.x, y, this.z, this.a); }
|
||||
_z(z: number) { return new XYZAColor(this.x, this.y, z, this.a); }
|
||||
_a(a: number) { return new XYZAColor(this.x, this.y, this.z, a); }
|
||||
|
||||
// ========================================================================
|
||||
// Conversion: to XYZA
|
||||
// ========================================================================
|
||||
|
||||
static EPSILON = Math.pow(6 / 29, 3);
|
||||
static KAPPA = Math.pow(29 / 3, 3);
|
||||
static WHITE = null as any; // Gotta do this late to avoid an error.
|
||||
|
||||
static fromRGBA(r: number, g: number, b: number, a?: number) {
|
||||
const R = bit2linear(r / 255),
|
||||
G = bit2linear(g / 255),
|
||||
B = bit2linear(b / 255);
|
||||
|
||||
return new XYZAColor(
|
||||
0.412453 * R + 0.357580 * G + 0.180423 * B,
|
||||
0.212671 * R + 0.715160 * G + 0.072169 * B,
|
||||
0.019334 * R + 0.119193 * G + 0.950227 * B,
|
||||
a === undefined ? 1 : a
|
||||
);
|
||||
}
|
||||
|
||||
static fromLUVA(l: number, u: number, v: number, alpha?: number) {
|
||||
const deltaGammaFactor = 1 / (XYZAColor.WHITE.x + 15 * XYZAColor.WHITE.y + 3 * XYZAColor.WHITE.z),
|
||||
uDeltaGamma = 4 * XYZAColor.WHITE.x * deltaGammaFactor,
|
||||
vDeltagamma = 9 * XYZAColor.WHITE.y * deltaGammaFactor;
|
||||
|
||||
// XYZAColor.EPSILON * XYZAColor.KAPPA = 8
|
||||
const Y = (l > 8) ? Math.pow((l + 16) / 116, 3) : l / XYZAColor.KAPPA,
|
||||
a = 1/3 * (((52 * l) / (u + 13 * l * uDeltaGamma)) - 1),
|
||||
b = -5 * Y,
|
||||
c = -1/3,
|
||||
d = Y * (((39 * l) / (v + 13 * l * vDeltagamma)) - 5),
|
||||
|
||||
X = (d - b) / (a - c),
|
||||
Z = X * a + b;
|
||||
|
||||
return new XYZAColor(X, Y, Z, alpha === undefined ? 1 : alpha);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Conversion: from XYZA
|
||||
// ========================================================================
|
||||
|
||||
toCSS() { return this.toRGBA().toCSS(); }
|
||||
toHex() { return this.toRGBA().toHex(); }
|
||||
|
||||
toRGBA() { return RGBAColor.fromXYZA(this.x, this.y, this.z, this.a); }
|
||||
toHSLA() { return this.toRGBA().toHSLA(); }
|
||||
toHSVA() { return this.toRGBA().toHSVA(); }
|
||||
toXYZA() { return this; }
|
||||
toLUVA() { return LUVAColor.fromXYZA(this.x, this.y, this.z, this.a); }
|
||||
}
|
||||
|
||||
// Assign this now that XYZAColor exists.
|
||||
XYZAColor.WHITE = new RGBAColor(255,255,255,1).toXYZA();
|
||||
|
||||
|
||||
class LUVAColor implements BaseColor {
|
||||
|
||||
readonly l: number;
|
||||
readonly u: number;
|
||||
readonly v: number;
|
||||
readonly a: number;
|
||||
|
||||
constructor(l: number, u: number, v: number, a?: number) {
|
||||
this.l = l || 0;
|
||||
this.u = u || 0;
|
||||
this.v = v || 0;
|
||||
this.a = a || 0;
|
||||
}
|
||||
|
||||
eq(other?: BaseColor | null, ignoreAlpha = false): boolean {
|
||||
if ( other instanceof LUVAColor )
|
||||
return this.l === other.l && this.u === other.u && this.v === other.v && (ignoreAlpha || this.a === other.a);
|
||||
return other ? this.eq(other.toLUVA(), ignoreAlpha) : false;
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Updates
|
||||
// ========================================================================
|
||||
|
||||
_l(l: number) { return new LUVAColor(l, this.u, this.v, this.a); }
|
||||
_u(u: number) { return new LUVAColor(this.l, u, this.v, this.a); }
|
||||
_v(v: number) { return new LUVAColor(this.l, this.u, v, this.a); }
|
||||
_a(a: number) { return new LUVAColor(this.l, this.u, this.v, a); }
|
||||
|
||||
// ========================================================================
|
||||
// Conversion: to LUVA
|
||||
// ========================================================================
|
||||
|
||||
static fromXYZA(X: number, Y: number, Z: number, a?: number) {
|
||||
const deltaGammaFactor = 1 / (XYZAColor.WHITE.x + 15 * XYZAColor.WHITE.y + 3 * XYZAColor.WHITE.z),
|
||||
uDeltaGamma = 4 * XYZAColor.WHITE.x * deltaGammaFactor,
|
||||
vDeltagamma = 9 * XYZAColor.WHITE.y * deltaGammaFactor,
|
||||
|
||||
yGamma = Y / XYZAColor.WHITE.y;
|
||||
|
||||
let deltaDivider = (X + 15 * Y + 3 * Z);
|
||||
if (deltaDivider === 0) {
|
||||
deltaDivider = 1;
|
||||
}
|
||||
|
||||
const deltaFactor = 1 / deltaDivider,
|
||||
|
||||
uDelta = 4 * X * deltaFactor,
|
||||
vDelta = 9 * Y * deltaFactor,
|
||||
|
||||
L = (yGamma > XYZAColor.EPSILON) ? 116 * Math.pow(yGamma, 1/3) - 16 : XYZAColor.KAPPA * yGamma,
|
||||
u = 13 * L * (uDelta - uDeltaGamma),
|
||||
v = 13 * L * (vDelta - vDeltagamma);
|
||||
|
||||
return new LUVAColor(L, u, v, a === undefined ? 1 : a);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Conversion: from LUVA
|
||||
// ========================================================================
|
||||
|
||||
toCSS() { return this.toRGBA().toCSS(); }
|
||||
toHex() { return this.toRGBA().toHex(); }
|
||||
|
||||
toRGBA() { return this.toXYZA().toRGBA(); }
|
||||
toHSLA() { return this.toRGBA().toHSLA(); }
|
||||
toHSVA() { return this.toRGBA().toHSVA(); }
|
||||
toXYZA() { return XYZAColor.fromLUVA(this.l, this.u, this.v, this.a); }
|
||||
toLUVA() { return this;}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
type ColorType = {
|
||||
_canvas?: HTMLCanvasElement;
|
||||
_context?: CanvasRenderingContext2D;
|
||||
|
||||
getCanvas(): HTMLCanvasElement;
|
||||
getContext(): CanvasRenderingContext2D
|
||||
|
||||
CVDMatrix: Record<string, CVDMatrix>;
|
||||
|
||||
RGBA: typeof RGBAColor;
|
||||
HSVA: typeof HSVAColor;
|
||||
HSLA: typeof HSLAColor;
|
||||
XYZA: typeof XYZAColor;
|
||||
LUVA: typeof LUVAColor;
|
||||
|
||||
fromCSS(input: string): RGBAColor | null;
|
||||
}
|
||||
|
||||
|
||||
export const Color: ColorType = {
|
||||
CVDMatrix: {
|
||||
protanope: [ // reds are greatly reduced (1% men)
|
||||
0.0, 2.02344, -2.52581,
|
||||
0.0, 1.0, 0.0,
|
||||
0.0, 0.0, 1.0
|
||||
],
|
||||
deuteranope: [ // greens are greatly reduced (1% men)
|
||||
1.0, 0.0, 0.0,
|
||||
0.494207, 0.0, 1.24827,
|
||||
0.0, 0.0, 1.0
|
||||
],
|
||||
tritanope: [ // blues are greatly reduced (0.003% population)
|
||||
1.0, 0.0, 0.0,
|
||||
0.0, 1.0, 0.0,
|
||||
-0.395913, 0.801109, 0.0
|
||||
]
|
||||
},
|
||||
|
||||
getCanvas() {
|
||||
if ( ! Color._canvas )
|
||||
Color._canvas = document.createElement('canvas');
|
||||
return Color._canvas;
|
||||
},
|
||||
getContext: () => {
|
||||
if ( ! Color._context )
|
||||
Color._context = Color.getCanvas().getContext('2d') as CanvasRenderingContext2D;
|
||||
return Color._context;
|
||||
},
|
||||
|
||||
RGBA: RGBAColor,
|
||||
HSVA: HSVAColor,
|
||||
HSLA: HSLAColor,
|
||||
XYZA: XYZAColor,
|
||||
LUVA: LUVAColor,
|
||||
|
||||
fromCSS(input: string) {
|
||||
return RGBAColor.fromCSS(input);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
export class ColorAdjuster {
|
||||
|
||||
private _base: string;
|
||||
private _contrast: number;
|
||||
private _mode: number;
|
||||
|
||||
private _dark: boolean = false;
|
||||
private _cache: Map<string, string> = new Map;
|
||||
|
||||
private _luv: number = 0;
|
||||
private _luma: number = 0;
|
||||
|
||||
constructor(base = '#232323', mode = 0, contrast = 4.5) {
|
||||
this._contrast = contrast;
|
||||
this._base = base;
|
||||
this._mode = mode;
|
||||
|
||||
this.rebuildContrast();
|
||||
}
|
||||
|
||||
get contrast() { return this._contrast }
|
||||
set contrast(val) { this._contrast = val; this.rebuildContrast() }
|
||||
|
||||
get base() { return this._base }
|
||||
set base(val) { this._base = val; this.rebuildContrast() }
|
||||
|
||||
get dark() { return this._dark }
|
||||
|
||||
get mode() { return this._mode }
|
||||
set mode(val) { this._mode = val; this.rebuildContrast() }
|
||||
|
||||
|
||||
rebuildContrast() {
|
||||
this._cache = new Map;
|
||||
|
||||
const base = RGBAColor.fromCSS(this._base);
|
||||
if ( ! base )
|
||||
throw new Error('Invalid base color');
|
||||
|
||||
const lum = base.luminance(),
|
||||
dark = this._dark = lum < 0.5;
|
||||
|
||||
if ( dark ) {
|
||||
this._luv = new XYZAColor(
|
||||
0,
|
||||
(this._contrast * (base.toXYZA().y + 0.05) - 0.05),
|
||||
0,
|
||||
1
|
||||
).toLUVA().l;
|
||||
|
||||
this._luma = this._contrast * (base.luminance() + 0.05) - 0.05;
|
||||
|
||||
} else {
|
||||
this._luv = new XYZAColor(
|
||||
0,
|
||||
(base.toXYZA().y + 0.05) / this._contrast - 0.05,
|
||||
0,
|
||||
1
|
||||
).toLUVA().l;
|
||||
|
||||
this._luma = (base.luminance() + 0.05) / this._contrast - 0.05;
|
||||
}
|
||||
}
|
||||
|
||||
process(color: BaseColor | string, throw_errors = false) {
|
||||
if ( this._mode === -1 )
|
||||
return '';
|
||||
|
||||
else if ( this._mode === 0 )
|
||||
return color;
|
||||
|
||||
if ( typeof color !== 'string' )
|
||||
color = color.toCSS();
|
||||
|
||||
if ( ! color )
|
||||
return null;
|
||||
|
||||
if ( this._cache.has(color) )
|
||||
return this._cache.get(color);
|
||||
|
||||
let rgb;
|
||||
|
||||
try {
|
||||
rgb = RGBAColor.fromCSS(color);
|
||||
} catch(err) {
|
||||
if ( throw_errors )
|
||||
throw err;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( ! rgb )
|
||||
return null;
|
||||
|
||||
if ( this._mode === 1 ) {
|
||||
// HSL Luma
|
||||
const luma = rgb.luminance();
|
||||
|
||||
if ( this._dark ? luma < this._luma : luma > this._luma )
|
||||
rgb = rgb.toHSLA().targetLuminance(this._luma).toRGBA();
|
||||
|
||||
} else if ( this._mode === 2 ) {
|
||||
// LUV
|
||||
const luv = rgb.toLUVA();
|
||||
if ( this._dark ? luv.l < this._luv : luv.l > this._luv )
|
||||
rgb = luv._l(this._luv).toRGBA();
|
||||
|
||||
} else if ( this._mode === 3 ) {
|
||||
// HSL Loop (aka BTTV Style)
|
||||
if ( this._dark )
|
||||
while ( rgb.get_Y() < 0.5 ) {
|
||||
const hsl = rgb.toHSLA();
|
||||
rgb = hsl._l(Math.min(Math.max(0, 0.1 + 0.9 * hsl.l), 1)).toRGBA();
|
||||
}
|
||||
|
||||
else
|
||||
while ( rgb.get_Y() >= 0.5 ) {
|
||||
const hsl = rgb.toHSLA();
|
||||
rgb = hsl._l(Math.min(Math.max(0, 0.9 * hsl.l), 1)).toRGBA();
|
||||
}
|
||||
|
||||
} else if ( this._mode === 4 ) {
|
||||
// RGB Loop
|
||||
let i = 0;
|
||||
if ( this._dark )
|
||||
while ( rgb.luminance() < 0.15 && i++ < 127 )
|
||||
rgb = rgb.brighten();
|
||||
|
||||
else
|
||||
while ( rgb.luminance() > 0.3 && i++ < 127 )
|
||||
rgb = rgb.brighten(-1);
|
||||
}
|
||||
|
||||
const out = rgb.toCSS();
|
||||
this._cache.set(color, out);
|
||||
return out;
|
||||
}
|
||||
}
|
|
@ -6,11 +6,28 @@
|
|||
// ============================================================================
|
||||
|
||||
import {EventEmitter} from 'utilities/events';
|
||||
import Module from 'utilities/module';
|
||||
import Module, { GenericModule } from 'utilities/module';
|
||||
|
||||
export default class Elemental extends Module {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
declare module 'utilities/types' {
|
||||
interface ModuleMap {
|
||||
'site.elemental': Elemental;
|
||||
}
|
||||
}
|
||||
|
||||
export default class Elemental extends Module<'site.elemental'> {
|
||||
|
||||
private _wrappers: Map<string, ElementalWrapper<any>>;
|
||||
private _observer: MutationObserver | null;
|
||||
private _watching: Set<ElementalWrapper<any>>;
|
||||
private _live_watching: ElementalWrapper<any>[] | null;
|
||||
private _route?: string | null;
|
||||
private _timer?: number | null;
|
||||
|
||||
private _timeout?: ReturnType<typeof setTimeout> | null;
|
||||
private _clean_all?: ReturnType<typeof requestAnimationFrame> | null;
|
||||
|
||||
constructor(name?: string, parent?: GenericModule) {
|
||||
super(name, parent);
|
||||
|
||||
this._pruneLive = this._pruneLive.bind(this);
|
||||
|
||||
|
@ -21,27 +38,35 @@ export default class Elemental extends Module {
|
|||
this._live_watching = null;
|
||||
}
|
||||
|
||||
|
||||
/** @internal */
|
||||
onDisable() {
|
||||
this._stopWatching();
|
||||
}
|
||||
|
||||
|
||||
define(key, selector, routes, opts = null, limit = 0, timeout = 5000, remove = true) {
|
||||
define<TElement extends HTMLElement = HTMLElement>(
|
||||
key: string,
|
||||
selector: string,
|
||||
routes?: string[] | false | null,
|
||||
opts: MutationObserverInit | null = null,
|
||||
limit = 0,
|
||||
timeout = 5000,
|
||||
remove = true
|
||||
) {
|
||||
if ( this._wrappers.has(key) )
|
||||
return this._wrappers.get(key);
|
||||
return this._wrappers.get(key) as ElementalWrapper<TElement>;
|
||||
|
||||
if ( ! selector || typeof selector !== 'string' || ! selector.length )
|
||||
throw new Error('cannot find definition and no selector provided');
|
||||
|
||||
const wrapper = new ElementalWrapper(key, selector, routes, opts, limit, timeout, remove, this);
|
||||
const wrapper = new ElementalWrapper<TElement>(key, selector, routes, opts, limit, timeout, remove, this);
|
||||
this._wrappers.set(key, wrapper);
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
|
||||
route(route) {
|
||||
route(route: string | null) {
|
||||
this._route = route;
|
||||
this._timer = Date.now();
|
||||
this._updateLiveWatching();
|
||||
|
@ -76,24 +101,27 @@ export default class Elemental extends Module {
|
|||
}
|
||||
|
||||
|
||||
_isActive(watcher, now) {
|
||||
private _isActive(watcher: ElementalWrapper<any>, now: number) {
|
||||
if ( watcher.routes === false )
|
||||
return false;
|
||||
|
||||
if ( this._route && watcher.routes.length && ! watcher.routes.includes(this._route) )
|
||||
return false;
|
||||
|
||||
if ( watcher.timeout > 0 && (now - this._timer) > watcher.timeout )
|
||||
if ( watcher.timeout > 0 && (now - (this._timer as number)) > watcher.timeout )
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
_updateLiveWatching() {
|
||||
private _updateLiveWatching() {
|
||||
if ( this._timeout ) {
|
||||
clearTimeout(this._timeout);
|
||||
this._timeout = null;
|
||||
}
|
||||
|
||||
const lw = this._live_watching = [],
|
||||
const lw: ElementalWrapper<any>[] = this._live_watching = [],
|
||||
now = Date.now();
|
||||
let min_timeout = Number.POSITIVE_INFINITY;
|
||||
|
||||
|
@ -115,16 +143,17 @@ export default class Elemental extends Module {
|
|||
this._startWatching();
|
||||
}
|
||||
|
||||
_pruneLive() {
|
||||
private _pruneLive() {
|
||||
this._updateLiveWatching();
|
||||
}
|
||||
|
||||
_checkWatchers(muts) {
|
||||
for(const watcher of this._live_watching)
|
||||
watcher.checkElements(muts);
|
||||
private _checkWatchers(muts: Node[]) {
|
||||
if ( this._live_watching )
|
||||
for(const watcher of this._live_watching)
|
||||
watcher.checkElements(muts as Element[]);
|
||||
}
|
||||
|
||||
_startWatching() {
|
||||
private _startWatching() {
|
||||
if ( ! this._observer && this._live_watching && this._live_watching.length ) {
|
||||
this.log.info('Installing MutationObserver.');
|
||||
|
||||
|
@ -136,7 +165,7 @@ export default class Elemental extends Module {
|
|||
}
|
||||
}
|
||||
|
||||
_stopWatching() {
|
||||
private _stopWatching() {
|
||||
if ( this._observer ) {
|
||||
this.log.info('Stopping MutationObserver.');
|
||||
this._observer.disconnect();
|
||||
|
@ -152,7 +181,7 @@ export default class Elemental extends Module {
|
|||
}
|
||||
|
||||
|
||||
listen(inst, ensure_live = true) {
|
||||
listen(inst: ElementalWrapper<any>, ensure_live = true) {
|
||||
if ( this._watching.has(inst) )
|
||||
return;
|
||||
|
||||
|
@ -163,7 +192,7 @@ export default class Elemental extends Module {
|
|||
this._updateLiveWatching();
|
||||
}
|
||||
|
||||
unlisten(inst) {
|
||||
unlisten(inst: ElementalWrapper<any>) {
|
||||
if ( ! this._watching.has(inst) )
|
||||
return;
|
||||
|
||||
|
@ -175,20 +204,64 @@ export default class Elemental extends Module {
|
|||
|
||||
let elemental_id = 0;
|
||||
|
||||
export class ElementalWrapper extends EventEmitter {
|
||||
constructor(name, selector, routes, opts, limit, timeout, remove, elemental) {
|
||||
type ElementalParam = `_ffz$elemental$${number}`;
|
||||
type ElementalRemoveParam = `_ffz$elemental_remove$${number}`;
|
||||
|
||||
declare global {
|
||||
interface HTMLElement {
|
||||
[key: ElementalParam]: MutationObserver | null;
|
||||
[key: ElementalRemoveParam]: MutationObserver | null;
|
||||
}
|
||||
}
|
||||
|
||||
type ElementalWrapperEvents<TElement extends HTMLElement> = {
|
||||
mount: [element: TElement];
|
||||
unmount: [element: TElement];
|
||||
mutate: [element: TElement, mutations: MutationRecord[]];
|
||||
}
|
||||
|
||||
export class ElementalWrapper<
|
||||
TElement extends HTMLElement = HTMLElement
|
||||
> extends EventEmitter<ElementalWrapperEvents<TElement>> {
|
||||
|
||||
readonly id: number;
|
||||
readonly name: string;
|
||||
readonly selector: string;
|
||||
readonly routes: string[] | false;
|
||||
readonly opts: MutationObserverInit | null;
|
||||
readonly limit: number;
|
||||
readonly timeout: number;
|
||||
readonly check_removal: boolean;
|
||||
count: number;
|
||||
readonly instances: Set<TElement>;
|
||||
readonly elemental: Elemental;
|
||||
|
||||
readonly param: ElementalParam;
|
||||
readonly remove_param: ElementalRemoveParam;
|
||||
|
||||
private _stimer?: ReturnType<typeof setTimeout> | null;
|
||||
|
||||
constructor(
|
||||
name: string,
|
||||
selector: string,
|
||||
routes: string[] | false | undefined | null,
|
||||
opts: MutationObserverInit | null,
|
||||
limit: number,
|
||||
timeout: number,
|
||||
remove: boolean,
|
||||
elemental: Elemental
|
||||
) {
|
||||
super();
|
||||
|
||||
this.id = elemental_id++;
|
||||
this.param = `_ffz$elemental$${this.id}`;
|
||||
this.remove_param = `_ffz$elemental_remove$${this.id}`;
|
||||
this.mut_param = `_ffz$elemental_mutating${this.id}`;
|
||||
|
||||
this._schedule = this._schedule.bind(this);
|
||||
|
||||
this.name = name;
|
||||
this.selector = selector;
|
||||
this.routes = routes || [];
|
||||
this.routes = routes ?? [];
|
||||
this.opts = opts;
|
||||
this.limit = limit;
|
||||
this.timeout = timeout;
|
||||
|
@ -199,7 +272,6 @@ export class ElementalWrapper extends EventEmitter {
|
|||
|
||||
this.count = 0;
|
||||
this.instances = new Set;
|
||||
this.observers = new Map;
|
||||
this.elemental = elemental;
|
||||
|
||||
this.check();
|
||||
|
@ -224,7 +296,8 @@ export class ElementalWrapper extends EventEmitter {
|
|||
}
|
||||
|
||||
_schedule() {
|
||||
clearTimeout(this._stimer);
|
||||
if ( this._stimer )
|
||||
clearTimeout(this._stimer);
|
||||
this._stimer = null;
|
||||
|
||||
if ( this.limit === 0 || this.count < this.limit )
|
||||
|
@ -234,18 +307,19 @@ export class ElementalWrapper extends EventEmitter {
|
|||
}
|
||||
|
||||
check() {
|
||||
const matches = document.querySelectorAll(this.selector);
|
||||
for(const el of matches)
|
||||
const matches = document.querySelectorAll<TElement>(this.selector);
|
||||
// TypeScript is stupid and thinks NodeListOf<Element> doesn't have an iterator
|
||||
for(const el of matches as unknown as Iterable<TElement>)
|
||||
this.add(el);
|
||||
}
|
||||
|
||||
checkElements(els) {
|
||||
checkElements(els: Iterable<Element>) {
|
||||
if ( this.atLimit )
|
||||
return this.schedule();
|
||||
|
||||
for(const el of els) {
|
||||
const matches = el.querySelectorAll(this.selector);
|
||||
for(const match of matches)
|
||||
const matches = el.querySelectorAll<TElement>(this.selector);
|
||||
for(const match of matches as unknown as Iterable<TElement>)
|
||||
this.add(match);
|
||||
|
||||
if ( this.atLimit )
|
||||
|
@ -264,66 +338,66 @@ export class ElementalWrapper extends EventEmitter {
|
|||
return Array.from(this.instances);
|
||||
}
|
||||
|
||||
each(fn) {
|
||||
each(fn: (element: TElement) => void) {
|
||||
for(const el of this.instances)
|
||||
fn(el);
|
||||
}
|
||||
|
||||
add(el) {
|
||||
if ( this.instances.has(el) )
|
||||
add(element: TElement) {
|
||||
if ( this.instances.has(element) )
|
||||
return;
|
||||
|
||||
this.instances.add(el);
|
||||
this.instances.add(element);
|
||||
this.count++;
|
||||
|
||||
if ( this.check_removal ) {
|
||||
if ( this.check_removal && element.parentNode ) {
|
||||
const remove_check = new MutationObserver(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if ( ! document.contains(el) )
|
||||
this.remove(el);
|
||||
if ( ! document.contains(element) )
|
||||
this.remove(element);
|
||||
});
|
||||
});
|
||||
|
||||
remove_check.observe(el.parentNode, {childList: true});
|
||||
el[this.remove_param] = remove_check;
|
||||
remove_check.observe(element.parentNode, {childList: true});
|
||||
(element as HTMLElement)[this.remove_param] = remove_check;
|
||||
}
|
||||
|
||||
if ( this.opts ) {
|
||||
const observer = new MutationObserver(muts => {
|
||||
if ( ! document.contains(el) ) {
|
||||
this.remove(el);
|
||||
if ( ! document.contains(element) ) {
|
||||
this.remove(element);
|
||||
} else if ( ! this.__running.size )
|
||||
this.emit('mutate', el, muts);
|
||||
this.emit('mutate', element, muts);
|
||||
});
|
||||
|
||||
observer.observe(el, this.opts);
|
||||
el[this.param] = observer;
|
||||
observer.observe(element, this.opts);
|
||||
(element as HTMLElement)[this.param] = observer;
|
||||
}
|
||||
|
||||
this.schedule();
|
||||
this.emit('mount', el);
|
||||
this.emit('mount', element);
|
||||
}
|
||||
|
||||
remove(el) {
|
||||
const observer = el[this.param];
|
||||
remove(element: TElement) {
|
||||
const observer = element[this.param];
|
||||
if ( observer ) {
|
||||
observer.disconnect();
|
||||
el[this.param] = null;
|
||||
(element as HTMLElement)[this.param] = null;
|
||||
}
|
||||
|
||||
const remove_check = el[this.remove_param];
|
||||
const remove_check = element[this.remove_param];
|
||||
if ( remove_check ) {
|
||||
remove_check.disconnect();
|
||||
el[this.remove_param] = null;
|
||||
(element as HTMLElement)[this.remove_param] = null;
|
||||
}
|
||||
|
||||
if ( ! this.instances.has(el) )
|
||||
if ( ! this.instances.has(element) )
|
||||
return;
|
||||
|
||||
this.instances.delete(el);
|
||||
this.instances.delete(element);
|
||||
this.count--;
|
||||
|
||||
this.schedule();
|
||||
this.emit('unmount', el);
|
||||
this.emit('unmount', element);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,13 +5,56 @@
|
|||
// ============================================================================
|
||||
|
||||
import {parse, tokensToRegExp, tokensToFunction} from 'path-to-regexp';
|
||||
import Module from 'utilities/module';
|
||||
import {has, deep_equals} from 'utilities/object';
|
||||
import Module, { GenericModule } from 'utilities/module';
|
||||
import {has, deep_equals, sleep} from 'utilities/object';
|
||||
import type Fine from './fine';
|
||||
import type { OptionalPromise } from 'utilities/types';
|
||||
|
||||
declare module 'utilities/types' {
|
||||
interface ModuleEventMap {
|
||||
'site.router': FineRouterEvents;
|
||||
}
|
||||
interface ModuleMap {
|
||||
'site.router': FineRouter;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default class FineRouter extends Module {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
type FineRouterEvents = {
|
||||
':updated-route-names': [];
|
||||
':updated-routes': [];
|
||||
|
||||
':route': [route: RouteInfo | null, match: unknown];
|
||||
};
|
||||
|
||||
export type RouteInfo = {
|
||||
name: string;
|
||||
domain: string | null;
|
||||
|
||||
};
|
||||
|
||||
|
||||
export default class FineRouter extends Module<'site.router', FineRouterEvents> {
|
||||
|
||||
// Dependencies
|
||||
fine: Fine = null as any;
|
||||
|
||||
// Storage
|
||||
routes: Record<string, RouteInfo>;
|
||||
route_names: Record<string, string>;
|
||||
private __routes: RouteInfo[];
|
||||
|
||||
// State
|
||||
current: RouteInfo | null;
|
||||
current_name: string | null;
|
||||
current_state: unknown | null;
|
||||
match: unknown | null;
|
||||
location: unknown | null;
|
||||
|
||||
|
||||
|
||||
constructor(name?: string, parent?: GenericModule) {
|
||||
super(name, parent);
|
||||
this.inject('..fine');
|
||||
|
||||
this.__routes = [];
|
||||
|
@ -20,16 +63,18 @@ export default class FineRouter extends Module {
|
|||
this.route_names = {};
|
||||
this.current = null;
|
||||
this.current_name = null;
|
||||
this.current_state = null;
|
||||
this.match = null;
|
||||
this.location = null;
|
||||
}
|
||||
|
||||
onEnable() {
|
||||
/** @internal */
|
||||
onEnable(): OptionalPromise<void> {
|
||||
const thing = this.fine.searchTree(null, n => n.props && n.props.history),
|
||||
history = this.history = thing && thing.props && thing.props.history;
|
||||
|
||||
if ( ! history )
|
||||
return new Promise(r => setTimeout(r, 50)).then(() => this.onEnable());
|
||||
return sleep(50).then(() => this.onEnable());
|
||||
|
||||
history.listen(location => {
|
||||
if ( this.enabled )
|
||||
|
@ -43,7 +88,7 @@ export default class FineRouter extends Module {
|
|||
this.history.push(this.getURL(route, data, opts), state);
|
||||
}
|
||||
|
||||
_navigateTo(location) {
|
||||
private _navigateTo(location) {
|
||||
this.log.debug('New Location', location);
|
||||
const host = window.location.host,
|
||||
path = location.pathname,
|
||||
|
@ -66,7 +111,7 @@ export default class FineRouter extends Module {
|
|||
this._pickRoute();
|
||||
}
|
||||
|
||||
_pickRoute() {
|
||||
private _pickRoute() {
|
||||
const path = this.location,
|
||||
host = this.domain;
|
||||
|
||||
|
@ -85,7 +130,6 @@ export default class FineRouter extends Module {
|
|||
this.current_name = route.name;
|
||||
this.match = match;
|
||||
this.emit(':route', route, match);
|
||||
this.emit(`:route:${route.name}`, ...match);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -117,7 +161,7 @@ export default class FineRouter extends Module {
|
|||
return r.url(data, opts);
|
||||
}
|
||||
|
||||
getRoute(name) {
|
||||
getRoute(name: string) {
|
||||
return this.routes[name];
|
||||
}
|
||||
|
||||
|
@ -132,7 +176,7 @@ export default class FineRouter extends Module {
|
|||
return this.route_names;
|
||||
}
|
||||
|
||||
getRouteName(route) {
|
||||
getRouteName(route: string) {
|
||||
if ( ! this.route_names[route] )
|
||||
this.route_names[route] = route
|
||||
.replace(/^dash-([a-z])/, (_, letter) =>
|
||||
|
@ -143,7 +187,7 @@ export default class FineRouter extends Module {
|
|||
return this.route_names[route];
|
||||
}
|
||||
|
||||
routeName(route, name, process = true) {
|
||||
routeName(route: string | Record<string, string>, name?: string, process: boolean = true) {
|
||||
if ( typeof route === 'object' ) {
|
||||
for(const key in route)
|
||||
if ( has(route, key) )
|
||||
|
@ -154,10 +198,12 @@ export default class FineRouter extends Module {
|
|||
return;
|
||||
}
|
||||
|
||||
this.route_names[route] = name;
|
||||
if ( name ) {
|
||||
this.route_names[route] = name;
|
||||
|
||||
if ( process )
|
||||
this.emit(':updated-route-names');
|
||||
if ( process )
|
||||
this.emit(':updated-route-names');
|
||||
}
|
||||
}
|
||||
|
||||
route(name, path, domain = null, state_fn = null, process = true) {
|
||||
|
@ -173,6 +219,7 @@ export default class FineRouter extends Module {
|
|||
this._pickRoute();
|
||||
this.emit(':updated-routes');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -200,4 +247,4 @@ export default class FineRouter extends Module {
|
|||
this.emit(':updated-routes');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,798 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Fine Lib
|
||||
// It controls React.
|
||||
// ============================================================================
|
||||
|
||||
import {EventEmitter} from 'utilities/events';
|
||||
import Module from 'utilities/module';
|
||||
|
||||
|
||||
export default class Fine extends Module {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this._wrappers = new Map;
|
||||
this._known_classes = new Map;
|
||||
this._observer = null;
|
||||
this._waiting = [];
|
||||
this._live_waiting = null;
|
||||
}
|
||||
|
||||
|
||||
async onEnable(tries=0) {
|
||||
// TODO: Move awaitElement to utilities/dom
|
||||
if ( ! this.root_element )
|
||||
this.root_element = await this.parent.awaitElement(this.selector || 'body #root');
|
||||
|
||||
if ( ! this.root_element || ! this.root_element._reactRootContainer ) {
|
||||
if ( tries > 500 )
|
||||
throw new Error('Unable to find React after 25 seconds');
|
||||
this.root_element = null;
|
||||
return new Promise(r => setTimeout(r, 50)).then(() => this.onEnable(tries+1));
|
||||
}
|
||||
|
||||
this.react_root = this.root_element._reactRootContainer;
|
||||
if ( this.react_root._internalRoot && this.react_root._internalRoot.current )
|
||||
this.react_root = this.react_root._internalRoot;
|
||||
|
||||
this.react = this.react_root.current.child;
|
||||
}
|
||||
|
||||
onDisable() {
|
||||
this.react_root = this.root_element = this.react = this.accessor = null;
|
||||
}
|
||||
|
||||
|
||||
static findAccessor(element) {
|
||||
for(const key in element)
|
||||
if ( key.startsWith('__reactInternalInstance$') )
|
||||
return key;
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Low Level Accessors
|
||||
// ========================================================================
|
||||
|
||||
getReactInstance(element) {
|
||||
if ( ! this.accessor )
|
||||
this.accessor = Fine.findAccessor(element);
|
||||
if ( ! this.accessor )
|
||||
return;
|
||||
|
||||
return element[this.accessor] || (element._reactRootContainer && element._reactRootContainer._internalRoot && element._reactRootContainer._internalRoot.current) || (element._reactRootContainer && element._reactRootContainer.current);
|
||||
}
|
||||
|
||||
getOwner(instance) {
|
||||
if ( instance._reactInternalFiber )
|
||||
instance = instance._reactInternalFiber;
|
||||
else if ( instance instanceof Node )
|
||||
instance = this.getReactInstance(instance);
|
||||
|
||||
if ( ! instance )
|
||||
return null;
|
||||
|
||||
return instance.return;
|
||||
}
|
||||
|
||||
getParentNode(instance, max_depth = 100, traverse_roots = false) {
|
||||
/*if ( instance._reactInternalFiber )
|
||||
instance = instance._reactInternalFiber;
|
||||
else if ( instance instanceof Node )
|
||||
instance = this.getReactInstance(instance);
|
||||
|
||||
while( instance )
|
||||
if ( instance.stateNode instanceof Node )
|
||||
return instance.stateNode
|
||||
else
|
||||
instance = instance.parent;*/
|
||||
|
||||
return this.searchParent(instance, n => n instanceof Node, max_depth, 0, traverse_roots);
|
||||
}
|
||||
|
||||
getChildNode(instance, max_depth = 100, traverse_roots = false) {
|
||||
/*if ( instance._reactInternalFiber )
|
||||
instance = instance._reactInternalFiber;
|
||||
else if ( instance instanceof Node )
|
||||
instance = this.getReactInstance(instance);
|
||||
|
||||
while( instance )
|
||||
if ( instance.stateNode instanceof Node )
|
||||
return instance.stateNode
|
||||
else {
|
||||
max_depth--;
|
||||
if ( max_depth < 0 )
|
||||
return null;
|
||||
instance = instance.child;
|
||||
}*/
|
||||
|
||||
return this.searchTree(instance, n => n instanceof Node, max_depth, 0, traverse_roots);
|
||||
}
|
||||
|
||||
getHostNode(instance, max_depth = 100) {
|
||||
return this.getChildNode(instance, max_depth);
|
||||
}
|
||||
|
||||
getParent(instance) {
|
||||
return this.getOwner(instance);
|
||||
}
|
||||
|
||||
getFirstChild(node) {
|
||||
if ( node._reactInternalFiber )
|
||||
node = node._reactInternalFiber;
|
||||
else if ( node instanceof Node )
|
||||
node = this.getReactInstance(node);
|
||||
|
||||
if ( ! node )
|
||||
return null;
|
||||
|
||||
return node.child;
|
||||
}
|
||||
|
||||
getChildren(node) {
|
||||
if ( node._reactInternalFiber )
|
||||
node = node._reactInternalFiber;
|
||||
else if ( node instanceof Node )
|
||||
node = this.getReactInstance(node);
|
||||
|
||||
if ( ! node )
|
||||
return null;
|
||||
|
||||
const children = [];
|
||||
let child = node.child;
|
||||
while(child) {
|
||||
children.push(child);
|
||||
child = child.sibling;
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
searchParent(node, criteria, max_depth=15, depth=0, traverse_roots = true) {
|
||||
if ( node._reactInternalFiber )
|
||||
node = node._reactInternalFiber;
|
||||
else if ( node instanceof Node )
|
||||
node = this.getReactInstance(node);
|
||||
|
||||
if ( ! node || node._ffz_no_scan || depth > max_depth )
|
||||
return null;
|
||||
|
||||
if ( typeof criteria === 'string' ) {
|
||||
const wrapper = this._wrappers.get(criteria);
|
||||
if ( ! wrapper )
|
||||
throw new Error('invalid critera');
|
||||
|
||||
if ( ! wrapper._class )
|
||||
return null;
|
||||
|
||||
criteria = n => n && n.constructor === wrapper._class;
|
||||
}
|
||||
|
||||
const inst = node.stateNode;
|
||||
if ( inst && criteria(inst) )
|
||||
return inst;
|
||||
|
||||
if ( node.return ) {
|
||||
const result = this.searchParent(node.return, criteria, max_depth, depth+1, traverse_roots);
|
||||
if ( result )
|
||||
return result;
|
||||
}
|
||||
|
||||
// Stupid code for traversing up into another React root.
|
||||
if ( traverse_roots && inst && inst.containerInfo ) {
|
||||
const parent = inst.containerInfo.parentElement,
|
||||
parent_node = parent && this.getReactInstance(parent);
|
||||
|
||||
if ( parent_node ) {
|
||||
const result = this.searchParent(parent_node, criteria, max_depth, depth+1, traverse_roots);
|
||||
if ( result )
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
searchNode(node, criteria, max_depth=15, depth=0, traverse_roots = true) {
|
||||
if ( ! node )
|
||||
node = this.react;
|
||||
else if ( node._reactInternalFiber )
|
||||
node = node._reactInternalFiber;
|
||||
else if ( node instanceof Node )
|
||||
node = this.getReactInstance(node);
|
||||
|
||||
if ( ! node || node._ffz_no_scan || depth > max_depth )
|
||||
return null;
|
||||
|
||||
if ( typeof criteria === 'string' ) {
|
||||
const wrapper = this._wrappers.get(criteria);
|
||||
if ( ! wrapper )
|
||||
throw new Error('invalid critera');
|
||||
|
||||
if ( ! wrapper._class )
|
||||
return null;
|
||||
|
||||
criteria = n => n && n.constructor === wrapper._class;
|
||||
}
|
||||
|
||||
if ( node && criteria(node) )
|
||||
return node;
|
||||
|
||||
if ( node.child ) {
|
||||
let child = node.child;
|
||||
while(child) {
|
||||
const result = this.searchNode(child, criteria, max_depth, depth+1, traverse_roots);
|
||||
if ( result )
|
||||
return result;
|
||||
child = child.sibling;
|
||||
}
|
||||
}
|
||||
|
||||
const inst = node.stateNode;
|
||||
if ( traverse_roots && inst && inst.props && inst.props.root ) {
|
||||
const root = inst.props.root._reactRootContainer;
|
||||
if ( root ) {
|
||||
let child = root._internalRoot && root._internalRoot.current || root.current;
|
||||
while(child) {
|
||||
const result = this.searchNode(child, criteria, max_depth, depth+1, traverse_roots);
|
||||
if ( result )
|
||||
return result;
|
||||
|
||||
child = child.sibling;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
searchTree(node, criteria, max_depth=15, depth=0, traverse_roots = true, multi = false) {
|
||||
if ( ! node )
|
||||
node = this.react;
|
||||
else if ( node._reactInternalFiber )
|
||||
node = node._reactInternalFiber;
|
||||
else if ( node instanceof Node )
|
||||
node = this.getReactInstance(node);
|
||||
|
||||
if ( multi ) {
|
||||
if ( !(multi instanceof Set) )
|
||||
multi = new Set;
|
||||
}
|
||||
|
||||
if ( multi && ! (multi instanceof Set) )
|
||||
multi = new Set;
|
||||
|
||||
if ( ! node || node._ffz_no_scan || depth > max_depth )
|
||||
return multi ? multi : null;
|
||||
|
||||
if ( typeof criteria === 'string' ) {
|
||||
const wrapper = this._wrappers.get(criteria);
|
||||
if ( ! wrapper )
|
||||
throw new Error('invalid critera');
|
||||
|
||||
if ( ! wrapper._class )
|
||||
return multi ? multi : null;
|
||||
|
||||
criteria = n => n && n.constructor === wrapper._class;
|
||||
}
|
||||
|
||||
const inst = node.stateNode;
|
||||
if ( inst && criteria(inst, node) ) {
|
||||
if ( multi )
|
||||
multi.add(inst);
|
||||
else
|
||||
return inst;
|
||||
}
|
||||
|
||||
if ( node.child ) {
|
||||
let child = node.child;
|
||||
while(child) {
|
||||
const result = this.searchTree(child, criteria, max_depth, depth+1, traverse_roots, multi);
|
||||
if ( result && ! multi )
|
||||
return result;
|
||||
child = child.sibling;
|
||||
}
|
||||
}
|
||||
|
||||
if ( traverse_roots && inst && inst.props && inst.props.root ) {
|
||||
const root = inst.props.root._reactRootContainer;
|
||||
if ( root ) {
|
||||
let child = root._internalRoot && root._internalRoot.current || root.current;
|
||||
while(child) {
|
||||
const result = this.searchTree(child, criteria, max_depth, depth+1, traverse_roots, multi);
|
||||
if ( result && ! multi )
|
||||
return result;
|
||||
|
||||
child = child.sibling;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( multi )
|
||||
return multi;
|
||||
}
|
||||
|
||||
|
||||
findAllMatching(node, criteria, max_depth=15, single_class = true, parents=false, depth=0, traverse_roots=true) {
|
||||
const matches = new Set;
|
||||
let crit = n => ! matches.has(n) && criteria(n);
|
||||
|
||||
while(true) {
|
||||
const match = parents ?
|
||||
this.searchParent(node, crit, max_depth, depth, traverse_roots) :
|
||||
this.searchTree(node, crit, max_depth, depth, traverse_roots);
|
||||
|
||||
if ( ! match )
|
||||
break;
|
||||
|
||||
if ( single_class && ! matches.size ) {
|
||||
const klass = match.constructor;
|
||||
crit = n => ! matches.has(n) && (n instanceof klass) && criteria(n);
|
||||
}
|
||||
|
||||
matches.add(match);
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
|
||||
searchAll(node, criterias, max_depth=15, depth=0, data, traverse_roots = true) {
|
||||
if ( ! node )
|
||||
node = this.react;
|
||||
else if ( node._reactInternalFiber )
|
||||
node = node._reactInternalFiber;
|
||||
else if ( node instanceof Node )
|
||||
node = this.getReactInstance(node);
|
||||
|
||||
if ( ! data )
|
||||
data = {
|
||||
seen: new Set,
|
||||
classes: criterias.map(() => null),
|
||||
out: criterias.map(() => ({
|
||||
cls: null, instances: new Set, depth: null
|
||||
})),
|
||||
max_depth: depth
|
||||
};
|
||||
|
||||
if ( ! node || node._ffz_no_scan || depth > max_depth )
|
||||
return data.out;
|
||||
|
||||
if ( depth > data.max_depth )
|
||||
data.max_depth = depth;
|
||||
|
||||
const inst = node.stateNode;
|
||||
if ( inst ) {
|
||||
const cls = inst.constructor,
|
||||
idx = data.classes.indexOf(cls);
|
||||
|
||||
if ( idx !== -1 )
|
||||
data.out[idx].instances.add(inst);
|
||||
|
||||
else if ( ! data.seen.has(cls) ) {
|
||||
let i = criterias.length;
|
||||
while(i-- > 0)
|
||||
if ( criterias[i](inst) ) {
|
||||
data.classes[i] = data.out[i].cls = cls;
|
||||
data.out[i].instances.add(inst);
|
||||
data.out[i].depth = depth;
|
||||
break;
|
||||
}
|
||||
|
||||
data.seen.add(cls);
|
||||
}
|
||||
}
|
||||
|
||||
let child = node.child;
|
||||
while(child) {
|
||||
this.searchAll(child, criterias, max_depth, depth+1, data, traverse_roots);
|
||||
child = child.sibling;
|
||||
}
|
||||
|
||||
if ( traverse_roots && inst && inst.props && inst.props.root ) {
|
||||
const root = inst.props.root._reactRootContainer;
|
||||
if ( root ) {
|
||||
let child = root._internalRoot && root._internalRoot.current || root.current;
|
||||
while(child) {
|
||||
this.searchAll(child, criterias, max_depth, depth+1, data, traverse_roots);
|
||||
child = child.sibling;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data.out;
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Class Wrapping
|
||||
// ========================================================================
|
||||
|
||||
route(route) {
|
||||
this._route = route;
|
||||
this._updateLiveWaiting();
|
||||
}
|
||||
|
||||
|
||||
_updateLiveWaiting() {
|
||||
const lw = this._live_waiting = [],
|
||||
crt = this._waiting_crit = [],
|
||||
route = this._route;
|
||||
|
||||
if ( this._waiting )
|
||||
for(const waiter of this._waiting)
|
||||
if ( ! route || ! waiter.routes.length || waiter.routes.includes(route) ) {
|
||||
lw.push(waiter);
|
||||
crt.push(waiter.criteria);
|
||||
}
|
||||
|
||||
if ( ! this._live_waiting.length )
|
||||
this._stopWaiting();
|
||||
else if ( ! this._waiting_timer )
|
||||
this._startWaiting();
|
||||
}
|
||||
|
||||
|
||||
define(key, criteria, routes) {
|
||||
if ( this._wrappers.has(key) )
|
||||
return this._wrappers.get(key);
|
||||
|
||||
if ( ! criteria )
|
||||
throw new Error('cannot find definition and no criteria provided');
|
||||
|
||||
const wrapper = new FineWrapper(key, criteria, routes, this);
|
||||
this._wrappers.set(key, wrapper);
|
||||
|
||||
const data = this.searchAll(this.react, [criteria], 1000)[0];
|
||||
if ( data.cls ) {
|
||||
wrapper._set(data.cls, data.instances);
|
||||
this._known_classes.set(data.cls, wrapper);
|
||||
|
||||
} else if ( routes !== false ) {
|
||||
this._waiting.push(wrapper);
|
||||
this._updateLiveWaiting();
|
||||
}
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
|
||||
wrap(key, cls) {
|
||||
let wrapper;
|
||||
if ( this._wrappers.has(key) )
|
||||
wrapper = this._wrappers.get(key);
|
||||
else {
|
||||
wrapper = new FineWrapper(key, null, undefined, this);
|
||||
this._wrappers.set(key, wrapper);
|
||||
}
|
||||
|
||||
if ( cls ) {
|
||||
if ( wrapper._class || wrapper.criteria )
|
||||
throw new Error('tried setting a class on an already initialized FineWrapper');
|
||||
|
||||
wrapper._set(cls, new Set);
|
||||
this._known_classes.set(cls, wrapper);
|
||||
}
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
|
||||
_checkWaiters(nodes) {
|
||||
if ( ! this._live_waiting )
|
||||
return;
|
||||
|
||||
if ( ! Array.isArray(nodes) )
|
||||
nodes = [nodes];
|
||||
|
||||
for(let node of nodes) {
|
||||
if ( ! node )
|
||||
node = this.react;
|
||||
else if ( node._reactInternalFiber )
|
||||
node = node._reactInternalFiber;
|
||||
else if ( node instanceof Node )
|
||||
node = this.getReactInstance(node);
|
||||
|
||||
if ( ! node || node._ffz_no_scan || ! this._live_waiting.length )
|
||||
continue;
|
||||
|
||||
const data = this.searchAll(node, this._waiting_crit, 1000);
|
||||
let i = data.length;
|
||||
while(i-- > 0) {
|
||||
if ( data[i].cls ) {
|
||||
const d = data[i],
|
||||
w = this._live_waiting.splice(i, 1)[0];
|
||||
|
||||
this._waiting_crit.splice(i, 1);
|
||||
|
||||
const idx = this._waiting.indexOf(w);
|
||||
if ( idx !== -1 )
|
||||
this._waiting.splice(idx, 1);
|
||||
|
||||
this.log.debug(`Found class for "${w.name}" at depth ${d.depth}`);
|
||||
w._set(d.cls, d.instances);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! this._live_waiting.length )
|
||||
this._stopWaiting();
|
||||
}
|
||||
|
||||
|
||||
_startWaiting() {
|
||||
this.log.info('Installing MutationObserver.');
|
||||
this._waiting_timer = setInterval(() => this._checkWaiters(), 500);
|
||||
|
||||
if ( ! this._observer )
|
||||
this._observer = new MutationObserver(mutations =>
|
||||
this._checkWaiters(mutations.map(x => x.target))
|
||||
);
|
||||
|
||||
this._observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
_stopWaiting() {
|
||||
this.log.info('Stopping MutationObserver.');
|
||||
|
||||
if ( this._observer )
|
||||
this._observer.disconnect();
|
||||
|
||||
if ( this._waiting_timer )
|
||||
clearInterval(this._waiting_timer);
|
||||
|
||||
this._live_waiting = null;
|
||||
this._waiting_crit = null;
|
||||
this._waiting_timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
const EVENTS = {
|
||||
'will-mount': 'UNSAFE_componentWillMount',
|
||||
mount: 'componentDidMount',
|
||||
render: 'render',
|
||||
'receive-props': 'UNSAFE_componentWillReceiveProps',
|
||||
'should-update': 'shouldComponentUpdate',
|
||||
'will-update': 'UNSAFE_componentWillUpdate',
|
||||
update: 'componentDidUpdate',
|
||||
unmount: 'componentWillUnmount'
|
||||
}
|
||||
|
||||
|
||||
export class FineWrapper extends EventEmitter {
|
||||
constructor(name, criteria, routes, fine) {
|
||||
super();
|
||||
|
||||
this.name = name;
|
||||
this.criteria = criteria;
|
||||
this.fine = fine;
|
||||
|
||||
this.instances = new Set;
|
||||
this.routes = routes || [];
|
||||
|
||||
this._wrapped = new Set;
|
||||
this._class = null;
|
||||
}
|
||||
|
||||
get first() {
|
||||
return this.toArray()[0];
|
||||
}
|
||||
|
||||
toArray() {
|
||||
return Array.from(this.instances);
|
||||
}
|
||||
|
||||
check(node = null, max_depth = 1000) {
|
||||
if ( this._class )
|
||||
return;
|
||||
|
||||
const instances = this.fine.findAllMatching(node, this.criteria, max_depth);
|
||||
if ( instances.size ) {
|
||||
const insts = Array.from(instances);
|
||||
this._set(insts[0].constructor, insts);
|
||||
}
|
||||
}
|
||||
|
||||
ready(fn) {
|
||||
if ( this._class )
|
||||
fn(this._class, this.instances);
|
||||
else
|
||||
this.once('set', fn);
|
||||
}
|
||||
|
||||
each(fn) {
|
||||
for(const inst of this.instances)
|
||||
fn(inst);
|
||||
}
|
||||
|
||||
updateInstances(node = null, max_depth = 1000) {
|
||||
if ( ! this._class )
|
||||
return;
|
||||
|
||||
const instances = this.fine.findAllMatching(node, n => n.constructor === this._class, max_depth);
|
||||
|
||||
for(const inst of instances) {
|
||||
inst._ffz_mounted = true;
|
||||
this.instances.add(inst);
|
||||
}
|
||||
}
|
||||
|
||||
_set(cls, instances) {
|
||||
if ( this._class )
|
||||
throw new Error('already have a class');
|
||||
|
||||
this._class = cls;
|
||||
this._wrapped.add('UNSAFE_componentWillMount');
|
||||
this._wrapped.add('componentWillUnmount');
|
||||
|
||||
cls._ffz_wrapper = this;
|
||||
|
||||
const t = this,
|
||||
_instances = this.instances,
|
||||
proto = cls.prototype,
|
||||
o_mount = proto.UNSAFE_componentWillMount,
|
||||
o_unmount = proto.componentWillUnmount,
|
||||
|
||||
mount = proto.UNSAFE_componentWillMount = o_mount ?
|
||||
function(...args) {
|
||||
this._ffz_mounted = true;
|
||||
_instances.add(this);
|
||||
t.emit('will-mount', this, ...args);
|
||||
return o_mount.apply(this, args);
|
||||
} :
|
||||
function(...args) {
|
||||
this._ffz_mounted = true;
|
||||
_instances.add(this);
|
||||
t.emit('will-mount', this, ...args);
|
||||
},
|
||||
|
||||
unmount = proto.componentWillUnmount = o_unmount ?
|
||||
function(...args) {
|
||||
t.emit('unmount', this, ...args);
|
||||
_instances.delete(this);
|
||||
this._ffz_mounted = false;
|
||||
return o_unmount.apply(this, args);
|
||||
} :
|
||||
function(...args) {
|
||||
t.emit('unmount', this, ...args);
|
||||
_instances.delete(this);
|
||||
this._ffz_mounted = false;
|
||||
};
|
||||
|
||||
this.__UNSAFE_componentWillMount = [mount, o_mount];
|
||||
this.__componentWillUnmount = [unmount, o_unmount];
|
||||
|
||||
for(const event of this.events())
|
||||
this._maybeWrap(event);
|
||||
|
||||
if ( instances )
|
||||
for(const inst of instances) {
|
||||
// How do we check mounted state for fibers?
|
||||
// Just assume they're mounted for now I guess.
|
||||
inst._ffz_mounted = true;
|
||||
_instances.add(inst);
|
||||
}
|
||||
|
||||
this.emit('set', cls, _instances);
|
||||
}
|
||||
|
||||
_add(instances) {
|
||||
for(const inst of instances)
|
||||
this.instances.add(inst);
|
||||
}
|
||||
|
||||
|
||||
_maybeWrap(event) {
|
||||
const key = EVENTS[event];
|
||||
if ( ! this._class || ! key || this._wrapped.has(key) )
|
||||
return;
|
||||
|
||||
this._wrap(event, key);
|
||||
}
|
||||
|
||||
_wrap(event, key) {
|
||||
if ( this._wrapped.has(key) )
|
||||
return;
|
||||
|
||||
const t = this,
|
||||
proto = this._class.prototype,
|
||||
original = proto[key],
|
||||
|
||||
fn = proto[key] = original ?
|
||||
function(...args) {
|
||||
if ( ! this._ffz_mounted ) {
|
||||
this._ffz_mounted = true;
|
||||
t.instances.add(this);
|
||||
t.emit('late-mount', this);
|
||||
}
|
||||
|
||||
t.emit(event, this, ...args);
|
||||
return original.apply(this, args);
|
||||
} :
|
||||
|
||||
key === 'shouldComponentUpdate' ?
|
||||
function(...args) {
|
||||
if ( ! this._ffz_mounted ) {
|
||||
this._ffz_mounted = true;
|
||||
t.instances.add(this);
|
||||
t.emit('late-mount', this);
|
||||
}
|
||||
|
||||
t.emit(event, this, ...args);
|
||||
return true;
|
||||
}
|
||||
:
|
||||
function(...args) {
|
||||
if ( ! this._ffz_mounted ) {
|
||||
this._ffz_mounted = true;
|
||||
t.instances.add(this);
|
||||
t.emit('late-mount', this);
|
||||
}
|
||||
|
||||
t.emit(event, this, ...args);
|
||||
};
|
||||
|
||||
this[`__${key}`] = [fn, original];
|
||||
}
|
||||
|
||||
_unwrap(key) {
|
||||
if ( ! this._wrapped.has(key) )
|
||||
return;
|
||||
|
||||
const k = `__${key}`,
|
||||
proto = this._class.prototype,
|
||||
[fn, original] = this[k];
|
||||
|
||||
if ( proto[key] !== fn )
|
||||
throw new Error('unable to unwrap -- prototype modified');
|
||||
|
||||
proto[key] = original;
|
||||
this[k] = undefined;
|
||||
this._wrapped.delete(key);
|
||||
}
|
||||
|
||||
|
||||
forceUpdate() {
|
||||
for(const inst of this.instances)
|
||||
try {
|
||||
inst.forceUpdate();
|
||||
this.fine.emit('site:dom-update', this.name, inst);
|
||||
|
||||
} catch(err) {
|
||||
this.fine.log.capture(err, {
|
||||
tags: {
|
||||
fine_wrapper: this.name
|
||||
}
|
||||
});
|
||||
|
||||
this.fine.log.error(`An error occurred when calling forceUpdate on an instance of ${this.name}`, err);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
on(event, fn, ctx) {
|
||||
this._maybeWrap(event);
|
||||
return super.on(event, fn, ctx);
|
||||
}
|
||||
|
||||
once(event, fn, ctx) {
|
||||
this._maybeWrap(event);
|
||||
return super.once(event, fn, ctx);
|
||||
}
|
||||
|
||||
many(event, ttl, fn, ctx) {
|
||||
this._maybeWrap(event);
|
||||
return super.many(event, ttl, fn, ctx);
|
||||
}
|
||||
|
||||
waitFor(event) {
|
||||
this._maybeWrap(event);
|
||||
return super.waitFor(event);
|
||||
}
|
||||
}
|
1032
src/utilities/compat/fine.ts
Normal file
1032
src/utilities/compat/fine.ts
Normal file
File diff suppressed because it is too large
Load diff
75
src/utilities/compat/react-types.ts
Normal file
75
src/utilities/compat/react-types.ts
Normal file
|
@ -0,0 +1,75 @@
|
|||
|
||||
// You might be wondering why we're homebrewing React types when we could just
|
||||
// reply on @types/react.
|
||||
//
|
||||
// It's simple. TypeScript is obtuse and refuses to NOT use @types/react if
|
||||
// the package is installed. That breaks our own JSX use, and so we can't use
|
||||
// those types.
|
||||
|
||||
declare global {
|
||||
interface Node {
|
||||
[key: ReactAccessor]: ReactNode | undefined;
|
||||
_reactRootContainer?: ReactRoot;
|
||||
_ffz_no_scan?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
export type ReactAccessor = `__reactInternalInstance$${string}`;
|
||||
|
||||
export type ReactRoot = {
|
||||
_internalRoot?: ReactRoot;
|
||||
current: ReactNode | null;
|
||||
};
|
||||
|
||||
export type ReactNode = {
|
||||
alternate: ReactNode | null;
|
||||
child: ReactNode | null;
|
||||
return: ReactNode | null;
|
||||
sibling: ReactNode | null;
|
||||
stateNode: ReactStateNode | Node | null;
|
||||
};
|
||||
|
||||
|
||||
export type ReactStateNode<
|
||||
TProps extends {} = {},
|
||||
TState extends {} = {},
|
||||
TSnapshot extends {} = {}
|
||||
> = {
|
||||
|
||||
// FFZ Helpers
|
||||
_ffz_no_scan?: boolean;
|
||||
_ffz_mounted?: boolean;
|
||||
|
||||
// Access to the internal node.
|
||||
_reactInternalFiber: ReactNode | null;
|
||||
|
||||
// Stuff
|
||||
props: TProps;
|
||||
state: TState | null;
|
||||
|
||||
// Lifecycle Methods
|
||||
componentDidMount?(): void;
|
||||
componentDidUpdate?(prevProps: TProps, prevState: TState, snapshot: TSnapshot | null): void;
|
||||
componentWillUnmount?(): void;
|
||||
shouldComponentUpdate?(nextProps: TProps, nextState: TState): boolean;
|
||||
getSnapshotBeforeUpdate?(prevProps: TProps, prevState: TState): TSnapshot | null;
|
||||
componentDidCatch?(error: any, info: any): void;
|
||||
|
||||
/** @deprecated Will be removed in React 17 */
|
||||
UNSAFE_componentWillMount?(): void;
|
||||
/** @deprecated Will be removed in React 17 */
|
||||
UNSAFE_componentWillReceiveProps?(nextProps: TProps): void;
|
||||
/** @deprecated Will be removed in React 17 */
|
||||
UNSAFE_componentWillUpdate?(nextProps: TProps, nextState: TState): void;
|
||||
|
||||
setState(
|
||||
updater: Partial<TState> | ((state: TState, props: TProps) => Partial<TState>),
|
||||
callback?: () => void
|
||||
): void;
|
||||
|
||||
// TODO: Implement proper return type.
|
||||
render(): any;
|
||||
|
||||
forceUpdate(callback?: () => void): void;
|
||||
|
||||
};
|
|
@ -5,7 +5,7 @@
|
|||
// It controls Twitch PubSub.
|
||||
// ============================================================================
|
||||
|
||||
import Module, { GenericModule, ModuleEvents } from 'utilities/module';
|
||||
import Module, { GenericModule } from 'utilities/module';
|
||||
import { FFZEvent } from 'utilities/events';
|
||||
|
||||
declare global {
|
||||
|
@ -15,6 +15,16 @@ declare global {
|
|||
}
|
||||
}
|
||||
|
||||
declare module 'utilities/types' {
|
||||
interface ModuleEventMap {
|
||||
'site.subpump': SubpumpEvents;
|
||||
}
|
||||
interface ModuleMap {
|
||||
'site.subpump': Subpump;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* This is a rough map of the parts of Twitch's PubSub client that we
|
||||
|
@ -236,51 +246,7 @@ export default class Subpump extends Module<'site.subpump', SubpumpEvents> {
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
hookOldClient(client) {
|
||||
const t = this,
|
||||
orig_message = client._onMessage;
|
||||
|
||||
this.is_old = true;
|
||||
|
||||
client._unbindPrimary(client._primarySocket);
|
||||
|
||||
client._onMessage = function(e) {
|
||||
if ( t.handleMessage(e) )
|
||||
return;
|
||||
|
||||
return orig_message.call(this, e);
|
||||
};
|
||||
|
||||
client._bindPrimary(client._primarySocket);
|
||||
|
||||
const listener = client._listens,
|
||||
orig_on = listener.on,
|
||||
orig_off = listener.off;
|
||||
|
||||
listener.on = function(topic, fn, ctx) {
|
||||
const has_topic = !! listener._events?.[topic],
|
||||
out = orig_on.call(this, topic, fn, ctx);
|
||||
|
||||
if ( ! has_topic )
|
||||
t.emit(':add-topic', topic)
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
listener.off = function(topic, fn) {
|
||||
const has_topic = !! listener._events?.[topic],
|
||||
out = orig_off.call(this, topic, fn);
|
||||
|
||||
if ( has_topic && ! listener._events?.[topic] )
|
||||
t.emit(':remove-topic', topic);
|
||||
|
||||
return out;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
inject(topic: string, message: any) {
|
||||
simulateMessage(topic: string, message: any) {
|
||||
if ( ! this.instance )
|
||||
throw new Error('No PubSub instance available');
|
||||
|
||||
|
|
|
@ -137,20 +137,24 @@ export class EventEmitter<
|
|||
this.__dead_events = 0;
|
||||
}
|
||||
|
||||
private __cleanListeners() {
|
||||
protected __cleanListeners() {
|
||||
if ( ! this.__dead_events )
|
||||
return;
|
||||
return [];
|
||||
|
||||
const new_listeners: Record<string, ListenerInfo[]> = {},
|
||||
old_listeners = this.__listeners;
|
||||
old_listeners = this.__listeners,
|
||||
removed: string[] = [];
|
||||
|
||||
for(const [key, val] of Object.entries(old_listeners)) {
|
||||
if ( val?.length )
|
||||
new_listeners[key] = val;
|
||||
else
|
||||
removed.push(key);
|
||||
}
|
||||
|
||||
this.__listeners = new_listeners;
|
||||
this.__dead_events = 0;
|
||||
return removed;
|
||||
}
|
||||
|
||||
private __sortListeners(event: string) {
|
||||
|
@ -266,6 +270,8 @@ export class EventEmitter<
|
|||
this.__sortListeners(event);
|
||||
} else
|
||||
this.__listeners[event] = [info];
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -334,6 +340,8 @@ export class EventEmitter<
|
|||
this.__sortListeners(event);
|
||||
} else
|
||||
this.__listeners[event] = [info];
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -409,7 +417,7 @@ export class EventEmitter<
|
|||
this.off(evt as any, fn, ctx);
|
||||
}
|
||||
|
||||
return;
|
||||
return this;
|
||||
}
|
||||
|
||||
if ( this.__running.has(event) )
|
||||
|
@ -417,7 +425,7 @@ export class EventEmitter<
|
|||
|
||||
let list = this.__listeners[event];
|
||||
if ( ! list )
|
||||
return;
|
||||
return this;
|
||||
|
||||
// If fn and ctx were both not provided, then clear the list.
|
||||
if ( ! fn && ! ctx )
|
||||
|
@ -439,6 +447,8 @@ export class EventEmitter<
|
|||
// dead event so we can clean it up later.
|
||||
if ( ! list )
|
||||
this.__dead_events++;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,20 +1,23 @@
|
|||
'use strict';
|
||||
|
||||
import type { DefinitionNode, DocumentNode, FieldNode, FragmentDefinitionNode, OperationDefinitionNode, SelectionNode, SelectionSetNode } from 'graphql';
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// GraphQL Document Manipulation
|
||||
// ============================================================================
|
||||
|
||||
export const MERGE_METHODS = {
|
||||
Document: (a, b) => {
|
||||
export const MERGE_METHODS: Record<string, (a: any, b: any) => any> = {
|
||||
Document: (a: DocumentNode, b: DocumentNode) => {
|
||||
if ( a.definitions && b.definitions )
|
||||
a.definitions = mergeList(a.definitions, b.definitions);
|
||||
(a as any).definitions = mergeList(a.definitions as DefinitionNode[], b.definitions as any);
|
||||
else if ( b.definitions )
|
||||
a.definitions = b.definitions;
|
||||
(a as any).definitions = b.definitions;
|
||||
|
||||
return a;
|
||||
},
|
||||
|
||||
Field: (a, b) => {
|
||||
Field: (a: FieldNode, b: FieldNode) => {
|
||||
if ( a.name && (! b.name || b.name.value !== a.name.value) )
|
||||
return a;
|
||||
|
||||
|
@ -22,14 +25,14 @@ export const MERGE_METHODS = {
|
|||
// TODO: directives
|
||||
|
||||
if ( a.selectionSet && b.selectionSet )
|
||||
a.selectionSet = merge(a.selectionSet, b.selectionSet);
|
||||
(a as any).selectionSet = merge(a.selectionSet, b.selectionSet);
|
||||
else if ( b.selectionSet )
|
||||
a.selectionSet = b.selectionSet;
|
||||
(a as any).selectionSet = b.selectionSet;
|
||||
|
||||
return a;
|
||||
},
|
||||
|
||||
OperationDefinition: (a, b) => {
|
||||
OperationDefinition: (a: OperationDefinitionNode, b: OperationDefinitionNode) => {
|
||||
if ( a.operation !== b.operation )
|
||||
return a;
|
||||
|
||||
|
@ -37,14 +40,14 @@ export const MERGE_METHODS = {
|
|||
// TODO: directives
|
||||
|
||||
if ( a.selectionSet && b.selectionSet )
|
||||
a.selectionSet = merge(a.selectionSet, b.selectionSet);
|
||||
(a as any).selectionSet = merge(a.selectionSet, b.selectionSet);
|
||||
else if ( b.selectionSet )
|
||||
a.selectionSet = b.selectionSet;
|
||||
(a as any).selectionSet = b.selectionSet;
|
||||
|
||||
return a;
|
||||
},
|
||||
|
||||
FragmentDefinition: (a, b) => {
|
||||
FragmentDefinition: (a: FragmentDefinitionNode, b: FragmentDefinitionNode) => {
|
||||
if ( a.typeCondition && b.typeCondition ) {
|
||||
if ( a.typeCondition.kind !== b.typeCondition.kind )
|
||||
return a;
|
||||
|
@ -56,16 +59,16 @@ export const MERGE_METHODS = {
|
|||
// TODO: directives
|
||||
|
||||
if ( a.selectionSet && b.selectionSet )
|
||||
a.selectionSet = merge(a.selectionSet, b.selectionSet);
|
||||
(a as any).selectionSet = merge(a.selectionSet, b.selectionSet);
|
||||
else if ( b.selectionSet )
|
||||
a.selectionSet = b.selectionSet;
|
||||
(a as any).selectionSet = b.selectionSet;
|
||||
|
||||
return a;
|
||||
},
|
||||
|
||||
SelectionSet: (a, b) => {
|
||||
SelectionSet: (a: SelectionSetNode, b: SelectionSetNode) => {
|
||||
if ( a.selections && b.selections )
|
||||
a.selections = mergeList(a.selections, b.selections);
|
||||
a.selections = mergeList(a.selections as SelectionNode[], b.selections as any);
|
||||
else if ( b.selections )
|
||||
a.selections = b.selections;
|
||||
|
||||
|
@ -73,10 +76,10 @@ export const MERGE_METHODS = {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
export function mergeList(a, b) {
|
||||
// TODO: Type safe this
|
||||
export function mergeList(a: any[], b: any[]) {
|
||||
let has_operation = false;
|
||||
const a_names = {};
|
||||
const a_names: Record<string, any> = {};
|
||||
for(const item of a) {
|
||||
if ( ! item || ! item.name || item.name.kind !== 'Name' )
|
||||
continue;
|
||||
|
@ -114,7 +117,7 @@ export function mergeList(a, b) {
|
|||
}
|
||||
|
||||
|
||||
export default function merge(a, b) {
|
||||
export default function merge(a: any, b: any) {
|
||||
if ( a.kind !== b.kind )
|
||||
return a;
|
||||
|
||||
|
@ -122,4 +125,4 @@ export default function merge(a, b) {
|
|||
return MERGE_METHODS[a.kind](a, b);
|
||||
|
||||
return a;
|
||||
}
|
||||
}
|
|
@ -5,10 +5,10 @@
|
|||
// Modules are cool.
|
||||
// ============================================================================
|
||||
|
||||
import EventEmitter, { EventKey, EventListener, EventMap, NamespacedEventArgs, NamespacedEventKey, NamespacedEvents } from 'utilities/events';
|
||||
import EventEmitter, { EventListener, EventMap, NamespacedEventArgs, NamespacedEventKey, NamespacedEvents } from 'utilities/events';
|
||||
import {has} from 'utilities/object';
|
||||
import type Logger from './logging';
|
||||
import type { AddonInfo, KnownEvents, ModuleKeys, ModuleMap, ModuleMap, ModuleMap, ModuleMap, OptionalPromise } from './types';
|
||||
import type { AddonInfo, KnownEvents, ModuleKeys, ModuleMap, OptionalPromise } from './types';
|
||||
import type { Addon } from './addon';
|
||||
|
||||
|
||||
|
@ -233,7 +233,7 @@ export class Module<
|
|||
// to need to put conditional checks literally everywhere we use the
|
||||
// logger system. Just no.
|
||||
if ( ! this.__log )
|
||||
this.__log = this.parent && (this.parent as Module).log?.get?.(this.name);
|
||||
this.__log = this.parent && (this.parent as GenericModule).log?.get?.(this.name);
|
||||
return this.__log as Logger;
|
||||
}
|
||||
|
||||
|
@ -805,8 +805,10 @@ export class Module<
|
|||
}
|
||||
|
||||
resolve<
|
||||
TPath extends ModuleKeys,
|
||||
TReturn extends GenericModule = ModuleMap[TPath]
|
||||
TPath extends string,
|
||||
TReturn = TPath extends keyof ModuleMap
|
||||
? ModuleMap[TPath]
|
||||
: GenericModule
|
||||
>(name: TPath): TReturn | null {
|
||||
let module = this.__resolve(name);
|
||||
if ( !(module instanceof Module) )
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
|
||||
import {BAD_HOTKEYS, TWITCH_EMOTE_V2, WORD_SEPARATORS} from 'utilities/constants';
|
||||
import type { ExtractSegments, ExtractType } from 'utilities/types';
|
||||
|
||||
const HOP = Object.prototype.hasOwnProperty;
|
||||
|
||||
|
@ -78,8 +79,8 @@ export class TranslatableError extends Error {
|
|||
}
|
||||
|
||||
toString() {
|
||||
const ffz = window.FrankerFaceZ?.get?.(),
|
||||
i18n = ffz?.resolve?.('i18n');
|
||||
const ffz = window.FrankerFaceZ?.get(),
|
||||
i18n = ffz?.resolve('i18n');
|
||||
|
||||
if ( i18n && this.i18n_key )
|
||||
return i18n.t(this.i18n_key, this.message, this.data);
|
||||
|
@ -460,7 +461,7 @@ export function once<TFunc extends AnyFunction, TReturn = Awaited<ReturnType<TFu
|
|||
* @param b The second array
|
||||
* @returns Whether or not they match
|
||||
*/
|
||||
export function array_equals(a: any[], b: any[]) {
|
||||
export function array_equals(a?: any[] | null, b?: any[] | null) {
|
||||
if ( ! Array.isArray(a) || ! Array.isArray(b) || a.length !== b.length )
|
||||
return false;
|
||||
|
||||
|
@ -655,40 +656,6 @@ export function substr_count(str: string, needle: string) {
|
|||
|
||||
// These types are all used by get()
|
||||
|
||||
export type ExtractSegments<Input extends string> =
|
||||
Input extends `${infer Match}.${infer Rest}`
|
||||
? [ Match, ...ExtractSegments<Rest> ]
|
||||
: [ Input ];
|
||||
|
||||
export type ArrayShift<T extends any[]> = T extends [any, ...infer Rest]
|
||||
// This is needed to avoid it returning an empty array. There's probably
|
||||
// a more elegant solution, but I don't know it.
|
||||
? Rest extends [any, ...any[]]
|
||||
? Rest
|
||||
: undefined
|
||||
: undefined;
|
||||
|
||||
export type ExtractType<T, Path extends string[], Key = Path[0], Rest = ArrayShift<Path>> =
|
||||
Key extends "@each"
|
||||
? ExtractEach<T, Rest>
|
||||
:
|
||||
Key extends "@last"
|
||||
? T extends any[]
|
||||
? ExtractEach<T, Rest>
|
||||
: never
|
||||
:
|
||||
Key extends keyof T
|
||||
? Rest extends string[]
|
||||
? ExtractType<T[Key], Rest>
|
||||
: T[Key]
|
||||
:
|
||||
null;
|
||||
|
||||
export type ExtractEach<T, Rest> =
|
||||
Rest extends string[]
|
||||
? { [K in keyof T]: ExtractType<T[K], Rest> }
|
||||
: T;
|
||||
|
||||
/**
|
||||
* Get a value from an object at a path.
|
||||
* @param path The path to follow, using periods to go down a level.
|
||||
|
|
|
@ -15,8 +15,8 @@ type ParseContext = {
|
|||
export type PathNode = {
|
||||
title: string;
|
||||
key: string;
|
||||
page: boolean;
|
||||
tab: boolean;
|
||||
page?: boolean;
|
||||
tab?: boolean;
|
||||
}
|
||||
|
||||
function parseAST(ctx: ParseContext) {
|
||||
|
@ -142,4 +142,4 @@ function parseJSON(ctx: ParseContext) {
|
|||
}
|
||||
|
||||
return JSON.parse(path.slice(start, ctx.i));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -335,8 +335,10 @@ export class TranslationCore {
|
|||
}
|
||||
|
||||
toLocaleString(thing: any) {
|
||||
if ( thing && thing.toLocaleString )
|
||||
return thing.toLocaleString(this._locale);
|
||||
if ( thing?.toLocaleString )
|
||||
return thing.toLocaleString(this._locale) as string;
|
||||
else if ( typeof thing !== 'string' )
|
||||
return `${thing}`;
|
||||
return thing;
|
||||
}
|
||||
|
||||
|
@ -638,171 +640,6 @@ function listToString(list: any[]): string {
|
|||
// Plural Handling
|
||||
// ============================================================================
|
||||
|
||||
/*
|
||||
const CARDINAL_TO_LANG = {
|
||||
arabic: ['ar'],
|
||||
czech: ['cs'],
|
||||
danish: ['da'],
|
||||
german: ['de', 'el', 'en', 'es', 'fi', 'hu', 'it', 'nl', 'no', 'nb', 'tr', 'sv'],
|
||||
hebrew: ['he'],
|
||||
persian: ['fa'],
|
||||
polish: ['pl'],
|
||||
serbian: ['sr'],
|
||||
french: ['fr', 'pt'],
|
||||
russian: ['ru','uk'],
|
||||
slov: ['sl']
|
||||
}
|
||||
|
||||
const CARDINAL_TYPES = {
|
||||
other: () => 5,
|
||||
|
||||
arabic(n) {
|
||||
if ( n === 0 ) return 0;
|
||||
if ( n === 1 ) return 1;
|
||||
if ( n === 2 ) return 2;
|
||||
const n1 = n % 1000;
|
||||
if ( n1 >= 3 && n1 <= 10 ) return 3;
|
||||
return n1 >= 11 ? 4 : 5;
|
||||
},
|
||||
|
||||
czech: (n,i,v) => {
|
||||
if ( v !== 0 ) return 4;
|
||||
if ( i === 1 ) return 1;
|
||||
if ( i >= 2 && i <= 4 ) return 3;
|
||||
return 5;
|
||||
},
|
||||
|
||||
danish: (n,i,v,t) => (n === 1 || (t !== 0 && (i === 0 || i === 1))) ? 1 : 5,
|
||||
french: (n, i) => (i === 0 || i === 1) ? 1 : 5,
|
||||
german: n => n === 1 ? 1 : 5,
|
||||
|
||||
hebrew(n) {
|
||||
if ( n === 1 ) return 1;
|
||||
if ( n === 2 ) return 2;
|
||||
return (n > 10 && n % 10 === 0) ? 4 : 5;
|
||||
},
|
||||
|
||||
persian: (n, i) => (i === 0 || n === 1) ? 1 : 5,
|
||||
|
||||
slov(n, i, v) {
|
||||
if ( v !== 0 ) return 3;
|
||||
const n1 = n % 100;
|
||||
if ( n1 === 1 ) return 1;
|
||||
if ( n1 === 2 ) return 2;
|
||||
if ( n1 === 3 || n1 === 4 ) return 3;
|
||||
return 5;
|
||||
},
|
||||
|
||||
serbian(n, i, v, t) {
|
||||
if ( v !== 0 ) return 5;
|
||||
const i1 = i % 10, i2 = i % 100;
|
||||
const t1 = t % 10, t2 = t % 100;
|
||||
if ( i1 === 1 && i2 !== 11 ) return 1;
|
||||
if ( t1 === 1 && t2 !== 11 ) return 1;
|
||||
if ( i1 >= 2 && i1 <= 4 && !(i2 >= 12 && i2 <= 14) ) return 3;
|
||||
if ( t1 >= 2 && t1 <= 4 && !(t2 >= 12 && t2 <= 14) ) return 3;
|
||||
return 5;
|
||||
},
|
||||
|
||||
polish(n, i, v) {
|
||||
if ( v !== 0 ) return 5;
|
||||
if ( n === 1 ) return 1;
|
||||
const n1 = n % 10, n2 = n % 100;
|
||||
if ( n1 >= 2 && n1 <= 4 && !(n2 >= 12 && n2 <= 14) ) return 3;
|
||||
if ( i !== 1 && (n1 === 0 || n1 === 1) ) return 4;
|
||||
if ( n1 >= 5 && n1 <= 9 ) return 4;
|
||||
if ( n2 >= 12 && n2 <= 14 ) return 4;
|
||||
return 5;
|
||||
},
|
||||
|
||||
russian(n,i,v) {
|
||||
const n1 = n % 10, n2 = n % 100;
|
||||
if ( n1 === 1 && n2 !== 11 ) return 1;
|
||||
if ( v === 0 && (n1 >= 2 && n1 <= 4) && (n2 < 12 || n2 > 14) ) return 3;
|
||||
return ( v === 0 && (n1 === 0 || (n1 >= 5 && n1 <= 9) || (n2 >= 11 || n2 <= 14)) ) ? 4 : 5
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const ORDINAL_TO_LANG = {
|
||||
english: ['en'],
|
||||
hungarian: ['hu'],
|
||||
italian: ['it'],
|
||||
one: ['fr', 'lo', 'ms'],
|
||||
swedish: ['sv'],
|
||||
ukranian: ['uk']
|
||||
};
|
||||
|
||||
const ORDINAL_TYPES = {
|
||||
other: () => 5,
|
||||
one: n => n === 1 ? 1 : 5,
|
||||
|
||||
english(n) {
|
||||
const n1 = n % 10, n2 = n % 100;
|
||||
if ( n1 === 1 && n2 !== 11 ) return 1;
|
||||
if ( n1 === 2 && n2 !== 12 ) return 2;
|
||||
if ( n1 === 3 && n2 !== 13 ) return 3;
|
||||
return 5;
|
||||
},
|
||||
|
||||
ukranian(n) {
|
||||
const n1 = n % 10, n2 = n % 100;
|
||||
if ( n1 === 3 && n2 !== 13 ) return 3;
|
||||
return 5;
|
||||
},
|
||||
|
||||
hungarian: n => (n === 1 || n === 5) ? 1 : 5,
|
||||
italian: n => (n === 11 || n === 8 || n === 80 || n === 800) ? 4 : 5,
|
||||
|
||||
swedish(n) {
|
||||
const n1 = n % 10, n2 = n % 100;
|
||||
return ((n1 === 1 || n1 === 2) && (n2 !== 11 && n2 !== 12)) ? 1 : 5;
|
||||
}
|
||||
}
|
||||
|
||||
const PLURAL_TO_NAME = [
|
||||
'zero', // 0
|
||||
'one', // 1
|
||||
'two', // 2
|
||||
'few', // 3
|
||||
'many', // 4
|
||||
'other' // 5
|
||||
];
|
||||
|
||||
const CARDINAL_LANG_TO_TYPE = {},
|
||||
ORDINAL_LANG_TO_TYPE = {};
|
||||
|
||||
for(const type of Object.keys(CARDINAL_TO_LANG))
|
||||
for(const lang of CARDINAL_TO_LANG[type])
|
||||
CARDINAL_LANG_TO_TYPE[lang] = type;
|
||||
|
||||
for(const type of Object.keys(ORDINAL_TO_LANG))
|
||||
for(const lang of ORDINAL_TO_LANG[type])
|
||||
ORDINAL_LANG_TO_TYPE[lang] = type;
|
||||
|
||||
function executePlural(fn, input) {
|
||||
input = Math.abs(Number(input));
|
||||
const i = Math.floor(input);
|
||||
let v, t;
|
||||
|
||||
if ( i === input ) {
|
||||
v = 0;
|
||||
t = 0;
|
||||
} else {
|
||||
t = `${input}`.split('.')[1]
|
||||
v = t ? t.length : 0;
|
||||
t = t ? Number(t) : 0;
|
||||
}
|
||||
|
||||
return PLURAL_TO_NAME[fn(
|
||||
input,
|
||||
i,
|
||||
v,
|
||||
t
|
||||
)]
|
||||
}
|
||||
*/
|
||||
|
||||
let cardinal_i18n: Intl.PluralRules | null = null,
|
||||
cardinal_locale: string | null = null;
|
||||
|
||||
|
@ -830,27 +667,3 @@ export function getOrdinalName(locale: string, input: number) {
|
|||
|
||||
return ordinal_i18n.select(input);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
export function getCardinalName(locale: string, input: number) {
|
||||
let type = CARDINAL_LANG_TO_TYPE[locale];
|
||||
if ( ! type ) {
|
||||
const idx = locale.indexOf('-');
|
||||
type = (idx !== -1 && CARDINAL_LANG_TO_TYPE[locale.slice(0, idx)]) || 'other';
|
||||
CARDINAL_LANG_TO_TYPE[locale] = type;
|
||||
}
|
||||
|
||||
return executePlural(CARDINAL_TYPES[type], input);
|
||||
}
|
||||
|
||||
export function getOrdinalName(locale, input) {
|
||||
let type = ORDINAL_LANG_TO_TYPE[locale];
|
||||
if ( ! type ) {
|
||||
const idx = locale.indexOf('-');
|
||||
type = (idx !== -1 && ORDINAL_LANG_TO_TYPE[locale.slice(0, idx)]) || 'other';
|
||||
ORDINAL_LANG_TO_TYPE[locale] = type;
|
||||
}
|
||||
|
||||
return executePlural(ORDINAL_TYPES[type], input);
|
||||
}*/
|
||||
|
|
|
@ -5,10 +5,19 @@
|
|||
// Get data, from Twitch.
|
||||
// ============================================================================
|
||||
|
||||
import Module from 'utilities/module';
|
||||
import Module, { GenericModule } from 'utilities/module';
|
||||
import {get, debounce, TranslatableError} from 'utilities/object';
|
||||
import type Apollo from './compat/apollo';
|
||||
import type { DocumentNode } from 'graphql';
|
||||
|
||||
const LANGUAGE_MATCHER = /^auto___lang_(\w+)$/;
|
||||
declare module 'utilities/types' {
|
||||
interface ModuleEventMap {
|
||||
|
||||
}
|
||||
interface ModuleMap {
|
||||
'site.twitch_data': TwitchData;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PaginatedResult
|
||||
|
@ -25,10 +34,23 @@ const LANGUAGE_MATCHER = /^auto___lang_(\w+)$/;
|
|||
* @extends Module
|
||||
*/
|
||||
export default class TwitchData extends Module {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.site = this.parent;
|
||||
apollo: Apollo = null as any;
|
||||
site: GenericModule = null as any;
|
||||
|
||||
|
||||
private _waiting_user_ids: Map<string, unknown>;
|
||||
private _waiting_user_logins: Map<string, unknown>;
|
||||
private _waiting_stream_ids: Map<string, unknown>;
|
||||
private _waiting_stream_logins: Map<string, unknown>;
|
||||
|
||||
private tag_cache: Map<string, unknown>;
|
||||
private _waiting_tags: Map<string, unknown>;
|
||||
|
||||
constructor(name?: string, parent?: GenericModule) {
|
||||
super(name, parent);
|
||||
|
||||
this.site = this.parent as GenericModule;
|
||||
|
||||
this.inject('site.apollo');
|
||||
|
||||
|
@ -41,17 +63,22 @@ export default class TwitchData extends Module {
|
|||
this.tag_cache = new Map;
|
||||
this._waiting_tags = new Map;
|
||||
|
||||
this._loadTags = debounce(this._loadTags, 50);
|
||||
this._loadStreams = debounce(this._loadStreams, 50);
|
||||
// The return type doesn't match, because this method returns
|
||||
// a void and not a Promise. We don't care.
|
||||
this._loadStreams = debounce(this._loadStreams, 50) as any;
|
||||
}
|
||||
|
||||
queryApollo(query, variables, options) {
|
||||
let thing;
|
||||
if ( ! variables && ! options && query.query )
|
||||
queryApollo(
|
||||
query: DocumentNode | {query: DocumentNode, variables: any},
|
||||
variables?: any,
|
||||
options?: any
|
||||
) {
|
||||
let thing: {query: DocumentNode, variables: any};
|
||||
if ( ! variables && ! options && 'query' in query && query.query )
|
||||
thing = query;
|
||||
else {
|
||||
thing = {
|
||||
query,
|
||||
query: query as DocumentNode,
|
||||
variables
|
||||
};
|
||||
|
||||
|
@ -62,13 +89,17 @@ export default class TwitchData extends Module {
|
|||
return this.apollo.client.query(thing);
|
||||
}
|
||||
|
||||
mutate(mutation, variables, options) {
|
||||
let thing;
|
||||
if ( ! variables && ! options && mutation.mutation )
|
||||
mutate(
|
||||
mutation: DocumentNode | {mutation: DocumentNode, variables: any},
|
||||
variables?: any,
|
||||
options?: any
|
||||
) {
|
||||
let thing: {mutation: DocumentNode, variables: any};
|
||||
if ( ! variables && ! options && 'mutation' in mutation && mutation.mutation )
|
||||
thing = mutation;
|
||||
else {
|
||||
thing = {
|
||||
mutation,
|
||||
mutation: mutation as DocumentNode,
|
||||
variables
|
||||
};
|
||||
|
||||
|
@ -95,6 +126,7 @@ export default class TwitchData extends Module {
|
|||
// ========================================================================
|
||||
|
||||
async getBadges() {
|
||||
|
||||
const data = await this.queryApollo(
|
||||
await import(/* webpackChunkName: 'queries' */ './data/global-badges.gql')
|
||||
);
|
||||
|
@ -116,7 +148,11 @@ export default class TwitchData extends Module {
|
|||
* next page of results.
|
||||
* @returns {PaginatedResult} The results
|
||||
*/
|
||||
async getMatchingCategories(query, first = 15, cursor = null) {
|
||||
async getMatchingCategories(
|
||||
query: string,
|
||||
first: number = 15,
|
||||
cursor: string | null = null
|
||||
) {
|
||||
const data = await this.queryApollo(
|
||||
await import(/* webpackChunkName: 'queries' */ './data/search-category.gql'),
|
||||
{
|
||||
|
@ -852,7 +888,7 @@ export default class TwitchData extends Module {
|
|||
*
|
||||
* console.log(await this.twitch_data.getMatchingTags("Rainbo"));
|
||||
*/
|
||||
async getMatchingTags(query) {
|
||||
async getMatchingTags(query: string) {
|
||||
const data = await this.queryApollo({
|
||||
query: await import(/* webpackChunkName: 'queries' */ './data/tag-search.gql'),
|
||||
variables: {
|
|
@ -1,7 +1,5 @@
|
|||
import type ExperimentManager from "../experiments";
|
||||
import type TranslationManager from "../i18n";
|
||||
import type LoadTracker from "../load_tracker";
|
||||
import type { LoadEvents } from "../load_tracker";
|
||||
import type Chat from "../modules/chat";
|
||||
import type Actions from "../modules/chat/actions/actions";
|
||||
import type Badges from "../modules/chat/badges";
|
||||
|
@ -11,25 +9,17 @@ import type Overrides from "../modules/chat/overrides";
|
|||
import type EmoteCard from "../modules/emote_card";
|
||||
import type LinkCard from "../modules/link_card";
|
||||
import type MainMenu from "../modules/main_menu";
|
||||
import type Metadata from "../modules/metadata";
|
||||
import type TooltipProvider from "../modules/tooltips";
|
||||
import type { TooltipEvents } from "../modules/tooltips";
|
||||
import type TranslationUI from "../modules/translation_ui";
|
||||
import type PubSub from "../pubsub";
|
||||
import type { SettingsEvents } from "../settings";
|
||||
import type SettingsManager from "../settings";
|
||||
import type SocketClient from "../socket";
|
||||
import type StagingSelector from "../staging";
|
||||
import type Apollo from "./compat/apollo";
|
||||
import type Elemental from "./compat/elemental";
|
||||
import type Fine from "./compat/fine";
|
||||
import type Subpump from "./compat/subpump";
|
||||
import type { SubpumpEvents } from "./compat/subpump";
|
||||
import type WebMunch from "./compat/webmunch";
|
||||
import type CSSTweaks from "./css-tweaks";
|
||||
import type { NamespacedEvents } from "./events";
|
||||
import type TwitchData from "./twitch-data";
|
||||
import type Vue from "./vue";
|
||||
|
||||
/**
|
||||
* AddonInfo represents the data contained in an add-on's manifest.
|
||||
|
@ -98,6 +88,52 @@ export type AddonInfo = {
|
|||
|
||||
};
|
||||
|
||||
// These types are used by get()
|
||||
|
||||
export type ExtractSegments<Input extends string> =
|
||||
Input extends `${infer Match}.${infer Rest}`
|
||||
? [ Match, ...ExtractSegments<Rest> ]
|
||||
: [ Input ];
|
||||
|
||||
export type ArrayShift<T extends any[]> = T extends [any, ...infer Rest]
|
||||
// This is needed to avoid it returning an empty array. There's probably
|
||||
// a more elegant solution, but I don't know it.
|
||||
? Rest extends [any, ...any[]]
|
||||
? Rest
|
||||
: undefined
|
||||
: undefined;
|
||||
|
||||
export type ExtractType<T, Path extends string[], Key = Path[0], Rest = ArrayShift<Path>> =
|
||||
Key extends "@each"
|
||||
? ExtractEach<T, Rest>
|
||||
:
|
||||
Key extends "@last"
|
||||
? T extends any[]
|
||||
? ExtractEach<T, Rest>
|
||||
: never
|
||||
:
|
||||
Key extends keyof T
|
||||
? Rest extends string[]
|
||||
? ExtractType<T[Key], Rest>
|
||||
: T[Key]
|
||||
:
|
||||
never;
|
||||
|
||||
export type ExtractEach<T, Rest> =
|
||||
Rest extends string[]
|
||||
? { [K in keyof T]: ExtractType<T[K], Rest> }
|
||||
: T;
|
||||
|
||||
|
||||
|
||||
export type AnyFunction = (...args: any[]) => any;
|
||||
|
||||
export interface ClassType<T> {
|
||||
new (...args: any[]): T;
|
||||
prototype: T;
|
||||
}
|
||||
|
||||
|
||||
export type OptionallyThisCallable<TThis, TArgs extends any[], TReturn> = TReturn | ((this: TThis, ...args: TArgs) => TReturn);
|
||||
export type OptionallyCallable<TArgs extends any[], TReturn> = TReturn | ((...args: TArgs) => TReturn);
|
||||
|
||||
|
@ -105,6 +141,11 @@ export type OptionalPromise<T> = T | Promise<T>;
|
|||
|
||||
export type OptionalArray<T> = T | T[];
|
||||
|
||||
export type UnionToIntersection<Union> = (
|
||||
Union extends any ? (k: Union) => void : never
|
||||
) extends (k: infer Intersection) => void ? Intersection : never;
|
||||
|
||||
|
||||
export type RecursivePartial<T> = {
|
||||
[K in keyof T]?: T[K] extends object
|
||||
? RecursivePartial<T[K]>
|
||||
|
@ -112,6 +153,47 @@ export type RecursivePartial<T> = {
|
|||
};
|
||||
|
||||
|
||||
export type JoinKeyPaths<K, P, Separator extends string = '.'> = K extends string ?
|
||||
P extends string ?
|
||||
`${K}${P extends '' ? '' : Separator}${P}`
|
||||
: never : never;
|
||||
|
||||
export type ObjectKeyPaths<T, Separator extends string = '.', Prefix extends string = ''> =
|
||||
T extends object ?
|
||||
(Prefix extends '' ? never : Prefix) | { [K in keyof T]-?: JoinKeyPaths<K, ObjectKeyPaths<T[K], Separator>, Separator> }[keyof T]
|
||||
: Prefix;
|
||||
|
||||
|
||||
export type ExtractFunctionNames<T, IncludeOptional extends boolean = false> = {
|
||||
[K in keyof T]:
|
||||
T[K] extends AnyFunction
|
||||
? K
|
||||
: IncludeOptional extends true ?
|
||||
T[K] extends AnyFunction | undefined
|
||||
? K
|
||||
: never
|
||||
: never;
|
||||
}[keyof T];
|
||||
|
||||
/**
|
||||
* Extract all of the functions from a type. If IncludeOptional is set to
|
||||
* true, then also include functions that are possibly undefined. If
|
||||
* UnwrapOptional is set to true, which it is by default, the possibly
|
||||
* undefined functions are unwrapped in the resulting type. This makes it
|
||||
* easier to extract function parameters, etc.
|
||||
*/
|
||||
export type ExtractFunctions<T, IncludeOptional extends boolean = false, UnwrapOptional extends boolean = true> = {
|
||||
[K in ExtractFunctionNames<T, IncludeOptional>]: T[K] extends undefined | (infer U)
|
||||
? UnwrapOptional extends true
|
||||
? U
|
||||
: T[K]
|
||||
: T[K];
|
||||
};
|
||||
|
||||
|
||||
export type MaybeParameters<T> = T extends AnyFunction ? Parameters<T> : never[];
|
||||
|
||||
|
||||
export type ClientVersion = {
|
||||
major: number;
|
||||
minor: number;
|
||||
|
@ -135,6 +217,15 @@ export type Mousetrap = {
|
|||
export type DomFragment = Node | string | null | undefined | DomFragment[];
|
||||
|
||||
|
||||
export interface SettingsTypeMap {
|
||||
|
||||
};
|
||||
|
||||
export interface ProviderTypeMap {
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
// TODO: Move this event into addons.
|
||||
type AddonEvent = {
|
||||
|
@ -142,15 +233,9 @@ type AddonEvent = {
|
|||
};
|
||||
|
||||
|
||||
export type KnownEvents =
|
||||
AddonEvent &
|
||||
NamespacedEvents<'load_tracker', LoadEvents> &
|
||||
NamespacedEvents<'settings', SettingsEvents> &
|
||||
NamespacedEvents<'site.subpump', SubpumpEvents> &
|
||||
NamespacedEvents<'tooltips', TooltipEvents>;
|
||||
export interface ModuleEventMap { };
|
||||
|
||||
|
||||
export type ModuleMap = {
|
||||
export interface ModuleMap {
|
||||
'chat': Chat;
|
||||
'chat.actions': Actions;
|
||||
'chat.badges': Badges;
|
||||
|
@ -161,24 +246,22 @@ export type ModuleMap = {
|
|||
'experiments': ExperimentManager;
|
||||
'i18n': TranslationManager;
|
||||
'link_card': LinkCard;
|
||||
'load_tracker': LoadTracker;
|
||||
'main_menu': MainMenu;
|
||||
'metadata': Metadata;
|
||||
'pubsub': PubSub;
|
||||
'settings': SettingsManager;
|
||||
'site.apollo': Apollo;
|
||||
'site.css_tweaks': CSSTweaks;
|
||||
'site.elemental': Elemental;
|
||||
'site.fine': Fine;
|
||||
'site.subpump': Subpump;
|
||||
'site.twitch_data': TwitchData;
|
||||
'site.web_munch': WebMunch;
|
||||
'socket': SocketClient;
|
||||
'staging': StagingSelector;
|
||||
'tooltips': TooltipProvider;
|
||||
'translation_ui': TranslationUI;
|
||||
'vue': Vue;
|
||||
};
|
||||
|
||||
|
||||
export type KnownEvents = AddonEvent & UnionToIntersection<{
|
||||
[K in keyof ModuleEventMap]: NamespacedEvents<K, ModuleEventMap[K]>
|
||||
}[keyof ModuleEventMap]>;
|
||||
|
||||
export type ModuleKeys = string & keyof ModuleMap;
|
||||
|
|
|
@ -5,14 +5,46 @@
|
|||
// Loads Vue + Translation Shim
|
||||
// ============================================================================
|
||||
|
||||
import Module from 'utilities/module';
|
||||
import Module, { GenericModule } from 'utilities/module';
|
||||
import {has} from 'utilities/object';
|
||||
import {DEBUG} from 'utilities/constants';
|
||||
import type TranslationManager from '../i18n';
|
||||
import type { VueConstructor } from 'vue';
|
||||
import type Vue from 'vue';
|
||||
import type { CombinedVueInstance } from 'vue/types/vue';
|
||||
import type { MessageNode } from '@ffz/icu-msgparser';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
ffzVue: VueConstructor<Vue>;
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'utilities/types' {
|
||||
interface ModuleEventMap {
|
||||
|
||||
}
|
||||
interface ModuleMap {
|
||||
vue: VueModule
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class Vue extends Module {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
export class VueModule extends Module<'vue'> {
|
||||
|
||||
private _components: Record<string, any> | null;
|
||||
|
||||
Vue: VueConstructor<Vue> | null = null;
|
||||
|
||||
i18n: TranslationManager = null as any;
|
||||
|
||||
_vue_i18n?: CombinedVueInstance<any, any, any, any, any> | null;
|
||||
|
||||
key: string;
|
||||
|
||||
constructor(name?: string, parent?: GenericModule) {
|
||||
super(name, parent);
|
||||
this.key = 'ffz-stuff';
|
||||
|
||||
this._components = {};
|
||||
this.inject('i18n');
|
||||
}
|
||||
|
@ -36,7 +68,7 @@ export class Vue extends Module {
|
|||
|
||||
this.component(Components.default);
|
||||
|
||||
Vue.use(ObserveVisibility);
|
||||
Vue.use(ObserveVisibility as any);
|
||||
Vue.mixin(Clickaway.mixin);
|
||||
|
||||
/*if ( ! DEBUG && this.root.raven )
|
||||
|
@ -50,30 +82,30 @@ export class Vue extends Module {
|
|||
Vue.use(this);
|
||||
}
|
||||
|
||||
component(name, component) {
|
||||
if ( typeof name === 'function' ) {
|
||||
for(const key of name.keys())
|
||||
this.component(key.slice(2, key.length - 4), name(key).default);
|
||||
component(id: string | any, constructor?: any) {
|
||||
if ( typeof id === 'function' ) {
|
||||
for(const key of id.keys())
|
||||
this.component(key.slice(2, key.length - 4), id(key).default);
|
||||
|
||||
} else if ( typeof name === 'object' ) {
|
||||
for(const key in name)
|
||||
if ( has(name, key) )
|
||||
this.component(key, name[key]);
|
||||
} else if ( typeof id === 'object' ) {
|
||||
for(const key in id)
|
||||
if ( has(id, key) )
|
||||
this.component(key, id[key]);
|
||||
|
||||
} else if ( this.Vue )
|
||||
this.Vue.component(name, component);
|
||||
this.Vue.component(id, constructor);
|
||||
|
||||
else
|
||||
this._components[name] = component;
|
||||
else if ( this._components )
|
||||
this._components[id] = constructor;
|
||||
}
|
||||
|
||||
install(vue) {
|
||||
install(vue: typeof Vue) {
|
||||
// This is a mess. I'm sure there's an easier way to tie the systems
|
||||
// together. However, for now, this works.
|
||||
|
||||
const t = this;
|
||||
if ( ! this._vue_i18n ) {
|
||||
this._vue_i18n = new this.Vue({
|
||||
this._vue_i18n = new vue({
|
||||
data() {
|
||||
return {
|
||||
locale: t.i18n.locale,
|
||||
|
@ -82,47 +114,51 @@ export class Vue extends Module {
|
|||
},
|
||||
|
||||
methods: {
|
||||
tNumber_(val, format) {
|
||||
tNumber_(val: number, format?: string) {
|
||||
this.locale;
|
||||
return t.i18n.formatNumber(val, format);
|
||||
},
|
||||
|
||||
tDate_(val, format) {
|
||||
tDate_(val: string | Date, format?: string) {
|
||||
this.locale;
|
||||
return t.i18n.formatDate(val, format);
|
||||
},
|
||||
|
||||
tTime_(val, format) {
|
||||
tTime_(val: string | Date, format?: string) {
|
||||
this.locale;
|
||||
return t.i18n.formatTime(val, format);
|
||||
},
|
||||
|
||||
tDateTime_(val, format) {
|
||||
tDateTime_(val: string | Date, format?: string) {
|
||||
this.locale;
|
||||
return t.i18n.formatDateTime(val, format);
|
||||
},
|
||||
|
||||
t_(key, phrase, options) {
|
||||
this.locale && this.phrases[key];
|
||||
t_(key: string, phrase: string, options: any) {
|
||||
// Access properties to trigger reactivity.
|
||||
this.locale && (this as any).phrases[key];
|
||||
return t.i18n.t(key, phrase, options);
|
||||
},
|
||||
|
||||
tList_(key, phrase, options) {
|
||||
this.locale && this.phrases[key];
|
||||
tList_(key: string, phrase: string, options: any) {
|
||||
// Access properties to trigger reactivity.
|
||||
this.locale && (this as any).phrases[key];
|
||||
return t.i18n.tList(key, phrase, options);
|
||||
},
|
||||
|
||||
tNode_(node, data) {
|
||||
tNode_(node: MessageNode, data: any) {
|
||||
// Access properties to trigger reactivity.
|
||||
this.locale;
|
||||
return t.i18n.formatNode(node, data);
|
||||
},
|
||||
|
||||
setLocale(locale) {
|
||||
setLocale(locale: string) {
|
||||
t.i18n.locale = locale;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// On i18n events, update values for reactivity.
|
||||
this.on('i18n:transform', () => {
|
||||
this._vue_i18n.locale = this.i18n.locale;
|
||||
this._vue_i18n.phrases = {};
|
||||
|
@ -165,11 +201,11 @@ export class Vue extends Module {
|
|||
render(createElement) {
|
||||
return createElement(
|
||||
this.tag || 'span',
|
||||
this.$i18n.tList_(
|
||||
(this as any).$i18n.tList_(
|
||||
this.phrase,
|
||||
this.default,
|
||||
Object.assign({}, this.data, this.$scopedSlots)
|
||||
).map(out => {
|
||||
).map((out: any) => {
|
||||
if ( typeof out === 'function' )
|
||||
return out();
|
||||
return out;
|
||||
|
@ -198,29 +234,29 @@ export class Vue extends Module {
|
|||
return t.i18n;
|
||||
},
|
||||
t(key, phrase, options) {
|
||||
return this.$i18n.t_(key, phrase, options);
|
||||
return (this as any).$i18n.t_(key, phrase, options);
|
||||
},
|
||||
tList(key, phrase, options) {
|
||||
return this.$i18n.tList_(key, phrase, options);
|
||||
return (this as any).$i18n.tList_(key, phrase, options);
|
||||
},
|
||||
tNode(node, data) {
|
||||
return this.$i18n.tNode_(node, data);
|
||||
return (this as any).$i18n.tNode_(node, data);
|
||||
},
|
||||
tNumber(val, format) {
|
||||
return this.$i18n.tNumber_(val, format);
|
||||
return (this as any).$i18n.tNumber_(val, format);
|
||||
},
|
||||
tDate(val, format) {
|
||||
return this.$i18n.tDate_(val, format);
|
||||
return (this as any).$i18n.tDate_(val, format);
|
||||
},
|
||||
tTime(val, format) {
|
||||
return this.$i18n.tTime_(val, format);
|
||||
return (this as any).$i18n.tTime_(val, format);
|
||||
},
|
||||
tDateTime(val, format) {
|
||||
return this.$i18n.tDateTime_(val, format);
|
||||
return (this as any).$i18n.tDateTime_(val, format);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default Vue;
|
||||
export default VueModule;
|
|
@ -298,7 +298,7 @@
|
|||
.ffz--field-inline {
|
||||
flex: 1;
|
||||
width: unset;
|
||||
min-width: 150px;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.ffz--field-icon {
|
||||
|
@ -554,4 +554,4 @@
|
|||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
],
|
||||
"allowJs": true,
|
||||
"jsx": "preserve",
|
||||
"jsxFactory": "createElement",
|
||||
"alwaysStrict": true,
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
|
|
12
types/import-types.d.ts
vendored
12
types/import-types.d.ts
vendored
|
@ -2,3 +2,15 @@ declare module "*.scss" {
|
|||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module "*.json" {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module "*.gql" {
|
||||
import { DocumentNode } from "graphql";
|
||||
const Schema: DocumentNode;
|
||||
|
||||
export = Schema;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue