459 lines
16 KiB
JavaScript
459 lines
16 KiB
JavaScript
/*
|
|
Copyright 2015 OpenMarket Ltd
|
|
Copyright 2017 New Vector Ltd
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
const DEBUG = 0;
|
|
|
|
// utility to turn #rrggbb or rgb(r,g,b) into [red,green,blue]
|
|
function colorToRgb(color) {
|
|
if (!color) {
|
|
return [0, 0, 0];
|
|
}
|
|
|
|
if (color[0] === '#') {
|
|
color = color.slice(1);
|
|
if (color.length === 3) {
|
|
color = color[0] + color[0] +
|
|
color[1] + color[1] +
|
|
color[2] + color[2];
|
|
}
|
|
const val = parseInt(color, 16);
|
|
const r = (val >> 16) & 255;
|
|
const g = (val >> 8) & 255;
|
|
const b = val & 255;
|
|
return [r, g, b];
|
|
} else {
|
|
const match = color.match(/rgb\((.*?),(.*?),(.*?)\)/);
|
|
if (match) {
|
|
return [
|
|
parseInt(match[1]),
|
|
parseInt(match[2]),
|
|
parseInt(match[3]),
|
|
];
|
|
}
|
|
}
|
|
return [0, 0, 0];
|
|
}
|
|
|
|
// utility to turn [red,green,blue] into #rrggbb
|
|
function rgbToColor(rgb) {
|
|
const val = (rgb[0] << 16) | (rgb[1] << 8) | rgb[2];
|
|
return '#' + (0x1000000 + val).toString(16).slice(1);
|
|
}
|
|
|
|
class Tinter {
|
|
constructor() {
|
|
// The default colour keys to be replaced as referred to in CSS
|
|
// (should be overridden by .mx_theme_accentColor and .mx_theme_secondaryAccentColor)
|
|
this.keyRgb = [
|
|
"rgb(118, 207, 166)", // Vector Green
|
|
"rgb(234, 245, 240)", // Vector Light Green
|
|
"rgb(211, 239, 225)", // roomsublist-label-bg-color (20% Green overlaid on Light Green)
|
|
];
|
|
|
|
// Some algebra workings for calculating the tint % of Vector Green & Light Green
|
|
// x * 118 + (1 - x) * 255 = 234
|
|
// x * 118 + 255 - 255 * x = 234
|
|
// x * 118 - x * 255 = 234 - 255
|
|
// (255 - 118) x = 255 - 234
|
|
// x = (255 - 234) / (255 - 118) = 0.16
|
|
|
|
// The colour keys to be replaced as referred to in SVGs
|
|
this.keyHex = [
|
|
"#76CFA6", // Vector Green
|
|
"#EAF5F0", // Vector Light Green
|
|
"#D3EFE1", // roomsublist-label-bg-color (20% Green overlaid on Light Green)
|
|
"#FFFFFF", // white highlights of the SVGs (for switching to dark theme)
|
|
"#000000", // black lowlights of the SVGs (for switching to dark theme)
|
|
];
|
|
|
|
// track the replacement colours actually being used
|
|
// defaults to our keys.
|
|
this.colors = [
|
|
this.keyHex[0],
|
|
this.keyHex[1],
|
|
this.keyHex[2],
|
|
this.keyHex[3],
|
|
this.keyHex[4],
|
|
];
|
|
|
|
// track the most current tint request inputs (which may differ from the
|
|
// end result stored in this.colors
|
|
this.currentTint = [
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
];
|
|
|
|
this.cssFixups = [
|
|
// { theme: {
|
|
// style: a style object that should be fixed up taken from a stylesheet
|
|
// attr: name of the attribute to be clobbered, e.g. 'color'
|
|
// index: ordinal of primary, secondary or tertiary
|
|
// },
|
|
// }
|
|
];
|
|
|
|
// CSS attributes to be fixed up
|
|
this.cssAttrs = [
|
|
"color",
|
|
"backgroundColor",
|
|
"borderColor",
|
|
"borderTopColor",
|
|
"borderBottomColor",
|
|
"borderLeftColor",
|
|
];
|
|
|
|
this.svgAttrs = [
|
|
"fill",
|
|
"stroke",
|
|
];
|
|
|
|
// List of functions to call when the tint changes.
|
|
this.tintables = [];
|
|
|
|
// the currently loaded theme (if any)
|
|
this.theme = undefined;
|
|
|
|
// whether to force a tint (e.g. after changing theme)
|
|
this.forceTint = false;
|
|
}
|
|
|
|
/**
|
|
* Register a callback to fire when the tint changes.
|
|
* This is used to rewrite the tintable SVGs with the new tint.
|
|
*
|
|
* It's not possible to unregister a tintable callback. So this can only be
|
|
* used to register a static callback. If a set of tintables will change
|
|
* over time then the best bet is to register a single callback for the
|
|
* entire set.
|
|
*
|
|
* To ensure the tintable work happens at least once, it is also called as
|
|
* part of registration.
|
|
*
|
|
* @param {Function} tintable Function to call when the tint changes.
|
|
*/
|
|
registerTintable(tintable) {
|
|
this.tintables.push(tintable);
|
|
tintable();
|
|
}
|
|
|
|
getKeyRgb() {
|
|
return this.keyRgb;
|
|
}
|
|
|
|
tint(primaryColor, secondaryColor, tertiaryColor) {
|
|
return;
|
|
// eslint-disable-next-line no-unreachable
|
|
this.currentTint[0] = primaryColor;
|
|
this.currentTint[1] = secondaryColor;
|
|
this.currentTint[2] = tertiaryColor;
|
|
|
|
this.calcCssFixups();
|
|
|
|
if (DEBUG) {
|
|
console.log("Tinter.tint(" + primaryColor + ", " +
|
|
secondaryColor + ", " +
|
|
tertiaryColor + ")");
|
|
}
|
|
|
|
if (!primaryColor) {
|
|
primaryColor = this.keyRgb[0];
|
|
secondaryColor = this.keyRgb[1];
|
|
tertiaryColor = this.keyRgb[2];
|
|
}
|
|
|
|
if (!secondaryColor) {
|
|
const x = 0.16; // average weighting factor calculated from vector green & light green
|
|
const rgb = colorToRgb(primaryColor);
|
|
rgb[0] = x * rgb[0] + (1 - x) * 255;
|
|
rgb[1] = x * rgb[1] + (1 - x) * 255;
|
|
rgb[2] = x * rgb[2] + (1 - x) * 255;
|
|
secondaryColor = rgbToColor(rgb);
|
|
}
|
|
|
|
if (!tertiaryColor) {
|
|
const x = 0.19;
|
|
const rgb1 = colorToRgb(primaryColor);
|
|
const rgb2 = colorToRgb(secondaryColor);
|
|
rgb1[0] = x * rgb1[0] + (1 - x) * rgb2[0];
|
|
rgb1[1] = x * rgb1[1] + (1 - x) * rgb2[1];
|
|
rgb1[2] = x * rgb1[2] + (1 - x) * rgb2[2];
|
|
tertiaryColor = rgbToColor(rgb1);
|
|
}
|
|
|
|
if (this.forceTint == false &&
|
|
this.colors[0] === primaryColor &&
|
|
this.colors[1] === secondaryColor &&
|
|
this.colors[2] === tertiaryColor) {
|
|
return;
|
|
}
|
|
|
|
this.forceTint = false;
|
|
|
|
this.colors[0] = primaryColor;
|
|
this.colors[1] = secondaryColor;
|
|
this.colors[2] = tertiaryColor;
|
|
|
|
if (DEBUG) {
|
|
console.log("Tinter.tint final: (" + primaryColor + ", " +
|
|
secondaryColor + ", " +
|
|
tertiaryColor + ")");
|
|
}
|
|
|
|
// go through manually fixing up the stylesheets.
|
|
this.applyCssFixups();
|
|
|
|
// tell all the SVGs to go fix themselves up
|
|
// we don't do this as a dispatch otherwise it will visually lag
|
|
this.tintables.forEach(function(tintable) {
|
|
tintable();
|
|
});
|
|
}
|
|
|
|
tintSvgWhite(whiteColor) {
|
|
this.currentTint[3] = whiteColor;
|
|
|
|
if (!whiteColor) {
|
|
whiteColor = this.colors[3];
|
|
}
|
|
if (this.colors[3] === whiteColor) {
|
|
return;
|
|
}
|
|
this.colors[3] = whiteColor;
|
|
this.tintables.forEach(function(tintable) {
|
|
tintable();
|
|
});
|
|
}
|
|
|
|
tintSvgBlack(blackColor) {
|
|
this.currentTint[4] = blackColor;
|
|
|
|
if (!blackColor) {
|
|
blackColor = this.colors[4];
|
|
}
|
|
if (this.colors[4] === blackColor) {
|
|
return;
|
|
}
|
|
this.colors[4] = blackColor;
|
|
this.tintables.forEach(function(tintable) {
|
|
tintable();
|
|
});
|
|
}
|
|
|
|
|
|
setTheme(theme) {
|
|
this.theme = theme;
|
|
|
|
// update keyRgb from the current theme CSS itself, if it defines it
|
|
if (document.getElementById('mx_theme_accentColor')) {
|
|
this.keyRgb[0] = window.getComputedStyle(
|
|
document.getElementById('mx_theme_accentColor')).color;
|
|
}
|
|
if (document.getElementById('mx_theme_secondaryAccentColor')) {
|
|
this.keyRgb[1] = window.getComputedStyle(
|
|
document.getElementById('mx_theme_secondaryAccentColor')).color;
|
|
}
|
|
if (document.getElementById('mx_theme_tertiaryAccentColor')) {
|
|
this.keyRgb[2] = window.getComputedStyle(
|
|
document.getElementById('mx_theme_tertiaryAccentColor')).color;
|
|
}
|
|
|
|
this.calcCssFixups();
|
|
this.forceTint = true;
|
|
|
|
this.tint(this.currentTint[0], this.currentTint[1], this.currentTint[2]);
|
|
|
|
if (theme === 'dark') {
|
|
// abuse the tinter to change all the SVG's #fff to #2d2d2d
|
|
// XXX: obviously this shouldn't be hardcoded here.
|
|
this.tintSvgWhite('#2d2d2d');
|
|
this.tintSvgBlack('#dddddd');
|
|
} else {
|
|
this.tintSvgWhite('#ffffff');
|
|
this.tintSvgBlack('#000000');
|
|
}
|
|
}
|
|
|
|
calcCssFixups() {
|
|
// cache our fixups
|
|
if (this.cssFixups[this.theme]) return;
|
|
|
|
if (DEBUG) {
|
|
console.debug("calcCssFixups start for " + this.theme + " (checking " +
|
|
document.styleSheets.length +
|
|
" stylesheets)");
|
|
}
|
|
|
|
this.cssFixups[this.theme] = [];
|
|
|
|
for (let i = 0; i < document.styleSheets.length; i++) {
|
|
const ss = document.styleSheets[i];
|
|
try {
|
|
if (!ss) continue; // well done safari >:(
|
|
// Chromium apparently sometimes returns null here; unsure why.
|
|
// see $14534907369972FRXBx:matrix.org in HQ
|
|
// ...ah, it's because there's a third party extension like
|
|
// privacybadger inserting its own stylesheet in there with a
|
|
// resource:// URI or something which results in a XSS error.
|
|
// See also #vector:matrix.org/$145357669685386ebCfr:matrix.org
|
|
// ...except some browsers apparently return stylesheets without
|
|
// hrefs, which we have no choice but ignore right now
|
|
|
|
// XXX seriously? we are hardcoding the name of vector's CSS file in
|
|
// here?
|
|
//
|
|
// Why do we need to limit it to vector's CSS file anyway - if there
|
|
// are other CSS files affecting the doc don't we want to apply the
|
|
// same transformations to them?
|
|
//
|
|
// Iterating through the CSS looking for matches to hack on feels
|
|
// pretty horrible anyway. And what if the application skin doesn't use
|
|
// Vector Green as its primary color?
|
|
// --richvdh
|
|
|
|
// Yes, tinting assumes that you are using the Riot skin for now.
|
|
// The right solution will be to move the CSS over to react-sdk.
|
|
// And yes, the default assets for the base skin might as well use
|
|
// Vector Green as any other colour.
|
|
// --matthew
|
|
|
|
// stylesheets we don't have permission to access (eg. ones from extensions) have a null
|
|
// href and will throw exceptions if we try to access their rules.
|
|
if (!ss.href || !ss.href.match(new RegExp('/theme-' + this.theme + '.css$'))) continue;
|
|
if (ss.disabled) continue;
|
|
if (!ss.cssRules) continue;
|
|
|
|
if (DEBUG) console.debug("calcCssFixups checking " + ss.cssRules.length + " rules for " + ss.href);
|
|
|
|
for (let j = 0; j < ss.cssRules.length; j++) {
|
|
const rule = ss.cssRules[j];
|
|
if (!rule.style) continue;
|
|
if (rule.selectorText && rule.selectorText.match(/#mx_theme/)) continue;
|
|
for (let k = 0; k < this.cssAttrs.length; k++) {
|
|
const attr = this.cssAttrs[k];
|
|
for (let l = 0; l < this.keyRgb.length; l++) {
|
|
if (rule.style[attr] === this.keyRgb[l]) {
|
|
this.cssFixups[this.theme].push({
|
|
style: rule.style,
|
|
attr: attr,
|
|
index: l,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// Catch any random exceptions that happen here: all sorts of things can go
|
|
// wrong with this (nulls, SecurityErrors) and mostly it's for other
|
|
// stylesheets that we don't want to proces anyway. We should not propagate an
|
|
// exception out since this will cause the app to fail to start.
|
|
console.log("Failed to calculate CSS fixups for a stylesheet: " + ss.href, e);
|
|
}
|
|
}
|
|
if (DEBUG) {
|
|
console.log("calcCssFixups end (" +
|
|
this.cssFixups[this.theme].length +
|
|
" fixups)");
|
|
}
|
|
}
|
|
|
|
applyCssFixups() {
|
|
if (DEBUG) {
|
|
console.log("applyCssFixups start (" +
|
|
this.cssFixups[this.theme].length +
|
|
" fixups)");
|
|
}
|
|
for (let i = 0; i < this.cssFixups[this.theme].length; i++) {
|
|
const cssFixup = this.cssFixups[this.theme][i];
|
|
try {
|
|
cssFixup.style[cssFixup.attr] = this.colors[cssFixup.index];
|
|
} catch (e) {
|
|
// Firefox Quantum explodes if you manually edit the CSS in the
|
|
// inspector and then try to do a tint, as apparently all the
|
|
// fixups are then stale.
|
|
console.error("Failed to apply cssFixup in Tinter! ", e.name);
|
|
}
|
|
}
|
|
if (DEBUG) console.log("applyCssFixups end");
|
|
}
|
|
|
|
// XXX: we could just move this all into TintableSvg, but as it's so similar
|
|
// to the CSS fixup stuff in Tinter (just that the fixups are stored in TintableSvg)
|
|
// keeping it here for now.
|
|
calcSvgFixups(svgs) {
|
|
// go through manually fixing up SVG colours.
|
|
// we could do this by stylesheets, but keeping the stylesheets
|
|
// updated would be a PITA, so just brute-force search for the
|
|
// key colour; cache the element and apply.
|
|
|
|
if (DEBUG) console.log("calcSvgFixups start for " + svgs);
|
|
const fixups = [];
|
|
for (let i = 0; i < svgs.length; i++) {
|
|
let svgDoc;
|
|
try {
|
|
svgDoc = svgs[i].contentDocument;
|
|
} catch (e) {
|
|
let msg = 'Failed to get svg.contentDocument of ' + svgs[i].toString();
|
|
if (e.message) {
|
|
msg += e.message;
|
|
}
|
|
if (e.stack) {
|
|
msg += ' | stack: ' + e.stack;
|
|
}
|
|
console.error(msg);
|
|
}
|
|
if (!svgDoc) continue;
|
|
const tags = svgDoc.getElementsByTagName("*");
|
|
for (let j = 0; j < tags.length; j++) {
|
|
const tag = tags[j];
|
|
for (let k = 0; k < this.svgAttrs.length; k++) {
|
|
const attr = this.svgAttrs[k];
|
|
for (let l = 0; l < this.keyHex.length; l++) {
|
|
if (tag.getAttribute(attr) &&
|
|
tag.getAttribute(attr).toUpperCase() === this.keyHex[l]) {
|
|
fixups.push({
|
|
node: tag,
|
|
attr: attr,
|
|
index: l,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (DEBUG) console.log("calcSvgFixups end");
|
|
|
|
return fixups;
|
|
}
|
|
|
|
applySvgFixups(fixups) {
|
|
if (DEBUG) console.log("applySvgFixups start for " + fixups);
|
|
for (let i = 0; i < fixups.length; i++) {
|
|
const svgFixup = fixups[i];
|
|
svgFixup.node.setAttribute(svgFixup.attr, this.colors[svgFixup.index]);
|
|
}
|
|
if (DEBUG) console.log("applySvgFixups end");
|
|
}
|
|
}
|
|
|
|
if (global.singletonTinter === undefined) {
|
|
global.singletonTinter = new Tinter();
|
|
}
|
|
export default global.singletonTinter;
|