mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-06-27 21:05:53 +00:00
4.20.72
* Added: Setting to change the height of chat actions. * Added: Profiles can now be toggled via hotkey. * Added: Profiles can now be imported from URL as well as File. * Added: Profiles with update URLs can have automatic updates disabled. * Fixed: Support for the `clips.twitch.tv` domain. * Fixed: Badges that make use of foreground text are no longer white in light themes. * Fixed: Mod Icons appearing smaller than normal. * Changed: Allow several types of actions, unrelated to moderation, to be used on a person's own chat messages. * Changed: Better warn users in the Control Center when the current profile is set to automatically update. * Changed: Make the FFZ Control Center remember which profile is selected when re-opening / refreshing. * API Added: Add-ons can now target specific supported flavors. Choices thus far are `main` and `clips`. * API Added: The `site.menu_button` now has `addToast(...)` and can display multiple toasts. Toasts can also time out. * API Fixed: `openFile(...)` never resolving if the user closes the dialog without selecting a file.
This commit is contained in:
parent
0d433c3ebd
commit
9086230686
61 changed files with 2267 additions and 222 deletions
135
package-lock.json
generated
135
package-lock.json
generated
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "frankerfacez",
|
||||
"version": "4.20.59",
|
||||
"version": "4.20.71",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@ -1373,9 +1373,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"aws4": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.0.tgz",
|
||||
"integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==",
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz",
|
||||
"integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==",
|
||||
"dev": true
|
||||
},
|
||||
"babel-eslint": {
|
||||
|
@ -2207,15 +2207,14 @@
|
|||
}
|
||||
},
|
||||
"clone-deep": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-2.0.2.tgz",
|
||||
"integrity": "sha512-SZegPTKjCgpQH63E+eN6mVEEPdQBOUzjyJm5Pora4lrwWRFS8I0QAxV/KD6vV/i0WuijHZWQC1fMsPEdxfdVCQ==",
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
|
||||
"integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"for-own": "^1.0.0",
|
||||
"is-plain-object": "^2.0.4",
|
||||
"kind-of": "^6.0.0",
|
||||
"shallow-clone": "^1.0.0"
|
||||
"kind-of": "^6.0.2",
|
||||
"shallow-clone": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"code-point-at": {
|
||||
|
@ -4009,15 +4008,6 @@
|
|||
"integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=",
|
||||
"dev": true
|
||||
},
|
||||
"for-own": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz",
|
||||
"integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"for-in": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"forever-agent": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
|
||||
|
@ -4413,13 +4403,33 @@
|
|||
"dev": true
|
||||
},
|
||||
"har-validator": {
|
||||
"version": "5.1.3",
|
||||
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
|
||||
"integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
|
||||
"version": "5.1.5",
|
||||
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz",
|
||||
"integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ajv": "^6.5.5",
|
||||
"ajv": "^6.12.3",
|
||||
"har-schema": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"json-schema-traverse": "^0.4.1",
|
||||
"uri-js": "^4.2.2"
|
||||
}
|
||||
},
|
||||
"fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"has": {
|
||||
|
@ -5171,9 +5181,9 @@
|
|||
}
|
||||
},
|
||||
"js-base64": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.2.tgz",
|
||||
"integrity": "sha512-1hgLrLIrmCgZG+ID3VoLNLOSwjGnoZa8tyrUdEteMeIzsT6PH7PMLyUvbDwzNE56P3PNxyvuIOx4Uh2E5rzQIw==",
|
||||
"version": "2.6.4",
|
||||
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz",
|
||||
"integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==",
|
||||
"dev": true
|
||||
},
|
||||
"js-cookie": {
|
||||
|
@ -5411,12 +5421,6 @@
|
|||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
|
||||
},
|
||||
"lodash.tail": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.tail/-/lodash.tail-4.1.1.tgz",
|
||||
"integrity": "sha1-0jM6NtnncXyK0vfKyv7HwytERmQ=",
|
||||
"dev": true
|
||||
},
|
||||
"lodash.throttle": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
|
||||
|
@ -5789,24 +5793,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"mixin-object": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz",
|
||||
"integrity": "sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"for-in": "^0.1.3",
|
||||
"is-extendable": "^0.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"for-in": {
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.8.tgz",
|
||||
"integrity": "sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE=",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
|
||||
|
@ -7577,29 +7563,22 @@
|
|||
}
|
||||
},
|
||||
"sass-loader": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-7.1.0.tgz",
|
||||
"integrity": "sha512-+G+BKGglmZM2GUSfT9TLuEp6tzehHPjAMoRRItOojWIqIGPloVCMhNIQuG639eJ+y033PaGTSjLaTHts8Kw79w==",
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-7.3.1.tgz",
|
||||
"integrity": "sha512-tuU7+zm0pTCynKYHpdqaPpe+MMTQ76I9TPZ7i4/5dZsigE350shQWe5EZNl5dBidM49TPET75tNqRbcsUZWeNA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"clone-deep": "^2.0.1",
|
||||
"clone-deep": "^4.0.1",
|
||||
"loader-utils": "^1.0.1",
|
||||
"lodash.tail": "^4.1.1",
|
||||
"neo-async": "^2.5.0",
|
||||
"pify": "^3.0.0",
|
||||
"semver": "^5.5.0"
|
||||
"pify": "^4.0.1",
|
||||
"semver": "^6.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"pify": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
|
||||
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
|
||||
"dev": true
|
||||
},
|
||||
"semver": {
|
||||
"version": "5.7.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz",
|
||||
"integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==",
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
|
@ -7832,22 +7811,12 @@
|
|||
}
|
||||
},
|
||||
"shallow-clone": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-1.0.0.tgz",
|
||||
"integrity": "sha512-oeXreoKR/SyNJtRJMAKPDSvd28OqEwG4eR/xc856cRGBII7gX9lvAqDxusPm0846z/w/hWYjI1NpKwJ00NHzRA==",
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz",
|
||||
"integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-extendable": "^0.1.1",
|
||||
"kind-of": "^5.0.0",
|
||||
"mixin-object": "^2.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"kind-of": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
|
||||
"integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==",
|
||||
"dev": true
|
||||
}
|
||||
"kind-of": "^6.0.2"
|
||||
}
|
||||
},
|
||||
"shebang-command": {
|
||||
|
@ -8222,9 +8191,9 @@
|
|||
}
|
||||
},
|
||||
"spdx-license-ids": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz",
|
||||
"integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==",
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz",
|
||||
"integrity": "sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==",
|
||||
"dev": true
|
||||
},
|
||||
"spdy": {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "frankerfacez",
|
||||
"author": "Dan Salvato LLC",
|
||||
"version": "4.20.71",
|
||||
"version": "4.20.72",
|
||||
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
|
@ -46,9 +46,9 @@
|
|||
"json-loader": "^0.5.7",
|
||||
"jszip": "^3.6.0",
|
||||
"node-sass": "^4.14.1",
|
||||
"sass-loader": "^7.1.0",
|
||||
"raw-loader": "^3.1.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"sass-loader": "^7.1.0",
|
||||
"semver": "^7.3.2",
|
||||
"terser-webpack-plugin": "^3.0.6",
|
||||
"vue": "^2.6.11",
|
||||
|
|
|
@ -27,6 +27,8 @@ export default class AddonManager extends Module {
|
|||
|
||||
this.load_requires = ['settings'];
|
||||
|
||||
this.target = this.parent.flavor || 'unknown';
|
||||
|
||||
this.has_dev = false;
|
||||
this.reload_required = false;
|
||||
this.addons = {};
|
||||
|
@ -50,6 +52,7 @@ export default class AddonManager extends Module {
|
|||
getAddons: () => Object.values(this.addons),
|
||||
hasAddon: id => this.hasAddon(id),
|
||||
getVersion: id => this.getVersion(id),
|
||||
doesAddonTarget: id => this.doesAddonTarget(id),
|
||||
isAddonEnabled: id => this.isAddonEnabled(id),
|
||||
isAddonExternal: id => this.isAddonExternal(id),
|
||||
enableAddon: id => this.enableAddon(id),
|
||||
|
@ -81,13 +84,27 @@ export default class AddonManager extends Module {
|
|||
// We do not await enabling add-ons because that would delay the
|
||||
// main script's execution.
|
||||
for(const id of this.enabled_addons)
|
||||
if ( this.hasAddon(id) )
|
||||
if ( this.hasAddon(id) && this.doesAddonTarget(id) )
|
||||
this._enableAddon(id);
|
||||
|
||||
this.emit(':ready');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
doesAddonTarget(id) {
|
||||
const data = this.addons[id];
|
||||
if ( ! data )
|
||||
return false;
|
||||
|
||||
const targets = data.targets ?? ['main'];
|
||||
if ( ! Array.isArray(targets) )
|
||||
return false;
|
||||
|
||||
return targets.includes(this.target);
|
||||
}
|
||||
|
||||
|
||||
generateLog() {
|
||||
const out = ['Known'];
|
||||
for(const [id, addon] of Object.entries(this.addons))
|
||||
|
@ -323,7 +340,8 @@ export default class AddonManager extends Module {
|
|||
this.settings.provider.set('addons.enabled', this.enabled_addons);
|
||||
|
||||
// Actually load it.
|
||||
this._enableAddon(id);
|
||||
if ( this.doesAddonTarget(id) )
|
||||
this._enableAddon(id);
|
||||
}
|
||||
|
||||
async disableAddon(id, save = true) {
|
||||
|
|
10
src/entry.js
10
src/entry.js
|
@ -6,17 +6,19 @@
|
|||
return;
|
||||
|
||||
const DEBUG = localStorage.ffzDebugMode == 'true' && document.body.classList.contains('ffz-dev'),
|
||||
HOST = location.hostname,
|
||||
FLAVOR =
|
||||
location.hostname.includes('player') ? 'player' :
|
||||
(location.pathname === '/p/ffz_bridge/' ? 'bridge' : 'avalon'),
|
||||
HOST.includes('player') ? 'player' :
|
||||
HOST.includes('clips') ? 'clips' :
|
||||
(location.pathname === '/p/ffz_bridge/' ? 'bridge' : 'avalon'),
|
||||
SERVER = DEBUG ? '//localhost:8000' : '//cdn.frankerfacez.com',
|
||||
CLIPS = /clips\.twitch\.tv/.test(location.hostname) ? 'clips/' : '',
|
||||
//CLIPS = /clips\.twitch\.tv/.test(location.hostname) ? 'clips/' : '',
|
||||
|
||||
script = document.createElement('script');
|
||||
|
||||
script.id = 'ffz-script';
|
||||
script.async = true;
|
||||
script.crossOrigin = 'anonymous';
|
||||
script.src = `${SERVER}/script/${CLIPS}${FLAVOR}.js?_=${Date.now()}`;
|
||||
script.src = `${SERVER}/script/${FLAVOR}.js?_=${Date.now()}`;
|
||||
document.head.appendChild(script);
|
||||
})();
|
|
@ -27,6 +27,7 @@ class FrankerFaceZ extends Module {
|
|||
|
||||
FrankerFaceZ.instance = this;
|
||||
|
||||
this.flavor = 'main';
|
||||
this.name = 'frankerfacez';
|
||||
this.__state = 0;
|
||||
this.__modules.core = this;
|
||||
|
|
|
@ -26,6 +26,23 @@ export default class Actions extends Module {
|
|||
this.actions = {};
|
||||
this.renderers = {};
|
||||
|
||||
this.settings.add('chat.actions.size', {
|
||||
default: 16,
|
||||
ui: {
|
||||
path: 'Chat > Actions @{"always_list_pages": true} >> Appearance',
|
||||
title: 'Action Size',
|
||||
description: "How tall actions should be, in pixels. This may be affected by your browser's zoom and font size settings.",
|
||||
component: 'setting-text-box',
|
||||
process(val) {
|
||||
val = parseInt(val, 10);
|
||||
if ( isNaN(val) || ! isFinite(val) || val <= 0 )
|
||||
return 16;
|
||||
|
||||
return val;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.settings.add('chat.actions.reasons', {
|
||||
default: [
|
||||
{v: {text: 'One-Man Spam', i18n: 'chat.reasons.spam'}},
|
||||
|
@ -444,7 +461,7 @@ export default class Actions extends Module {
|
|||
contents = def.render.call(this, ap, createElement, color);
|
||||
|
||||
actions.push(<button
|
||||
class={`ffz-tooltip tw-pd-x-05 ffz-mod-icon mod-icon tw-c-text-alt-2${disabled ? ' disabled' : ''}${has_color ? ' colored' : ''}`}
|
||||
class={`ffz-tooltip tw-pd-x-05 mod-icon ffz-mod-icon tw-c-text-alt-2${disabled ? ' disabled' : ''}${has_color ? ' colored' : ''}`}
|
||||
data-tooltip-type="action"
|
||||
data-action={data.action}
|
||||
data-options={data.options ? JSON.stringify(data.options) : null}
|
||||
|
@ -678,7 +695,7 @@ export default class Actions extends Module {
|
|||
|
||||
had_action = true;
|
||||
list.push(<button
|
||||
class={`ffz-tooltip ffz-mod-icon mod-icon tw-c-text-alt-2${disabled ? ' disabled' : ''}${has_color ? ' colored' : ''}${keys ? ` ffz-modifier-${keys}` : ''}${hover ? ' ffz-hover' : ''}`}
|
||||
class={`ffz-tooltip mod-icon ffz-mod-icon tw-c-text-alt-2${disabled ? ' disabled' : ''}${has_color ? ' colored' : ''}${keys ? ` ffz-modifier-${keys}` : ''}${hover ? ' ffz-hover' : ''}`}
|
||||
disabled={disabled}
|
||||
data-tooltip-type="action"
|
||||
data-action={data.action}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
'use strict';
|
||||
|
||||
import {createElement} from 'utilities/dom';
|
||||
import {durationForChat} from 'utilities/time';
|
||||
|
||||
|
||||
// ============================================================================
|
||||
|
@ -71,6 +70,8 @@ export const edit_overrides = {
|
|||
title: 'Change Name & Color',
|
||||
description: 'Allows you to set local overrides for a user\'s name and color in chat.',
|
||||
|
||||
can_self: true,
|
||||
|
||||
tooltip() {
|
||||
return this.i18n.t('chat.actions.edit_overrides', 'Change Name & Color')
|
||||
},
|
||||
|
@ -103,6 +104,8 @@ export const open_url = {
|
|||
title: 'Open URL',
|
||||
description: '{options.url}',
|
||||
|
||||
can_self: true,
|
||||
|
||||
tooltip(data) {
|
||||
const url = this.replaceVariables(data.options.url, data);
|
||||
|
||||
|
@ -150,6 +153,8 @@ export const chat = {
|
|||
title: 'Chat Command',
|
||||
description: '{options.command}',
|
||||
|
||||
can_self: true,
|
||||
|
||||
editor: () => import(/* webpackChunkName: 'main-menu' */ './components/edit-chat.vue'),
|
||||
|
||||
tooltip(data) {
|
||||
|
|
|
@ -93,7 +93,7 @@ export function generateOverrideCSS(data, style) {
|
|||
|
||||
export function generateBadgeCSS(badge, version, data, style, is_dark, badge_version = 2, color_fixer, fg_fixer, scale = 1, clickable = false) {
|
||||
let color = data.color || 'transparent',
|
||||
fore = data.fore || '#fff',
|
||||
fore = data.fore || is_dark ? '#fff' : '#000',
|
||||
base_image = data.image || (data.addon ? null : `${BASE_IMAGE}${badge_version}/${badge}${data.svg ? '.svg' : `/${version}/`}`),
|
||||
trans = false,
|
||||
invert = false,
|
||||
|
@ -176,7 +176,6 @@ export default class Badges extends Module {
|
|||
|
||||
this.inject('i18n');
|
||||
this.inject('settings');
|
||||
this.inject('socket');
|
||||
this.inject('tooltips');
|
||||
this.inject('experiments');
|
||||
|
||||
|
|
|
@ -61,7 +61,6 @@ export default class Emotes extends Module {
|
|||
|
||||
this.EmoteTypes = EmoteTypes;
|
||||
|
||||
this.inject('socket');
|
||||
this.inject('settings');
|
||||
this.inject('experiments');
|
||||
|
||||
|
@ -164,7 +163,7 @@ export default class Emotes extends Module {
|
|||
}
|
||||
}
|
||||
|
||||
this.socket.on(':command:follow_sets', this.updateFollowSets, this);
|
||||
this.on('socket:command:follow_sets', this.updateFollowSets, this);
|
||||
|
||||
this.loadGlobalSets();
|
||||
}
|
||||
|
|
|
@ -36,7 +36,6 @@ export default class Chat extends Module {
|
|||
this.inject('settings');
|
||||
this.inject('i18n');
|
||||
this.inject('tooltips');
|
||||
this.inject('socket');
|
||||
this.inject('experiments');
|
||||
|
||||
this.inject(Badges);
|
||||
|
@ -920,6 +919,8 @@ export default class Chat extends Module {
|
|||
|
||||
|
||||
onEnable() {
|
||||
this.socket = this.resolve('socket');
|
||||
|
||||
if ( this.context.get('chat.filtering.color-mentions') )
|
||||
this.createColorCache().then(() => this.emit(':update-lines'));
|
||||
|
||||
|
@ -1642,6 +1643,9 @@ export default class Chat extends Module {
|
|||
if ( provider == null )
|
||||
provider = this.experiments.getAssignment('api_links') ? 'test' : 'socket';
|
||||
|
||||
if ( provider === 'socket' && ! this.socket )
|
||||
provider = 'test';
|
||||
|
||||
if ( provider === 'socket' ) {
|
||||
timeout(this.socket.call('get_link', url), 15000)
|
||||
.then(data => handle(true, data))
|
||||
|
|
|
@ -77,7 +77,8 @@ export default class Room {
|
|||
if ( this.manager.rooms[this._login] === this )
|
||||
this.manager.rooms[this._login] = null;
|
||||
|
||||
this.manager.socket.unsubscribe(this, `room.${this.login}`);
|
||||
if ( this.manager.socket )
|
||||
this.manager.socket.unsubscribe(this, `room.${this.login}`);
|
||||
}
|
||||
|
||||
if ( this.manager.room_ids[this._id] === this )
|
||||
|
@ -158,7 +159,8 @@ export default class Room {
|
|||
const old_room = this.manager.rooms[this._login];
|
||||
if ( old_room === this ) {
|
||||
this.manager.rooms[this._login] = null;
|
||||
this.manager.socket.unsubscribe(this, `room.${this.login}`);
|
||||
if ( this.manager.socket )
|
||||
this.manager.socket.unsubscribe(this, `room.${this.login}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -173,7 +175,8 @@ export default class Room {
|
|||
// Make sure we didn't have a funky loop thing happen.
|
||||
this._login = val;
|
||||
this.manager.rooms[val] = this;
|
||||
this.manager.socket.subscribe(this, `room.${val}`);
|
||||
if ( this.manager.socket )
|
||||
this.manager.socket.subscribe(this, `room.${val}`);
|
||||
this.manager.emit(':room-update-login', this, val);
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
:data-tip="act.appearance.tooltip"
|
||||
:class="{'ffz-tooltip': tooltip, 'tw-pd-05': pad, 'colored': color && color.length > 0}"
|
||||
data-tooltip-type="action"
|
||||
class="ffz-mod-icon mod-icon tw-c-text-alt-2 tw-font-size-4"
|
||||
class="mod-icon ffz-mod-icon tw-c-text-alt-2 tw-font-size-4"
|
||||
>
|
||||
<component
|
||||
:is="renderer.component"
|
||||
|
|
|
@ -273,6 +273,11 @@ export default {
|
|||
if ( ! item )
|
||||
return;
|
||||
|
||||
if ( this.$refs.page && this.$refs.page.onBeforeChange ) {
|
||||
if ( this.$refs.page.onBeforeChange(this.currentItem, item) === false )
|
||||
return;
|
||||
}
|
||||
|
||||
this.changeItem(item);
|
||||
|
||||
// Asynchronously walk down the tab tree, so that
|
||||
|
|
|
@ -27,6 +27,19 @@
|
|||
</span>
|
||||
</div>
|
||||
</section>
|
||||
<section v-if="context.currentProfile.url && ! context.currentProfile.pause_updates && item.profile_warning !== false" class="tw-border-t tw-pd-t-1 tw-pd-b-2">
|
||||
<div class="tw-c-background-accent tw-c-text-overlay tw-pd-1">
|
||||
<h3 class="ffz-i-attention">
|
||||
{{ t('setting.profiles.updates', 'This profile will update automatically.') }}
|
||||
</h3>
|
||||
|
||||
<span>
|
||||
{{ t('setting.profile.updates-about',
|
||||
'This profile is set to automatically update. When it does, any changed settings within it will be reset. Profile Rules will be reset as well.')
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
<section v-if="context.has_update" class="tw-border-t tw-pd-t-1 tw-pd-b-2">
|
||||
<div class="tw-c-background-accent tw-c-text-overlay tw-pd-1">
|
||||
<h3 class="ffz-i-arrows-cw">
|
||||
|
@ -44,7 +57,7 @@
|
|||
>
|
||||
<markdown :source="t(item.desc_i18n_key || `${item.i18n_key}.description`, item.description)" />
|
||||
</section>
|
||||
<template v-if="! item.contents || ! item.contents.length">
|
||||
<template v-if="! item.contents || ! item.contents.length || item.always_list_pages">
|
||||
<ul class="tw-border-t tw-pd-y-1">
|
||||
<li
|
||||
v-for="i in item.items"
|
||||
|
|
|
@ -67,23 +67,6 @@
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="url" class="tw-c-background-accent-alt-2 tw-c-text-overlay tw-pd-1 tw-mg-b-1">
|
||||
<h5 class="ffz-i-download-cloud">
|
||||
{{ t('setting.profile.updates', 'This profile will update automatically from the following URL:') }}
|
||||
</h5>
|
||||
|
||||
<div>
|
||||
<a
|
||||
:href="url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="tw-link tw-c-text-overlay"
|
||||
>
|
||||
{{ url }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ffz--menu-container tw-border-t">
|
||||
<header>
|
||||
{{ t('setting.data_management.profiles.edit.general', 'General') }}
|
||||
|
@ -114,13 +97,76 @@
|
|||
class="tw-full-width tw-border-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 ffz-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="ffz--widget">
|
||||
<div class="tw-flex tw-align-items-center">
|
||||
<label for="ffz:editor:hotkey">
|
||||
{{ t('setting.data_management.profiles.edit.hotkey', 'Hotkey') }}
|
||||
</label>
|
||||
|
||||
<key-picker
|
||||
id="ffz:editor:hotkey"
|
||||
ref="hotkey"
|
||||
v-model="hotkey"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<section class="tw-mg-t-05 tw-c-text-alt-2">
|
||||
<markdown :source="t('setting.data_management.profiles.hotkey.desc', 'Setting a hotkey allows you to toggle a profile on or off at any time by using the hotkey.\n\n**Note:** A profile that is toggled on may still be inactive due to its rules.')" />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="url" class="ffz--menu-container tw-border-t">
|
||||
<header>
|
||||
<figure class="tw-inline tw-mg-r-05 ffz-i-download-cloud" />
|
||||
{{ t('setting.data_management.profiles.edit.updates', 'Automatic Updates') }}
|
||||
</header>
|
||||
|
||||
<section class="tw-pd-b-1 tw-c-text-alt-2">
|
||||
{{ t('setting.data_management.profiles.edit.updates.description',
|
||||
'This profile has an associated URL for automatic updates. When updates are enabled and the profile updates, all settings associated with the profile will be reset. The profile\'s rules will be reset as well. The Name, Description, and Hotkey will not reset.')
|
||||
}}
|
||||
</section>
|
||||
|
||||
<div class="ffz--widget tw-flex tw-flex-nowrap">
|
||||
<label for="ffz:editor:url">
|
||||
{{ t('setting.data_management.profiles.edit.url', 'Update URL') }}
|
||||
</label>
|
||||
|
||||
<input
|
||||
id="ffz:editor:url"
|
||||
readonly
|
||||
:value="url"
|
||||
class="tw-full-width tw-border-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 ffz-input"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="ffz--widget ffz--checkbox">
|
||||
<div class="tw-flex tw-align-items-center ffz-checkbox">
|
||||
<input
|
||||
id="ffz:editor:update"
|
||||
ref="update"
|
||||
:checked="! pause"
|
||||
type="checkbox"
|
||||
class="ffz-checkbox__input"
|
||||
@change="onPauseChange"
|
||||
>
|
||||
|
||||
<label for="ffz:editor:update" class="ffz-checkbox__label">
|
||||
<span class="tw-mg-l-1">
|
||||
{{ t('setting.data_management.profiles.edit.update', 'Automatically update this profile.') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ffz--menu-container tw-border-t">
|
||||
<header>
|
||||
{{ t('setting.data_management.profiles.edit.rules', 'Rules') }}
|
||||
</header>
|
||||
<section class="tw-pd-b-1">
|
||||
<section class="tw-pd-b-1 tw-c-text-alt-2">
|
||||
{{ t('setting.data_management.profiles.edit.rules.description',
|
||||
'Rules allows you to define a series of conditions under which this profile will be active. When there are multiple rules, they must all match for the profile to activate. Please use an `Or` rule to create a profile that activates by matching one of several rules.')
|
||||
}}
|
||||
|
@ -152,10 +198,14 @@ export default {
|
|||
old_name: null,
|
||||
old_desc: null,
|
||||
old_rules: null,
|
||||
old_hotkey: null,
|
||||
old_pause: null,
|
||||
|
||||
name: null,
|
||||
desc: null,
|
||||
hotkey: null,
|
||||
url: null,
|
||||
pause: null,
|
||||
unsaved: false,
|
||||
|
||||
rules: null,
|
||||
|
@ -184,6 +234,16 @@ export default {
|
|||
this.unsaved = true;
|
||||
},
|
||||
|
||||
hotkey() {
|
||||
if ( this.hotkey !== this.old_hotkey )
|
||||
this.unsaved = true;
|
||||
},
|
||||
|
||||
pause() {
|
||||
if ( this.pause !== this.old_pause )
|
||||
this.unsaved = true;
|
||||
},
|
||||
|
||||
rules: {
|
||||
handler() {
|
||||
if ( ! deep_equals(this.rules, this.old_rules) )
|
||||
|
@ -204,6 +264,10 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
onPauseChange() {
|
||||
this.pause = ! this.$refs.update.checked;
|
||||
},
|
||||
|
||||
resetExport() {
|
||||
this.export_error = false;
|
||||
this.export_error_message = null;
|
||||
|
@ -246,8 +310,10 @@ export default {
|
|||
profile.description :
|
||||
'';
|
||||
|
||||
this.old_hotkey = this.hotkey = profile ? profile.hotkey : null;
|
||||
this.old_rules = this.rules = profile ? deep_copy(profile.context) : [];
|
||||
this.url = profile ? profile.url : null;
|
||||
this.old_url = this.url = profile ? profile.url : null;
|
||||
this.old_pause = this.pause = profile ? profile.pause_updates : null;
|
||||
this.unsaved = ! profile;
|
||||
},
|
||||
|
||||
|
@ -272,14 +338,18 @@ export default {
|
|||
this.item.profile = this.context.createProfile({
|
||||
name: this.name,
|
||||
description: this.desc,
|
||||
context: this.rules
|
||||
context: this.rules,
|
||||
hotkey: this.hotkey,
|
||||
pause_updates: this.pause
|
||||
});
|
||||
|
||||
} else if ( this.unsaved ) {
|
||||
const changes = {
|
||||
name: this.name,
|
||||
description: this.desc,
|
||||
context: this.rules
|
||||
context: this.rules,
|
||||
hotkey: this.hotkey,
|
||||
pause_updates: this.pause
|
||||
};
|
||||
|
||||
// Disable i18n if required.
|
||||
|
|
|
@ -40,6 +40,8 @@
|
|||
{{ t('setting.profiles.drag', 'Drag profiles to change their priority.') }}
|
||||
</div>
|
||||
<button
|
||||
:class="{'tw-button--disabled': importing}"
|
||||
:disabled="importing"
|
||||
class="tw-mg-l-1 tw-button tw-button--text"
|
||||
@click="edit()"
|
||||
>
|
||||
|
@ -47,14 +49,73 @@
|
|||
{{ t('setting.profiles.new', 'New Profile') }}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="tw-mg-l-1 tw-button tw-button--text"
|
||||
@click="doImport"
|
||||
<div
|
||||
v-on-clickaway="closeMenu"
|
||||
class="tw-relative"
|
||||
>
|
||||
<span class="tw-button__text ffz-i-upload">
|
||||
{{ t('setting.import', 'Import…') }}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
:class="{'tw-button--disabled': importing}"
|
||||
:disabled="importing"
|
||||
class="tw-mg-l-1 tw-button tw-button--text"
|
||||
@click="toggleMenu"
|
||||
>
|
||||
<span class="tw-button__text ffz-i-upload">
|
||||
{{ t('setting.import', 'Import…') }}
|
||||
</span>
|
||||
<span class="tw-button__icon tw-button__icon--right">
|
||||
<figure class="ffz-i-down-dir" />
|
||||
</span>
|
||||
</button>
|
||||
<balloon
|
||||
v-if="menu_open"
|
||||
color="background-alt-2"
|
||||
dir="down-right"
|
||||
:size="menu_pasting ? 'md' : 'sm'"
|
||||
>
|
||||
<simplebar class="ffz-mh-30">
|
||||
<div v-if="menu_pasting" class="tw-pd-1">
|
||||
<div class="tw-flex tw-align-items-center">
|
||||
<input
|
||||
ref="paste"
|
||||
:placeholder="t('setting.paste-url.url', '[url]')"
|
||||
class="tw-flex-grow-1 tw-border-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 ffz-input"
|
||||
@keydown.enter="doImportURL"
|
||||
>
|
||||
<button
|
||||
class="tw-mg-l-05 tw-button"
|
||||
@click="doImportURL"
|
||||
>
|
||||
<span class="tw-button__text ffz-i-plus">
|
||||
{{ t('setting.import.do', 'Import') }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="tw-pd-y-1">
|
||||
<button
|
||||
class="ffz-interactable ffz-interactable--hover-enabled ffz-interactable--default tw-interactive tw-full-width"
|
||||
@click="preparePaste"
|
||||
>
|
||||
<div class="tw-flex tw-align-items-center tw-pd-y-05 tw-pd-x-1">
|
||||
<div class="tw-flex-grow-1 tw-mg-r-1 ffz-i-download-cloud">
|
||||
{{ t('setting.import.url', 'From URL') }}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
class="ffz-interactable ffz-interactable--hover-enabled ffz-interactable--default tw-interactive tw-full-width"
|
||||
@click="doImport"
|
||||
>
|
||||
<div class="tw-flex tw-align-items-center tw-pd-y-05 tw-pd-x-1">
|
||||
<div class="tw-flex-grow-1 tw-mg-r-1 ffz-i-upload">
|
||||
{{ t('setting.import.file', 'From File') }}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</simplebar>
|
||||
</balloon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="import_error" class="tw-c-background-accent-alt-2 tw-c-text-overlay tw-pd-1 tw-mg-b-1 tw-flex tw-align-items-start">
|
||||
|
@ -82,6 +143,7 @@
|
|||
{{ import_message }}
|
||||
</section>
|
||||
<button
|
||||
v-if="import_closable"
|
||||
class="tw-button tw-button--text tw-relative tw-tooltip__container"
|
||||
@click="resetImport"
|
||||
>
|
||||
|
@ -173,9 +235,12 @@
|
|||
<span class="ffz-i-ellipsis-vert" />
|
||||
</div>
|
||||
|
||||
<div v-if="p.url" class="tw-flex tw-flex-shrink-0 tw-align-items-center tw-mg-r-1 tw-relative tw-tooltip__container tw-font-size-4">
|
||||
<span class="ffz-i-download-cloud" />
|
||||
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-left">
|
||||
<div
|
||||
v-if="p.url"
|
||||
class="tw-flex tw-flex-shrink-0 tw-align-items-center tw-mg-r-1 tw-relative tw-tooltip__container tw-font-size-4"
|
||||
>
|
||||
<span :class="`ffz-i-download-cloud${p.pause_updates ? ' ffz-unmatched-item' : ''}`" />
|
||||
<div v-if="! p.pause_updates" class="tw-tooltip tw-tooltip--down tw-tooltip--align-left">
|
||||
<div class="tw-mg-b-05">
|
||||
{{ t('setting.profile.updates', 'This profile will update automatically from the following URL:') }}
|
||||
</div>
|
||||
|
@ -241,7 +306,12 @@ export default {
|
|||
|
||||
data() {
|
||||
return {
|
||||
menu_open: false,
|
||||
menu_pasting: false,
|
||||
|
||||
importing: false,
|
||||
import_error: false,
|
||||
import_closable: true,
|
||||
import_error_message: null,
|
||||
import_message: null,
|
||||
import_profiles: null,
|
||||
|
@ -272,6 +342,24 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
closeMenu() {
|
||||
this.menu_open = false;
|
||||
this.menu_pasting = false;
|
||||
},
|
||||
|
||||
toggleMenu() {
|
||||
this.menu_open = ! this.menu_open;
|
||||
this.menu_pasting = false;
|
||||
},
|
||||
|
||||
preparePaste() {
|
||||
this.menu_open = true;
|
||||
this.menu_pasting = true;
|
||||
requestAnimationFrame(() => {
|
||||
this.$refs.paste.focus()
|
||||
});
|
||||
},
|
||||
|
||||
onProxyCheck() {
|
||||
const val = this.$refs.proxied.checked;
|
||||
this.context.setProxied(val);
|
||||
|
@ -304,7 +392,10 @@ export default {
|
|||
},
|
||||
|
||||
resetImport() {
|
||||
this.importing = false;
|
||||
this.import_url = null;
|
||||
this.import_error = false;
|
||||
this.import_closable = true;
|
||||
this.import_error_message = null;
|
||||
this.import_message = null;
|
||||
this.import_profiles = null;
|
||||
|
@ -313,12 +404,48 @@ export default {
|
|||
this.import_data = null;
|
||||
},
|
||||
|
||||
async doImport() {
|
||||
async doImportURL() {
|
||||
const url = this.$refs.paste.value;
|
||||
|
||||
this.closeMenu();
|
||||
this.resetImport();
|
||||
this.import_url = url;
|
||||
this.importing = true;
|
||||
this.import_closable = false;
|
||||
|
||||
this.import_message = this.t('setting.backup-restore.http-loading', 'Loading...');
|
||||
|
||||
let data;
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if ( ! response.ok )
|
||||
throw new Error;
|
||||
|
||||
data = await response.json();
|
||||
|
||||
} catch(err) {
|
||||
this.import_message = null;
|
||||
this.import_error = true;
|
||||
this.import_error_message = this.t('setting.backup-restore.http-error', 'Unable to read JSON from the provided URL.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.import_message = null;
|
||||
this.importData(data);
|
||||
},
|
||||
|
||||
async doImport() {
|
||||
this.closeMenu();
|
||||
this.resetImport();
|
||||
this.importing = true;
|
||||
|
||||
let file, contents;
|
||||
try {
|
||||
file = await openFile('application/json,application/zip');
|
||||
if ( ! file ) {
|
||||
this.resetImport();
|
||||
return;
|
||||
}
|
||||
|
||||
// We might get a different MIME than expected, roll with it.
|
||||
if ( file.type.toLowerCase().includes('zip') ) {
|
||||
|
@ -345,6 +472,10 @@ export default {
|
|||
return;
|
||||
}
|
||||
|
||||
this.importData(data);
|
||||
},
|
||||
|
||||
importData(data) {
|
||||
if ( data && data.type === 'full' ) {
|
||||
let profiles = data.values && data.values.profiles;
|
||||
if ( profiles === undefined )
|
||||
|
@ -374,6 +505,9 @@ export default {
|
|||
},
|
||||
|
||||
importProfile(profile_data, data) {
|
||||
if ( this.import_url && ! profile_data.url )
|
||||
profile_data.url = this.import_url;
|
||||
|
||||
this.import_profile = profile_data;
|
||||
this.import_profile_data = data;
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
ref="button"
|
||||
:class="{active: opened}"
|
||||
tabindex="0"
|
||||
class="tw-c-background-alt tw-block tw-border tw-border-radius-medium tw-font-size-6 tw-full-width ffz-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05"
|
||||
class="tw-flex tw-align-items-center tw-border-radius-medium tw-font-size-6 tw-full-width ffz-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05"
|
||||
@keyup.up.stop.prevent="focusShow"
|
||||
@keyup.left.stop.prevent="focusShow"
|
||||
@keyup.down.stop.prevent="focusShow"
|
||||
|
@ -51,23 +51,32 @@
|
|||
@keyup.enter="changeProfile(p)"
|
||||
@click="changeProfile(p)"
|
||||
>
|
||||
<div
|
||||
v-if="! p.toggled"
|
||||
class="tw-tooltip__container ffz--profile-row__icon ffz-i-cancel tw-absolute"
|
||||
>
|
||||
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
|
||||
{{ t('setting.profiles.disabled', 'This profile is disabled.') }}
|
||||
<div class="ffz--profile-row__icon-tray tw-flex">
|
||||
<div
|
||||
v-if="p.url"
|
||||
:class="`tw-tooltip__container ffz--profile-row__icon ffz-i-download-cloud tw-relative${p.pause_updates ? ' ffz-unmatched-item' : ''}`"
|
||||
>
|
||||
<div v-if="! p.pause_updates" class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
|
||||
{{ t('setting.profiles.updates', 'This profile will update automatically.') }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="! p.toggled"
|
||||
class="tw-tooltip__container ffz--profile-row__icon ffz-i-cancel tw-relative"
|
||||
>
|
||||
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
|
||||
{{ t('setting.profiles.disabled', 'This profile is disabled.') }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="p.live"
|
||||
class="tw-tooltip__container ffz--profile-row__icon ffz-i-ok tw-relative"
|
||||
>
|
||||
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
|
||||
{{ t('setting.profiles.active', 'This profile is enabled and active.') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="p.live"
|
||||
class="tw-tooltip__container ffz--profile-row__icon ffz-i-ok tw-absolute"
|
||||
>
|
||||
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
|
||||
{{ t('setting.profiles.active', 'This profile is enabled and active.') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<h4>{{ p.i18n_key ? t(p.i18n_key, p.title, p) : p.title }}</h4>
|
||||
<div v-if="p.description" class="description">
|
||||
|
@ -220,6 +229,17 @@ export default {
|
|||
|
||||
changeProfile(profile) {
|
||||
this.context.currentProfile = profile;
|
||||
|
||||
try {
|
||||
window.history.replaceState({
|
||||
...window.history.state,
|
||||
ffzccp: profile.id
|
||||
}, document.title);
|
||||
} catch(err) {
|
||||
/* no-op */
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
this.focusHide();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
:open-up="openUp"
|
||||
:nullable="true"
|
||||
:value="color"
|
||||
class="tw-mg-05"
|
||||
@input="onInput"
|
||||
/>
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
:id="item.full_key"
|
||||
ref="control"
|
||||
:value="value"
|
||||
class="tw-mg-05"
|
||||
@input="onInput"
|
||||
/>
|
||||
|
||||
|
|
|
@ -686,13 +686,15 @@ export default class MainMenu extends Module {
|
|||
description: profile.description,
|
||||
desc_i18n_key: profile.desc_i18n_key || profile.i18n_key && `${profile.i18n_key}.description`,
|
||||
|
||||
hotkey: profile.hotkey,
|
||||
url: profile.url,
|
||||
pause_updates: profile.pause_updates,
|
||||
|
||||
move: idx => context.manager.moveProfile(profile.id, idx),
|
||||
save: () => profile.save(),
|
||||
update: data => {
|
||||
profile.data = deep_copy(data)
|
||||
profile.save()
|
||||
profile.data = deep_copy(data);
|
||||
profile.save();
|
||||
},
|
||||
|
||||
toggle: () => profile.toggled = ! profile.toggled,
|
||||
|
@ -717,9 +719,14 @@ export default class MainMenu extends Module {
|
|||
settings = this.settings,
|
||||
provider = settings.provider,
|
||||
context = this.context,
|
||||
[profiles, profile_keys] = this.getProfiles();
|
||||
[profiles, profile_keys] = this.getProfiles(),
|
||||
state = window.history.state,
|
||||
profile_id = state?.ffzccp;
|
||||
|
||||
let currentProfile = profile_keys[0];
|
||||
if ( profile_id != null )
|
||||
currentProfile = profile_keys[profile_id];
|
||||
|
||||
if ( ! currentProfile ) {
|
||||
for(let i=profiles.length - 1; i >= 0; i--) {
|
||||
if ( profiles[i].live ) {
|
||||
|
@ -735,7 +742,7 @@ export default class MainMenu extends Module {
|
|||
const _c = {
|
||||
profiles,
|
||||
profile_keys,
|
||||
currentProfile: profile_keys[0] || profiles[0],
|
||||
currentProfile,
|
||||
|
||||
exclusive: this.exclusive,
|
||||
can_proxy: context._context.can_proxy,
|
||||
|
|
|
@ -151,7 +151,7 @@ export default class RavenLogger extends Module {
|
|||
autoBreadcrumbs: {
|
||||
console: false
|
||||
},
|
||||
release: (window.FrankerFaceZ || window.FFZPlayer || window.FFZBridge).version_info.toString(),
|
||||
release: (window.FrankerFaceZ || window.FFZPlayer || window.FFZBridge || window.FFZClips).version_info.toString(),
|
||||
environment: DEBUG ? 'development' : 'production',
|
||||
captureUnhandledRejections: false,
|
||||
ignoreErrors: [
|
||||
|
|
|
@ -442,6 +442,16 @@ export default class SettingsContext extends EventEmitter {
|
|||
return this._get(key, key, []);
|
||||
}
|
||||
|
||||
getChanges(key, fn, ctx) {
|
||||
this.onChange(key, fn, ctx);
|
||||
fn.call(ctx, this.get(key));
|
||||
}
|
||||
|
||||
onChange(key, fn, ctx) {
|
||||
this.on(`changed:${key}`, fn, ctx);
|
||||
}
|
||||
|
||||
|
||||
uses(key) {
|
||||
if ( key.startsWith('context.') )
|
||||
return null;
|
||||
|
|
|
@ -365,24 +365,30 @@ export default class SettingsManager extends Module {
|
|||
}
|
||||
|
||||
|
||||
checkUpdates() {
|
||||
async checkUpdates() {
|
||||
await this.awaitProvider();
|
||||
await this.provider.awaitReady();
|
||||
|
||||
if ( ! this.provider.shouldUpdate )
|
||||
return;
|
||||
|
||||
const promises = [];
|
||||
for(const profile of this.__profiles) {
|
||||
if ( ! profile || ! profile.url )
|
||||
if ( ! profile || ! profile.url || profile.pause_updates )
|
||||
continue;
|
||||
|
||||
const out = profile.checkUpdate();
|
||||
promises.push(out instanceof Promise ? out : Promise.resolve(out));
|
||||
}
|
||||
|
||||
Promise.all(promises).then(data => {
|
||||
let success = 0;
|
||||
for(const thing of data)
|
||||
if ( thing )
|
||||
success++;
|
||||
const data = await Promise.all(promises);
|
||||
|
||||
this.log.info(`Successfully refreshed ${success} of ${data.length} profiles from remote URLs.`);
|
||||
});
|
||||
let success = 0;
|
||||
for(const thing of data)
|
||||
if ( thing )
|
||||
success++;
|
||||
|
||||
this.log.info(`Successfully refreshed ${success} of ${data.length} profiles from remote URLs.`);
|
||||
}
|
||||
|
||||
|
||||
|
@ -619,8 +625,10 @@ export default class SettingsManager extends Module {
|
|||
let reordered = false,
|
||||
changed = false;
|
||||
|
||||
for(const profile of old_profiles)
|
||||
for(const profile of old_profiles) {
|
||||
profile.off('toggled', this._onProfileToggled, this);
|
||||
profile.hotkey_enabled = false;
|
||||
}
|
||||
|
||||
for(const profile_data of raw_profiles) {
|
||||
const id = profile_data.id,
|
||||
|
@ -665,8 +673,10 @@ export default class SettingsManager extends Module {
|
|||
changed = true;
|
||||
}
|
||||
|
||||
for(const profile of profiles)
|
||||
for(const profile of profiles) {
|
||||
profile.on('toggled', this._onProfileToggled, this);
|
||||
profile.hotkey_enabled = true;
|
||||
}
|
||||
|
||||
if ( ! changed && ! old_ids.size || suppress_events )
|
||||
return;
|
||||
|
@ -707,6 +717,7 @@ export default class SettingsManager extends Module {
|
|||
this.__profiles.unshift(profile);
|
||||
|
||||
profile.on('toggled', this._onProfileToggled, this);
|
||||
profile.hotkey_enabled = true;
|
||||
|
||||
this._saveProfiles();
|
||||
this.emit(':profile-created', profile);
|
||||
|
@ -793,6 +804,8 @@ export default class SettingsManager extends Module {
|
|||
|
||||
context(env) { return this.main_context.context(env) }
|
||||
get(key) { return this.main_context.get(key); }
|
||||
getChanges(key, fn, ctx) { return this.main_context.getChanges(key, fn, ctx); }
|
||||
onChange(key, fn, ctx) { return this.main_context.onChange(key, fn, ctx); }
|
||||
uses(key) { return this.main_context.uses(key) }
|
||||
update(key) { return this.main_context.update(key) }
|
||||
|
||||
|
|
|
@ -10,6 +10,28 @@ import {createTester} from 'utilities/filtering';
|
|||
|
||||
const fetchJSON = (url, options) => fetch(url, options).then(r => r.ok ? r.json() : null).catch(() => null);
|
||||
|
||||
// TODO: Move this into its own file.
|
||||
const BAD_SHORTCUTS = [
|
||||
'f',
|
||||
'space',
|
||||
'k',
|
||||
'shift+up',
|
||||
'shift+down',
|
||||
'esc',
|
||||
'm',
|
||||
'?',
|
||||
'alt+t',
|
||||
'alt+x'
|
||||
];
|
||||
|
||||
function isValidShortcut(key) {
|
||||
if ( ! key )
|
||||
return false;
|
||||
|
||||
key = key.toLowerCase().trim();
|
||||
return ! BAD_SHORTCUTS.includes(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instances of SettingsProfile are used for getting and setting raw settings
|
||||
* values, enumeration, and emit events when the raw settings are changed.
|
||||
|
@ -19,6 +41,9 @@ export default class SettingsProfile extends EventEmitter {
|
|||
constructor(manager, data) {
|
||||
super();
|
||||
|
||||
this.onShortcut = this.onShortcut.bind(this);
|
||||
this._hotkey_enabled = false;
|
||||
|
||||
this.manager = manager;
|
||||
this.provider = manager.provider;
|
||||
|
||||
|
@ -34,6 +59,8 @@ export default class SettingsProfile extends EventEmitter {
|
|||
|
||||
name: this.name,
|
||||
i18n_key: this.i18n_key,
|
||||
hotkey: this.hotkey,
|
||||
pause_updates: this.pause_updates,
|
||||
|
||||
description: this.description,
|
||||
desc_i18n_key: this.desc_i18n_key,
|
||||
|
@ -86,14 +113,23 @@ export default class SettingsProfile extends EventEmitter {
|
|||
|
||||
|
||||
async checkUpdate() {
|
||||
if ( ! this.url )
|
||||
if ( ! this.url || this.pause_updates )
|
||||
return false;
|
||||
|
||||
const data = await fetchJSON(this.url);
|
||||
if ( ! data || ! data.type === 'profile' || ! data.profile || ! data.values )
|
||||
return false;
|
||||
|
||||
// We don't want to override general settings.
|
||||
delete data.profile.id;
|
||||
delete data.profile.name;
|
||||
delete data.profile.i18n_key;
|
||||
delete data.profile.hotkey;
|
||||
delete data.profile.description;
|
||||
delete data.profile.desc_i18n_key;
|
||||
delete data.profile.url;
|
||||
delete data.profile.pause_updates;
|
||||
|
||||
this.data = data.profile;
|
||||
|
||||
const old_keys = new Set(this.keys());
|
||||
|
@ -110,6 +146,63 @@ export default class SettingsProfile extends EventEmitter {
|
|||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Hotkey
|
||||
// ========================================================================
|
||||
|
||||
get hotkey() {
|
||||
return this._hotkey;
|
||||
}
|
||||
|
||||
set hotkey(key) {
|
||||
if ( key === this._hotkey )
|
||||
return;
|
||||
|
||||
this._hotkey = key;
|
||||
if ( this._hotkey_enabled )
|
||||
this._updateHotkey();
|
||||
}
|
||||
|
||||
get hotkey_enabled() {
|
||||
return this._hotkey_enabled;
|
||||
}
|
||||
|
||||
set hotkey_enabled(val) {
|
||||
this._hotkey_enabled = !! val;
|
||||
this._updateHotkey();
|
||||
}
|
||||
|
||||
_updateHotkey() {
|
||||
const Mousetrap = this.Mousetrap = this.Mousetrap || window.Mousetrap;
|
||||
if ( ! Mousetrap )
|
||||
return;
|
||||
|
||||
const key = this._hotkey;
|
||||
|
||||
if ( this._bound_key && (this._bound_key !== key || ! this._hotkey_enabled) ) {
|
||||
Mousetrap.unbind(this._bound_key);
|
||||
this._bound_key = null;
|
||||
}
|
||||
|
||||
if ( ! this._hotkey_enabled )
|
||||
return;
|
||||
|
||||
if ( key && isValidShortcut(key) ) {
|
||||
Mousetrap.bind(key, this.onShortcut);
|
||||
this._bound_key = key;
|
||||
}
|
||||
}
|
||||
|
||||
onShortcut(e) {
|
||||
this.toggled = ! this.toggled;
|
||||
|
||||
if ( e ) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Toggled
|
||||
// ========================================================================
|
||||
|
|
|
@ -41,6 +41,7 @@ export class SettingsProvider extends EventEmitter {
|
|||
|
||||
static supportsBlobs = false;
|
||||
static allowTransfer = true;
|
||||
static shouldUpdate = true;
|
||||
|
||||
awaitReady() {
|
||||
if ( this.ready )
|
||||
|
@ -50,6 +51,7 @@ export class SettingsProvider extends EventEmitter {
|
|||
}
|
||||
|
||||
get allowTransfer() { return this.constructor.allowTransfer; }
|
||||
get shouldUpdate() { return this.constructor.shouldUpdate; }
|
||||
|
||||
broadcastTransfer() { throw new Error('Not Implemented') } // eslint-disable-line class-methods-use-this
|
||||
disableEvents() { throw new Error('Not Implemented') } // eslint-disable-line class-methods-use-this
|
||||
|
@ -977,6 +979,7 @@ export class CrossOriginStorageBridge extends SettingsProvider {
|
|||
static description = 'This provider uses an `<iframe>` to synchronize storage across subdomains. Due to the `<iframe>`, this provider takes longer than others to load, but should perform roughly the same once loaded. You should be using this on non-www subdomains of Twitch unless you don\'t want your settings to automatically synchronize for some reason.';
|
||||
static supportsBlobs = true;
|
||||
static allowTransfer = false;
|
||||
static shouldUpdate = false;
|
||||
|
||||
get supportsBlobs() {
|
||||
return this._blobs;
|
||||
|
@ -1153,7 +1156,7 @@ export class CrossOriginStorageBridge extends SettingsProvider {
|
|||
this.onReply(msg);
|
||||
|
||||
else
|
||||
this.log.warn('Unknown Message', msg.ffz_type, msg);
|
||||
this.manager.log.warn('Unknown Message', msg.ffz_type, msg);
|
||||
}
|
||||
|
||||
onChange(msg) {
|
||||
|
@ -1175,7 +1178,7 @@ export class CrossOriginStorageBridge extends SettingsProvider {
|
|||
success = msg.ffz_type === 'reply',
|
||||
cbs = this._rpc.get(id);
|
||||
if ( ! cbs )
|
||||
return this.log.warn('Received reply for unknown ID', id);
|
||||
return this.manager.log.warn('Received reply for unknown ID', id);
|
||||
|
||||
this._rpc.delete(id);
|
||||
cbs[success ? 0 : 1](msg);
|
||||
|
|
208
src/sites/clips/chat.jsx
Normal file
208
src/sites/clips/chat.jsx
Normal file
|
@ -0,0 +1,208 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Chat Hooks
|
||||
// ============================================================================
|
||||
|
||||
import {get} from 'utilities/object';
|
||||
import {ColorAdjuster} from 'utilities/color';
|
||||
|
||||
import Module from 'utilities/module';
|
||||
|
||||
import Line from './line';
|
||||
|
||||
|
||||
export default class Chat extends Module {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.colors = new ColorAdjuster;
|
||||
this.inverse_colors = new ColorAdjuster;
|
||||
|
||||
this.inject('settings');
|
||||
this.inject('i18n');
|
||||
|
||||
this.settings.add('theme.is-dark', {
|
||||
requires: ['context.ui.theme'],
|
||||
process(ctx) {
|
||||
return ctx.get('context.ui.theme') === 1
|
||||
}
|
||||
});
|
||||
|
||||
this.inject('chat');
|
||||
|
||||
this.inject('site.twitch_data');
|
||||
this.inject('site.fine');
|
||||
this.inject('site.css_tweaks');
|
||||
|
||||
this.inject(Line);
|
||||
|
||||
this.ChatController = this.fine.define(
|
||||
'clip-chat-controller',
|
||||
n => n.filterChatLines
|
||||
);
|
||||
}
|
||||
|
||||
onEnable() {
|
||||
this.chat.context.on('changed:chat.font-size', this.updateChatCSS, this);
|
||||
this.chat.context.on('changed:chat.font-family', this.updateChatCSS, this);
|
||||
this.chat.context.on('changed:chat.lines.emote-alignment', this.updateChatCSS, this);
|
||||
this.chat.context.on('changed:chat.adjustment-mode', this.updateColors, this);
|
||||
this.chat.context.on('changed:chat.adjustment-contrast', this.updateColors, this);
|
||||
this.chat.context.on('changed:theme.is-dark', this.updateColors, this);
|
||||
|
||||
this.chat.context.getChanges('chat.lines.alternate', val =>
|
||||
this.css_tweaks.toggle('chat-rows', val));
|
||||
|
||||
this.chat.context.getChanges('chat.lines.borders', this.updateLineBorders, this);
|
||||
|
||||
this.ChatController.on('mount', this.chatMounted, this);
|
||||
this.ChatController.on('unmount', this.chatMounted, this);
|
||||
this.ChatController.on('update', this.chatUpdated, this);
|
||||
this.ChatController.on('receive-props', this.chatUpdated, this);
|
||||
|
||||
this.ChatController.ready((cls, instances) => {
|
||||
for(const inst of instances)
|
||||
this.chatMounted(inst);
|
||||
});
|
||||
|
||||
this.loadBadges();
|
||||
this.updateChatCSS();
|
||||
this.updateColors();
|
||||
}
|
||||
|
||||
|
||||
updateLineBorders() {
|
||||
const mode = this.chat.context.get('chat.lines.borders');
|
||||
|
||||
this.css_tweaks.toggle('chat-borders', mode > 0);
|
||||
this.css_tweaks.toggle('chat-borders-3d', mode === 2);
|
||||
this.css_tweaks.toggle('chat-borders-3d-inset', mode === 3);
|
||||
this.css_tweaks.toggle('chat-borders-wide', mode === 4);
|
||||
}
|
||||
|
||||
|
||||
updateChatCSS() {
|
||||
const size = this.chat.context.get('chat.font-size'),
|
||||
emote_alignment = this.chat.context.get('chat.lines.emote-alignment'),
|
||||
lh = Math.round((20/12) * size);
|
||||
|
||||
let font = this.chat.context.get('chat.font-family') || 'inherit';
|
||||
if ( font.indexOf(' ') !== -1 && font.indexOf(',') === -1 && font.indexOf('"') === -1 && font.indexOf("'") === -1 )
|
||||
font = `"${font}"`;
|
||||
|
||||
this.css_tweaks.setVariable('chat-font-size', `${size/10}rem`);
|
||||
this.css_tweaks.setVariable('chat-line-height', `${lh/10}rem`);
|
||||
this.css_tweaks.setVariable('chat-font-family', font);
|
||||
|
||||
this.css_tweaks.toggle('chat-font', size !== 12 || font);
|
||||
|
||||
this.css_tweaks.toggle('emote-alignment-padded', emote_alignment === 1);
|
||||
this.css_tweaks.toggle('emote-alignment-baseline', emote_alignment === 2);
|
||||
}
|
||||
|
||||
|
||||
updateColors() {
|
||||
const is_dark = this.chat.context.get('theme.is-dark'),
|
||||
mode = this.chat.context.get('chat.adjustment-mode'),
|
||||
contrast = this.chat.context.get('chat.adjustment-contrast'),
|
||||
c = this.colors,
|
||||
ic = this.inverse_colors;
|
||||
|
||||
// TODO: Get the background color from the theme system.
|
||||
// Updated: Use the lightest/darkest colors from alternating rows for better readibility.
|
||||
c._base = is_dark ? '#191919' : '#e0e0e0'; //#0e0c13' : '#faf9fa';
|
||||
c.mode = mode;
|
||||
c.contrast = contrast;
|
||||
|
||||
ic._base = is_dark ? '#dad8de' : '#19171c';
|
||||
ic.mode = mode;
|
||||
ic.contrast = contrast;
|
||||
|
||||
this.line.updateLines();
|
||||
}
|
||||
|
||||
|
||||
async loadBadges() {
|
||||
let data;
|
||||
try {
|
||||
data = await this.twitch_data.getBadges();
|
||||
} catch(err) {
|
||||
this.log.warn('Error loading badge data.', err);
|
||||
return;
|
||||
}
|
||||
|
||||
if ( data )
|
||||
this.chat.badges.updateTwitchBadges(data);
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Room Handling
|
||||
// ========================================================================
|
||||
|
||||
addRoom(thing, props) {
|
||||
if ( ! props )
|
||||
props = thing.props;
|
||||
|
||||
const channel_id = get('data.clip.broadcaster.id', props);
|
||||
if ( ! channel_id )
|
||||
return null;
|
||||
|
||||
const room = thing._ffz_room = this.chat.getRoom(channel_id, null, false, true);
|
||||
room.ref(thing);
|
||||
return room;
|
||||
}
|
||||
|
||||
|
||||
removeRoom(thing) { // eslint-disable-line class-methods-use-this
|
||||
if ( ! thing._ffz_room )
|
||||
return;
|
||||
|
||||
thing._ffz_room.unref(thing);
|
||||
thing._ffz_room = null;
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Chat Controller
|
||||
// ========================================================================
|
||||
|
||||
chatMounted(chat, props) {
|
||||
if ( ! props )
|
||||
props = chat.props;
|
||||
|
||||
if ( ! this.addRoom(chat, props) )
|
||||
return;
|
||||
|
||||
this.updateRoomBadges(chat, get('data.clip.video.owner.broadcastBadges', props));
|
||||
}
|
||||
|
||||
|
||||
chatUmounted(chat) {
|
||||
this.removeRoom(chat);
|
||||
}
|
||||
|
||||
|
||||
chatUpdated(chat, props) {
|
||||
if ( ! chat._ffz_room || props?.data?.clip?.broadcaster?.id !== chat._ffz_room.id ) {
|
||||
this.chatUmounted(chat);
|
||||
this.chatMounted(chat, props);
|
||||
return;
|
||||
}
|
||||
|
||||
const new_room_badges = get('data.clip.video.owner.broadcastBadges', props),
|
||||
old_room_badges = get('data.clip.video.owner.broadcastBadges', chat.props);
|
||||
|
||||
if ( new_room_badges !== old_room_badges )
|
||||
this.updateRoomBadges(chat, new_room_badges);
|
||||
}
|
||||
|
||||
updateRoomBadges(chat, badges) { // eslint-disable-line class-methods-use-this
|
||||
const room = chat._ffz_room;
|
||||
if ( ! room )
|
||||
return;
|
||||
|
||||
room.updateBadges(badges);
|
||||
}
|
||||
}
|
15
src/sites/clips/css_tweaks/chat-borders-3d-inset.scss
Normal file
15
src/sites/clips/css_tweaks/chat-borders-3d-inset.scss
Normal file
|
@ -0,0 +1,15 @@
|
|||
.clips-chat-replay > div {
|
||||
padding-top: calc(.5rem - 1px) !important;
|
||||
|
||||
border-top: 1px solid #aaa;
|
||||
border-bottom-color: var rgba(255,255,255,0.5);
|
||||
|
||||
.tw-root--theme-dark & {
|
||||
border-top-color: #000;
|
||||
border-bottom-color: rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-top-color: transparent !important;
|
||||
}
|
||||
}
|
15
src/sites/clips/css_tweaks/chat-borders-3d.scss
Normal file
15
src/sites/clips/css_tweaks/chat-borders-3d.scss
Normal file
|
@ -0,0 +1,15 @@
|
|||
.clips-chat-replay > div {
|
||||
padding-top: calc(.5rem - 1px) !important;
|
||||
|
||||
border-top: 1px solid rgba(255,255,255,0.5);
|
||||
border-bottom-color: #aaa;
|
||||
|
||||
.tw-root--theme-dark & {
|
||||
border-top-color: rgba(255,255,255,0.1);
|
||||
border-bottom-color: #000;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-top-color: transparent !important;
|
||||
}
|
||||
}
|
8
src/sites/clips/css_tweaks/chat-borders-wide.scss
Normal file
8
src/sites/clips/css_tweaks/chat-borders-wide.scss
Normal file
|
@ -0,0 +1,8 @@
|
|||
.clips-chat-replay > div {
|
||||
padding-top: calc(.5rem - 1px) !important;
|
||||
border-top: 1px solid var(--color-border-base);
|
||||
|
||||
&:first-child {
|
||||
border-top-color: transparent !important;
|
||||
}
|
||||
}
|
8
src/sites/clips/css_tweaks/chat-borders.scss
Normal file
8
src/sites/clips/css_tweaks/chat-borders.scss
Normal file
|
@ -0,0 +1,8 @@
|
|||
.clips-chat-replay > div {
|
||||
padding-bottom: calc(.5rem - 1px) !important;
|
||||
border-bottom: 1px solid var(--color-border-base);
|
||||
|
||||
&:last-child:nth-child(odd) {
|
||||
border-bottom-color: transparent !important;
|
||||
}
|
||||
}
|
9
src/sites/clips/css_tweaks/chat-font.scss
Normal file
9
src/sites/clips/css_tweaks/chat-font.scss
Normal file
|
@ -0,0 +1,9 @@
|
|||
.clips-chat-replay {
|
||||
a.clip-chat__message-author,
|
||||
.message {
|
||||
font-size: var(--ffz-chat-font-size) !important;
|
||||
}
|
||||
|
||||
line-height: var(--ffz-chat-line-height);
|
||||
font-family: var(--ffz-chat-font-family);
|
||||
}
|
13
src/sites/clips/css_tweaks/chat-rows.scss
Normal file
13
src/sites/clips/css_tweaks/chat-rows.scss
Normal file
|
@ -0,0 +1,13 @@
|
|||
.clips-chat-replay > div {
|
||||
&:not(.ffz-custom-color) {
|
||||
background-color: transparent !important;
|
||||
|
||||
&:nth-child(2n+0) {
|
||||
background-color: rgba(0,0,0,0.1) !important;
|
||||
|
||||
.tw-root--theme-dark & {
|
||||
background-color: rgba(255,255,255,0.05) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
8
src/sites/clips/css_tweaks/emote-alignment-baseline.scss
Normal file
8
src/sites/clips/css_tweaks/emote-alignment-baseline.scss
Normal file
|
@ -0,0 +1,8 @@
|
|||
.message > div > .chat-line__message--emote {
|
||||
vertical-align: baseline;
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
.message > div > .chat-line__message--emote.ffz-emoji {
|
||||
padding-top: 0px;
|
||||
}
|
3
src/sites/clips/css_tweaks/emote-alignment-padded.scss
Normal file
3
src/sites/clips/css_tweaks/emote-alignment-padded.scss
Normal file
|
@ -0,0 +1,3 @@
|
|||
.message > div > .chat-line__message--emote {
|
||||
margin: -1px 0 0;
|
||||
}
|
9
src/sites/clips/css_tweaks/full-width.scss
Normal file
9
src/sites/clips/css_tweaks/full-width.scss
Normal file
|
@ -0,0 +1,9 @@
|
|||
@media (min-width: 1200px) {
|
||||
.clips-watch {
|
||||
max-width: #{"min(calc(100vw - 2rem),calc(calc(16 / 9 * calc(100vh - 15rem)) + 36rem))"};
|
||||
}
|
||||
|
||||
.clips-sidebar {
|
||||
height: unset !important;
|
||||
}
|
||||
}
|
6
src/sites/clips/css_tweaks/global-font.scss
Normal file
6
src/sites/clips/css_tweaks/global-font.scss
Normal file
|
@ -0,0 +1,6 @@
|
|||
html body {
|
||||
font-family: var(--ffz-global-font) !important;
|
||||
|
||||
--font-base: var(--ffz-global-font) !important;
|
||||
--font-display: var(--ffz-global-font) !important;
|
||||
}
|
5
src/sites/clips/css_tweaks/player-ext-mouse.scss
Normal file
5
src/sites/clips/css_tweaks/player-ext-mouse.scss
Normal file
|
@ -0,0 +1,5 @@
|
|||
.video-player .extension-overlay__iframe,
|
||||
.video-player .extension-overlay,
|
||||
.video-player .extension-view__iframe {
|
||||
pointer-events: none !important;
|
||||
}
|
3
src/sites/clips/css_tweaks/player-hide-mouse.scss
Normal file
3
src/sites/clips/css_tweaks/player-hide-mouse.scss
Normal file
|
@ -0,0 +1,3 @@
|
|||
.video-player__overlay[data-controls="false"][data-paused="false"][data-ended="false"] {
|
||||
cursor: none;
|
||||
}
|
3
src/sites/clips/css_tweaks/player-volume.scss
Normal file
3
src/sites/clips/css_tweaks/player-volume.scss
Normal file
|
@ -0,0 +1,3 @@
|
|||
.video-player .volume-slider__slider-container {
|
||||
opacity: 1 !important;
|
||||
}
|
19
src/sites/clips/css_tweaks/square-avatars.scss
Normal file
19
src/sites/clips/css_tweaks/square-avatars.scss
Normal file
|
@ -0,0 +1,19 @@
|
|||
.user-avatar-animated,
|
||||
.search-result-card,
|
||||
.ffz-avatar,
|
||||
.tw-avatar {
|
||||
--border-radius-rounded: 0 !important;
|
||||
}
|
||||
|
||||
.user-avatar-card__halo,
|
||||
.player-streaminfo__picture img[src] {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.bits-leaderboard-medal .tw-avatar {
|
||||
--border-radius-rounded: 9000px !important;
|
||||
}
|
||||
|
||||
.user-avatar-animated .user-avatar-animated__halo {
|
||||
visibility: hidden;
|
||||
}
|
218
src/sites/clips/index.jsx
Normal file
218
src/sites/clips/index.jsx
Normal file
|
@ -0,0 +1,218 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Standalone Clips
|
||||
// ============================================================================
|
||||
|
||||
import {createElement} from 'utilities/dom';
|
||||
import { sleep, has } from 'utilities/object';
|
||||
|
||||
import BaseSite from '../base';
|
||||
|
||||
import Fine from 'utilities/compat/fine';
|
||||
import FineRouter from 'utilities/compat/fine-router';
|
||||
import Apollo from 'utilities/compat/apollo';
|
||||
import TwitchData from 'utilities/twitch-data';
|
||||
import CSSTweaks from 'utilities/css-tweaks';
|
||||
|
||||
import Player from './player';
|
||||
import Chat from './chat';
|
||||
import Theme from './theme';
|
||||
|
||||
import MAIN_URL from './styles/clips-main.scss';
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// The Site
|
||||
// ============================================================================
|
||||
|
||||
export default class ClipsSite extends BaseSite {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.inject('settings');
|
||||
this.inject('i18n');
|
||||
|
||||
this.inject(Fine);
|
||||
this.inject('router', FineRouter);
|
||||
this.inject(Apollo, false);
|
||||
this.inject(TwitchData);
|
||||
this.inject('css_tweaks', CSSTweaks);
|
||||
|
||||
this.css_tweaks.loader = require.context(
|
||||
'!raw-loader!sass-loader!./css_tweaks', false, /\.s?css$/, 'lazy-once'
|
||||
);
|
||||
|
||||
this.css_tweaks.rules = {
|
||||
'unfollow-button': '.follow-btn__follow-btn--following,.follow-btn--following',
|
||||
'player-ext': '.video-player .extension-taskbar,.video-player .extension-container,.video-player .extensions-dock__layout,.video-player .extensions-notifications,.video-player .extensions-video-overlay-size-container,.video-player .extensions-dock__layout',
|
||||
'player-ext-hover': '.video-player__overlay[data-controls="false"] .extension-taskbar,.video-player__overlay[data-controls="false"] .extension-container,.video-player__overlay[data-controls="false"] .extensions-dock__layout,.video-player__overlay[data-controls="false"] .extensions-notifications,.video-player__overlay[data-controls="false"] .extensions-video-overlay-size-container',
|
||||
'dark-toggle': 'div[data-a-target="dark-mode-toggle"],div[data-a-target="dark-mode-toggle"] + .tw-border-b'
|
||||
};
|
||||
|
||||
this.inject(Player);
|
||||
this.inject(Chat);
|
||||
this.inject('theme', Theme);
|
||||
|
||||
this.ClipsMenu = this.fine.define(
|
||||
'clips-menu',
|
||||
n => n.props?.changeTheme && has(n.state, 'dropdownOpen')
|
||||
)
|
||||
|
||||
document.head.appendChild(createElement('link', {
|
||||
href: MAIN_URL,
|
||||
rel: 'stylesheet',
|
||||
type: 'text/css',
|
||||
crossOrigin: 'anonymous'
|
||||
}));
|
||||
}
|
||||
|
||||
onLoad() {
|
||||
this.router.route(ClipsSite.CLIP_ROUTES, 'clips.twitch.tv');
|
||||
}
|
||||
|
||||
onEnable() {
|
||||
const thing = this.fine.searchNode(null, n => n.memoizedProps?.store),
|
||||
store = this.store = thing?.memoizedProps?.store;
|
||||
|
||||
if ( ! store )
|
||||
return sleep(50).then(() => this.onEnable());
|
||||
|
||||
this.ClipsMenu.on('mount', this.updateMenu, this);
|
||||
this.ClipsMenu.on('update', this.updateMenu, this);
|
||||
this.ClipsMenu.ready((cls, instances) => {
|
||||
for(const inst of instances)
|
||||
this.updateMenu(inst);
|
||||
});
|
||||
|
||||
this.on('i18n:update', () => {
|
||||
for(const inst of this.ClipsMenu.instances)
|
||||
this.updateMenu(inst);
|
||||
});
|
||||
|
||||
store.subscribe(() => this.updateContext());
|
||||
this.updateContext();
|
||||
|
||||
this.settings.updateContext({
|
||||
clips: true
|
||||
});
|
||||
|
||||
// Window Size
|
||||
const update_size = () => this.settings.updateContext({
|
||||
size: {
|
||||
height: window.innerHeight,
|
||||
width: window.innerWidth
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('resize', update_size);
|
||||
update_size();
|
||||
|
||||
this.router.on(':route', (route, match) => {
|
||||
this.log.info('Navigation', route && route.name, match && match[0]);
|
||||
this.fine.route(route && route.name);
|
||||
this.settings.updateContext({
|
||||
route,
|
||||
route_data: match
|
||||
});
|
||||
});
|
||||
|
||||
const current = this.router.current;
|
||||
this.fine.route(current && current.name);
|
||||
this.settings.updateContext({
|
||||
route: current,
|
||||
route_data: this.router.match
|
||||
});
|
||||
|
||||
this.settings.getChanges('channel.hide-unfollow', val =>
|
||||
this.css_tweaks.toggleHide('unfollow-button', val));
|
||||
|
||||
this.settings.getChanges('clips.layout.big', val =>
|
||||
this.css_tweaks.toggle('full-width', val));
|
||||
}
|
||||
|
||||
updateContext() {
|
||||
try {
|
||||
const state = this.store.getState(),
|
||||
history = this.router && this.router.history;
|
||||
|
||||
this.settings.updateContext({
|
||||
location: history?.location,
|
||||
ui: state?.ui,
|
||||
session: state?.session
|
||||
});
|
||||
|
||||
} catch(err) {
|
||||
this.log.error('Error updating context.', err);
|
||||
}
|
||||
}
|
||||
|
||||
getSession() {
|
||||
return this.store?.getState?.()?.session;
|
||||
}
|
||||
|
||||
getUser() {
|
||||
if ( this._user )
|
||||
return this._user;
|
||||
|
||||
const session = this.getSession();
|
||||
return this._user = session?.user;
|
||||
}
|
||||
|
||||
updateMenu(inst) {
|
||||
const outer = this.fine.getChildNode(inst),
|
||||
container = outer && outer.querySelector('.clips-top-nav-user__dropdown--open');
|
||||
|
||||
if ( ! container )
|
||||
return;
|
||||
|
||||
const should_render = inst.state.dropdownOpen;
|
||||
|
||||
let lbl, btn, cont = container.querySelector('.ffz--cc-button');
|
||||
if ( ! cont ) {
|
||||
if ( ! should_render )
|
||||
return;
|
||||
|
||||
const handler = () => {
|
||||
const win = window.open(
|
||||
'https://twitch.tv/popout/frankerfacez/chat?ffz-settings',
|
||||
'_blank',
|
||||
'resizable=yes,scrollbars=yes,width=850,height=600'
|
||||
);
|
||||
|
||||
if ( win )
|
||||
win.focus();
|
||||
|
||||
inst.toggleDropdown(false);
|
||||
}
|
||||
|
||||
cont = (<div class="ffz--cc-button">
|
||||
{btn = (<button
|
||||
class="tw-full-width ffz-interactable ffz-interactable--hover-enabled ffz-interactable--default tw-interactive"
|
||||
onclick={handler}
|
||||
>
|
||||
<div class="tw-align-items-center tw-c-text-alt tw-flex tw-pd-x-2 tw-pd-y-05">
|
||||
{lbl = <div class="ffz--label tw-flex-grow-1 tw-ellipsis" />}
|
||||
</div>
|
||||
</button>)}
|
||||
</div>)
|
||||
|
||||
container.insertBefore(cont, container.lastElementChild);
|
||||
|
||||
} else if ( ! should_render ) {
|
||||
cont.remove();
|
||||
return;
|
||||
} else {
|
||||
btn = cont.querySelector('button');
|
||||
lbl = cont.querySelector('.ffz--label');
|
||||
}
|
||||
|
||||
lbl.textContent = btn.title = this.i18n.t('site.menu_button', 'FrankerFaceZ Control Center');
|
||||
//ver.textContent = this.resolve('core').constructor.version_info.toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ClipsSite.CLIP_ROUTES = {
|
||||
'clip-page': '/:slug'
|
||||
};
|
213
src/sites/clips/line.jsx
Normal file
213
src/sites/clips/line.jsx
Normal file
|
@ -0,0 +1,213 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Twitch Player
|
||||
// ============================================================================
|
||||
|
||||
import Module from 'utilities/module';
|
||||
|
||||
import {createElement} from 'react';
|
||||
import { split_chars } from 'utilities/object';
|
||||
|
||||
export default class Line extends Module {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.inject('settings');
|
||||
this.inject('i18n');
|
||||
|
||||
this.inject('chat');
|
||||
this.inject('site.fine');
|
||||
|
||||
this.ChatLine = this.fine.define(
|
||||
'clip-chat-line',
|
||||
n => n.renderFragments && n.renderUserBadges
|
||||
);
|
||||
|
||||
this.render = true;
|
||||
|
||||
window.toggleLines = () => {
|
||||
this.render = ! this.render;
|
||||
this.updateLines();
|
||||
}
|
||||
}
|
||||
|
||||
onEnable() {
|
||||
this.chat.context.on('changed:chat.emotes.2x', this.updateLines, this);
|
||||
this.chat.context.on('changed:chat.emoji.style', this.updateLines, this);
|
||||
this.chat.context.on('changed:chat.bits.stack', this.updateLines, this);
|
||||
this.chat.context.on('changed:chat.badges.style', this.updateLines, this);
|
||||
this.chat.context.on('changed:chat.badges.hidden', this.updateLines, this);
|
||||
this.chat.context.on('changed:chat.badges.custom-mod', this.updateLines, this);
|
||||
this.chat.context.on('changed:chat.rich.enabled', this.updateLines, this);
|
||||
this.chat.context.on('changed:chat.rich.hide-tokens', this.updateLines, this);
|
||||
this.chat.context.on('changed:chat.rich.all-links', this.updateLines, this);
|
||||
this.chat.context.on('changed:chat.rich.minimum-level', this.updateLines, this);
|
||||
this.chat.context.on('changed:tooltip.link-images', this.maybeUpdateLines, this);
|
||||
this.chat.context.on('changed:tooltip.link-nsfw-images', this.maybeUpdateLines, this);
|
||||
|
||||
this.on('chat:update-lines-by-user', this.updateLinesByUser, this);
|
||||
this.on('chat:update-lines', this.updateLines, this);
|
||||
this.on('i18n:update', this.updateLines, this);
|
||||
|
||||
this.site = this.resolve('site');
|
||||
|
||||
this.ChatLine.ready(cls => {
|
||||
const t = this,
|
||||
old_render = cls.prototype.render;
|
||||
|
||||
cls.prototype.render = function() {
|
||||
try {
|
||||
this._ffz_no_scan = true;
|
||||
if ( ! t.render )
|
||||
return old_render.call(this);
|
||||
|
||||
|
||||
const msg = t.standardizeMessage(this.props.node, this.props.video),
|
||||
is_action = msg.is_action,
|
||||
user = msg.user,
|
||||
color = t.parent.colors.process(user.color),
|
||||
|
||||
u = t.site.getUser();
|
||||
|
||||
const tokens = msg.ffz_tokens = msg.ffz_tokens || t.chat.tokenizeMessage(msg, u);
|
||||
|
||||
return (<div
|
||||
data-a-target="tw-animation-target"
|
||||
class="ffz--clip-chat-line tw-animation tw-animation--animate tw-animation--duration-short tw-animation--fill-mode-both tw-animation--slide-in-bottom tw-animation--timing-ease"
|
||||
data-room-id={msg.roomID}
|
||||
data-room={msg.roomLogin}
|
||||
data-user-id={user.id}
|
||||
data-user={user.login}
|
||||
>
|
||||
<span class="chat-line__message--badges">{
|
||||
t.chat.badges.render(msg, createElement)
|
||||
}</span>
|
||||
<a
|
||||
class="clip-chat__message-author tw-font-size-5 tw-strong tw-link notranslate"
|
||||
href={`https://www.twitch.tv/${user.login}/clips`}
|
||||
style={{color}}
|
||||
>
|
||||
<span class="chat-author__display_name">{ user.displayName }</span>
|
||||
{user.isIntl && <span class="chat-author__intl-login"> ({user.login}) </span>}
|
||||
</a>
|
||||
<div class="tw-inline-block tw-mg-r-05">{
|
||||
is_action ? '' : ':'
|
||||
}</div>
|
||||
<span class="message" style={{color: is_action ? color : null}}>{
|
||||
t.chat.renderTokens(tokens, createElement)
|
||||
}</span>
|
||||
</div>);
|
||||
|
||||
} catch(err) {
|
||||
t.log.error(err);
|
||||
t.log.capture(err, {extra:{props: this.props}});
|
||||
}
|
||||
|
||||
return old_render.call(this);
|
||||
}
|
||||
|
||||
this.ChatLine.forceUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
updateLinesByUser(id, login) {
|
||||
for(const inst of this.ChatLine.instances) {
|
||||
const msg = inst.props.node,
|
||||
user = msg?.commentor;
|
||||
if ( user && ((id && id == user.id) || (login && login == user.login)) ) {
|
||||
msg._ffz_message = null;
|
||||
inst.forceUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
maybeUpdateLines() {
|
||||
if ( this.chat.context.get('chat.rich.all-links') )
|
||||
this.updateLines();
|
||||
}
|
||||
|
||||
|
||||
updateLines() {
|
||||
for(const inst of this.ChatLine.instances) {
|
||||
const msg = inst.props.node;
|
||||
if ( msg )
|
||||
msg._ffz_message = null;
|
||||
}
|
||||
|
||||
this.ChatLine.forceUpdate();
|
||||
}
|
||||
|
||||
|
||||
standardizeMessage(msg, video) {
|
||||
if ( ! msg || ! msg.message )
|
||||
return msg;
|
||||
|
||||
if ( msg._ffz_message )
|
||||
return msg._ffz_message;
|
||||
|
||||
const room = this.chat.getRoom(video.owner.id, null, true, true),
|
||||
author = msg.commenter || {},
|
||||
badges = {};
|
||||
|
||||
if ( msg.message.userBadges )
|
||||
for(const badge of msg.message.userBadges)
|
||||
if ( badge )
|
||||
badges[badge.setID] = badge.version;
|
||||
|
||||
const out = msg._ffz_message = {
|
||||
user: {
|
||||
color: author.chatColor,
|
||||
id: author.id,
|
||||
login: author.login,
|
||||
displayName: author.displayName,
|
||||
isIntl: author.login && author.displayName && author.displayName.trim().toLowerCase() !== author.login,
|
||||
type: 'user'
|
||||
},
|
||||
roomLogin: room && room.login,
|
||||
roomID: room && room.id,
|
||||
badges,
|
||||
messageParts: msg.message.fragments
|
||||
};
|
||||
|
||||
this.detokenizeMessage(out, msg);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
detokenizeMessage(msg) { // eslint-disable-line class-methods-use-this
|
||||
const out = [],
|
||||
parts = msg.messageParts,
|
||||
l = parts.length,
|
||||
emotes = {};
|
||||
|
||||
let idx = 0;
|
||||
|
||||
for(let i=0; i < l; i++) {
|
||||
const part = parts[i],
|
||||
text = part && part.text;
|
||||
|
||||
if ( ! text || ! text.length )
|
||||
continue;
|
||||
|
||||
const len = split_chars(text).length;
|
||||
|
||||
if ( part.emote ) {
|
||||
const id = part.emote.emoteID,
|
||||
em = emotes[id] = emotes[id] || [];
|
||||
|
||||
em.push({startIndex: idx, endIndex: idx + len - 1});
|
||||
}
|
||||
|
||||
out.push(text);
|
||||
idx += len;
|
||||
}
|
||||
|
||||
msg.message = out.join('');
|
||||
msg.ffz_emotes = emotes;
|
||||
|
||||
return msg;
|
||||
}
|
||||
}
|
23
src/sites/clips/player.jsx
Normal file
23
src/sites/clips/player.jsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Twitch Player
|
||||
// ============================================================================
|
||||
|
||||
import PlayerBase from 'src/sites/shared/player';
|
||||
|
||||
export default class Player extends PlayerBase {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.Player = this.fine.define(
|
||||
'highwind-player',
|
||||
n => n.setPlayerActive && n.props?.playerEvents && n.props?.mediaPlayerInstance
|
||||
);
|
||||
|
||||
this.PlayerSource = this.fine.define(
|
||||
'player-source',
|
||||
n => n.setSrc && n.setInitialPlaybackSettings
|
||||
);
|
||||
}
|
||||
}
|
43
src/sites/clips/styles/clips-main.scss
Normal file
43
src/sites/clips/styles/clips-main.scss
Normal file
|
@ -0,0 +1,43 @@
|
|||
@import 'styles/main.scss';
|
||||
|
||||
.tw-root--theme-dark, html {
|
||||
body {
|
||||
background-color: var(--color-background-body) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.clips-top-nav-user {
|
||||
background-color: var(--color-background-base) !important;
|
||||
}
|
||||
|
||||
.chat-line__message--emote {
|
||||
vertical-align: middle;
|
||||
margin: -.5rem 0;
|
||||
}
|
||||
|
||||
.chat-author__display-name,
|
||||
.chat-author__intl-login {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ffz-emoji {
|
||||
width: calc(var(--ffz-chat-font-size) * 1.5);
|
||||
height: calc(var(--ffz-chat-font-size) * 1.5);
|
||||
|
||||
&.preview-image {
|
||||
width: 7.2rem;
|
||||
height: 7.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.clips-chat-replay {
|
||||
&.tw-flex {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
& > div {
|
||||
margin: 0 -1rem !important;
|
||||
padding: 0.5rem;
|
||||
display: block !important;
|
||||
}
|
||||
}
|
464
src/sites/clips/theme.js
Normal file
464
src/sites/clips/theme.js
Normal file
|
@ -0,0 +1,464 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Menu Module
|
||||
// ============================================================================
|
||||
|
||||
import Module from 'utilities/module';
|
||||
import {createElement} from 'utilities/dom';
|
||||
import {Color} from 'utilities/color';
|
||||
|
||||
//import THEME_CSS from 'site/styles/theme.scss';
|
||||
//import NORMALIZER_CSS_URL from './styles/color_normalizer.scss';
|
||||
|
||||
const COLORS = [
|
||||
-0.62, -0.578, -0.539, -0.469, -0.4, -0.32, -0.21, -0.098, // 1-8
|
||||
0, // 9
|
||||
0.08, 0.151, 0.212, 0.271, 0.31, 0.351 // 10-15
|
||||
];
|
||||
|
||||
|
||||
const ACCENT_COLORS = {
|
||||
//dark: {'c':{'accent': 9,'background-accent':8,'background-accent-alt':7,'background-accent-alt-2':6,'background-button':7,'background-button-active':7,'background-button-focus':8,'background-button-hover':8,'background-button-primary-active':7,'background-button-primary-default':9,'background-button-primary-hover':8,'background-graph':2,'background-graph-fill':8,'background-input-checkbox-checked':9,'background-input-checked':8,'background-interactable-active':9,'background-interactable-selected':9,'background-interactable-hover':8,'background-progress-countdown-status':9,'background-progress-status':9,'background-range-fill':9,'background-subscriber-stream-tag-active':4,'background-subscriber-stream-tag-default':4,'background-subscriber-stream-tag-hover':3,'background-toggle-checked':9,/*'background-tooltip':1,*/'background-top-nav':6,'border-brand':9,'border-button':7,'border-button-active':8,'border-button-focus':9,'border-button-hover':8,'border-input-checkbox-checked':9,'border-input-checkbox-focus':9,'border-input-focus':9,'border-interactable-selected':10,'border-subscriber-stream-tag':5,'border-tab-active':11,'border-tab-focus':11,'border-tab-hover':11,'border-toggle-focus':7,'border-toggle-hover':7,'border-whisper-incoming':10,'fill-brand':9,'text-button-text':10,'text-button-text-focus':'o1','text-button-text-hover':'o1','text-link':10,'text-link-active':10,'text-link-focus':10,'text-link-hover':10,'text-link-visited':10,'text-overlay-link-active':13,'text-overlay-link-focus':13,'text-overlay-link-hover':13,'text-tab-active':11,'background-chat':1,'background-chat-alt':3,'background-chat-header':2,'background-modal':3,'text-button-text-active':'o2'/*,'text-tooltip':1*/},'s':{'button-active':[8,'0 0 6px 0',''],'button-focus':[8,'0 0 6px 0',''],'input-focus':[8,'0 0 10px -2px',''],'interactable-focus':[8,'0 0 6px 0',''],'tab-focus':[11,'0 4px 6px -4px',''],'input':[5,'inset 0 0 0 1px','']}},
|
||||
//light: {'c':{'accent': 9,'background-accent':8,'background-accent-alt':7,'background-accent-alt-2':6,'background-button':7,'background-button-active':7,'background-button-focus':8,'background-button-hover':8,'background-button-primary-active':7,'background-button-primary-default':9,'background-button-primary-hover':8,'background-graph':15,'background-graph-fill':9,'background-input-checkbox-checked':9,'background-input-checked':8,'background-interactable-active':9,'background-interactable-selected':9,'background-interactable-hover':8,'background-progress-countdown-status':8,'background-progress-status':8,'background-range-fill':9,'background-subscriber-stream-tag-active':13,'background-subscriber-stream-tag-default':13,'background-subscriber-stream-tag-hover':14,'background-toggle-checked':9,/*'background-tooltip':1,*/'background-top-nav':7,'border-brand':9,'border-button':7,'border-button-active':8,'border-button-focus':9,'border-button-hover':8,'border-input-checkbox-checked':9,'border-input-checkbox-focus':9,'border-input-focus':9,'border-interactable-selected':9,'border-subscriber-stream-tag':10,'border-tab-active':8,'border-tab-focus':8,'border-tab-hover':8,'border-toggle-focus':8,'border-toggle-hover':8,'border-whisper-incoming':10,'fill-brand':9,'text-button-text':8,'text-button-text-focus':'o1','text-button-text-hover':'o1','text-link':8,'text-link-active':9,'text-link-focus':9,'text-link-hover':9,'text-link-visited':9,'text-overlay-link-active':13,'text-overlay-link-focus':13,'text-overlay-link-hover':13,'text-tab-active':8},'s':{'button-active':[8,'0 0 6px 0',''],'button-focus':[8,'0 0 6px 0',''],'input-focus':[10,'0 0 10px -2px',''],'interactable-focus':[8,'0 0 6px 1px',''],'tab-focus':[8,'0 4px 6px -4px','']}},
|
||||
//dark: {'c':{'background-accent':8,'background-accent-alt':7,'background-accent-alt-2':6,'background-button':7,'background-button-active':7,'background-button-focus':8,'background-button-hover':8,'background-button-primary-active':7,'background-button-primary-default':9,'background-button-primary-hover':8,'background-graph':2,'background-graph-fill':8,'background-input-checkbox-checked':9,'background-input-checked':8,'background-interactable-selected':9,'background-progress-countdown-status':9,'background-progress-status':9,'background-range-fill':9,'background-subscriber-stream-tag-active':4,'background-subscriber-stream-tag-default':4,'background-subscriber-stream-tag-hover':3,'background-toggle-checked':9,'background-top-nav':6,'border-brand':9,'border-button':7,'border-button-active':8,'border-button-focus':9,'border-button-hover':8,'border-input-checkbox-checked':9,'border-input-checkbox-focus':9,'border-input-focus':9,'border-interactable-selected':10,'border-subscriber-stream-tag':5,'border-tab-active':11,'border-tab-focus':11,'border-tab-hover':11,'border-toggle-focus':7,'border-toggle-hover':7,'border-whisper-incoming':10,'fill-brand':9,'text-button-text':10,'text-button-text-focus':'o1','text-button-text-hover':'o1','text-link':10,'text-link-active':10,'text-link-focus':10,'text-link-hover':10,'text-link-visited':10,'text-overlay-link-active':13,'text-overlay-link-focus':13,'text-overlay-link-hover':13,'text-tab-active':11,'background-chat':1,'background-chat-alt':3,'background-chat-header':2,'background-modal':3,'text-button-text-active':'o2'},'s':{'button-active':[8,'0 0 6px 0',''],'button-focus':[8,'0 0 6px 0',''],'input-focus':[8,'0 0 10px -2px',''],'interactable-focus':[8,'0 0 6px 0',''],'tab-focus':[11,'0 4px 6px -4px',''],'input':[5,'inset 0 0 0 1px','']}},
|
||||
//light: {'c':{'background-accent':8,'background-accent-alt':7,'background-accent-alt-2':6,'background-button':7,'background-button-active':7,'background-button-focus':8,'background-button-hover':8,'background-button-primary-active':7,'background-button-primary-default':9,'background-button-primary-hover':8,'background-graph':15,'background-graph-fill':9,'background-input-checkbox-checked':9,'background-input-checked':8,'background-interactable-selected':9,'background-progress-countdown-status':8,'background-progress-status':8,'background-range-fill':9,'background-subscriber-stream-tag-active':13,'background-subscriber-stream-tag-default':13,'background-subscriber-stream-tag-hover':14,'background-toggle-checked':9,'background-top-nav':7,'border-brand':9,'border-button':7,'border-button-active':8,'border-button-focus':9,'border-button-hover':8,'border-input-checkbox-checked':9,'border-input-checkbox-focus':9,'border-input-focus':9,'border-interactable-selected':9,'border-subscriber-stream-tag':10,'border-tab-active':8,'border-tab-focus':8,'border-tab-hover':8,'border-toggle-focus':8,'border-toggle-hover':8,'border-whisper-incoming':10,'fill-brand':9,'text-button-text':8,'text-button-text-focus':'o1','text-button-text-hover':'o1','text-link':8,'text-link-active':9,'text-link-focus':9,'text-link-hover':9,'text-link-visited':9,'text-overlay-link-active':13,'text-overlay-link-focus':13,'text-overlay-link-hover':13,'text-tab-active':8},'s':{'button-active':[8,'0 0 6px 0',''],'button-focus':[8,'0 0 6px 0',''],'input-focus':[10,'0 0 10px -2px',''],'interactable-focus':[8,'0 0 6px 1px',''],'tab-focus':[8,'0 4px 6px -4px','']}},
|
||||
dark: {'c':{'background-accent':8,'background-accent-alt':7,'background-accent-alt-2':6,'background-button':7,'background-button-active':7,'background-button-focus':8,'background-button-hover':8,'background-button-primary-active':7,'background-button-primary-default':9,'background-button-primary-hover':8,'background-chat':1,'background-chat-alt':3,'background-chat-header':2,'background-graph':2,'background-graph-fill':8,'background-input-checkbox-checked':10,'background-input-checked':8,'background-interactable-selected':9,'background-modal':3,'background-progress-countdown-status':9,'background-progress-status':9,'background-range-fill':10,'background-subscriber-stream-tag-active':4,'background-subscriber-stream-tag-default':4,'background-subscriber-stream-tag-hover':3,'background-toggle-checked':9,'background-top-nav':6,'border-brand':9,'border-button':7,'border-button-active':8,'border-button-focus':9,'border-button-hover':8,'border-input-checkbox-checked':10,'border-input-checkbox-focus':10,'border-input-focus':10,'border-interactable-selected':10,'border-range-handle':10,'border-subscriber-stream-tag':5,'border-tab-active':11,'border-tab-focus':11,'border-tab-hover':11,'border-toggle-checked':10,'border-toggle-focus':10,'border-whisper-incoming':10,'fill-brand':9,'text-button-text-active':'o2','text-link':10,'text-link-active':10,'text-link-focus':10,'text-link-hover':10,'text-link-visited':10,'text-overlay-link-active':13,'text-overlay-link-focus':13,'text-overlay-link-hover':13,'text-tab-active':11,'text-toggle-checked-icon':10,'text-tooltip':1,'text-button-text':10},'s':{'button-active':[8,' 0 0 6px 0',''],'button-focus':[8,' 0 0 6px 0',''],'input':[5,' inset 0 0 0 1px',''],'input-focus':[8,' 0 0 10px -2px',''],'interactable-focus':[8,' 0 0 6px 0',''],'tab-focus':[11,' 0 4px 6px -4px','']}},
|
||||
light: {'c':{'background-accent':8,'background-accent-alt':7,'background-accent-alt-2':6,'background-button':7,'background-button-active':7,'background-button-focus':8,'background-button-hover':8,'background-button-primary-active':7,'background-button-primary-default':9,'background-button-primary-hover':8,'background-chat':1,'background-chat-alt':3,'background-chat-header':2,'background-graph':15,'background-graph-fill':9,'background-input-checkbox-checked':9,'background-input-checked':8,'background-interactable-selected':9,'background-modal':3,'background-progress-countdown-status':8,'background-progress-status':8,'background-range-fill':9,'background-subscriber-stream-tag-active':13,'background-subscriber-stream-tag-default':13,'background-subscriber-stream-tag-hover':14,'background-toggle-checked':9,'background-top-nav':7,'border-brand':9,'border-button':7,'border-button-active':8,'border-button-focus':9,'border-button-hover':8,'border-input-checkbox-checked':9,'border-input-checkbox-focus':9,'border-input-focus':9,'border-interactable-selected':9,'border-range-handle':9,'border-subscriber-stream-tag':10,'border-tab-active':8,'border-tab-focus':8,'border-tab-hover':8,'border-toggle-checked':9,'border-toggle-focus':9,'border-whisper-incoming':10,'fill-brand':9,'text-button-text-active':'o2','text-link':8,'text-link-active':9,'text-link-focus':9,'text-link-hover':9,'text-link-visited':9,'text-overlay-link-active':13,'text-overlay-link-focus':13,'text-overlay-link-hover':13,'text-tab-active':8,'text-toggle-checked-icon':9,'text-tooltip':1,'text-button-text':8,'background-tooltip':1,'text-button-text-focus':'o1','text-button-text-hover':'o1'},'s':{'button-active':[8,' 0 0 6px 0',''],'button-focus':[8,' 0 0 6px 0',''],'input':[5,' inset 0 0 0 1px',''],'input-focus':[10,' 0 0 10px -2px',''],'interactable-focus':[8,' 0 0 6px 1px',''],'tab-focus':[8,' 0 4px 6px -4px','']}},
|
||||
accent_dark: {'c':{'accent-hover':10,'accent':9,'accent-primary-1':1,'accent-primary-2':5,'accent-primary-3':6,'accent-primary-4':7,'accent-primary-5':8},'s':{}},
|
||||
accent_light: {'c':{'accent-hover':10,'accent':9,'accent-primary-1':1,'accent-primary-2':5,'accent-primary-3':6,'accent-primary-4':7,'accent-primary-5':8},'s':{}}
|
||||
};
|
||||
|
||||
|
||||
export default class ThemeEngine extends Module {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.inject('settings');
|
||||
|
||||
this.inject('site.fine');
|
||||
this.inject('site.css_tweaks');
|
||||
this.inject('site.router');
|
||||
|
||||
// Font
|
||||
|
||||
this.settings.add('theme.font.size', {
|
||||
default: 13,
|
||||
process(ctx, val) {
|
||||
if ( typeof val !== 'number' )
|
||||
try {
|
||||
val = parseFloat(val);
|
||||
} catch(err) { val = null; }
|
||||
|
||||
if ( ! val || val < 1 || isNaN(val) || ! isFinite(val) || val > 25 )
|
||||
val = 13;
|
||||
|
||||
return val;
|
||||
},
|
||||
changed: () => this.updateFont()
|
||||
});
|
||||
|
||||
|
||||
// Colors
|
||||
|
||||
this.settings.add('theme.color.background', {
|
||||
default: '',
|
||||
changed: () => this.updateCSS()
|
||||
});
|
||||
|
||||
this.settings.add('theme.color.text', {
|
||||
default: '',
|
||||
changed: () => this.updateCSS()
|
||||
});
|
||||
|
||||
this.settings.add('theme.color.accent', {
|
||||
default: '',
|
||||
changed: () => this.updateCSS()
|
||||
});
|
||||
|
||||
this.settings.add('theme.color.tooltip.background', {
|
||||
default: '',
|
||||
changed: () => this.updateCSS()
|
||||
});
|
||||
|
||||
this.settings.add('theme.color.tooltip.text', {
|
||||
default: '',
|
||||
changed: () => this.updateCSS()
|
||||
});
|
||||
|
||||
|
||||
this.settings.add('theme.color.chat-background', {
|
||||
default: '',
|
||||
changed: () => this.updateCSS()
|
||||
});
|
||||
|
||||
this.settings.add('theme.color.chat-text', {
|
||||
default: '',
|
||||
changed: () => this.updateCSS()
|
||||
});
|
||||
|
||||
this.settings.add('theme.color.chat-accent', {
|
||||
default: '',
|
||||
changed: () => this.updateCSS()
|
||||
});
|
||||
|
||||
this.settings.add('theme.can-dark', {
|
||||
requires: ['context.route.name'],
|
||||
process() {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
this.settings.add('theme.is-dark', {
|
||||
requires: ['context.force-dark', 'theme.can-dark', 'context.ui.theme', 'context.ui.theatreModeEnabled', 'context.route.name', 'context.location.search'],
|
||||
process(ctx) {
|
||||
const force = ctx.get('context.force-theme');
|
||||
if ( force != null )
|
||||
return force;
|
||||
|
||||
return ctx.get('context.ui.theatreModeEnabled') || (ctx.get('theme.can-dark') && ctx.get('context.ui.theme') === 1);
|
||||
},
|
||||
changed: () => this.updateCSS()
|
||||
});
|
||||
|
||||
this.settings.add('theme.tooltips-dark', {
|
||||
requires: ['theme.is-dark'],
|
||||
process(ctx) {
|
||||
return ! ctx.get('theme.is-dark')
|
||||
}
|
||||
});
|
||||
|
||||
this._style = null;
|
||||
this._normalizer = null;
|
||||
}
|
||||
|
||||
/*updateOldCSS() {
|
||||
const dark = this.settings.get('theme.is-dark'),
|
||||
gray = this.settings.get('theme.dark');
|
||||
|
||||
//document.body.classList.toggle('tw-root--theme-dark', dark);
|
||||
document.body.classList.toggle('tw-root--theme-ffz', gray);
|
||||
|
||||
this.css_tweaks.setVariable('border-color', dark ? (gray ? '#2a2a2a' : '#2c2541') : '#dad8de');
|
||||
}*/
|
||||
|
||||
updateFont() {
|
||||
let size = this.settings.get('theme.font.size');
|
||||
if ( typeof size === 'string' && /^[0-9.]+$/.test(size) )
|
||||
size = parseFloat(size);
|
||||
else if ( typeof size !== 'number' )
|
||||
size = null;
|
||||
|
||||
if ( ! size || isNaN(size) || ! isFinite(size) || size < 1 || size === 13 ) {
|
||||
this.css_tweaks.delete('font-size');
|
||||
return;
|
||||
}
|
||||
|
||||
size = size / 10;
|
||||
|
||||
this.css_tweaks.set('font-size', `html body {
|
||||
--font-size-1: ${(54/13) * size}rem;
|
||||
--font-size-2: ${(36/13) * size}rem;
|
||||
--font-size-3: ${(24/13) * size}rem;
|
||||
--font-size-4: ${(18/13) * size}rem;
|
||||
--font-size-5: ${(14/13) * size}rem;
|
||||
--font-size-6: ${size}rem;
|
||||
--font-size-7: ${(12/13) * size}rem;
|
||||
--font-size-8: ${(12/13) * size}rem;
|
||||
--font-size-base: ${size}rem;
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
updateCSS() {
|
||||
//this.updateOldCSS();
|
||||
|
||||
this.css_tweaks.setVariable('border-color', 'var(--color-border-base)');
|
||||
|
||||
if ( ! this.settings.get('theme.can-dark') ) {
|
||||
this.toggleNormalizer(false);
|
||||
this.toggleAccentNormal(true);
|
||||
this.css_tweaks.delete('colors');
|
||||
return;
|
||||
}
|
||||
|
||||
let hide_dark = false;
|
||||
let dark = this.settings.get('theme.is-dark');
|
||||
const bits = [], accent_bits = [];
|
||||
|
||||
const background = Color.RGBA.fromCSS(this.settings.get('theme.color.background'));
|
||||
if ( background ) {
|
||||
background.a = 1;
|
||||
bits.push(`--color-background-body: ${background.toCSS()};`);
|
||||
|
||||
const hsla = background.toHSLA(),
|
||||
luma = hsla.l;
|
||||
dark = luma < 0.5;
|
||||
|
||||
// Make sure the Twitch theme is set correctly.
|
||||
try {
|
||||
const store = this.resolve('site').store,
|
||||
theme = store.getState().ui.theme,
|
||||
wanted_theme = dark ? 1 : 0;
|
||||
|
||||
if( theme !== wanted_theme )
|
||||
store.dispatch({
|
||||
type: 'core.ui.THEME_CHANGED',
|
||||
theme: wanted_theme
|
||||
});
|
||||
} catch(err) {
|
||||
// TODO: Try again in a bit.
|
||||
/* no-op */
|
||||
}
|
||||
|
||||
hide_dark = true;
|
||||
|
||||
bits.push(`--color-background-input-focus: ${background.toCSS()};`);
|
||||
bits.push(`--color-background-base: ${hsla._l(luma + (dark ? .05 : -.05)).toCSS()};`);
|
||||
bits.push(`--color-background-alt: ${hsla._l(luma + (dark ? .1 : -.1)).toCSS()};`);
|
||||
bits.push(`--color-background-alt-2: ${hsla._l(luma + (dark ? .15 : -.15)).toCSS()};`);
|
||||
}
|
||||
|
||||
this.css_tweaks.toggleHide('dark-toggle', hide_dark);
|
||||
|
||||
let text = Color.RGBA.fromCSS(this.settings.get('theme.color.text'));
|
||||
if ( ! text && background ) {
|
||||
text = Color.RGBA.fromCSS(dark ? '#FFF' : '#000');
|
||||
}
|
||||
|
||||
if ( text ) {
|
||||
bits.push(`--color-text-base: ${text.toCSS()};`);
|
||||
bits.push(`--color-text-input: ${text.toCSS()};`);
|
||||
|
||||
const hsla = text.toHSLA(),
|
||||
alpha = hsla.a;
|
||||
|
||||
bits.push(`--color-text-label: ${text.toCSS()};`);
|
||||
bits.push(`--color-text-label-optional: ${hsla._a(alpha - 0.4).toCSS()};`);
|
||||
|
||||
bits.push(`--color-text-alt: ${hsla._a(alpha - 0.2).toCSS()};`);
|
||||
bits.push(`--color-text-alt-2: ${hsla._a(alpha - 0.4).toCSS()};`);
|
||||
}
|
||||
|
||||
// Accent
|
||||
const accent = Color.RGBA.fromCSS(this.settings.get('theme.color.accent'));
|
||||
this.toggleAccentNormal(! accent);
|
||||
if ( accent ) {
|
||||
accent.a = 1;
|
||||
|
||||
const hsla = accent.toHSLA(),
|
||||
luma = hsla.l;
|
||||
|
||||
const colors = COLORS.map(x => {
|
||||
if ( x === 0 )
|
||||
return accent.toCSS();
|
||||
|
||||
return hsla._l(luma + x).toCSS()
|
||||
});
|
||||
|
||||
for(let i=0; i < colors.length; i++) {
|
||||
bits.push(`--ffz-color-accent-${i+1}:${colors[i]};`);
|
||||
}
|
||||
|
||||
let source = dark ? ACCENT_COLORS.dark : ACCENT_COLORS.light;
|
||||
|
||||
for(const [key,val] of Object.entries(source.c)) {
|
||||
if ( typeof val !== 'number' )
|
||||
continue;
|
||||
|
||||
bits.push(`--color-${key}:${colors[val-1]};`);
|
||||
}
|
||||
|
||||
source = dark ? ACCENT_COLORS.accent_dark : ACCENT_COLORS.accent_light;
|
||||
|
||||
for(const [key,val] of Object.entries(source.c)) {
|
||||
if ( typeof val !== 'number' )
|
||||
continue;
|
||||
|
||||
accent_bits.push(`--color-${key}:${colors[val-1]} !important;`);
|
||||
}
|
||||
}
|
||||
|
||||
// Tooltips
|
||||
let tooltip_bg = Color.RGBA.fromCSS(this.settings.get('theme.color.tooltip.background')),
|
||||
tooltip_dark;
|
||||
if ( ! tooltip_bg && background )
|
||||
tooltip_bg = Color.RGBA.fromCSS(dark ? '#FFF' : '#000');
|
||||
|
||||
if ( tooltip_bg ) {
|
||||
bits.push(`--color-background-tooltip: ${tooltip_bg.toCSS()};`);
|
||||
|
||||
const hsla = tooltip_bg.toHSLA(),
|
||||
luma = hsla.l;
|
||||
|
||||
tooltip_dark = luma < 0.5;
|
||||
} else
|
||||
tooltip_dark = ! dark;
|
||||
|
||||
let tooltip_text = Color.RGBA.fromCSS(this.settings.get('theme.color.tooltip.text'));
|
||||
const has_tt_text = tooltip_text || tooltip_bg;
|
||||
if ( ! tooltip_text )
|
||||
tooltip_text = Color.RGBA.fromCSS(tooltip_dark ? '#FFF' : '#000');
|
||||
|
||||
if ( tooltip_text ) {
|
||||
if ( has_tt_text )
|
||||
bits.push(`--color-text-tooltip: ${tooltip_text.toCSS()};`);
|
||||
|
||||
const hsla = tooltip_text.toHSLA(),
|
||||
alpha = hsla.a;
|
||||
|
||||
bits.push(`--color-text-tooltip-alt: ${hsla._a(alpha - 0.2).toCSS()};`);
|
||||
bits.push(`--color-text-tooltip-alt-2: ${hsla._a(alpha - 0.4).toCSS()};`);
|
||||
}
|
||||
|
||||
|
||||
// Chat
|
||||
const chat_bits = [],
|
||||
chat_background = Color.RGBA.fromCSS(this.settings.get('theme.color.chat-background'));
|
||||
let chat_dark = dark;
|
||||
if ( chat_background ) {
|
||||
chat_background.a = 1;
|
||||
chat_bits.push(`--color-background-body: ${chat_background.toCSS()};`);
|
||||
|
||||
const hsla = chat_background.toHSLA(),
|
||||
luma = hsla.l;
|
||||
chat_dark = luma < 0.5;
|
||||
|
||||
chat_bits.push(`--color-background-input-focus: ${chat_background.toCSS()};`);
|
||||
chat_bits.push(`--color-background-base: ${hsla._l(luma + (chat_dark ? .05 : -.05)).toCSS()};`);
|
||||
chat_bits.push(`--color-background-alt: ${hsla._l(luma + (chat_dark ? .1 : -.1)).toCSS()};`);
|
||||
chat_bits.push(`--color-background-alt-2: ${hsla._l(luma + (chat_dark ? .15 : -.15)).toCSS()};`);
|
||||
}
|
||||
|
||||
let chat_text = Color.RGBA.fromCSS(this.settings.get('theme.color.chat-text'));
|
||||
if ( ! chat_text && chat_background ) {
|
||||
chat_text = Color.RGBA.fromCSS(chat_dark ? '#FFF' : '#000');
|
||||
}
|
||||
|
||||
if ( chat_text ) {
|
||||
chat_bits.push(`--color-text-base: ${chat_text.toCSS()};`);
|
||||
chat_bits.push(`--color-text-input: ${chat_text.toCSS()};`);
|
||||
|
||||
const hsla = chat_text.toHSLA(),
|
||||
alpha = hsla.a;
|
||||
|
||||
chat_bits.push(`--color-text-label: ${chat_text.toCSS()};`);
|
||||
chat_bits.push(`--color-text-label-optional: ${hsla._a(alpha - 0.4).toCSS()};`);
|
||||
|
||||
chat_bits.push(`--color-text-alt: ${hsla._a(alpha - 0.2).toCSS()};`);
|
||||
chat_bits.push(`--color-text-alt-2: ${hsla._a(alpha - 0.4).toCSS()};`);
|
||||
}
|
||||
|
||||
// Accent
|
||||
const chat_accent = Color.RGBA.fromCSS(this.settings.get('theme.color.chat-accent')),
|
||||
chat_accent_bits = [];
|
||||
//this.toggleAccentNormal(! accent);
|
||||
if ( chat_accent ) {
|
||||
chat_accent.a = 1;
|
||||
|
||||
const hsla = chat_accent.toHSLA(),
|
||||
luma = hsla.l;
|
||||
|
||||
const colors = COLORS.map(x => {
|
||||
if ( x === 0 )
|
||||
return chat_accent.toCSS();
|
||||
|
||||
return hsla._l(luma + x).toCSS()
|
||||
});
|
||||
|
||||
for(let i=0; i < colors.length; i++) {
|
||||
chat_bits.push(`--ffz-color-accent-${i+1}:${colors[i]};`);
|
||||
}
|
||||
|
||||
let source = chat_dark ? ACCENT_COLORS.dark : ACCENT_COLORS.light;
|
||||
|
||||
for(const [key,val] of Object.entries(source.c)) {
|
||||
if ( typeof val !== 'number' )
|
||||
continue;
|
||||
|
||||
chat_bits.push(`--color-${key}:${colors[val-1]};`);
|
||||
}
|
||||
|
||||
source = chat_dark ? ACCENT_COLORS.accent_dark : ACCENT_COLORS.accent_light;
|
||||
|
||||
for(const [key,val] of Object.entries(source.c)) {
|
||||
if ( typeof val !== 'number' )
|
||||
continue;
|
||||
|
||||
chat_accent_bits.push(`--color-${key}:${colors[val-1]} !important;`);
|
||||
}
|
||||
}
|
||||
|
||||
if ( chat_bits.length )
|
||||
this.css_tweaks.set('chat-colors', `.chat-shell {${chat_bits.join('\n')}}.chat-shell .tw-accent-region{${chat_accent_bits.join('\n')}}`);
|
||||
else
|
||||
this.css_tweaks.delete('chat-colors');
|
||||
|
||||
this.toggleNormalizer(chat_bits.length || bits.length);
|
||||
|
||||
if ( bits.length )
|
||||
this.css_tweaks.set('colors', `body {${bits.join('\n')}}.channel-info-content .tw-accent-region,.channel-info-content .gocjHQ{${accent_bits.join('\n')}}`);
|
||||
else
|
||||
this.css_tweaks.delete('colors');
|
||||
}
|
||||
|
||||
toggleAccentNormal(enable) {
|
||||
if ( enable ) {
|
||||
const bits = [];
|
||||
for(let i=0; i < 15; i++)
|
||||
bits.push(`--ffz-color-accent-${i+1}:var(--color-twitch-purple-${i+1});`);
|
||||
|
||||
this.css_tweaks.set('accent-normal', `body {${bits.join('\n')}}`);
|
||||
} else
|
||||
this.css_tweaks.delete('accent-normal');
|
||||
}
|
||||
|
||||
toggleNormalizer(enable) { // eslint-disable-line class-methods-use-this
|
||||
// Intentionally disabled~
|
||||
/*if ( ! this._normalizer ) {
|
||||
if ( ! enable )
|
||||
return;
|
||||
|
||||
this._normalizer = createElement('link', {
|
||||
rel: 'stylesheet',
|
||||
type: 'text/css',
|
||||
href: NORMALIZER_CSS_URL
|
||||
});
|
||||
} else if ( ! enable ) {
|
||||
this._normalizer.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! document.head.contains(this._normalizer) )
|
||||
document.head.appendChild(this._normalizer);*/
|
||||
}
|
||||
|
||||
/*toggleStyle(enable) {
|
||||
if ( ! this._style ) {
|
||||
if ( ! enable )
|
||||
return;
|
||||
|
||||
this._style = createElement('link', {
|
||||
rel: 'stylesheet',
|
||||
type: 'text/css',
|
||||
href: THEME_CSS
|
||||
});
|
||||
|
||||
} else if ( ! enable ) {
|
||||
this._style.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
document.head.appendChild(this._style);
|
||||
}
|
||||
|
||||
updateSetting(enable) {
|
||||
this.toggleStyle(enable);
|
||||
this.updateCSS();
|
||||
}*/
|
||||
|
||||
onEnable() {
|
||||
//this.updateSetting(this.settings.get('theme.dark'));
|
||||
this.updateCSS();
|
||||
this.updateFont();
|
||||
}
|
||||
}
|
|
@ -7,7 +7,7 @@
|
|||
import Module from 'utilities/module';
|
||||
|
||||
import {createElement} from 'react';
|
||||
import { split_chars } from '../../../../utilities/object';
|
||||
import { split_chars } from 'utilities/object';
|
||||
|
||||
|
||||
export default class Line extends Module {
|
||||
|
|
|
@ -61,6 +61,8 @@ export default class Twilight extends BaseSite {
|
|||
|
||||
this.router.route(Twilight.DASH_ROUTES, 'dashboard.twitch.tv');
|
||||
this.router.route(Twilight.PLAYER_ROUTES, 'player.twitch.tv');
|
||||
this.router.route(Twilight.CLIP_ROUTES, 'clips.twitch.tv');
|
||||
|
||||
}
|
||||
|
||||
onEnable() {
|
||||
|
@ -344,6 +346,10 @@ Twilight.PLAYER_ROUTES = {
|
|||
'popout-player': '/'
|
||||
};
|
||||
|
||||
Twilight.CLIP_ROUTES = {
|
||||
'clip-page': '/:slug'
|
||||
};
|
||||
|
||||
|
||||
Twilight.DASH_ROUTES = {
|
||||
'dash-stream-manager': '/u/:userName/stream-manager',
|
||||
|
|
|
@ -661,6 +661,7 @@ export default class ChatHook extends Module {
|
|||
this._update_css_waiter = null;
|
||||
|
||||
const width = this.chat.context.get('chat.width'),
|
||||
action_size = this.chat.context.get('chat.actions.size'),
|
||||
size = this.chat.context.get('chat.font-size'),
|
||||
emote_alignment = this.chat.context.get('chat.lines.emote-alignment'),
|
||||
lh = Math.round((20/12) * size);
|
||||
|
@ -669,6 +670,7 @@ export default class ChatHook extends Module {
|
|||
if ( font.indexOf(' ') !== -1 && font.indexOf(',') === -1 && font.indexOf('"') === -1 && font.indexOf("'") === -1 )
|
||||
font = `"${font}"`;
|
||||
|
||||
this.css_tweaks.setVariable('chat-actions-size', `${action_size/10}rem`);
|
||||
this.css_tweaks.setVariable('chat-font-size', `${size/10}rem`);
|
||||
this.css_tweaks.setVariable('chat-line-height', `${lh/10}rem`);
|
||||
this.css_tweaks.setVariable('chat-font-family', font);
|
||||
|
@ -792,6 +794,7 @@ export default class ChatHook extends Module {
|
|||
this.chat.context.on('changed:chat.subs.gift-banner', () => this.GiftBanner.forceUpdate(), this);
|
||||
this.chat.context.on('changed:chat.width', this.updateChatCSS, this);
|
||||
this.settings.main_context.on('changed:chat.use-width', this.updateChatCSS, this);
|
||||
this.chat.context.on('changed:chat.actions.size', this.updateChatCSS, this);
|
||||
this.chat.context.on('changed:chat.font-size', this.updateChatCSS, this);
|
||||
this.chat.context.on('changed:chat.font-family', this.updateChatCSS, this);
|
||||
this.chat.context.on('changed:chat.lines.emote-alignment', this.updateChatCSS, this);
|
||||
|
|
|
@ -56,6 +56,7 @@ export default class ChatLine extends Module {
|
|||
|
||||
async onEnable() {
|
||||
this.on('chat.overrides:changed', id => this.updateLinesByUser(id), this);
|
||||
this.on('chat:update-lines-by-user', this.updateLinesByUser, this);
|
||||
this.on('chat:update-lines', this.updateLines, this);
|
||||
this.on('i18n:update', this.updateLines, this);
|
||||
|
||||
|
@ -998,15 +999,20 @@ other {# messages were deleted by a moderator.}
|
|||
for(const inst of this.ChatLine.instances) {
|
||||
const msg = inst.props.message,
|
||||
user = msg?.user;
|
||||
if ( user && (id && id == user.id) || (login && login == user.login) )
|
||||
if ( user && ((id && id == user.id) || (login && login == user.login)) ) {
|
||||
msg.ffz_tokens = null;
|
||||
msg.highlights = msg.mentioned = msg.mention_color = null;
|
||||
inst.forceUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
for(const inst of this.WhisperLine.instances) {
|
||||
const msg = inst.props.message?._ffz_message,
|
||||
user = msg?.user;
|
||||
if ( user && (id && id == user.id) || (login && login == user.login) )
|
||||
if ( user && ((id && id == user.id) || (login && login == user.login)) ) {
|
||||
msg._ffz_message = null;
|
||||
inst.forceUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -49,6 +49,16 @@ export default class Layout extends Module {
|
|||
t => t.getCardSlideInContent && t.props && has(t.props, 'tooltipContent')
|
||||
);*/
|
||||
|
||||
this.settings.add('clips.layout.big', {
|
||||
default: false,
|
||||
ui: {
|
||||
path: 'Appearance > Layout >> Clips',
|
||||
title: 'Allow the player to get larger on `clips.twitch.tv` pages.',
|
||||
description: 'This only affects windows at least 1,200 pixels wide and attempts to make the player and chat replay as large as possible.',
|
||||
component: 'setting-check-box'
|
||||
}
|
||||
});
|
||||
|
||||
this.settings.add('layout.portrait', {
|
||||
default: false,
|
||||
ui: {
|
||||
|
|
|
@ -21,13 +21,16 @@ export default class MenuButton extends SiteModule {
|
|||
this.inject('site.elemental');
|
||||
//this.inject('addons');
|
||||
|
||||
this.pauseToasts = this.pauseToasts.bind(this);
|
||||
this.unpauseToasts = this.unpauseToasts.bind(this);
|
||||
|
||||
this.should_enable = true;
|
||||
this._pill_content = null;
|
||||
this._has_update = false;
|
||||
this._important_update = false;
|
||||
this._new_settings = 0;
|
||||
this._error = null;
|
||||
this._loading = false;
|
||||
this.toasts = [];
|
||||
|
||||
this.settings.add('ffz.show-new-settings', {
|
||||
default: true,
|
||||
|
@ -87,22 +90,6 @@ export default class MenuButton extends SiteModule {
|
|||
this.update();
|
||||
}
|
||||
|
||||
get has_error() {
|
||||
return this._error != null;
|
||||
}
|
||||
|
||||
get error() {
|
||||
return this._error;
|
||||
}
|
||||
|
||||
set error(val) {
|
||||
if ( val === this._error )
|
||||
return;
|
||||
|
||||
this._error = val;
|
||||
this.update();
|
||||
}
|
||||
|
||||
get new_settings() {
|
||||
return this._new_settings;
|
||||
}
|
||||
|
@ -188,6 +175,30 @@ export default class MenuButton extends SiteModule {
|
|||
return null;
|
||||
}
|
||||
|
||||
updateToasts() {
|
||||
requestAnimationFrame(() => this._updateToasts());
|
||||
}
|
||||
|
||||
_updateToasts() {
|
||||
for(const inst of this.NavBar.instances)
|
||||
this.updateButtonToast(inst);
|
||||
|
||||
for(const inst of this.SquadBar.instances)
|
||||
this.updateButtonToast(inst);
|
||||
|
||||
for(const inst of this.MultiController.instances)
|
||||
this.updateButtonToast(inst);
|
||||
|
||||
//for(const inst of this.SunlightDash.instances)
|
||||
// this.updateButtonToast(inst);
|
||||
|
||||
for(const el of this.SunlightNav.instances)
|
||||
this.updateButtonToast(null, el, true);
|
||||
|
||||
for(const inst of this.ModBar.instances)
|
||||
this.updateButtonToast(inst);
|
||||
}
|
||||
|
||||
update() {
|
||||
requestAnimationFrame(() => this._update());
|
||||
}
|
||||
|
@ -247,14 +258,205 @@ export default class MenuButton extends SiteModule {
|
|||
this.on('i18n:update', this.update);
|
||||
this.on('addons:data-loaded', this.update);
|
||||
this.on('settings:change-provider', () => {
|
||||
this.error = {
|
||||
i18n: 'site.menu_button.changed',
|
||||
text: 'The FrankerFaceZ settings provider has changed. Please refresh this tab to avoid strange behavior.'
|
||||
};
|
||||
this.addError('site.menu_button.changed',
|
||||
'The FrankerFaceZ settings provider has changed. Please refresh this tab to avoid strange behavior.'
|
||||
);
|
||||
this.update()
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
addError(i18n, text, icon = 'ffz-i-attention') {
|
||||
this.addToast({
|
||||
icon,
|
||||
text_i18n: i18n,
|
||||
text
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
addToast(data) {
|
||||
/*if ( ! data.id )
|
||||
data.id = generateUUID();*/
|
||||
|
||||
this.toasts.push(data);
|
||||
// TODO: Sort by ending time?
|
||||
if ( this.toasts.length > 5 )
|
||||
return;
|
||||
|
||||
this.updateToasts();
|
||||
}
|
||||
|
||||
|
||||
pauseToasts() {
|
||||
const length = Math.min(5, this.toasts.length),
|
||||
now = performance.now();
|
||||
|
||||
this._toasts_paused = true;
|
||||
|
||||
for(let i=0; i < length; i++) {
|
||||
const toast = this.toasts[i];
|
||||
if ( ! toast || ! toast.started )
|
||||
continue;
|
||||
|
||||
if ( toast._timer ) {
|
||||
clearTimeout(toast._timer);
|
||||
toast._timer = null;
|
||||
}
|
||||
|
||||
const elapsed = now - toast.started,
|
||||
remaining = toast.timeout - elapsed;
|
||||
|
||||
toast.remaining = remaining;
|
||||
toast.percentage = 100 * remaining / toast.timeout;
|
||||
|
||||
if ( toast.el )
|
||||
toast.el.replaceWith(this.renderToast(toast));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
unpauseToasts() {
|
||||
const length = Math.min(5, this.toasts.length),
|
||||
now = performance.now();
|
||||
this._toasts_paused = false;
|
||||
|
||||
for(let i=0; i < length; i++) {
|
||||
const toast = this.toasts[i];
|
||||
if ( ! toast || ! toast.started )
|
||||
continue;
|
||||
|
||||
if ( toast.remaining ) {
|
||||
const elapsed = toast.timeout - toast.remaining;
|
||||
toast.started = now - elapsed;
|
||||
}
|
||||
|
||||
if ( toast.el )
|
||||
toast.el.replaceWith(this.renderToast(toast));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
renderToasts() {
|
||||
const length = Math.min(5, this.toasts.length);
|
||||
if ( ! length )
|
||||
return null;
|
||||
|
||||
const out = [];
|
||||
for(let i=0; i < length; i++) {
|
||||
out.push(this.renderToast(this.toasts[i]));
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
renderToast(data) {
|
||||
if ( ! data._remove )
|
||||
data._remove = () => {
|
||||
if ( data._timer ) {
|
||||
clearTimeout(data._timer);
|
||||
data._timer = null;
|
||||
}
|
||||
|
||||
data.el = null;
|
||||
|
||||
const idx = this.toasts.indexOf(data);
|
||||
if ( idx !== -1 ) {
|
||||
this.toasts.splice(idx, 1);
|
||||
if ( ! this.toasts.length )
|
||||
this._toasts_paused = false;
|
||||
|
||||
this.updateToasts();
|
||||
}
|
||||
}
|
||||
|
||||
let progress_bar = null;
|
||||
|
||||
if ( data.timeout ) {
|
||||
const now = performance.now();
|
||||
if ( ! data.started )
|
||||
data.started = now;
|
||||
|
||||
const elapsed = now - data.started,
|
||||
remaining = data.timeout - elapsed;
|
||||
let percentage;
|
||||
|
||||
if ( this._toasts_paused )
|
||||
percentage = data.percentage;
|
||||
|
||||
else if ( ! data._timer )
|
||||
data._timer = setTimeout(data._remove, remaining);
|
||||
|
||||
if ( percentage == null )
|
||||
percentage = data.percentage = 100 * remaining / data.timeout;
|
||||
|
||||
progress_bar = (<div class="ffz-toast--progress tw-absolute tw-overflow-hidden tw-z-below">
|
||||
<div
|
||||
class="tw-border-radius-rounded tw-progress-bar tw-progress-bar--countdown tw-progress-bar--default tw-progress-bar--mask"
|
||||
role="progressbar"
|
||||
aria-valuenow={percentage}
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
>
|
||||
<div
|
||||
class={`tw-block tw-border-bottom-left-radius-rounded tw-border-top-left-radius-rounded tw-progress-bar__fill`}
|
||||
data-a-target="tw-progress-bar-animation"
|
||||
style={{
|
||||
width: `${percentage}%`,
|
||||
'animation-duration': `${remaining / 1000}s`
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
data.el = (<div class={`ffz-toast ffz-balloon ffz-balloon--lg tw-mg-y-1${this._toasts_paused ? ' ffz-toast--paused' : ''}`}>
|
||||
<div class="tw-pd-1 tw-border-radius-large tw-c-background-base tw-c-text-inherit tw-elevation-4 tw-relative">
|
||||
<div class="tw-flex tw-align-items-start">
|
||||
{data.icon && (
|
||||
typeof data.icon === 'string' ?
|
||||
<figure class={`tw-font-size-3 tw-pd-r-1 ${data.icon}`} /> :
|
||||
data.icon
|
||||
)}
|
||||
{ data.render ? data.render.call(this, data) : null }
|
||||
{ (data.title || data.text) ? (<div class="tw-flex-grow-1">
|
||||
{ data.title ? (<header class="tw-semibold tw-font-size-3 tw-pd-b-05">
|
||||
{ data.title_i18n ? this.i18n.tList(data.title_i18n, data.title, data) : data.title}
|
||||
</header>) : null }
|
||||
{ data.text ? (<span class={`${data.lines ? 'ffz--line-clamp' : ''}`} style={{'--ffz-lines': data.lines}}>
|
||||
{ data.text_i18n ? this.i18n.tList(data.text_i18n, data.text, data) : data.text}
|
||||
</span>) : null }
|
||||
</div>) : null}
|
||||
{ ! data.unclosable && (<button
|
||||
class="tw-button-icon tw-mg-l-05 tw-relative tw-tooltip__container"
|
||||
onClick={data._remove}
|
||||
>
|
||||
<span class="tw-button-icon__icon">
|
||||
<figure class="ffz-i-cancel" />
|
||||
</span>
|
||||
</button>)}
|
||||
</div>
|
||||
{progress_bar}
|
||||
</div>
|
||||
</div>);
|
||||
|
||||
return data.el;
|
||||
}
|
||||
|
||||
|
||||
updateButtonToast(inst, container, is_sunlight) {
|
||||
const toast_el = (inst || container)._ffz_toast_el;
|
||||
if ( toast_el ) {
|
||||
toast_el.innerHTML = '';
|
||||
setChildren(toast_el, this.renderToasts(), false, true);
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateButton(inst, container, is_sunlight);
|
||||
}
|
||||
|
||||
|
||||
updateButton(inst, container, is_sunlight) {
|
||||
const root = this.fine.getChildNode(inst);
|
||||
let is_squad = false,
|
||||
|
@ -312,6 +514,7 @@ export default class MenuButton extends SiteModule {
|
|||
el.remove();
|
||||
|
||||
const addons = this.resolve('addons'),
|
||||
toasts = this.renderToasts(),
|
||||
pill = this.formatPill(),
|
||||
extra_pill = this.formatExtraPill();
|
||||
|
||||
|
@ -347,7 +550,7 @@ export default class MenuButton extends SiteModule {
|
|||
</div>
|
||||
</div>
|
||||
</div>)}
|
||||
{! this.has_error && (<div class={`tw-tooltip ${is_mod ? 'tw-tooltip--up tw-tooltip--align-left' : 'tw-tooltip--down tw-tooltip--align-right'}`}>
|
||||
<div class={`tw-tooltip ${is_mod ? 'tw-tooltip--up tw-tooltip--align-left' : 'tw-tooltip--down tw-tooltip--align-right'}`}>
|
||||
{this.i18n.t('site.menu_button', 'FrankerFaceZ Control Center')}
|
||||
{this.has_update && (<div class="tw-mg-t-1">
|
||||
{this.i18n.t('site.menu_button.update-desc', 'There is an update available. Please refresh your page.')}
|
||||
|
@ -372,8 +575,15 @@ export default class MenuButton extends SiteModule {
|
|||
{addons.has_dev && (<div class="tw-mg-t-1">
|
||||
{this.i18n.t('site.menu_button.addon-dev-desc', 'You have loaded add-on data from a local development server.')}
|
||||
</div>)}
|
||||
</div>)}
|
||||
</div>
|
||||
</div>
|
||||
{(inst || container)._ffz_toast_el = (<div
|
||||
class={`ffz-toast--container tw-absolute tw-block ${is_mod ? 'tw-tooltip--up tw-tooltip--align-left' : 'tw-tooltip--down tw-tooltip--align-right'}`}
|
||||
onmouseenter={this.pauseToasts}
|
||||
onmouseleave={this.unpauseToasts}
|
||||
>
|
||||
{toasts}
|
||||
</div>)}
|
||||
{this.has_update && (<div class="ffz-menu__extra-pill tw-absolute">
|
||||
<div class={`tw-pill ${this.important_update ? 'tw-pill--notification' : ''}`}>
|
||||
<figure class="ffz-i-arrows-cw" />
|
||||
|
@ -487,7 +697,7 @@ export default class MenuButton extends SiteModule {
|
|||
|
||||
const desc_key = profile.desc_i18n_key || profile.i18n_key && `${profile.i18n_key}.description`;
|
||||
|
||||
profiles.push(<div class="tw-relative tw-border-b tw-pd-y-05 tw-pd-l-1 tw-flex">
|
||||
profiles.push(<div class="tw-relative tw-border-b tw-pd-y-05 tw-pd-x-1 tw-flex">
|
||||
{toggle}
|
||||
<div>
|
||||
<h4>{ profile.i18n_key ? this.i18n.t(profile.i18n_key, profile.name, profile) : profile.name }</h4>
|
||||
|
@ -596,10 +806,10 @@ export default class MenuButton extends SiteModule {
|
|||
this.log.capture(err);
|
||||
this.log.error('Error enabling main menu.', err);
|
||||
|
||||
this.error = {
|
||||
i18n: 'site.menu_button.error',
|
||||
text: 'There was an error loading the FFZ Control Center. Please refresh and try again.'
|
||||
};
|
||||
this.addError(
|
||||
'site.menu_button.error',
|
||||
'There was an error loading the FFZ Control Center. Please refresh and try again.'
|
||||
);
|
||||
|
||||
this.loading = false;
|
||||
this.once(':clicked', this.loadMenu);
|
||||
|
|
|
@ -152,6 +152,12 @@
|
|||
.ffz-mod-icon {
|
||||
text-align: center;
|
||||
display: inline-flex;
|
||||
font-size: 1.6rem;
|
||||
font-size: var(--ffz-chat-actions-size);
|
||||
|
||||
&, &.mod-icon {
|
||||
height: unset;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
|
@ -182,11 +188,13 @@
|
|||
span {
|
||||
display: inline-block;
|
||||
min-width: 1.6rem;
|
||||
min-width: var(--ffz-chat-actions-size);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.mod-icon__image img {
|
||||
max-height: 1.6rem;
|
||||
max-height: var(--ffz-chat-actions-size);
|
||||
}
|
||||
|
||||
&.colored,
|
||||
|
@ -210,12 +218,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
.ffz--inline {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
|
||||
.ffz--giftee-name {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
|
|
|
@ -27,4 +27,20 @@
|
|||
.loading .ffz-i-zreknarf {
|
||||
animation: ffz-rotateplane 1.2s infinite linear;
|
||||
}
|
||||
|
||||
.ffz-toast--container {
|
||||
z-index: 1999;
|
||||
}
|
||||
|
||||
.ffz-toast {
|
||||
&.ffz-toast--paused .tw-progress-bar--countdown .tw-progress-bar__fill {
|
||||
animation: none !important;
|
||||
}
|
||||
|
||||
.ffz-toast--progress {
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: -.4rem;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
<template lang="html">
|
||||
<div class="ffz--key-widget">
|
||||
<div class="tw-relative tw-full-width tw-mg-05">
|
||||
<div class="tw-relative tw-full-width">
|
||||
<div
|
||||
ref="input"
|
||||
v-bind="$attrs"
|
||||
class="default-dimmable tw-block tw-border-radius-medium tw-font-size-6 tw-full-width ffz-input tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 tw-mg-y-05"
|
||||
class="default-dimmable tw-block tw-border-radius-medium tw-font-size-6 tw-full-width ffz-input tw-pd-l-1 tw-pd-r-3 tw-pd-y-05"
|
||||
tabindex="0"
|
||||
@click="startRecording"
|
||||
@keydown="onKey"
|
||||
|
|
88
src/utilities/css-tweaks.js
Normal file
88
src/utilities/css-tweaks.js
Normal file
|
@ -0,0 +1,88 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// CSS Tweaks
|
||||
// Tweak some CSS
|
||||
// ============================================================================
|
||||
|
||||
import Module from 'utilities/module';
|
||||
import {ManagedStyle} from 'utilities/dom';
|
||||
import {has, once} from 'utilities/object';
|
||||
|
||||
export default class CSSTweaks extends Module {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.rules = {};
|
||||
|
||||
this.loader = null;
|
||||
this.chunks = {};
|
||||
this.chunks_loaded = false;
|
||||
|
||||
this.populate = once(this.populate);
|
||||
}
|
||||
|
||||
get style() {
|
||||
if ( ! this._style )
|
||||
this._style = new ManagedStyle;
|
||||
|
||||
return this._style;
|
||||
}
|
||||
|
||||
toggleHide(key, val) {
|
||||
const k = `hide--${key}`;
|
||||
if ( ! val ) {
|
||||
if ( this._style )
|
||||
this._style.delete(k);
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! has(this.rules, key) )
|
||||
throw new Error(`unknown rule "${key}" for toggleHide`);
|
||||
|
||||
this.style.set(k, `${this.rules[key]}{display:none !important}`);
|
||||
}
|
||||
|
||||
async toggle(key, val) {
|
||||
if ( ! val ) {
|
||||
if ( this._style )
|
||||
this._style.delete(key);
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! this.chunks_loaded )
|
||||
await this.populate();
|
||||
|
||||
if ( ! has(this.chunks, key) )
|
||||
throw new Error(`unknown chunk "${key}" for toggle`);
|
||||
|
||||
this.style.set(key, this.chunks[key]);
|
||||
}
|
||||
|
||||
set(key, val) { return this.style.set(key, val); }
|
||||
delete(key) { this._style && this._style.delete(key) }
|
||||
|
||||
setVariable(key, val, scope = 'body') {
|
||||
this.style.set(`var--${key}`, `${scope}{--ffz-${key}:${val};}`);
|
||||
}
|
||||
|
||||
deleteVariable(key) {
|
||||
if ( this._style )
|
||||
this._style.delete(`var--${key}`);
|
||||
}
|
||||
|
||||
async populate() {
|
||||
if ( this.chunks_loaded || ! this.loader )
|
||||
return;
|
||||
|
||||
const promises = [];
|
||||
for(const key of this.loader.keys()) {
|
||||
const k = key.slice(2, key.length - (key.endsWith('.scss') ? 5 : 4));
|
||||
promises.push(this.loader(key).then(data => this.chunks[k] = data.default));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
this.chunks_loaded = true;
|
||||
}
|
||||
|
||||
}
|
|
@ -203,9 +203,27 @@ export function openFile(contentType, multiple) {
|
|||
input.accept = contentType;
|
||||
input.multiple = multiple;
|
||||
|
||||
let resolved = false;
|
||||
|
||||
const focuser = () => {
|
||||
off(window, 'focus', focuser);
|
||||
setTimeout(() => {
|
||||
if ( ! resolved ) {
|
||||
resolved = true;
|
||||
resolve(multiple ? [] : null);
|
||||
}
|
||||
}, 500);
|
||||
};
|
||||
|
||||
on(window, 'focus', focuser);
|
||||
|
||||
input.onchange = () => {
|
||||
const files = Array.from(input.files);
|
||||
resolve(multiple ? files : files[0])
|
||||
off(window, 'focus', focuser);
|
||||
if ( ! resolved ) {
|
||||
resolved = true;
|
||||
const files = Array.from(input.files);
|
||||
resolve(multiple ? files : files[0])
|
||||
}
|
||||
}
|
||||
|
||||
input.click();
|
||||
|
|
|
@ -15,6 +15,9 @@
|
|||
100% { transform: rotateY(90deg) rotateX(0deg) }
|
||||
}
|
||||
|
||||
.ffz--inline {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.ffz--player-meta-tray {
|
||||
position: absolute;
|
||||
|
|
|
@ -165,8 +165,8 @@ textarea.ffz-input {
|
|||
opacity: 0.5;
|
||||
}
|
||||
|
||||
label {
|
||||
width: 10rem;
|
||||
.ffz--checkbox {
|
||||
margin-left: 15rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
.ffz-main-menu {
|
||||
z-index: 100000;
|
||||
z-index: 1998;
|
||||
|
||||
.scrollable-area {
|
||||
overflow-anchor: none;
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.ffz--profile-row__icon {
|
||||
.ffz--profile-row__icon-tray {
|
||||
position: absolute;
|
||||
top: 0.5rem; right: 0.5rem;
|
||||
font-size: 1.6rem;
|
||||
|
|
|
@ -9,12 +9,15 @@ const VueLoaderPlugin = require('vue-loader/lib/plugin');
|
|||
const VERSION = semver.parse(require('./package.json').version);
|
||||
const PRODUCTION = process.env.NODE_ENV === 'production';
|
||||
|
||||
const ENTRY_POINTS = {
|
||||
bridge: './src/bridge.js',
|
||||
player: './src/player.js',
|
||||
avalon: './src/main.js',
|
||||
clips: './src/clips.js'
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
bridge: './src/bridge.js',
|
||||
player: './src/player.js',
|
||||
avalon: './src/main.js'
|
||||
},
|
||||
entry: ENTRY_POINTS,
|
||||
resolve: {
|
||||
extensions: ['.js', '.jsx'],
|
||||
alias: {
|
||||
|
@ -41,7 +44,7 @@ module.exports = {
|
|||
optimization: {
|
||||
splitChunks: {
|
||||
chunks(chunk) {
|
||||
return chunk.name !== 'avalon' && chunk.name !== 'bridge' && chunk.name !== 'player'
|
||||
return ! Object.keys(ENTRY_POINTS).includes(chunk.name);
|
||||
},
|
||||
cacheGroups: {
|
||||
vendors: false
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue