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": {
|
"devDependencies": {
|
||||||
"@ffz/fontello-cli": "^1.0.4",
|
"@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/safe-regex": "^1.1.6",
|
||||||
|
"@types/vue-clickaway": "^2.2.4",
|
||||||
"@types/webpack-env": "^1.18.4",
|
"@types/webpack-env": "^1.18.4",
|
||||||
"browserslist": "^4.21.10",
|
"browserslist": "^4.21.10",
|
||||||
"copy-webpack-plugin": "^11.0.0",
|
"copy-webpack-plugin": "^11.0.0",
|
||||||
|
@ -66,7 +69,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ffz/icu-msgparser": "^2.0.0",
|
"@ffz/icu-msgparser": "^2.0.0",
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
"crypto-js": "^3.3.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dayjs": "^1.10.7",
|
"dayjs": "^1.10.7",
|
||||||
"denoflare-mqtt": "^0.0.2",
|
"denoflare-mqtt": "^0.0.2",
|
||||||
"displacejs": "^1.4.1",
|
"displacejs": "^1.4.1",
|
||||||
|
@ -74,7 +77,7 @@
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"graphql": "^16.0.1",
|
"graphql": "^16.0.1",
|
||||||
"graphql-tag": "^2.12.6",
|
"graphql-tag": "^2.12.6",
|
||||||
"js-cookie": "^2.2.1",
|
"js-cookie": "^3.0.5",
|
||||||
"jszip": "^3.7.1",
|
"jszip": "^3.7.1",
|
||||||
"markdown-it": "^12.2.0",
|
"markdown-it": "^12.2.0",
|
||||||
"markdown-it-link-attributes": "^3.0.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
|
specifier: ^2.11.8
|
||||||
version: 2.11.8
|
version: 2.11.8
|
||||||
crypto-js:
|
crypto-js:
|
||||||
specifier: ^3.3.0
|
specifier: ^4.2.0
|
||||||
version: 3.3.0
|
version: 4.2.0
|
||||||
dayjs:
|
dayjs:
|
||||||
specifier: ^1.10.7
|
specifier: ^1.10.7
|
||||||
version: 1.10.7
|
version: 1.10.7
|
||||||
|
@ -42,8 +42,8 @@ dependencies:
|
||||||
specifier: ^2.12.6
|
specifier: ^2.12.6
|
||||||
version: 2.12.6(graphql@16.0.1)
|
version: 2.12.6(graphql@16.0.1)
|
||||||
js-cookie:
|
js-cookie:
|
||||||
specifier: ^2.2.1
|
specifier: ^3.0.5
|
||||||
version: 2.2.1
|
version: 3.0.5
|
||||||
jszip:
|
jszip:
|
||||||
specifier: ^3.7.1
|
specifier: ^3.7.1
|
||||||
version: 3.7.1
|
version: 3.7.1
|
||||||
|
@ -97,9 +97,18 @@ devDependencies:
|
||||||
'@ffz/fontello-cli':
|
'@ffz/fontello-cli':
|
||||||
specifier: ^1.0.4
|
specifier: ^1.0.4
|
||||||
version: 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':
|
'@types/safe-regex':
|
||||||
specifier: ^1.1.6
|
specifier: ^1.1.6
|
||||||
version: 1.1.6
|
version: 1.1.6
|
||||||
|
'@types/vue-clickaway':
|
||||||
|
specifier: ^2.2.4
|
||||||
|
version: 2.2.4
|
||||||
'@types/webpack-env':
|
'@types/webpack-env':
|
||||||
specifier: ^1.18.4
|
specifier: ^1.18.4
|
||||||
version: 1.18.4
|
version: 1.18.4
|
||||||
|
@ -592,6 +601,10 @@ packages:
|
||||||
'@types/node': 20.5.7
|
'@types/node': 20.5.7
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/crypto-js@4.2.1:
|
||||||
|
resolution: {integrity: sha512-FSPGd9+OcSok3RsM0UZ/9fcvMOXJ1ENE/ZbLfOPlBWj7BgXtEAM8VYfTtT760GiLbQIMoVozwVuisjvsVwqYWw==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/eslint-scope@3.7.4:
|
/@types/eslint-scope@3.7.4:
|
||||||
resolution: {integrity: sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==}
|
resolution: {integrity: sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -638,6 +651,10 @@ packages:
|
||||||
'@types/node': 20.5.7
|
'@types/node': 20.5.7
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/js-cookie@3.0.6:
|
||||||
|
resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/json-schema@7.0.9:
|
/@types/json-schema@7.0.9:
|
||||||
resolution: {integrity: sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==}
|
resolution: {integrity: sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -701,6 +718,12 @@ packages:
|
||||||
'@types/node': 20.5.7
|
'@types/node': 20.5.7
|
||||||
dev: true
|
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:
|
/@types/webpack-env@1.18.4:
|
||||||
resolution: {integrity: sha512-I6e+9+HtWADAWeeJWDFQtdk4EVSAbj6Rtz4q8fJ7mSr1M0jzlFcs8/HZ+Xb5SHzVm1dxH7aUiI+A8kA8Gcrm0A==}
|
resolution: {integrity: sha512-I6e+9+HtWADAWeeJWDFQtdk4EVSAbj6Rtz4q8fJ7mSr1M0jzlFcs8/HZ+Xb5SHzVm1dxH7aUiI+A8kA8Gcrm0A==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -2173,8 +2196,8 @@ packages:
|
||||||
which: 2.0.2
|
which: 2.0.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/crypto-js@3.3.0:
|
/crypto-js@4.2.0:
|
||||||
resolution: {integrity: sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q==}
|
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/css-loader@6.8.1(webpack@5.88.2):
|
/css-loader@6.8.1(webpack@5.88.2):
|
||||||
|
@ -3701,8 +3724,9 @@ packages:
|
||||||
supports-color: 8.1.1
|
supports-color: 8.1.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/js-cookie@2.2.1:
|
/js-cookie@3.0.5:
|
||||||
resolution: {integrity: sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==}
|
resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/js-tokens@3.0.2:
|
/js-tokens@3.0.2:
|
||||||
|
@ -5652,7 +5676,6 @@ packages:
|
||||||
|
|
||||||
/vue@2.6.14:
|
/vue@2.6.14:
|
||||||
resolution: {integrity: sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ==}
|
resolution: {integrity: sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ==}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/vuedraggable@2.24.3:
|
/vuedraggable@2.24.3:
|
||||||
resolution: {integrity: sha512-6/HDXi92GzB+Hcs9fC6PAAozK1RLt1ewPTLjK0anTYguXLAeySDmcnqE8IC0xa7shvSzRjQXq3/+dsZ7ETGF3g==}
|
resolution: {integrity: sha512-6/HDXi92GzB+Hcs9fC6PAAozK1RLt1ewPTLjK0anTYguXLAeySDmcnqE8IC0xa7shvSzRjQXq3/+dsZ7ETGF3g==}
|
||||||
|
|
|
@ -18,7 +18,7 @@ import StagingSelector from './staging';
|
||||||
import LoadTracker from './load_tracker';
|
import LoadTracker from './load_tracker';
|
||||||
|
|
||||||
import Site from './sites/clips';
|
import Site from './sites/clips';
|
||||||
import Vue from 'utilities/vue';
|
import VueModule from 'utilities/vue';
|
||||||
|
|
||||||
import Tooltips from 'src/modules/tooltips';
|
import Tooltips from 'src/modules/tooltips';
|
||||||
import Chat from 'src/modules/chat';
|
import Chat from 'src/modules/chat';
|
||||||
|
@ -64,7 +64,7 @@ class FrankerFaceZ extends Module {
|
||||||
this.inject('site', Site);
|
this.inject('site', Site);
|
||||||
this.inject('addons', AddonManager);
|
this.inject('addons', AddonManager);
|
||||||
|
|
||||||
this.register('vue', Vue);
|
this.register('vue', VueModule);
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
// Startup
|
// Startup
|
||||||
|
|
|
@ -5,13 +5,33 @@
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
import {DEBUG, SERVER} from 'utilities/constants';
|
import {DEBUG, SERVER} from 'utilities/constants';
|
||||||
import Module from 'utilities/module';
|
import Module, { GenericModule } from 'utilities/module';
|
||||||
import {has, deep_copy} from 'utilities/object';
|
import {has, deep_copy, fetchJSON} from 'utilities/object';
|
||||||
import { getBuster } from 'utilities/time';
|
import { getBuster } from 'utilities/time';
|
||||||
|
|
||||||
import Cookie from 'js-cookie';
|
import Cookie from 'js-cookie';
|
||||||
import SHA1 from 'crypto-js/sha1';
|
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',
|
const OVERRIDE_COOKIE = 'experiment_overrides',
|
||||||
COOKIE_OPTIONS = {
|
COOKIE_OPTIONS = {
|
||||||
expires: 7,
|
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 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
|
// We don't load using this because we might want a newer file from the
|
||||||
// server.
|
// server. Because of our webpack settings, this is imported as a URL
|
||||||
import EXPERIMENTS from './experiments.json'; // eslint-disable-line no-unused-vars
|
// 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) {
|
function sortExperimentLog(a,b) {
|
||||||
if ( a.rarity < b.rarity )
|
if ( a.rarity < b.rarity )
|
||||||
|
@ -44,9 +123,24 @@ function sortExperimentLog(a,b) {
|
||||||
// Experiment Manager
|
// Experiment Manager
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
export default class ExperimentManager extends Module {
|
export default class ExperimentManager extends Module<'experiments', ExperimentEvents> {
|
||||||
constructor(...args) {
|
|
||||||
super(...args);
|
// 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;
|
this.get = this.getAssignment;
|
||||||
|
|
||||||
|
@ -81,23 +175,23 @@ export default class ExperimentManager extends Module {
|
||||||
|
|
||||||
unique_id: () => this.unique_id,
|
unique_id: () => this.unique_id,
|
||||||
|
|
||||||
ffz_data: () => deep_copy(this.experiments),
|
ffz_data: () => deep_copy(this.experiments) ?? {},
|
||||||
twitch_data: () => deep_copy(this.getTwitchExperiments()),
|
twitch_data: () => deep_copy(this.getTwitchExperiments()),
|
||||||
|
|
||||||
usingTwitchExperiment: key => this.usingTwitchExperiment(key),
|
usingTwitchExperiment: (key: string) => this.usingTwitchExperiment(key),
|
||||||
getTwitchAssignment: key => this.getTwitchAssignment(key),
|
getTwitchAssignment: (key: string) => this.getTwitchAssignment(key),
|
||||||
getTwitchType: type => this.getTwitchType(type),
|
getTwitchType: (type: TwitchExperimentType) => this.getTwitchType(type),
|
||||||
hasTwitchOverride: key => this.hasTwitchOverride(key),
|
hasTwitchOverride: (key: string) => this.hasTwitchOverride(key),
|
||||||
setTwitchOverride: (key, val) => this.setTwitchOverride(key, val),
|
setTwitchOverride: (key: string, val: string) => this.setTwitchOverride(key, val),
|
||||||
deleteTwitchOverride: key => this.deleteTwitchOverride(key),
|
deleteTwitchOverride: (key: string) => this.deleteTwitchOverride(key),
|
||||||
|
|
||||||
getAssignment: key => this.getAssignment(key),
|
getAssignment: (key: string) => this.getAssignment(key),
|
||||||
hasOverride: key => this.hasOverride(key),
|
hasOverride: (key: string) => this.hasOverride(key),
|
||||||
setOverride: (key, val) => this.setOverride(key, val),
|
setOverride: (key: string, val: any) => this.setOverride(key, val),
|
||||||
deleteOverride: key => this.deleteOverride(key),
|
deleteOverride: (key: string) => this.deleteOverride(key),
|
||||||
|
|
||||||
on: (...args) => this.on(...args),
|
on: (...args: Parameters<typeof this.on>) => this.on(...args),
|
||||||
off: (...args) => this.off(...args)
|
off: (...args: Parameters<typeof this.off>) => this.off(...args)
|
||||||
});
|
});
|
||||||
|
|
||||||
this.unique_id = Cookie.get('unique_id');
|
this.unique_id = Cookie.get('unique_id');
|
||||||
|
@ -112,7 +206,7 @@ export default class ExperimentManager extends Module {
|
||||||
if ( DEBUG )
|
if ( DEBUG )
|
||||||
return false;
|
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) )
|
if ( isNaN(ts) || ! isFinite(ts) )
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
@ -129,14 +223,17 @@ export default class ExperimentManager extends Module {
|
||||||
|
|
||||||
|
|
||||||
async loadExperiments() {
|
async loadExperiments() {
|
||||||
let data;
|
let data: Record<string, FFZExperimentData> | null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
data = await fetch(DEBUG ? EXPERIMENTS : `${SERVER}/script/experiments.json?_=${getBuster()}`).then(r =>
|
data = await fetchJSON(DEBUG
|
||||||
r.ok ? r.json() : null);
|
? EXPERIMENTS
|
||||||
|
: `${SERVER}/script/experiments.json?_=${getBuster()}`
|
||||||
|
);
|
||||||
|
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
this.log.warn('Unable to load experiment data.', err);
|
this.log.warn('Unable to load experiment data.', err);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( ! data )
|
if ( ! data )
|
||||||
|
@ -153,8 +250,8 @@ export default class ExperimentManager extends Module {
|
||||||
const new_val = this.getAssignment(key);
|
const new_val = this.getAssignment(key);
|
||||||
if ( old_val !== new_val ) {
|
if ( old_val !== new_val ) {
|
||||||
changed++;
|
changed++;
|
||||||
this.emit(':changed', key, new_val);
|
this.emit(':changed', key, new_val, old_val);
|
||||||
this.emit(`:changed:${key}`, new_val);
|
this.emit(`:changed:${key}`, new_val, old_val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -162,21 +259,25 @@ export default class ExperimentManager extends Module {
|
||||||
//this.emit(':loaded');
|
//this.emit(':loaded');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
onEnable() {
|
onEnable() {
|
||||||
this.on('pubsub:command:reload_experiments', this.loadExperiments, this);
|
this.on('pubsub:command:reload_experiments', this.loadExperiments, this);
|
||||||
this.on('pubsub:command:update_experiment', this.updateExperiment, this);
|
this.on('pubsub:command:update_experiment', this.updateExperiment, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
updateExperiment(key, data) {
|
updateExperiment(key: string, data: FFZExperimentData | ExperimentGroup[]) {
|
||||||
this.log.info(`Received updated data for experiment "${key}" via WebSocket.`, data);
|
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;
|
this.experiments[key].groups = data;
|
||||||
|
|
||||||
|
} else if ( data?.groups )
|
||||||
|
this.experiments[key] = data;
|
||||||
|
|
||||||
this._rebuildKey(key);
|
this._rebuildKey(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -261,7 +362,7 @@ export default class ExperimentManager extends Module {
|
||||||
|
|
||||||
// Twitch Experiments
|
// Twitch Experiments
|
||||||
|
|
||||||
getTwitchType(type) {
|
getTwitchType(type: number) {
|
||||||
const core = this.resolve('site')?.getCore?.();
|
const core = this.resolve('site')?.getCore?.();
|
||||||
if ( core?.experiments?.getExperimentType )
|
if ( core?.experiments?.getExperimentType )
|
||||||
return core.experiments.getExperimentType(type);
|
return core.experiments.getExperimentType(type);
|
||||||
|
@ -275,10 +376,9 @@ export default class ExperimentManager extends Module {
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
|
|
||||||
getTwitchTypeByKey(key) {
|
getTwitchTypeByKey(key: string) {
|
||||||
const core = this.resolve('site')?.getCore?.(),
|
const exps = this.getTwitchExperiments(),
|
||||||
exps = core && core.experiments,
|
exp = exps?.[key];
|
||||||
exp = exps?.experiments?.[key];
|
|
||||||
|
|
||||||
if ( exp?.t )
|
if ( exp?.t )
|
||||||
return this.getTwitchType(exp.t);
|
return this.getTwitchType(exp.t);
|
||||||
|
@ -286,30 +386,67 @@ export default class ExperimentManager extends Module {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
getTwitchExperiments() {
|
getTwitchExperiments(): Record<string, TwitchExperimentData> {
|
||||||
if ( window.__twilightSettings )
|
if ( window.__twilightSettings )
|
||||||
return window.__twilightSettings.experiments;
|
return window.__twilightSettings.experiments ?? {};
|
||||||
|
|
||||||
const core = this.resolve('site')?.getCore?.();
|
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?.();
|
const core = this.resolve('site')?.getCore?.();
|
||||||
return core && has(core.experiments.assignments, key)
|
return core && has(core.experiments.assignments, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
setTwitchOverride(key, value = null) {
|
private _getOverrideCookie() {
|
||||||
const overrides = Cookie.getJSON(OVERRIDE_COOKIE) || {};
|
const raw = Cookie.get(OVERRIDE_COOKIE);
|
||||||
const experiments = overrides.experiments = overrides.experiments || {};
|
let out: OverrideCookie;
|
||||||
const disabled = overrides.disabled = overrides.disabled || [];
|
|
||||||
|
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;
|
experiments[key] = value;
|
||||||
|
|
||||||
const idx = disabled.indexOf(key);
|
const idx = disabled.indexOf(key);
|
||||||
if (idx != -1)
|
if (idx != -1)
|
||||||
disabled.remove(idx);
|
disabled.splice(idx, 1);
|
||||||
Cookie.set(OVERRIDE_COOKIE, overrides, COOKIE_OPTIONS);
|
|
||||||
|
this._saveOverrideCookie(overrides);
|
||||||
|
|
||||||
const core = this.resolve('site')?.getCore?.();
|
const core = this.resolve('site')?.getCore?.();
|
||||||
if ( core )
|
if ( core )
|
||||||
|
@ -318,15 +455,17 @@ export default class ExperimentManager extends Module {
|
||||||
this._rebuildTwitchKey(key, true, value);
|
this._rebuildTwitchKey(key, true, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteTwitchOverride(key) {
|
deleteTwitchOverride(key: string) {
|
||||||
const overrides = Cookie.getJSON(OVERRIDE_COOKIE),
|
const overrides = this._getOverrideCookie(),
|
||||||
experiments = overrides?.experiments;
|
experiments = overrides.experiments;
|
||||||
if ( ! experiments || ! has(experiments, key) )
|
|
||||||
|
if ( ! has(experiments, key) )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const old_val = experiments[key];
|
const old_val = experiments[key];
|
||||||
delete experiments[key];
|
delete experiments[key];
|
||||||
Cookie.set(OVERRIDE_COOKIE, overrides, COOKIE_OPTIONS);
|
|
||||||
|
this._saveOverrideCookie(overrides);
|
||||||
|
|
||||||
const core = this.resolve('site')?.getCore?.();
|
const core = this.resolve('site')?.getCore?.();
|
||||||
if ( core )
|
if ( core )
|
||||||
|
@ -335,13 +474,14 @@ export default class ExperimentManager extends Module {
|
||||||
this._rebuildTwitchKey(key, false, old_val);
|
this._rebuildTwitchKey(key, false, old_val);
|
||||||
}
|
}
|
||||||
|
|
||||||
hasTwitchOverride(key) { // eslint-disable-line class-methods-use-this
|
hasTwitchOverride(key: string) { // eslint-disable-line class-methods-use-this
|
||||||
const overrides = Cookie.getJSON(OVERRIDE_COOKIE),
|
const overrides = this._getOverrideCookie(),
|
||||||
experiments = overrides?.experiments;
|
experiments = overrides.experiments;
|
||||||
return experiments && has(experiments, key);
|
|
||||||
|
return has(experiments, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
getTwitchAssignment(key, channel = null) {
|
getTwitchAssignment(key: string, channel: string | null = null) {
|
||||||
const core = this.resolve('site')?.getCore?.(),
|
const core = this.resolve('site')?.getCore?.(),
|
||||||
exps = core && core.experiments;
|
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);
|
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] )
|
if ( exps.overrides && exps.overrides[key] )
|
||||||
return 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];
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
getTwitchKeyFromName(name) {
|
getTwitchKeyFromName(name: string) {
|
||||||
const experiments = this.getTwitchExperiments();
|
const experiments = this.getTwitchExperiments();
|
||||||
if ( ! experiments )
|
if ( ! experiments )
|
||||||
return undefined;
|
return;
|
||||||
|
|
||||||
name = name.toLowerCase();
|
name = name.toLowerCase();
|
||||||
for(const key in experiments)
|
for(const key in experiments)
|
||||||
|
@ -381,30 +546,37 @@ export default class ExperimentManager extends Module {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getTwitchAssignmentByName(name, channel = null) {
|
getTwitchAssignmentByName(name: string, channel: string | null = null) {
|
||||||
return this.getTwitchAssignment(this.getTwitchKeyFromName(name), channel);
|
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?.(),
|
const core = this.resolve('site')?.getCore?.(),
|
||||||
exps = core.experiments,
|
exps = core.experiments,
|
||||||
|
|
||||||
old_val = has(exps.assignments, key) ?
|
old_val = has(exps.assignments, key) ?
|
||||||
exps.assignments[key] :
|
exps.assignments[key] as string :
|
||||||
undefined;
|
null;
|
||||||
|
|
||||||
if ( old_val !== new_val ) {
|
if ( old_val !== new_val ) {
|
||||||
const value = is_set ? new_val : old_val;
|
const value = is_set ? new_val : old_val;
|
||||||
this.emit(':twitch-changed', key, value);
|
this.emit(':twitch-changed', key, value, old_val);
|
||||||
this.emit(`:twitch-changed:${key}`, value);
|
this.emit(`:twitch-changed:${key}`, value, old_val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// FFZ Experiments
|
// FFZ Experiments
|
||||||
|
|
||||||
setOverride(key, value = null) {
|
setOverride(key: string, value: unknown = null) {
|
||||||
const overrides = this.settings.provider.get('experiment-overrides') || {};
|
const overrides = this.settings.provider.get('experiment-overrides', {});
|
||||||
overrides[key] = value;
|
overrides[key] = value;
|
||||||
|
|
||||||
this.settings.provider.set('experiment-overrides', overrides);
|
this.settings.provider.set('experiment-overrides', overrides);
|
||||||
|
@ -412,25 +584,30 @@ export default class ExperimentManager extends Module {
|
||||||
this._rebuildKey(key);
|
this._rebuildKey(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteOverride(key) {
|
deleteOverride(key: string) {
|
||||||
const overrides = this.settings.provider.get('experiment-overrides');
|
const overrides = this.settings.provider.get('experiment-overrides');
|
||||||
if ( ! overrides || ! has(overrides, key) )
|
if ( ! overrides || ! has(overrides, key) )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
delete overrides[key];
|
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);
|
this._rebuildKey(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
hasOverride(key) {
|
hasOverride(key: string) {
|
||||||
const overrides = this.settings.provider.get('experiment-overrides');
|
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) )
|
if ( this.cache.has(key) )
|
||||||
return this.cache.get(key);
|
return this.cache.get(key) as T;
|
||||||
|
|
||||||
const experiment = this.experiments[key];
|
const experiment = this.experiments[key];
|
||||||
if ( ! experiment ) {
|
if ( ! experiment ) {
|
||||||
|
@ -440,14 +617,14 @@ export default class ExperimentManager extends Module {
|
||||||
|
|
||||||
const overrides = this.settings.provider.get('experiment-overrides'),
|
const overrides = this.settings.provider.get('experiment-overrides'),
|
||||||
out = overrides && has(overrides, key) ?
|
out = overrides && has(overrides, key) ?
|
||||||
overrides[key] :
|
overrides[key] as T :
|
||||||
ExperimentManager.selectGroup(key, experiment, this.unique_id);
|
ExperimentManager.selectGroup<T>(key, experiment, this.unique_id ?? '');
|
||||||
|
|
||||||
this.cache.set(key, out);
|
this.cache.set(key, out);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
_rebuildKey(key) {
|
_rebuildKey(key: string) {
|
||||||
if ( ! this.cache.has(key) )
|
if ( ! this.cache.has(key) )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
@ -456,13 +633,17 @@ export default class ExperimentManager extends Module {
|
||||||
const new_val = this.getAssignment(key);
|
const new_val = this.getAssignment(key);
|
||||||
|
|
||||||
if ( new_val !== old_val ) {
|
if ( new_val !== old_val ) {
|
||||||
this.emit(':changed', key, new_val);
|
this.emit(':changed', key, new_val, old_val);
|
||||||
this.emit(`:changed:${key}`, new_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 || ''),
|
const seed = key + unique_id + (experiment.seed || ''),
|
||||||
total = experiment.groups.reduce((a,b) => a + b.weight, 0);
|
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) {
|
for(const group of experiment.groups) {
|
||||||
value -= group.weight / total;
|
value -= group.weight / total;
|
||||||
if ( value <= 0 )
|
if ( value <= 0 )
|
||||||
return group.value;
|
return group.value as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -7,6 +7,20 @@
|
||||||
import Module, { GenericModule } from 'utilities/module';
|
import Module, { GenericModule } from 'utilities/module';
|
||||||
import type SettingsManager from './settings';
|
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 = {
|
type PendingLoadData = {
|
||||||
pending: Set<string>;
|
pending: Set<string>;
|
||||||
timers: Record<string, ReturnType<typeof setTimeout> | null>;
|
timers: Record<string, ReturnType<typeof setTimeout> | null>;
|
||||||
|
|
|
@ -16,7 +16,7 @@ import TranslationManager from './i18n';
|
||||||
import SocketClient from './socket';
|
import SocketClient from './socket';
|
||||||
import PubSubClient from './pubsub';
|
import PubSubClient from './pubsub';
|
||||||
import Site from 'site';
|
import Site from 'site';
|
||||||
import Vue from 'utilities/vue';
|
import VueModule from 'utilities/vue';
|
||||||
import StagingSelector from './staging';
|
import StagingSelector from './staging';
|
||||||
import LoadTracker from './load_tracker';
|
import LoadTracker from './load_tracker';
|
||||||
|
|
||||||
|
@ -138,7 +138,7 @@ class FrankerFaceZ extends Module {
|
||||||
this.inject('site', Site);
|
this.inject('site', Site);
|
||||||
this.inject('addons', AddonManager);
|
this.inject('addons', AddonManager);
|
||||||
|
|
||||||
this.register('vue', Vue);
|
this.register('vue', VueModule);
|
||||||
|
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
|
@ -1554,4 +1554,4 @@ export function fixBadgeData(badge) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return badge;
|
return badge;
|
||||||
}
|
}
|
||||||
|
|
|
@ -633,7 +633,7 @@ export default {
|
||||||
// TODO: Update timestamps for pinned chat?
|
// 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 ) {
|
if ( item.chat ) {
|
||||||
|
@ -731,4 +731,4 @@ export default {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -173,7 +173,7 @@
|
||||||
@change="onTwitchChange($event)"
|
@change="onTwitchChange($event)"
|
||||||
>
|
>
|
||||||
<option
|
<option
|
||||||
v-if="exp.in_use === false"
|
v-if="exp.value === null"
|
||||||
:selected="exp.default"
|
:selected="exp.default"
|
||||||
>
|
>
|
||||||
{{ t('setting.experiments.unset', 'unset') }}
|
{{ 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" />
|
<figure class="ffz-i-discord tw-font-size-3" />
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<!--a
|
||||||
:data-title="t('home.twitter', 'Twitter')"
|
:data-title="t('home.twitter', 'Twitter')"
|
||||||
class="tw-flex-grow-1 tw-button ffz-tooltip ffz--twitter-button tw-mg-r-1"
|
class="tw-flex-grow-1 tw-button ffz-tooltip ffz--twitter-button tw-mg-r-1"
|
||||||
href="https://twitter.com/frankerfacez"
|
href="https://twitter.com/frankerfacez"
|
||||||
|
@ -175,7 +175,7 @@
|
||||||
<span class="tw-button__icon tw-pd-05">
|
<span class="tw-button__icon tw-pd-05">
|
||||||
<figure class="ffz-i-twitter tw-font-size-3" />
|
<figure class="ffz-i-twitter tw-font-size-3" />
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a-->
|
||||||
<a
|
<a
|
||||||
:data-title="t('home.github', 'GitHub')"
|
:data-title="t('home.github', 'GitHub')"
|
||||||
class="tw-flex-grow-1 tw-button ffz-tooltip ffz--github-button"
|
class="tw-flex-grow-1 tw-button ffz-tooltip ffz--github-button"
|
||||||
|
@ -189,7 +189,12 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</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
|
<a
|
||||||
:data-theme="theme"
|
:data-theme="theme"
|
||||||
class="twitter-timeline"
|
class="twitter-timeline"
|
||||||
|
@ -198,7 +203,7 @@
|
||||||
>
|
>
|
||||||
{{ t('home.tweets', 'Tweets by FrankerFaceZ') }}
|
{{ t('home.tweets', 'Tweets by FrankerFaceZ') }}
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template-->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -221,7 +226,7 @@ export default {
|
||||||
addons: null,
|
addons: null,
|
||||||
new_addons: null,
|
new_addons: null,
|
||||||
unseen: this.item.getUnseen(),
|
unseen: this.item.getUnseen(),
|
||||||
not_extension: ! EXTENSION
|
//not_extension: ! EXTENSION
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -243,7 +248,7 @@ export default {
|
||||||
ffz.off('addons:data-loaded', this.updateAddons, this);
|
ffz.off('addons:data-loaded', this.updateAddons, this);
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
/*mounted() {
|
||||||
let el;
|
let el;
|
||||||
if ( this.not_extension )
|
if ( this.not_extension )
|
||||||
document.head.appendChild(el = e('script', {
|
document.head.appendChild(el = e('script', {
|
||||||
|
@ -253,7 +258,7 @@ export default {
|
||||||
src: 'https://platform.twitter.com/widgets.js',
|
src: 'https://platform.twitter.com/widgets.js',
|
||||||
onLoad: () => el.remove()
|
onLoad: () => el.remove()
|
||||||
}));
|
}));
|
||||||
},
|
},*/
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
updateUnseen() {
|
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 = {
|
export type MetadataState = {
|
||||||
/** Whether or not the metadata is being rendered onto the player directly. */
|
/** 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',
|
icon: 'ffz-i-download',
|
||||||
|
|
||||||
click(src) {
|
click(src) {
|
||||||
const title = this.settings.get('context.title');
|
const title = this.settings.get('context.title') || 'Untitled';
|
||||||
const name = title.replace(/[\\/:"*?<>|]+/, '_') + '.mp4';
|
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);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
|
|
|
@ -20,6 +20,16 @@ declare global {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module 'utilities/types' {
|
||||||
|
interface ModuleEventMap {
|
||||||
|
tooltips: TooltipEvents;
|
||||||
|
}
|
||||||
|
interface ModuleMap {
|
||||||
|
tooltips: TooltipProvider;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export type TooltipEvents = {
|
export type TooltipEvents = {
|
||||||
/**
|
/**
|
||||||
* When this event is emitted, the tooltip provider will attempt to remove
|
* 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 {has, get as getter, array_equals, set_equals, map_equals, deep_equals} from 'utilities/object';
|
||||||
|
|
||||||
import * as DEFINITIONS from './typehandlers';
|
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.
|
* 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
|
* @param {Map} old_cache
|
||||||
* @returns Whether or not they changed.
|
* @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) )
|
if ( ! definition || ! Array.isArray(definition.requires) )
|
||||||
return false;
|
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
|
* The SettingsContext class provides a context through which to read
|
||||||
* settings values in addition to emitting events when settings values
|
* settings values in addition to emitting events when settings values
|
||||||
* are changed.
|
* are changed.
|
||||||
* @extends EventEmitter
|
|
||||||
*/
|
*/
|
||||||
export default class SettingsContext extends EventEmitter {
|
export default class SettingsContext extends EventEmitter<SettingsContextEvents> {
|
||||||
constructor(manager, context) {
|
|
||||||
|
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();
|
super();
|
||||||
|
|
||||||
if ( manager instanceof SettingsContext ) {
|
if ( manager instanceof SettingsContext ) {
|
||||||
|
@ -68,7 +106,7 @@ export default class SettingsContext extends EventEmitter {
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.manager.__contexts.push(this);
|
(this.manager as any).__contexts.push(this);
|
||||||
this._context = context || {};
|
this._context = context || {};
|
||||||
|
|
||||||
/*this._context_objects = new Set;
|
/*this._context_objects = new Set;
|
||||||
|
@ -93,7 +131,7 @@ export default class SettingsContext extends EventEmitter {
|
||||||
for(const profile of this.__profiles)
|
for(const profile of this.__profiles)
|
||||||
profile.off('changed', this._onChanged, this);
|
profile.off('changed', this._onChanged, this);
|
||||||
|
|
||||||
const contexts = this.manager.__contexts,
|
const contexts = (this.manager as any).__contexts,
|
||||||
idx = contexts.indexOf(this);
|
idx = contexts.indexOf(this);
|
||||||
|
|
||||||
if ( idx !== -1 )
|
if ( idx !== -1 )
|
||||||
|
@ -106,26 +144,26 @@ export default class SettingsContext extends EventEmitter {
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
_watchLS() {
|
_watchLS() {
|
||||||
if ( this.__ls_watched )
|
if ( this.__ls_listening )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.__ls_watched = true;
|
this.__ls_listening = true;
|
||||||
this.manager.on(':ls-update', this._onLSUpdate, this);
|
this.manager.on(':ls-update', this._onLSUpdate, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
_unwatchLS() {
|
_unwatchLS() {
|
||||||
if ( ! this.__ls_watched )
|
if ( ! this.__ls_listening )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.__ls_watched = false;
|
this.__ls_listening = false;
|
||||||
this.manager.off(':ls-update', this._onLSUpdate, this);
|
this.manager.off(':ls-update', this._onLSUpdate, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onLSUpdate(key) {
|
_onLSUpdate(key: string) {
|
||||||
const keys = this.__ls_wanted.get(`ls.${key}`);
|
const keys = this.__ls_wanted.get(`ls.${key}`);
|
||||||
if ( keys )
|
if ( keys )
|
||||||
for(const key of 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() {
|
selectProfiles() {
|
||||||
const new_profiles = [],
|
const new_profiles: SettingsProfile[] = [],
|
||||||
order = this.order = [];
|
order: number[] = this.order = [];
|
||||||
|
|
||||||
if ( ! this.manager.disable_profiles ) {
|
if ( ! this.manager.disable_profiles ) {
|
||||||
for(const profile of this.manager.__profiles)
|
for(const profile of this.manager.__profiles)
|
||||||
|
@ -171,13 +209,13 @@ export default class SettingsContext extends EventEmitter {
|
||||||
|
|
||||||
for(const profile of new_profiles)
|
for(const profile of new_profiles)
|
||||||
if ( ! this.__profiles.includes(profile) ) {
|
if ( ! this.__profiles.includes(profile) ) {
|
||||||
profile.on('changed', this._onChanged, this);
|
profile.on('changed', this._onChanged as any, this);
|
||||||
changed_ids.add(profile.id);
|
changed_ids.add(profile.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.__profiles = new_profiles;
|
this.__profiles = new_profiles;
|
||||||
this.emit('profiles_changed');
|
this.emit('profiles_changed');
|
||||||
this.rebuildCache(changed_ids);
|
this.rebuildCache(/*changed_ids*/);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,7 +241,7 @@ export default class SettingsContext extends EventEmitter {
|
||||||
const definition = this.manager.definitions.get(key);
|
const definition = this.manager.definitions.get(key);
|
||||||
let changed = false;
|
let changed = false;
|
||||||
|
|
||||||
if ( definition && definition.equals ) {
|
if ( ! Array.isArray(definition) && definition?.equals ) {
|
||||||
if ( definition.equals === 'requirements' )
|
if ( definition.equals === 'requirements' )
|
||||||
changed = compare_requirements(definition, this.__cache, old_cache);
|
changed = compare_requirements(definition, this.__cache, old_cache);
|
||||||
else if ( typeof definition.equals === 'function' )
|
else if ( typeof definition.equals === 'function' )
|
||||||
|
@ -224,7 +262,7 @@ export default class SettingsContext extends EventEmitter {
|
||||||
|
|
||||||
if ( changed ) {
|
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);
|
this.emit(`changed:${key}`, new_value, old_value as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( ! array_equals(new_uses, old_uses) ) {
|
if ( ! array_equals(new_uses, old_uses) ) {
|
||||||
|
@ -239,12 +277,12 @@ export default class SettingsContext extends EventEmitter {
|
||||||
// Context Control
|
// Context Control
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
context(context) {
|
context(context: ContextData) {
|
||||||
return new SettingsContext(this, context);
|
return new SettingsContext(this, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
updateContext(context) {
|
updateContext(context: ContextData) {
|
||||||
let changed = false;
|
let changed = false;
|
||||||
|
|
||||||
for(const key in context)
|
for(const key in context)
|
||||||
|
@ -258,7 +296,7 @@ export default class SettingsContext extends EventEmitter {
|
||||||
// This can catch a recursive structure error.
|
// This can catch a recursive structure error.
|
||||||
}
|
}
|
||||||
|
|
||||||
this._context[key] = val;
|
this._context[key] = val as any;
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -325,8 +363,8 @@ export default class SettingsContext extends EventEmitter {
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
|
|
||||||
setContext(context) {
|
setContext(context: ContextData) {
|
||||||
this._context_objects = new Set;
|
//this._context_objects = new Set;
|
||||||
this._context = {};
|
this._context = {};
|
||||||
this.updateContext(context);
|
this.updateContext(context);
|
||||||
}
|
}
|
||||||
|
@ -336,11 +374,14 @@ export default class SettingsContext extends EventEmitter {
|
||||||
// Data Access
|
// Data Access
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
_onChanged(key) {
|
_onChanged(key: SettingsKeys) {
|
||||||
this._update(key, key, []);
|
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) )
|
if ( ! this.__cache.has(key) )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
@ -349,7 +390,7 @@ export default class SettingsContext extends EventEmitter {
|
||||||
|
|
||||||
visited.push(key);
|
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),
|
old_meta = this.__meta.get(key),
|
||||||
new_value = this._get(key, key, []),
|
new_value = this._get(key, key, []),
|
||||||
new_meta = this.__meta.get(key),
|
new_meta = this.__meta.get(key),
|
||||||
|
@ -359,38 +400,41 @@ export default class SettingsContext extends EventEmitter {
|
||||||
|
|
||||||
if ( ! array_equals(new_uses, old_uses) ) {
|
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}`, new_uses, old_uses);
|
this.emit(`uses_changed:${key}` as any, new_uses, old_uses);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( old_value === new_value )
|
if ( old_value === new_value )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.emit('changed', key, new_value, old_value);
|
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);
|
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)
|
for(const req_key of definition.required_by)
|
||||||
if ( ! req_key.startsWith('context.') && ! req_key.startsWith('ls.') )
|
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) )
|
if ( visited.includes(key) )
|
||||||
throw new Error(`cyclic dependency when resolving setting "${initial}"`);
|
throw new Error(`cyclic dependency when resolving setting "${initial}"`);
|
||||||
|
|
||||||
visited.push(key);
|
visited.push(key);
|
||||||
|
|
||||||
const definition = this.manager.definitions.get(key),
|
const definition = this.manager.definitions.get(key);
|
||||||
raw_type = definition && definition.type,
|
const raw_type = ! Array.isArray(definition) && definition?.type,
|
||||||
type = raw_type ? DEFINITIONS[raw_type] : DEFINITIONS.basic;
|
type = raw_type ? DEFINITIONS[raw_type] : DEFINITIONS.basic;
|
||||||
|
|
||||||
if ( ! type )
|
if ( ! type )
|
||||||
throw new Error(`non-existent setting type "${raw_type}"`);
|
throw new Error(`non-existent setting type "${raw_type}"`);
|
||||||
|
|
||||||
const raw_value = this._getRaw(key, type),
|
const raw_value = this._getRaw(key, type),
|
||||||
meta = {
|
meta: SettingMetadata = {
|
||||||
uses: raw_value ? raw_value[1] : null
|
uses: raw_value ? raw_value[1] : null
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -421,8 +465,8 @@ export default class SettingsContext extends EventEmitter {
|
||||||
|
|
||||||
keys.add(key);
|
keys.add(key);
|
||||||
|
|
||||||
} else if ( ! req_key.startsWith('context.') && ! this.__cache.has(req_key) )
|
} else if ( ! req_key.startsWith('context.') && ! this.__cache.has(req_key as SettingsKeys) )
|
||||||
this._get(req_key, initial, Array.from(visited));
|
this._get(req_key as SettingsKeys, initial, Array.from(visited));
|
||||||
|
|
||||||
if ( definition.process )
|
if ( definition.process )
|
||||||
value = definition.process(this, value, meta);
|
value = definition.process(this, value, meta);
|
||||||
|
@ -440,70 +484,84 @@ export default class SettingsContext extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
hasProfile(profile) {
|
hasProfile(profile: number | SettingsProfile) {
|
||||||
if ( typeof profile === 'number' )
|
if ( typeof profile === 'number' ) {
|
||||||
for(const prof of this.__profiles)
|
for(const prof of this.__profiles)
|
||||||
if ( prof.id === profile )
|
if ( prof.id === profile )
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return this.__profiles.includes(profile);
|
return this.__profiles.includes(profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
_getRaw(key, type) {
|
_getRaw(key: SettingsKeys, type) {
|
||||||
if ( ! type )
|
if ( ! type )
|
||||||
throw new Error(`non-existent type for ${key}`)
|
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
|
// Data Access
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
update(key) {
|
update(key: SettingsKeys) {
|
||||||
this._update(key, key, []);
|
this._update(key, key, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
get(key) {
|
get<
|
||||||
|
K extends AllSettingsKeys,
|
||||||
|
TValue = SettingType<K>
|
||||||
|
>(key: K): TValue {
|
||||||
if ( key.startsWith('ls.') )
|
if ( key.startsWith('ls.') )
|
||||||
return this.manager.getLS(key.slice(3));
|
return this.manager.getLS(key.slice(3)) as TValue;
|
||||||
|
|
||||||
if ( key.startsWith('context.') )
|
if ( key.startsWith('context.') )
|
||||||
//return this.__context[key.slice(8)];
|
//return this.__context[key.slice(8)];
|
||||||
return getter(key.slice(8), this.__context);
|
return getter(key.slice(8), this.__context);
|
||||||
|
|
||||||
if ( this.__cache.has(key) )
|
if ( this.__cache.has(key as SettingsKeys) )
|
||||||
return this.__cache.get(key);
|
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);
|
this.onChange(key, fn, ctx);
|
||||||
fn.call(ctx, this.get(key));
|
fn.call(ctx, this.get(key), undefined as TValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange(key, fn, ctx) {
|
onChange<
|
||||||
this.on(`changed:${key}`, fn, ctx);
|
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.') )
|
if ( key.startsWith('ls.') )
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if ( key.startsWith('context.') )
|
if ( key.startsWith('context.') )
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if ( ! this.__meta.has(key) )
|
if ( ! this.__meta.has(key as SettingsKeys) )
|
||||||
this._get(key, key, []);
|
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
|
// Settings System
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
import { DEBUG } from 'utilities/constants';
|
||||||
import Module, { GenericModule, buildAddonProxy } from 'utilities/module';
|
import Module, { GenericModule, buildAddonProxy } from 'utilities/module';
|
||||||
import {deep_equals, has, debounce, deep_copy} from 'utilities/object';
|
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 SettingsProfile from './profile';
|
||||||
import SettingsContext from './context';
|
import SettingsContext from './context';
|
||||||
//import MigrationManager from './migration';
|
|
||||||
|
|
||||||
import * as PROCESSORS from './processors';
|
import * as PROCESSORS from './processors';
|
||||||
import * as VALIDATORS from './validators';
|
import * as VALIDATORS from './validators';
|
||||||
import * as FILTERS from './filters';
|
import * as FILTERS from './filters';
|
||||||
import * as CLEARABLES from './clearables';
|
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 { 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';
|
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 {
|
try {
|
||||||
target.postMessage(msg, '*');
|
(target as Window).postMessage(msg, '*');
|
||||||
return true;
|
return true;
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -36,14 +38,33 @@ function postMessage(target: Window, msg) {
|
||||||
export const NO_SYNC_KEYS = ['session'];
|
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.
|
// TODO: Check settings keys for better typing on events.
|
||||||
|
|
||||||
export type SettingsEvents = {
|
export type SettingsEvents = {
|
||||||
[key: `:changed:${string}`]: [value: any, old_value: any];
|
[K in keyof SettingsTypeMap as `:changed:${K}`]: [value: SettingsTypeMap[K], old_value: SettingsTypeMap[K]];
|
||||||
[key: `:uses_changed:${string}`]: [uses: number[], old_uses: number[]];
|
} & {
|
||||||
|
[key: `:uses_changed:${string}`]: [uses: number[] | null, old_uses: number[] | null];
|
||||||
|
|
||||||
':added-definition': [key: string, definition: SettingsDefinition<any>];
|
':added-definition': [key: SettingsKeys, definition: SettingDefinition<any>];
|
||||||
':removed-definition': [key: string, definition: SettingsDefinition<any>];
|
':removed-definition': [key: SettingsKeys, definition: SettingDefinition<any>];
|
||||||
|
|
||||||
':quota-exceeded': [];
|
':quota-exceeded': [];
|
||||||
':change-provider': [];
|
':change-provider': [];
|
||||||
|
@ -81,29 +102,35 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
||||||
// Storage of Things
|
// Storage of Things
|
||||||
clearables: Record<string, SettingsClearable>;
|
clearables: Record<string, SettingsClearable>;
|
||||||
filters: Record<string, FilterType<any, ContextData>>;
|
filters: Record<string, FilterType<any, ContextData>>;
|
||||||
processors: Record<string, SettingsProcessor<any>>;
|
processors: Record<string, SettingProcessor<any>>;
|
||||||
providers: Record<string, typeof SettingsProvider>;
|
providers: Record<string, typeof SettingsProvider>;
|
||||||
validators: Record<string, SettingsValidator<any>>;
|
validators: Record<string, SettingValidator<any>>;
|
||||||
|
|
||||||
// Storage of Settings
|
// Storage of Settings
|
||||||
ui_structures: Map<string, SettingsUiDefinition<any>>;
|
ui_structures: Map<string, SettingDefinition<any>>;
|
||||||
definitions: Map<string, SettingsDefinition<any> | string[]>;
|
definitions: Map<string, SettingDefinition<any> | string[]>;
|
||||||
|
|
||||||
// Storage of State
|
// 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;
|
main_context: SettingsContext;
|
||||||
|
|
||||||
|
private _context_proxies: Set<MessageEventSource>;
|
||||||
|
|
||||||
private _update_timer?: ReturnType<typeof setTimeout> | null;
|
private _update_timer?: ReturnType<typeof setTimeout> | null;
|
||||||
private _time_timer?: ReturnType<typeof setTimeout> | null;
|
private _time_timer?: ReturnType<typeof setTimeout> | null;
|
||||||
|
private _time_next?: number | null;
|
||||||
|
|
||||||
private _active_provider: string = 'local';
|
private _active_provider: string = 'local';
|
||||||
private _idb: IndexedDBProvider | null = null;
|
|
||||||
|
|
||||||
private _provider_waiter?: Promise<SettingsProvider> | null;
|
private _provider_waiter?: Promise<SettingsProvider> | null;
|
||||||
private _provider_resolve?: ((input: SettingsProvider) => void) | null;
|
private _provider_resolve?: ((input: SettingsProvider) => void) | null;
|
||||||
|
|
||||||
private __contexts: SettingsContext[];
|
/** @internal */
|
||||||
private __profiles: SettingsProfile[];
|
__contexts: SettingsContext[];
|
||||||
|
/** @internal */
|
||||||
|
__profiles: SettingsProfile[];
|
||||||
private __profile_ids: Record<number, SettingsProfile | null>;
|
private __profile_ids: Record<number, SettingsProfile | null>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -123,8 +150,6 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
||||||
this.providers[key] = provider;
|
this.providers[key] = provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
const test = this.resolve('load_tracker');
|
|
||||||
|
|
||||||
// This cannot be modified at a future time, as providers NEED
|
// This cannot be modified at a future time, as providers NEED
|
||||||
// to be ready very early in FFZ intitialization. Seal it.
|
// to be ready very early in FFZ intitialization. Seal it.
|
||||||
Object.seal(this.providers);
|
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.
|
// Also create the main context as early as possible.
|
||||||
this.main_context = new SettingsContext(this);
|
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.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);
|
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 => {
|
window.addEventListener('message', event => {
|
||||||
const type = event.data?.ffz_type;
|
const type = event.data?.ffz_type;
|
||||||
|
|
||||||
if ( type === 'request-context' ) {
|
if ( type === 'request-context' && event.source ) {
|
||||||
this._context_proxies.add(event.source);
|
this._context_proxies.add(event.source);
|
||||||
this._updateContextProxies(event.source);
|
this._updateContextProxies(event.source);
|
||||||
}
|
}
|
||||||
|
@ -234,7 +259,7 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
||||||
this.enable();
|
this.enable();
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateContextProxies(proxy) {
|
_updateContextProxies(proxy?: MessageEventSource) {
|
||||||
if ( ! proxy && ! this._context_proxies.size )
|
if ( ! proxy && ! this._context_proxies.size )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
@ -266,7 +291,7 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
||||||
|
|
||||||
generateLog() {
|
generateLog() {
|
||||||
const out = [];
|
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)}`);
|
out.push(`${key}: ${JSON.stringify(value)}`);
|
||||||
|
|
||||||
return out.join('\n');
|
return out.join('\n');
|
||||||
|
@ -390,6 +415,9 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
||||||
if ( ! this.__ls_timer )
|
if ( ! this.__ls_timer )
|
||||||
this.__ls_timer = setTimeout(this._updateLS, 0);
|
this.__ls_timer = setTimeout(this._updateLS, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _hookLS() {
|
private _hookLS() {
|
||||||
|
@ -532,7 +560,7 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
||||||
out.file('settings.json', JSON.stringify(settings));
|
out.file('settings.json', JSON.stringify(settings));
|
||||||
|
|
||||||
// Blob Settings
|
// Blob Settings
|
||||||
const metadata = {};
|
const metadata: Record<string, ExportedBlobMetadata> = {};
|
||||||
|
|
||||||
if ( this.provider instanceof AdvancedSettingsProvider && this.provider.supportsBlobs ) {
|
if ( this.provider instanceof AdvancedSettingsProvider && this.provider.supportsBlobs ) {
|
||||||
const keys = await this.provider.blobKeys();
|
const keys = await this.provider.blobKeys();
|
||||||
|
@ -542,7 +570,9 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
||||||
if ( ! blob )
|
if ( ! blob )
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
const md = {key};
|
const md: ExportedBlobMetadata = {
|
||||||
|
key
|
||||||
|
};
|
||||||
|
|
||||||
if ( blob instanceof File ) {
|
if ( blob instanceof File ) {
|
||||||
md.type = 'file';
|
md.type = 'file';
|
||||||
|
@ -664,9 +694,6 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
||||||
|
|
||||||
if ( this.providers[wanted] ) {
|
if ( this.providers[wanted] ) {
|
||||||
const provider = new (this.providers[wanted] as any)(this) as SettingsProvider;
|
const provider = new (this.providers[wanted] as any)(this) as SettingsProvider;
|
||||||
if ( wanted === 'idb' )
|
|
||||||
this._idb = provider as IndexedDBProvider;
|
|
||||||
|
|
||||||
this._active_provider = wanted;
|
this._active_provider = wanted;
|
||||||
return provider;
|
return provider;
|
||||||
}
|
}
|
||||||
|
@ -721,7 +748,7 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const old_provider = this.provider;
|
const old_provider = this.provider;
|
||||||
this.provider = null;
|
this.provider = null as any;
|
||||||
|
|
||||||
// Let all other tabs know what's up.
|
// Let all other tabs know what's up.
|
||||||
old_provider.broadcastTransfer();
|
old_provider.broadcastTransfer();
|
||||||
|
@ -1101,15 +1128,38 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
||||||
// Context Helpers
|
// Context Helpers
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
context(env) { return this.main_context.context(env) }
|
context(env: ContextData) { 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) }
|
|
||||||
|
|
||||||
updateContext(context: Partial<ContextData>) { return this.main_context.updateContext(context) }
|
get<
|
||||||
setContext(context: Partial<ContextData>) { return this.main_context.setContext(context) }
|
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> = {},
|
const overrides: Record<string, any> = {},
|
||||||
is_dev = DEBUG || addon?.dev;
|
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);
|
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);
|
return this.addUI(key, definition, addon_id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1142,7 +1199,10 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
||||||
// Definitions
|
// 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),
|
const old_definition = this.definitions.get(key),
|
||||||
required_by = (Array.isArray(old_definition)
|
required_by = (Array.isArray(old_definition)
|
||||||
|
@ -1179,8 +1239,10 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
||||||
if ( ! ui.key && ui.title )
|
if ( ! ui.key && ui.title )
|
||||||
ui.key = ui.title.toSnakeCase();
|
ui.key = ui.title.toSnakeCase();
|
||||||
|
|
||||||
if ( (ui.component === 'setting-select-box' || ui.component === 'setting-combo-box') && Array.isArray(ui.data) && ! ui.no_i18n
|
if ( (ui.component === 'setting-select-box' ||
|
||||||
&& key !== 'ffzap.core.highlight_sound' ) { // TODO: Remove workaround.
|
ui.component === 'setting-combo-box') &&
|
||||||
|
Array.isArray(ui.data) && ! ui.no_i18n
|
||||||
|
) {
|
||||||
const i18n_base = `${ui.i18n_key || `setting.entry.${key}`}.values`;
|
const i18n_base = `${ui.i18n_key || `setting.entry.${key}`}.values`;
|
||||||
for(const value of ui.data) {
|
for(const value of ui.data) {
|
||||||
if ( value.i18n_key === undefined && value.value !== undefined )
|
if ( value.i18n_key === undefined && value.value !== undefined )
|
||||||
|
@ -1190,7 +1252,7 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( definition.changed )
|
if ( definition.changed )
|
||||||
this.on(`:changed:${key}`, definition.changed);
|
this.on(`:changed:${key}` as any, definition.changed);
|
||||||
|
|
||||||
this.definitions.set(key, definition);
|
this.definitions.set(key, definition);
|
||||||
|
|
||||||
|
@ -1220,36 +1282,39 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
||||||
if ( Array.isArray(definition.requires) )
|
if ( Array.isArray(definition.requires) )
|
||||||
for(const req_key of definition.requires) {
|
for(const req_key of definition.requires) {
|
||||||
let req = this.definitions.get(req_key);
|
let req = this.definitions.get(req_key);
|
||||||
if ( req.required_by )
|
|
||||||
req = req.required_by;
|
|
||||||
if ( Array.isArray(req) ) {
|
if ( Array.isArray(req) ) {
|
||||||
const idx = req.indexOf(key);
|
const idx = req.indexOf(key);
|
||||||
if ( idx !== -1 )
|
if ( idx !== -1 )
|
||||||
req.splice(idx, 1);
|
req.splice(idx, 1);
|
||||||
}
|
|
||||||
|
} else if ( req?.required_by )
|
||||||
|
req = req.required_by;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( definition.changed )
|
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) {
|
// TODO: Update this because addUI doesn't use keys or types
|
||||||
if ( typeof key === 'object' ) {
|
addUI<
|
||||||
for(const k in key)
|
K extends string,
|
||||||
if ( has(key, k) )
|
TValue = K extends SettingsKeys ? SettingType<K> : unknown
|
||||||
this.add(k, key[k]);
|
>(key: K, definition: Partial<SettingUiDefinition<TValue>>, source?: string) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( ! definition.ui )
|
let def: SettingDefinition<TValue>;
|
||||||
definition = {ui: definition};
|
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 ?
|
ui.path_tokens = ui.path_tokens ?
|
||||||
format_path_tokens(ui.path_tokens) :
|
format_path_tokens(ui.path_tokens) :
|
||||||
ui.path ?
|
ui.path ?
|
||||||
|
@ -1260,12 +1325,12 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
||||||
ui.key = ui.title.toSnakeCase();
|
ui.key = ui.title.toSnakeCase();
|
||||||
|
|
||||||
const old_definition = this.ui_structures.get(key);
|
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
|
// Do not re-emit `added-definition` when re-adding an existing
|
||||||
// setting. Prevents the settings UI from goofing up.
|
// setting. Prevents the settings UI from goofing up.
|
||||||
if ( ! old_definition )
|
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' ) {
|
if ( typeof key === 'object' ) {
|
||||||
for(const [k, value] of Object.entries(key))
|
for(const [k, value] of Object.entries(key))
|
||||||
this.addProcessor(k, value);
|
this.addProcessor(k, value);
|
||||||
|
@ -1300,7 +1365,7 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
||||||
this.processors[key] = processor;
|
this.processors[key] = processor;
|
||||||
}
|
}
|
||||||
|
|
||||||
getProcessor<T>(key: string): SettingsProcessor<T> | null {
|
getProcessor<T>(key: string): SettingProcessor<T> | null {
|
||||||
return this.processors[key] ?? null;
|
return this.processors[key] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1308,7 +1373,7 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
||||||
return deep_copy(this.processors);
|
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' ) {
|
if ( typeof key === 'object' ) {
|
||||||
for(const [k, value] of Object.entries(key))
|
for(const [k, value] of Object.entries(key))
|
||||||
this.addValidator(k, value);
|
this.addValidator(k, value);
|
||||||
|
@ -1320,7 +1385,7 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
||||||
this.validators[key] = validator;
|
this.validators[key] = validator;
|
||||||
}
|
}
|
||||||
|
|
||||||
getValidator<T>(key: string): SettingsValidator<T> | null {
|
getValidator<T>(key: string): SettingValidator<T> | null {
|
||||||
return this.validators[key] ?? 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++) {
|
for(let i=0, l = tokens.length; i < l; i++) {
|
||||||
const token = tokens[i];
|
const token = tokens[i];
|
||||||
if ( typeof token === 'string' ) {
|
if ( typeof token === 'string' ) {
|
||||||
tokens[i] = {
|
tokens[i] = {
|
||||||
key: token.toSnakeCase(),
|
key: token.toSnakeCase(),
|
||||||
title: token
|
title: token
|
||||||
}
|
};
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -1346,5 +1411,5 @@ export function format_path_tokens(tokens) {
|
||||||
token.key = token.title.toSnakeCase();
|
token.key = token.title.toSnakeCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
return tokens;
|
return tokens as PathNode[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import type { SettingsDefinition, SettingsProcessor, SettingsUiDefinition } from "./types";
|
import type { SettingDefinition, SettingProcessor, SettingUiDefinition } from "./types";
|
||||||
|
|
||||||
const BAD = Symbol('BAD');
|
const BAD = Symbol('BAD');
|
||||||
type BadType = typeof BAD;
|
type BadType = typeof BAD;
|
||||||
|
@ -8,7 +8,7 @@ type BadType = typeof BAD;
|
||||||
function do_number(
|
function do_number(
|
||||||
input: number | BadType,
|
input: number | BadType,
|
||||||
default_value: number,
|
default_value: number,
|
||||||
definition: SettingsUiDefinition<number>
|
definition: SettingUiDefinition<number>
|
||||||
) {
|
) {
|
||||||
if ( typeof input !== 'number' || isNaN(input) || ! isFinite(input) )
|
if ( typeof input !== 'number' || isNaN(input) || ! isFinite(input) )
|
||||||
input = BAD;
|
input = BAD;
|
||||||
|
@ -38,7 +38,7 @@ function do_number(
|
||||||
return input === BAD ? default_value : input;
|
return input === BAD ? default_value : input;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const to_int: SettingsProcessor<number> = (
|
export const to_int: SettingProcessor<number> = (
|
||||||
value,
|
value,
|
||||||
default_value,
|
default_value,
|
||||||
definition
|
definition
|
||||||
|
@ -51,7 +51,7 @@ export const to_int: SettingsProcessor<number> = (
|
||||||
return do_number(value as number, default_value, definition);
|
return do_number(value as number, default_value, definition);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const to_float: SettingsProcessor<number> = (
|
export const to_float: SettingProcessor<number> = (
|
||||||
value: unknown,
|
value: unknown,
|
||||||
default_value,
|
default_value,
|
||||||
definition
|
definition
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { isValidBlob, deserializeBlob, serializeBlob, BlobLike, SerializedBlobLi
|
||||||
import {EventEmitter} from 'utilities/events';
|
import {EventEmitter} from 'utilities/events';
|
||||||
import {has, once} from 'utilities/object';
|
import {has, once} from 'utilities/object';
|
||||||
import type SettingsManager from '.';
|
import type SettingsManager from '.';
|
||||||
import type { OptionalArray, OptionalPromise } from '../utilities/types';
|
import type { OptionalArray, OptionalPromise, ProviderTypeMap } from '../utilities/types';
|
||||||
|
|
||||||
const DB_VERSION = 1,
|
const DB_VERSION = 1,
|
||||||
NOT_WWW_TWITCH = window.location.host !== 'www.twitch.tv',
|
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 flush(): OptionalPromise<void>;
|
||||||
|
|
||||||
abstract get<T>(key: string, default_value: T): T;
|
abstract get<K extends keyof ProviderTypeMap>(
|
||||||
abstract get<T>(key: string): T | null;
|
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 delete(key: string): void;
|
||||||
abstract clear(): void;
|
abstract clear(): void;
|
||||||
|
|
||||||
|
@ -301,8 +314,10 @@ export class LocalStorageProvider extends SettingsProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get<T>(
|
||||||
get<T>(key: string, default_value?: T): T {
|
key: string,
|
||||||
|
default_value?: T
|
||||||
|
): T {
|
||||||
return this._cached.has(key)
|
return this._cached.has(key)
|
||||||
? this._cached.get(key)
|
? this._cached.get(key)
|
||||||
: default_value;
|
: default_value;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import type SettingsManager from ".";
|
import type SettingsManager from ".";
|
||||||
import type { FilterData } from "../utilities/filtering";
|
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 SettingsContext from "./context";
|
||||||
import type { SettingsProvider } from "./providers";
|
import type { SettingsProvider } from "./providers";
|
||||||
|
|
||||||
|
@ -24,7 +25,7 @@ export type SettingsClearable = {
|
||||||
|
|
||||||
// Context
|
// Context
|
||||||
|
|
||||||
export type ContextData = RecursivePartial<{
|
export interface ConcreteContextData {
|
||||||
addonDev: boolean;
|
addonDev: boolean;
|
||||||
|
|
||||||
category: string;
|
category: string;
|
||||||
|
@ -62,17 +63,52 @@ export type ContextData = RecursivePartial<{
|
||||||
theme: number;
|
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
|
// Definitions
|
||||||
|
|
||||||
export type SettingsDefinition<T> = {
|
export type SettingDefinition<T> = {
|
||||||
|
|
||||||
default: T,
|
default: T,
|
||||||
type?: string;
|
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
|
// Dependencies
|
||||||
required_by?: string[];
|
required_by?: string[];
|
||||||
|
@ -82,17 +118,26 @@ export type SettingsDefinition<T> = {
|
||||||
__source?: string | null;
|
__source?: string | null;
|
||||||
|
|
||||||
// UI Stuff
|
// UI Stuff
|
||||||
ui?: SettingsUiDefinition<T>;
|
ui?: SettingUiDefinition<T>;
|
||||||
|
|
||||||
// Reactivity
|
// Reactivity
|
||||||
changed?: () => void;
|
changed?: () => void;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SettingsUiDefinition<T> = {
|
export type SettingUiDefinition<T> = {
|
||||||
|
i18n_key?: string;
|
||||||
|
key: string;
|
||||||
path: string;
|
path: string;
|
||||||
|
path_tokens?: PathNode[];
|
||||||
|
|
||||||
component: string;
|
component: string;
|
||||||
|
|
||||||
|
no_i18n?: boolean;
|
||||||
|
|
||||||
|
// TODO: Handle this better.
|
||||||
|
data: any;
|
||||||
|
|
||||||
process?: string;
|
process?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -128,6 +173,15 @@ export type ExportedFullDump = {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type ExportedBlobMetadata = {
|
||||||
|
key: string;
|
||||||
|
type?: string;
|
||||||
|
name?: string;
|
||||||
|
modified?: number;
|
||||||
|
mime?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Profiles
|
// Profiles
|
||||||
|
|
||||||
|
@ -153,16 +207,16 @@ export type SettingsProfileMetadata = {
|
||||||
|
|
||||||
// Processors
|
// Processors
|
||||||
|
|
||||||
export type SettingsProcessor<T> = (
|
export type SettingProcessor<T> = (
|
||||||
input: unknown,
|
input: unknown,
|
||||||
default_value: T,
|
default_value: T,
|
||||||
definition: SettingsUiDefinition<T>
|
definition: SettingUiDefinition<T>
|
||||||
) => T;
|
) => T;
|
||||||
|
|
||||||
|
|
||||||
// Validators
|
// Validators
|
||||||
|
|
||||||
export type SettingsValidator<T> = (
|
export type SettingValidator<T> = (
|
||||||
value: T,
|
value: T,
|
||||||
definition: SettingsUiDefinition<T>
|
definition: SettingUiDefinition<T>
|
||||||
) => boolean;
|
) => boolean;
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
'use strict';
|
'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) )
|
if ( typeof value !== 'number' || isNaN(value) || ! isFinite(value) )
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ function do_number(value: any, definition: SettingsUiDefinition<number>) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const process_to_int: SettingsValidator<number> = (
|
export const process_to_int: SettingValidator<number> = (
|
||||||
value,
|
value,
|
||||||
definition
|
definition
|
||||||
) => {
|
) => {
|
||||||
|
@ -41,7 +41,7 @@ export const process_to_int: SettingsValidator<number> = (
|
||||||
return do_number(value, definition);
|
return do_number(value, definition);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const process_to_float: SettingsValidator<number> = (
|
export const process_to_float: SettingValidator<number> = (
|
||||||
value,
|
value,
|
||||||
definition
|
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) {
|
updateSubscription(id, login) {
|
||||||
if ( this._subbed_login === login && this._subbed_id === id )
|
if ( this._subbed_login === login && this._subbed_id === id )
|
||||||
return;
|
return;
|
||||||
|
@ -725,4 +680,4 @@ export default class Channel extends Module {
|
||||||
err ? pair[1](err) : pair[0](id);
|
err ? pair[1](err) : pair[0](id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,64 @@
|
||||||
'use strict';
|
'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
|
// 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 {
|
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;
|
this.should_enable = true;
|
||||||
|
|
||||||
|
@ -19,12 +68,18 @@ export default class Loadable extends Module {
|
||||||
|
|
||||||
this.LoadableComponent = this.fine.define(
|
this.LoadableComponent = this.fine.define(
|
||||||
'loadable-component',
|
'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(
|
this.ErrorBoundaryComponent = this.fine.define(
|
||||||
'error-boundary-component',
|
'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();
|
this.overrides = new Map();
|
||||||
|
@ -44,9 +99,10 @@ export default class Loadable extends Module {
|
||||||
this.log.debug('Found Error Boundary component wrapper.');
|
this.log.debug('Found Error Boundary component wrapper.');
|
||||||
|
|
||||||
const t = this,
|
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 {
|
try {
|
||||||
const type = this.props.name;
|
const type = this.props.name;
|
||||||
if ( t.overrides.has(type) && ! t.shouldRender(type) )
|
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.');
|
this.log.debug('Found Loadable component wrapper.');
|
||||||
|
|
||||||
const t = this,
|
const t = this,
|
||||||
old_render = cls.prototype.render;
|
proto = cls.prototype,
|
||||||
|
old_render = proto.render;
|
||||||
|
|
||||||
cls.prototype.render = function() {
|
proto.render = function() {
|
||||||
try {
|
try {
|
||||||
const type = this.props.component;
|
const type = this.props.component;
|
||||||
if ( t.overrides.has(type) ) {
|
if ( t.overrides.has(type) && this.state ) {
|
||||||
let cmp = this.state.Component;
|
let cmp = this.state.Component;
|
||||||
if ( typeof cmp === 'function' && ! cmp.ffzWrapped ) {
|
if ( typeof cmp === 'function' && ! (cmp as any).ffzWrapped ) {
|
||||||
const React = t.site.getReact(),
|
const React = t.site.getReact(),
|
||||||
createElement = React && React.createElement;
|
createElement = React && React.createElement;
|
||||||
|
|
||||||
if ( createElement ) {
|
if ( createElement ) {
|
||||||
if ( ! cmp.ffzWrapper ) {
|
if ( ! (cmp as any).ffzWrapper ) {
|
||||||
const th = this;
|
const th = this;
|
||||||
function FFZWrapper(props, state) {
|
function FFZWrapper(props: any) {
|
||||||
if ( t.shouldRender(th.props.component, props, state) )
|
if ( t.shouldRender(th.props.component) )
|
||||||
return createElement(cmp, props);
|
return createElement(cmp, props);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
FFZWrapper.ffzWrapped = true;
|
FFZWrapper.ffzWrapped = true;
|
||||||
FFZWrapper.displayName = `FFZWrapper(${this.props.component})`;
|
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;
|
const existing = this.overrides.get(cmp) ?? true;
|
||||||
|
|
||||||
if ( state == null )
|
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) {
|
for(const inst of this.LoadableComponent.instances) {
|
||||||
const type = inst?.props?.component;
|
const type = inst.props?.component;
|
||||||
if ( type && type === cmp )
|
if ( type && type === cmp )
|
||||||
inst.forceUpdate();
|
inst.forceUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
for(const inst of this.ErrorBoundaryComponent.instances) {
|
for(const inst of this.ErrorBoundaryComponent.instances) {
|
||||||
const name = inst?.props?.name;
|
const name = inst.props?.name;
|
||||||
if ( name && name === cmp )
|
if ( name && name === cmp )
|
||||||
inst.forceUpdate();
|
inst.forceUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldRender(cmp, props) {
|
shouldRender(cmp: string) {
|
||||||
return this.overrides.get(cmp) ?? true;
|
return this.overrides.get(cmp) ?? true;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -4,12 +4,50 @@
|
||||||
// Sub Button
|
// Sub Button
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
import Module from 'utilities/module';
|
import Module, { GenericModule } from 'utilities/module';
|
||||||
import {createElement} from 'utilities/dom';
|
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 {
|
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;
|
this.should_enable = true;
|
||||||
|
|
||||||
|
@ -32,39 +70,20 @@ export default class SubButton extends Module {
|
||||||
|
|
||||||
this.SubButton = this.fine.define(
|
this.SubButton = this.fine.define(
|
||||||
'sub-button',
|
'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']
|
['user', 'user-home', 'user-video', 'user-clip', 'video', 'user-videos', 'user-clips', 'user-collections', 'user-events', 'user-followers', 'user-following']
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onEnable() {
|
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) => {
|
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)
|
for(const inst of instances)
|
||||||
this.updateSubButton(inst);
|
this.updateSubButton(inst);
|
||||||
|
|
||||||
this.SubButton.forceUpdate();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.SubButton.on('mount', this.updateSubButton, this);
|
this.SubButton.on('mount', this.updateSubButton, this);
|
||||||
|
@ -72,9 +91,9 @@ export default class SubButton extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
updateSubButton(inst) {
|
updateSubButton(inst: SubButtonNode) {
|
||||||
const container = this.fine.getChildNode(inst),
|
const container = this.fine.getChildNode<HTMLElement>(inst),
|
||||||
btn = container && container.querySelector('button[data-a-target="subscribe-button"]');
|
btn = container?.querySelector('button[data-a-target="subscribe-button"]');
|
||||||
if ( ! btn )
|
if ( ! btn )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
@ -102,4 +121,4 @@ export default class SubButton extends Module {
|
||||||
post.remove();
|
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 {EventEmitter} from 'utilities/events';
|
||||||
import Module from 'utilities/module';
|
import Module, { GenericModule } from 'utilities/module';
|
||||||
|
|
||||||
export default class Elemental extends Module {
|
declare module 'utilities/types' {
|
||||||
constructor(...args) {
|
interface ModuleMap {
|
||||||
super(...args);
|
'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);
|
this._pruneLive = this._pruneLive.bind(this);
|
||||||
|
|
||||||
|
@ -21,27 +38,35 @@ export default class Elemental extends Module {
|
||||||
this._live_watching = null;
|
this._live_watching = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
onDisable() {
|
onDisable() {
|
||||||
this._stopWatching();
|
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) )
|
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 )
|
if ( ! selector || typeof selector !== 'string' || ! selector.length )
|
||||||
throw new Error('cannot find definition and no selector provided');
|
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);
|
this._wrappers.set(key, wrapper);
|
||||||
|
|
||||||
return wrapper;
|
return wrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
route(route) {
|
route(route: string | null) {
|
||||||
this._route = route;
|
this._route = route;
|
||||||
this._timer = Date.now();
|
this._timer = Date.now();
|
||||||
this._updateLiveWatching();
|
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) )
|
if ( this._route && watcher.routes.length && ! watcher.routes.includes(this._route) )
|
||||||
return false;
|
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 false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
_updateLiveWatching() {
|
private _updateLiveWatching() {
|
||||||
if ( this._timeout ) {
|
if ( this._timeout ) {
|
||||||
clearTimeout(this._timeout);
|
clearTimeout(this._timeout);
|
||||||
this._timeout = null;
|
this._timeout = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lw = this._live_watching = [],
|
const lw: ElementalWrapper<any>[] = this._live_watching = [],
|
||||||
now = Date.now();
|
now = Date.now();
|
||||||
let min_timeout = Number.POSITIVE_INFINITY;
|
let min_timeout = Number.POSITIVE_INFINITY;
|
||||||
|
|
||||||
|
@ -115,16 +143,17 @@ export default class Elemental extends Module {
|
||||||
this._startWatching();
|
this._startWatching();
|
||||||
}
|
}
|
||||||
|
|
||||||
_pruneLive() {
|
private _pruneLive() {
|
||||||
this._updateLiveWatching();
|
this._updateLiveWatching();
|
||||||
}
|
}
|
||||||
|
|
||||||
_checkWatchers(muts) {
|
private _checkWatchers(muts: Node[]) {
|
||||||
for(const watcher of this._live_watching)
|
if ( this._live_watching )
|
||||||
watcher.checkElements(muts);
|
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 ) {
|
if ( ! this._observer && this._live_watching && this._live_watching.length ) {
|
||||||
this.log.info('Installing MutationObserver.');
|
this.log.info('Installing MutationObserver.');
|
||||||
|
|
||||||
|
@ -136,7 +165,7 @@ export default class Elemental extends Module {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_stopWatching() {
|
private _stopWatching() {
|
||||||
if ( this._observer ) {
|
if ( this._observer ) {
|
||||||
this.log.info('Stopping MutationObserver.');
|
this.log.info('Stopping MutationObserver.');
|
||||||
this._observer.disconnect();
|
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) )
|
if ( this._watching.has(inst) )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
@ -163,7 +192,7 @@ export default class Elemental extends Module {
|
||||||
this._updateLiveWatching();
|
this._updateLiveWatching();
|
||||||
}
|
}
|
||||||
|
|
||||||
unlisten(inst) {
|
unlisten(inst: ElementalWrapper<any>) {
|
||||||
if ( ! this._watching.has(inst) )
|
if ( ! this._watching.has(inst) )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
@ -175,20 +204,64 @@ export default class Elemental extends Module {
|
||||||
|
|
||||||
let elemental_id = 0;
|
let elemental_id = 0;
|
||||||
|
|
||||||
export class ElementalWrapper extends EventEmitter {
|
type ElementalParam = `_ffz$elemental$${number}`;
|
||||||
constructor(name, selector, routes, opts, limit, timeout, remove, elemental) {
|
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();
|
super();
|
||||||
|
|
||||||
this.id = elemental_id++;
|
this.id = elemental_id++;
|
||||||
this.param = `_ffz$elemental$${this.id}`;
|
this.param = `_ffz$elemental$${this.id}`;
|
||||||
this.remove_param = `_ffz$elemental_remove$${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._schedule = this._schedule.bind(this);
|
||||||
|
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.selector = selector;
|
this.selector = selector;
|
||||||
this.routes = routes || [];
|
this.routes = routes ?? [];
|
||||||
this.opts = opts;
|
this.opts = opts;
|
||||||
this.limit = limit;
|
this.limit = limit;
|
||||||
this.timeout = timeout;
|
this.timeout = timeout;
|
||||||
|
@ -199,7 +272,6 @@ export class ElementalWrapper extends EventEmitter {
|
||||||
|
|
||||||
this.count = 0;
|
this.count = 0;
|
||||||
this.instances = new Set;
|
this.instances = new Set;
|
||||||
this.observers = new Map;
|
|
||||||
this.elemental = elemental;
|
this.elemental = elemental;
|
||||||
|
|
||||||
this.check();
|
this.check();
|
||||||
|
@ -224,7 +296,8 @@ export class ElementalWrapper extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
_schedule() {
|
_schedule() {
|
||||||
clearTimeout(this._stimer);
|
if ( this._stimer )
|
||||||
|
clearTimeout(this._stimer);
|
||||||
this._stimer = null;
|
this._stimer = null;
|
||||||
|
|
||||||
if ( this.limit === 0 || this.count < this.limit )
|
if ( this.limit === 0 || this.count < this.limit )
|
||||||
|
@ -234,18 +307,19 @@ export class ElementalWrapper extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
check() {
|
check() {
|
||||||
const matches = document.querySelectorAll(this.selector);
|
const matches = document.querySelectorAll<TElement>(this.selector);
|
||||||
for(const el of matches)
|
// 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);
|
this.add(el);
|
||||||
}
|
}
|
||||||
|
|
||||||
checkElements(els) {
|
checkElements(els: Iterable<Element>) {
|
||||||
if ( this.atLimit )
|
if ( this.atLimit )
|
||||||
return this.schedule();
|
return this.schedule();
|
||||||
|
|
||||||
for(const el of els) {
|
for(const el of els) {
|
||||||
const matches = el.querySelectorAll(this.selector);
|
const matches = el.querySelectorAll<TElement>(this.selector);
|
||||||
for(const match of matches)
|
for(const match of matches as unknown as Iterable<TElement>)
|
||||||
this.add(match);
|
this.add(match);
|
||||||
|
|
||||||
if ( this.atLimit )
|
if ( this.atLimit )
|
||||||
|
@ -264,66 +338,66 @@ export class ElementalWrapper extends EventEmitter {
|
||||||
return Array.from(this.instances);
|
return Array.from(this.instances);
|
||||||
}
|
}
|
||||||
|
|
||||||
each(fn) {
|
each(fn: (element: TElement) => void) {
|
||||||
for(const el of this.instances)
|
for(const el of this.instances)
|
||||||
fn(el);
|
fn(el);
|
||||||
}
|
}
|
||||||
|
|
||||||
add(el) {
|
add(element: TElement) {
|
||||||
if ( this.instances.has(el) )
|
if ( this.instances.has(element) )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.instances.add(el);
|
this.instances.add(element);
|
||||||
this.count++;
|
this.count++;
|
||||||
|
|
||||||
if ( this.check_removal ) {
|
if ( this.check_removal && element.parentNode ) {
|
||||||
const remove_check = new MutationObserver(() => {
|
const remove_check = new MutationObserver(() => {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
if ( ! document.contains(el) )
|
if ( ! document.contains(element) )
|
||||||
this.remove(el);
|
this.remove(element);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
remove_check.observe(el.parentNode, {childList: true});
|
remove_check.observe(element.parentNode, {childList: true});
|
||||||
el[this.remove_param] = remove_check;
|
(element as HTMLElement)[this.remove_param] = remove_check;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( this.opts ) {
|
if ( this.opts ) {
|
||||||
const observer = new MutationObserver(muts => {
|
const observer = new MutationObserver(muts => {
|
||||||
if ( ! document.contains(el) ) {
|
if ( ! document.contains(element) ) {
|
||||||
this.remove(el);
|
this.remove(element);
|
||||||
} else if ( ! this.__running.size )
|
} else if ( ! this.__running.size )
|
||||||
this.emit('mutate', el, muts);
|
this.emit('mutate', element, muts);
|
||||||
});
|
});
|
||||||
|
|
||||||
observer.observe(el, this.opts);
|
observer.observe(element, this.opts);
|
||||||
el[this.param] = observer;
|
(element as HTMLElement)[this.param] = observer;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.schedule();
|
this.schedule();
|
||||||
this.emit('mount', el);
|
this.emit('mount', element);
|
||||||
}
|
}
|
||||||
|
|
||||||
remove(el) {
|
remove(element: TElement) {
|
||||||
const observer = el[this.param];
|
const observer = element[this.param];
|
||||||
if ( observer ) {
|
if ( observer ) {
|
||||||
observer.disconnect();
|
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 ) {
|
if ( remove_check ) {
|
||||||
remove_check.disconnect();
|
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;
|
return;
|
||||||
|
|
||||||
this.instances.delete(el);
|
this.instances.delete(element);
|
||||||
this.count--;
|
this.count--;
|
||||||
|
|
||||||
this.schedule();
|
this.schedule();
|
||||||
this.emit('unmount', el);
|
this.emit('unmount', element);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -5,13 +5,56 @@
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
import {parse, tokensToRegExp, tokensToFunction} from 'path-to-regexp';
|
import {parse, tokensToRegExp, tokensToFunction} from 'path-to-regexp';
|
||||||
import Module from 'utilities/module';
|
import Module, { GenericModule } from 'utilities/module';
|
||||||
import {has, deep_equals} from 'utilities/object';
|
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 {
|
type FineRouterEvents = {
|
||||||
constructor(...args) {
|
':updated-route-names': [];
|
||||||
super(...args);
|
':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.inject('..fine');
|
||||||
|
|
||||||
this.__routes = [];
|
this.__routes = [];
|
||||||
|
@ -20,16 +63,18 @@ export default class FineRouter extends Module {
|
||||||
this.route_names = {};
|
this.route_names = {};
|
||||||
this.current = null;
|
this.current = null;
|
||||||
this.current_name = null;
|
this.current_name = null;
|
||||||
|
this.current_state = null;
|
||||||
this.match = null;
|
this.match = null;
|
||||||
this.location = null;
|
this.location = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
onEnable() {
|
/** @internal */
|
||||||
|
onEnable(): OptionalPromise<void> {
|
||||||
const thing = this.fine.searchTree(null, n => n.props && n.props.history),
|
const thing = this.fine.searchTree(null, n => n.props && n.props.history),
|
||||||
history = this.history = thing && thing.props && thing.props.history;
|
history = this.history = thing && thing.props && thing.props.history;
|
||||||
|
|
||||||
if ( ! history )
|
if ( ! history )
|
||||||
return new Promise(r => setTimeout(r, 50)).then(() => this.onEnable());
|
return sleep(50).then(() => this.onEnable());
|
||||||
|
|
||||||
history.listen(location => {
|
history.listen(location => {
|
||||||
if ( this.enabled )
|
if ( this.enabled )
|
||||||
|
@ -43,7 +88,7 @@ export default class FineRouter extends Module {
|
||||||
this.history.push(this.getURL(route, data, opts), state);
|
this.history.push(this.getURL(route, data, opts), state);
|
||||||
}
|
}
|
||||||
|
|
||||||
_navigateTo(location) {
|
private _navigateTo(location) {
|
||||||
this.log.debug('New Location', location);
|
this.log.debug('New Location', location);
|
||||||
const host = window.location.host,
|
const host = window.location.host,
|
||||||
path = location.pathname,
|
path = location.pathname,
|
||||||
|
@ -66,7 +111,7 @@ export default class FineRouter extends Module {
|
||||||
this._pickRoute();
|
this._pickRoute();
|
||||||
}
|
}
|
||||||
|
|
||||||
_pickRoute() {
|
private _pickRoute() {
|
||||||
const path = this.location,
|
const path = this.location,
|
||||||
host = this.domain;
|
host = this.domain;
|
||||||
|
|
||||||
|
@ -85,7 +130,6 @@ export default class FineRouter extends Module {
|
||||||
this.current_name = route.name;
|
this.current_name = route.name;
|
||||||
this.match = match;
|
this.match = match;
|
||||||
this.emit(':route', route, match);
|
this.emit(':route', route, match);
|
||||||
this.emit(`:route:${route.name}`, ...match);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -117,7 +161,7 @@ export default class FineRouter extends Module {
|
||||||
return r.url(data, opts);
|
return r.url(data, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
getRoute(name) {
|
getRoute(name: string) {
|
||||||
return this.routes[name];
|
return this.routes[name];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,7 +176,7 @@ export default class FineRouter extends Module {
|
||||||
return this.route_names;
|
return this.route_names;
|
||||||
}
|
}
|
||||||
|
|
||||||
getRouteName(route) {
|
getRouteName(route: string) {
|
||||||
if ( ! this.route_names[route] )
|
if ( ! this.route_names[route] )
|
||||||
this.route_names[route] = route
|
this.route_names[route] = route
|
||||||
.replace(/^dash-([a-z])/, (_, letter) =>
|
.replace(/^dash-([a-z])/, (_, letter) =>
|
||||||
|
@ -143,7 +187,7 @@ export default class FineRouter extends Module {
|
||||||
return this.route_names[route];
|
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' ) {
|
if ( typeof route === 'object' ) {
|
||||||
for(const key in route)
|
for(const key in route)
|
||||||
if ( has(route, key) )
|
if ( has(route, key) )
|
||||||
|
@ -154,10 +198,12 @@ export default class FineRouter extends Module {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.route_names[route] = name;
|
if ( name ) {
|
||||||
|
this.route_names[route] = name;
|
||||||
|
|
||||||
if ( process )
|
if ( process )
|
||||||
this.emit(':updated-route-names');
|
this.emit(':updated-route-names');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
route(name, path, domain = null, state_fn = null, process = true) {
|
route(name, path, domain = null, state_fn = null, process = true) {
|
||||||
|
@ -173,6 +219,7 @@ export default class FineRouter extends Module {
|
||||||
this._pickRoute();
|
this._pickRoute();
|
||||||
this.emit(':updated-routes');
|
this.emit(':updated-routes');
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -200,4 +247,4 @@ export default class FineRouter extends Module {
|
||||||
this.emit(':updated-routes');
|
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.
|
// It controls Twitch PubSub.
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
import Module, { GenericModule, ModuleEvents } from 'utilities/module';
|
import Module, { GenericModule } from 'utilities/module';
|
||||||
import { FFZEvent } from 'utilities/events';
|
import { FFZEvent } from 'utilities/events';
|
||||||
|
|
||||||
declare global {
|
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
|
* 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> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
simulateMessage(topic: string, message: any) {
|
||||||
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) {
|
|
||||||
if ( ! this.instance )
|
if ( ! this.instance )
|
||||||
throw new Error('No PubSub instance available');
|
throw new Error('No PubSub instance available');
|
||||||
|
|
||||||
|
|
|
@ -137,20 +137,24 @@ export class EventEmitter<
|
||||||
this.__dead_events = 0;
|
this.__dead_events = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private __cleanListeners() {
|
protected __cleanListeners() {
|
||||||
if ( ! this.__dead_events )
|
if ( ! this.__dead_events )
|
||||||
return;
|
return [];
|
||||||
|
|
||||||
const new_listeners: Record<string, ListenerInfo[]> = {},
|
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)) {
|
for(const [key, val] of Object.entries(old_listeners)) {
|
||||||
if ( val?.length )
|
if ( val?.length )
|
||||||
new_listeners[key] = val;
|
new_listeners[key] = val;
|
||||||
|
else
|
||||||
|
removed.push(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.__listeners = new_listeners;
|
this.__listeners = new_listeners;
|
||||||
this.__dead_events = 0;
|
this.__dead_events = 0;
|
||||||
|
return removed;
|
||||||
}
|
}
|
||||||
|
|
||||||
private __sortListeners(event: string) {
|
private __sortListeners(event: string) {
|
||||||
|
@ -266,6 +270,8 @@ export class EventEmitter<
|
||||||
this.__sortListeners(event);
|
this.__sortListeners(event);
|
||||||
} else
|
} else
|
||||||
this.__listeners[event] = [info];
|
this.__listeners[event] = [info];
|
||||||
|
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -334,6 +340,8 @@ export class EventEmitter<
|
||||||
this.__sortListeners(event);
|
this.__sortListeners(event);
|
||||||
} else
|
} else
|
||||||
this.__listeners[event] = [info];
|
this.__listeners[event] = [info];
|
||||||
|
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -409,7 +417,7 @@ export class EventEmitter<
|
||||||
this.off(evt as any, fn, ctx);
|
this.off(evt as any, fn, ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( this.__running.has(event) )
|
if ( this.__running.has(event) )
|
||||||
|
@ -417,7 +425,7 @@ export class EventEmitter<
|
||||||
|
|
||||||
let list = this.__listeners[event];
|
let list = this.__listeners[event];
|
||||||
if ( ! list )
|
if ( ! list )
|
||||||
return;
|
return this;
|
||||||
|
|
||||||
// If fn and ctx were both not provided, then clear the list.
|
// If fn and ctx were both not provided, then clear the list.
|
||||||
if ( ! fn && ! ctx )
|
if ( ! fn && ! ctx )
|
||||||
|
@ -439,6 +447,8 @@ export class EventEmitter<
|
||||||
// dead event so we can clean it up later.
|
// dead event so we can clean it up later.
|
||||||
if ( ! list )
|
if ( ! list )
|
||||||
this.__dead_events++;
|
this.__dead_events++;
|
||||||
|
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,20 +1,23 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
import type { DefinitionNode, DocumentNode, FieldNode, FragmentDefinitionNode, OperationDefinitionNode, SelectionNode, SelectionSetNode } from 'graphql';
|
||||||
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// GraphQL Document Manipulation
|
// GraphQL Document Manipulation
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
export const MERGE_METHODS = {
|
export const MERGE_METHODS: Record<string, (a: any, b: any) => any> = {
|
||||||
Document: (a, b) => {
|
Document: (a: DocumentNode, b: DocumentNode) => {
|
||||||
if ( a.definitions && b.definitions )
|
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 )
|
else if ( b.definitions )
|
||||||
a.definitions = b.definitions;
|
(a as any).definitions = b.definitions;
|
||||||
|
|
||||||
return a;
|
return a;
|
||||||
},
|
},
|
||||||
|
|
||||||
Field: (a, b) => {
|
Field: (a: FieldNode, b: FieldNode) => {
|
||||||
if ( a.name && (! b.name || b.name.value !== a.name.value) )
|
if ( a.name && (! b.name || b.name.value !== a.name.value) )
|
||||||
return a;
|
return a;
|
||||||
|
|
||||||
|
@ -22,14 +25,14 @@ export const MERGE_METHODS = {
|
||||||
// TODO: directives
|
// TODO: directives
|
||||||
|
|
||||||
if ( a.selectionSet && b.selectionSet )
|
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 )
|
else if ( b.selectionSet )
|
||||||
a.selectionSet = b.selectionSet;
|
(a as any).selectionSet = b.selectionSet;
|
||||||
|
|
||||||
return a;
|
return a;
|
||||||
},
|
},
|
||||||
|
|
||||||
OperationDefinition: (a, b) => {
|
OperationDefinition: (a: OperationDefinitionNode, b: OperationDefinitionNode) => {
|
||||||
if ( a.operation !== b.operation )
|
if ( a.operation !== b.operation )
|
||||||
return a;
|
return a;
|
||||||
|
|
||||||
|
@ -37,14 +40,14 @@ export const MERGE_METHODS = {
|
||||||
// TODO: directives
|
// TODO: directives
|
||||||
|
|
||||||
if ( a.selectionSet && b.selectionSet )
|
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 )
|
else if ( b.selectionSet )
|
||||||
a.selectionSet = b.selectionSet;
|
(a as any).selectionSet = b.selectionSet;
|
||||||
|
|
||||||
return a;
|
return a;
|
||||||
},
|
},
|
||||||
|
|
||||||
FragmentDefinition: (a, b) => {
|
FragmentDefinition: (a: FragmentDefinitionNode, b: FragmentDefinitionNode) => {
|
||||||
if ( a.typeCondition && b.typeCondition ) {
|
if ( a.typeCondition && b.typeCondition ) {
|
||||||
if ( a.typeCondition.kind !== b.typeCondition.kind )
|
if ( a.typeCondition.kind !== b.typeCondition.kind )
|
||||||
return a;
|
return a;
|
||||||
|
@ -56,16 +59,16 @@ export const MERGE_METHODS = {
|
||||||
// TODO: directives
|
// TODO: directives
|
||||||
|
|
||||||
if ( a.selectionSet && b.selectionSet )
|
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 )
|
else if ( b.selectionSet )
|
||||||
a.selectionSet = b.selectionSet;
|
(a as any).selectionSet = b.selectionSet;
|
||||||
|
|
||||||
return a;
|
return a;
|
||||||
},
|
},
|
||||||
|
|
||||||
SelectionSet: (a, b) => {
|
SelectionSet: (a: SelectionSetNode, b: SelectionSetNode) => {
|
||||||
if ( a.selections && b.selections )
|
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 )
|
else if ( b.selections )
|
||||||
a.selections = b.selections;
|
a.selections = b.selections;
|
||||||
|
|
||||||
|
@ -73,10 +76,10 @@ export const MERGE_METHODS = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Type safe this
|
||||||
export function mergeList(a, b) {
|
export function mergeList(a: any[], b: any[]) {
|
||||||
let has_operation = false;
|
let has_operation = false;
|
||||||
const a_names = {};
|
const a_names: Record<string, any> = {};
|
||||||
for(const item of a) {
|
for(const item of a) {
|
||||||
if ( ! item || ! item.name || item.name.kind !== 'Name' )
|
if ( ! item || ! item.name || item.name.kind !== 'Name' )
|
||||||
continue;
|
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 )
|
if ( a.kind !== b.kind )
|
||||||
return a;
|
return a;
|
||||||
|
|
||||||
|
@ -122,4 +125,4 @@ export default function merge(a, b) {
|
||||||
return MERGE_METHODS[a.kind](a, b);
|
return MERGE_METHODS[a.kind](a, b);
|
||||||
|
|
||||||
return a;
|
return a;
|
||||||
}
|
}
|
|
@ -5,10 +5,10 @@
|
||||||
// Modules are cool.
|
// 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 {has} from 'utilities/object';
|
||||||
import type Logger from './logging';
|
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';
|
import type { Addon } from './addon';
|
||||||
|
|
||||||
|
|
||||||
|
@ -233,7 +233,7 @@ export class Module<
|
||||||
// to need to put conditional checks literally everywhere we use the
|
// to need to put conditional checks literally everywhere we use the
|
||||||
// logger system. Just no.
|
// logger system. Just no.
|
||||||
if ( ! this.__log )
|
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;
|
return this.__log as Logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -805,8 +805,10 @@ export class Module<
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve<
|
resolve<
|
||||||
TPath extends ModuleKeys,
|
TPath extends string,
|
||||||
TReturn extends GenericModule = ModuleMap[TPath]
|
TReturn = TPath extends keyof ModuleMap
|
||||||
|
? ModuleMap[TPath]
|
||||||
|
: GenericModule
|
||||||
>(name: TPath): TReturn | null {
|
>(name: TPath): TReturn | null {
|
||||||
let module = this.__resolve(name);
|
let module = this.__resolve(name);
|
||||||
if ( !(module instanceof Module) )
|
if ( !(module instanceof Module) )
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
|
||||||
import {BAD_HOTKEYS, TWITCH_EMOTE_V2, WORD_SEPARATORS} from 'utilities/constants';
|
import {BAD_HOTKEYS, TWITCH_EMOTE_V2, WORD_SEPARATORS} from 'utilities/constants';
|
||||||
|
import type { ExtractSegments, ExtractType } from 'utilities/types';
|
||||||
|
|
||||||
const HOP = Object.prototype.hasOwnProperty;
|
const HOP = Object.prototype.hasOwnProperty;
|
||||||
|
|
||||||
|
@ -78,8 +79,8 @@ export class TranslatableError extends Error {
|
||||||
}
|
}
|
||||||
|
|
||||||
toString() {
|
toString() {
|
||||||
const ffz = window.FrankerFaceZ?.get?.(),
|
const ffz = window.FrankerFaceZ?.get(),
|
||||||
i18n = ffz?.resolve?.('i18n');
|
i18n = ffz?.resolve('i18n');
|
||||||
|
|
||||||
if ( i18n && this.i18n_key )
|
if ( i18n && this.i18n_key )
|
||||||
return i18n.t(this.i18n_key, this.message, this.data);
|
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
|
* @param b The second array
|
||||||
* @returns Whether or not they match
|
* @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 )
|
if ( ! Array.isArray(a) || ! Array.isArray(b) || a.length !== b.length )
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
@ -655,40 +656,6 @@ export function substr_count(str: string, needle: string) {
|
||||||
|
|
||||||
// These types are all used by get()
|
// 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.
|
* Get a value from an object at a path.
|
||||||
* @param path The path to follow, using periods to go down a level.
|
* @param path The path to follow, using periods to go down a level.
|
||||||
|
|
|
@ -15,8 +15,8 @@ type ParseContext = {
|
||||||
export type PathNode = {
|
export type PathNode = {
|
||||||
title: string;
|
title: string;
|
||||||
key: string;
|
key: string;
|
||||||
page: boolean;
|
page?: boolean;
|
||||||
tab: boolean;
|
tab?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseAST(ctx: ParseContext) {
|
function parseAST(ctx: ParseContext) {
|
||||||
|
@ -142,4 +142,4 @@ function parseJSON(ctx: ParseContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return JSON.parse(path.slice(start, ctx.i));
|
return JSON.parse(path.slice(start, ctx.i));
|
||||||
}
|
}
|
||||||
|
|
|
@ -335,8 +335,10 @@ export class TranslationCore {
|
||||||
}
|
}
|
||||||
|
|
||||||
toLocaleString(thing: any) {
|
toLocaleString(thing: any) {
|
||||||
if ( thing && thing.toLocaleString )
|
if ( thing?.toLocaleString )
|
||||||
return thing.toLocaleString(this._locale);
|
return thing.toLocaleString(this._locale) as string;
|
||||||
|
else if ( typeof thing !== 'string' )
|
||||||
|
return `${thing}`;
|
||||||
return thing;
|
return thing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -638,171 +640,6 @@ function listToString(list: any[]): string {
|
||||||
// Plural Handling
|
// 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,
|
let cardinal_i18n: Intl.PluralRules | null = null,
|
||||||
cardinal_locale: string | null = null;
|
cardinal_locale: string | null = null;
|
||||||
|
|
||||||
|
@ -830,27 +667,3 @@ export function getOrdinalName(locale: string, input: number) {
|
||||||
|
|
||||||
return ordinal_i18n.select(input);
|
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.
|
// Get data, from Twitch.
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
import Module from 'utilities/module';
|
import Module, { GenericModule } from 'utilities/module';
|
||||||
import {get, debounce, TranslatableError} from 'utilities/object';
|
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
|
* PaginatedResult
|
||||||
|
@ -25,10 +34,23 @@ const LANGUAGE_MATCHER = /^auto___lang_(\w+)$/;
|
||||||
* @extends Module
|
* @extends Module
|
||||||
*/
|
*/
|
||||||
export default class TwitchData 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');
|
this.inject('site.apollo');
|
||||||
|
|
||||||
|
@ -41,17 +63,22 @@ export default class TwitchData extends Module {
|
||||||
this.tag_cache = new Map;
|
this.tag_cache = new Map;
|
||||||
this._waiting_tags = new Map;
|
this._waiting_tags = new Map;
|
||||||
|
|
||||||
this._loadTags = debounce(this._loadTags, 50);
|
// The return type doesn't match, because this method returns
|
||||||
this._loadStreams = debounce(this._loadStreams, 50);
|
// a void and not a Promise. We don't care.
|
||||||
|
this._loadStreams = debounce(this._loadStreams, 50) as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
queryApollo(query, variables, options) {
|
queryApollo(
|
||||||
let thing;
|
query: DocumentNode | {query: DocumentNode, variables: any},
|
||||||
if ( ! variables && ! options && query.query )
|
variables?: any,
|
||||||
|
options?: any
|
||||||
|
) {
|
||||||
|
let thing: {query: DocumentNode, variables: any};
|
||||||
|
if ( ! variables && ! options && 'query' in query && query.query )
|
||||||
thing = query;
|
thing = query;
|
||||||
else {
|
else {
|
||||||
thing = {
|
thing = {
|
||||||
query,
|
query: query as DocumentNode,
|
||||||
variables
|
variables
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -62,13 +89,17 @@ export default class TwitchData extends Module {
|
||||||
return this.apollo.client.query(thing);
|
return this.apollo.client.query(thing);
|
||||||
}
|
}
|
||||||
|
|
||||||
mutate(mutation, variables, options) {
|
mutate(
|
||||||
let thing;
|
mutation: DocumentNode | {mutation: DocumentNode, variables: any},
|
||||||
if ( ! variables && ! options && mutation.mutation )
|
variables?: any,
|
||||||
|
options?: any
|
||||||
|
) {
|
||||||
|
let thing: {mutation: DocumentNode, variables: any};
|
||||||
|
if ( ! variables && ! options && 'mutation' in mutation && mutation.mutation )
|
||||||
thing = mutation;
|
thing = mutation;
|
||||||
else {
|
else {
|
||||||
thing = {
|
thing = {
|
||||||
mutation,
|
mutation: mutation as DocumentNode,
|
||||||
variables
|
variables
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -95,6 +126,7 @@ export default class TwitchData extends Module {
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
async getBadges() {
|
async getBadges() {
|
||||||
|
|
||||||
const data = await this.queryApollo(
|
const data = await this.queryApollo(
|
||||||
await import(/* webpackChunkName: 'queries' */ './data/global-badges.gql')
|
await import(/* webpackChunkName: 'queries' */ './data/global-badges.gql')
|
||||||
);
|
);
|
||||||
|
@ -116,7 +148,11 @@ export default class TwitchData extends Module {
|
||||||
* next page of results.
|
* next page of results.
|
||||||
* @returns {PaginatedResult} The 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(
|
const data = await this.queryApollo(
|
||||||
await import(/* webpackChunkName: 'queries' */ './data/search-category.gql'),
|
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"));
|
* console.log(await this.twitch_data.getMatchingTags("Rainbo"));
|
||||||
*/
|
*/
|
||||||
async getMatchingTags(query) {
|
async getMatchingTags(query: string) {
|
||||||
const data = await this.queryApollo({
|
const data = await this.queryApollo({
|
||||||
query: await import(/* webpackChunkName: 'queries' */ './data/tag-search.gql'),
|
query: await import(/* webpackChunkName: 'queries' */ './data/tag-search.gql'),
|
||||||
variables: {
|
variables: {
|
|
@ -1,7 +1,5 @@
|
||||||
import type ExperimentManager from "../experiments";
|
import type ExperimentManager from "../experiments";
|
||||||
import type TranslationManager from "../i18n";
|
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 Chat from "../modules/chat";
|
||||||
import type Actions from "../modules/chat/actions/actions";
|
import type Actions from "../modules/chat/actions/actions";
|
||||||
import type Badges from "../modules/chat/badges";
|
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 EmoteCard from "../modules/emote_card";
|
||||||
import type LinkCard from "../modules/link_card";
|
import type LinkCard from "../modules/link_card";
|
||||||
import type MainMenu from "../modules/main_menu";
|
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 TranslationUI from "../modules/translation_ui";
|
||||||
import type PubSub from "../pubsub";
|
import type PubSub from "../pubsub";
|
||||||
import type { SettingsEvents } from "../settings";
|
|
||||||
import type SettingsManager from "../settings";
|
|
||||||
import type SocketClient from "../socket";
|
import type SocketClient from "../socket";
|
||||||
import type StagingSelector from "../staging";
|
import type StagingSelector from "../staging";
|
||||||
import type Apollo from "./compat/apollo";
|
import type Apollo from "./compat/apollo";
|
||||||
import type Elemental from "./compat/elemental";
|
import type Elemental from "./compat/elemental";
|
||||||
import type Fine from "./compat/fine";
|
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 WebMunch from "./compat/webmunch";
|
||||||
import type CSSTweaks from "./css-tweaks";
|
import type CSSTweaks from "./css-tweaks";
|
||||||
import type { NamespacedEvents } from "./events";
|
import type { NamespacedEvents } from "./events";
|
||||||
import type TwitchData from "./twitch-data";
|
import type TwitchData from "./twitch-data";
|
||||||
import type Vue from "./vue";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AddonInfo represents the data contained in an add-on's manifest.
|
* 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 OptionallyThisCallable<TThis, TArgs extends any[], TReturn> = TReturn | ((this: TThis, ...args: TArgs) => TReturn);
|
||||||
export type OptionallyCallable<TArgs extends any[], TReturn> = TReturn | ((...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 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> = {
|
export type RecursivePartial<T> = {
|
||||||
[K in keyof T]?: T[K] extends object
|
[K in keyof T]?: T[K] extends object
|
||||||
? RecursivePartial<T[K]>
|
? 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 = {
|
export type ClientVersion = {
|
||||||
major: number;
|
major: number;
|
||||||
minor: number;
|
minor: number;
|
||||||
|
@ -135,6 +217,15 @@ export type Mousetrap = {
|
||||||
export type DomFragment = Node | string | null | undefined | DomFragment[];
|
export type DomFragment = Node | string | null | undefined | DomFragment[];
|
||||||
|
|
||||||
|
|
||||||
|
export interface SettingsTypeMap {
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ProviderTypeMap {
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// TODO: Move this event into addons.
|
// TODO: Move this event into addons.
|
||||||
type AddonEvent = {
|
type AddonEvent = {
|
||||||
|
@ -142,15 +233,9 @@ type AddonEvent = {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export type KnownEvents =
|
export interface ModuleEventMap { };
|
||||||
AddonEvent &
|
|
||||||
NamespacedEvents<'load_tracker', LoadEvents> &
|
|
||||||
NamespacedEvents<'settings', SettingsEvents> &
|
|
||||||
NamespacedEvents<'site.subpump', SubpumpEvents> &
|
|
||||||
NamespacedEvents<'tooltips', TooltipEvents>;
|
|
||||||
|
|
||||||
|
export interface ModuleMap {
|
||||||
export type ModuleMap = {
|
|
||||||
'chat': Chat;
|
'chat': Chat;
|
||||||
'chat.actions': Actions;
|
'chat.actions': Actions;
|
||||||
'chat.badges': Badges;
|
'chat.badges': Badges;
|
||||||
|
@ -161,24 +246,22 @@ export type ModuleMap = {
|
||||||
'experiments': ExperimentManager;
|
'experiments': ExperimentManager;
|
||||||
'i18n': TranslationManager;
|
'i18n': TranslationManager;
|
||||||
'link_card': LinkCard;
|
'link_card': LinkCard;
|
||||||
'load_tracker': LoadTracker;
|
|
||||||
'main_menu': MainMenu;
|
'main_menu': MainMenu;
|
||||||
'metadata': Metadata;
|
|
||||||
'pubsub': PubSub;
|
'pubsub': PubSub;
|
||||||
'settings': SettingsManager;
|
|
||||||
'site.apollo': Apollo;
|
'site.apollo': Apollo;
|
||||||
'site.css_tweaks': CSSTweaks;
|
'site.css_tweaks': CSSTweaks;
|
||||||
'site.elemental': Elemental;
|
'site.elemental': Elemental;
|
||||||
'site.fine': Fine;
|
'site.fine': Fine;
|
||||||
'site.subpump': Subpump;
|
|
||||||
'site.twitch_data': TwitchData;
|
'site.twitch_data': TwitchData;
|
||||||
'site.web_munch': WebMunch;
|
'site.web_munch': WebMunch;
|
||||||
'socket': SocketClient;
|
'socket': SocketClient;
|
||||||
'staging': StagingSelector;
|
'staging': StagingSelector;
|
||||||
'tooltips': TooltipProvider;
|
|
||||||
'translation_ui': TranslationUI;
|
'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;
|
export type ModuleKeys = string & keyof ModuleMap;
|
||||||
|
|
|
@ -5,14 +5,46 @@
|
||||||
// Loads Vue + Translation Shim
|
// Loads Vue + Translation Shim
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
import Module from 'utilities/module';
|
import Module, { GenericModule } from 'utilities/module';
|
||||||
import {has} from 'utilities/object';
|
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 {
|
export class VueModule extends Module<'vue'> {
|
||||||
constructor(...args) {
|
|
||||||
super(...args);
|
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._components = {};
|
||||||
this.inject('i18n');
|
this.inject('i18n');
|
||||||
}
|
}
|
||||||
|
@ -36,7 +68,7 @@ export class Vue extends Module {
|
||||||
|
|
||||||
this.component(Components.default);
|
this.component(Components.default);
|
||||||
|
|
||||||
Vue.use(ObserveVisibility);
|
Vue.use(ObserveVisibility as any);
|
||||||
Vue.mixin(Clickaway.mixin);
|
Vue.mixin(Clickaway.mixin);
|
||||||
|
|
||||||
/*if ( ! DEBUG && this.root.raven )
|
/*if ( ! DEBUG && this.root.raven )
|
||||||
|
@ -50,30 +82,30 @@ export class Vue extends Module {
|
||||||
Vue.use(this);
|
Vue.use(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
component(name, component) {
|
component(id: string | any, constructor?: any) {
|
||||||
if ( typeof name === 'function' ) {
|
if ( typeof id === 'function' ) {
|
||||||
for(const key of name.keys())
|
for(const key of id.keys())
|
||||||
this.component(key.slice(2, key.length - 4), name(key).default);
|
this.component(key.slice(2, key.length - 4), id(key).default);
|
||||||
|
|
||||||
} else if ( typeof name === 'object' ) {
|
} else if ( typeof id === 'object' ) {
|
||||||
for(const key in name)
|
for(const key in id)
|
||||||
if ( has(name, key) )
|
if ( has(id, key) )
|
||||||
this.component(key, name[key]);
|
this.component(key, id[key]);
|
||||||
|
|
||||||
} else if ( this.Vue )
|
} else if ( this.Vue )
|
||||||
this.Vue.component(name, component);
|
this.Vue.component(id, constructor);
|
||||||
|
|
||||||
else
|
else if ( this._components )
|
||||||
this._components[name] = component;
|
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
|
// This is a mess. I'm sure there's an easier way to tie the systems
|
||||||
// together. However, for now, this works.
|
// together. However, for now, this works.
|
||||||
|
|
||||||
const t = this;
|
const t = this;
|
||||||
if ( ! this._vue_i18n ) {
|
if ( ! this._vue_i18n ) {
|
||||||
this._vue_i18n = new this.Vue({
|
this._vue_i18n = new vue({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
locale: t.i18n.locale,
|
locale: t.i18n.locale,
|
||||||
|
@ -82,47 +114,51 @@ export class Vue extends Module {
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
tNumber_(val, format) {
|
tNumber_(val: number, format?: string) {
|
||||||
this.locale;
|
this.locale;
|
||||||
return t.i18n.formatNumber(val, format);
|
return t.i18n.formatNumber(val, format);
|
||||||
},
|
},
|
||||||
|
|
||||||
tDate_(val, format) {
|
tDate_(val: string | Date, format?: string) {
|
||||||
this.locale;
|
this.locale;
|
||||||
return t.i18n.formatDate(val, format);
|
return t.i18n.formatDate(val, format);
|
||||||
},
|
},
|
||||||
|
|
||||||
tTime_(val, format) {
|
tTime_(val: string | Date, format?: string) {
|
||||||
this.locale;
|
this.locale;
|
||||||
return t.i18n.formatTime(val, format);
|
return t.i18n.formatTime(val, format);
|
||||||
},
|
},
|
||||||
|
|
||||||
tDateTime_(val, format) {
|
tDateTime_(val: string | Date, format?: string) {
|
||||||
this.locale;
|
this.locale;
|
||||||
return t.i18n.formatDateTime(val, format);
|
return t.i18n.formatDateTime(val, format);
|
||||||
},
|
},
|
||||||
|
|
||||||
t_(key, phrase, options) {
|
t_(key: string, phrase: string, options: any) {
|
||||||
this.locale && this.phrases[key];
|
// Access properties to trigger reactivity.
|
||||||
|
this.locale && (this as any).phrases[key];
|
||||||
return t.i18n.t(key, phrase, options);
|
return t.i18n.t(key, phrase, options);
|
||||||
},
|
},
|
||||||
|
|
||||||
tList_(key, phrase, options) {
|
tList_(key: string, phrase: string, options: any) {
|
||||||
this.locale && this.phrases[key];
|
// Access properties to trigger reactivity.
|
||||||
|
this.locale && (this as any).phrases[key];
|
||||||
return t.i18n.tList(key, phrase, options);
|
return t.i18n.tList(key, phrase, options);
|
||||||
},
|
},
|
||||||
|
|
||||||
tNode_(node, data) {
|
tNode_(node: MessageNode, data: any) {
|
||||||
|
// Access properties to trigger reactivity.
|
||||||
this.locale;
|
this.locale;
|
||||||
return t.i18n.formatNode(node, data);
|
return t.i18n.formatNode(node, data);
|
||||||
},
|
},
|
||||||
|
|
||||||
setLocale(locale) {
|
setLocale(locale: string) {
|
||||||
t.i18n.locale = locale;
|
t.i18n.locale = locale;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// On i18n events, update values for reactivity.
|
||||||
this.on('i18n:transform', () => {
|
this.on('i18n:transform', () => {
|
||||||
this._vue_i18n.locale = this.i18n.locale;
|
this._vue_i18n.locale = this.i18n.locale;
|
||||||
this._vue_i18n.phrases = {};
|
this._vue_i18n.phrases = {};
|
||||||
|
@ -165,11 +201,11 @@ export class Vue extends Module {
|
||||||
render(createElement) {
|
render(createElement) {
|
||||||
return createElement(
|
return createElement(
|
||||||
this.tag || 'span',
|
this.tag || 'span',
|
||||||
this.$i18n.tList_(
|
(this as any).$i18n.tList_(
|
||||||
this.phrase,
|
this.phrase,
|
||||||
this.default,
|
this.default,
|
||||||
Object.assign({}, this.data, this.$scopedSlots)
|
Object.assign({}, this.data, this.$scopedSlots)
|
||||||
).map(out => {
|
).map((out: any) => {
|
||||||
if ( typeof out === 'function' )
|
if ( typeof out === 'function' )
|
||||||
return out();
|
return out();
|
||||||
return out;
|
return out;
|
||||||
|
@ -198,29 +234,29 @@ export class Vue extends Module {
|
||||||
return t.i18n;
|
return t.i18n;
|
||||||
},
|
},
|
||||||
t(key, phrase, options) {
|
t(key, phrase, options) {
|
||||||
return this.$i18n.t_(key, phrase, options);
|
return (this as any).$i18n.t_(key, phrase, options);
|
||||||
},
|
},
|
||||||
tList(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) {
|
tNode(node, data) {
|
||||||
return this.$i18n.tNode_(node, data);
|
return (this as any).$i18n.tNode_(node, data);
|
||||||
},
|
},
|
||||||
tNumber(val, format) {
|
tNumber(val, format) {
|
||||||
return this.$i18n.tNumber_(val, format);
|
return (this as any).$i18n.tNumber_(val, format);
|
||||||
},
|
},
|
||||||
tDate(val, format) {
|
tDate(val, format) {
|
||||||
return this.$i18n.tDate_(val, format);
|
return (this as any).$i18n.tDate_(val, format);
|
||||||
},
|
},
|
||||||
tTime(val, format) {
|
tTime(val, format) {
|
||||||
return this.$i18n.tTime_(val, format);
|
return (this as any).$i18n.tTime_(val, format);
|
||||||
},
|
},
|
||||||
tDateTime(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 {
|
.ffz--field-inline {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
width: unset;
|
width: unset;
|
||||||
min-width: 150px;
|
min-width: 140px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ffz--field-icon {
|
.ffz--field-icon {
|
||||||
|
@ -554,4 +554,4 @@
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
],
|
],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
|
"jsxFactory": "createElement",
|
||||||
"alwaysStrict": true,
|
"alwaysStrict": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"strictNullChecks": 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;
|
const content: string;
|
||||||
export default content;
|
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