Background color
This commit is contained in:
parent
0df275bfe3
commit
57790f3d2e
@ -83,10 +83,22 @@ export default class Chat extends EventListenerBase<{
|
||||
}
|
||||
|
||||
public setBackground(url: string): Promise<void> {
|
||||
const item = document.createElement('div');
|
||||
item.classList.add('chat-background-item');
|
||||
|
||||
const theme = rootScope.settings.themes.find(t => t.name === rootScope.settings.theme);
|
||||
|
||||
let item: HTMLElement;
|
||||
if(theme.background.type === 'color' && document.documentElement.style.cursor === 'grabbing') {
|
||||
const _item = this.backgroundEl.lastElementChild as HTMLElement;
|
||||
if(_item && _item.dataset.type === theme.background.type) {
|
||||
item = _item;
|
||||
}
|
||||
}
|
||||
|
||||
if(!item) {
|
||||
item = document.createElement('div');
|
||||
item.classList.add('chat-background-item');
|
||||
item.dataset.type = theme.background.type;
|
||||
}
|
||||
|
||||
if(theme.background.type === 'color') {
|
||||
item.style.backgroundColor = theme.background.color;
|
||||
item.style.backgroundImage = 'none';
|
||||
@ -94,7 +106,13 @@ export default class Chat extends EventListenerBase<{
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
const cb = () => {
|
||||
const prev = this.backgroundEl.children[this.backgroundEl.childElementCount - 1] as HTMLElement;
|
||||
const prev = this.backgroundEl.lastElementChild as HTMLElement;
|
||||
|
||||
if(prev === item) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
this.backgroundEl.append(item);
|
||||
|
||||
// * одного недостаточно, при обновлении страницы все равно фон появляется неплавно
|
||||
|
336
src/components/colorPicker.ts
Normal file
336
src/components/colorPicker.ts
Normal file
@ -0,0 +1,336 @@
|
||||
import { ColorHsla, hexaToHsla, hslaToRgba, rgbaToHexa as rgbaToHexa, rgbaToHsla } from "../helpers/color";
|
||||
import { clamp } from "../helpers/number";
|
||||
import InputField, { InputState } from "./inputField";
|
||||
|
||||
type EventPosition = {x: number, y: number, isTouch?: boolean};
|
||||
export type ColorPickerColor = {
|
||||
hsl: string;
|
||||
rgb: string;
|
||||
hex: string;
|
||||
hsla: string;
|
||||
rgba: string;
|
||||
hexa: string;
|
||||
rgbaArray: import("f:/tweb/src/helpers/color").ColorRgba;
|
||||
};
|
||||
|
||||
export default class ColorPicker {
|
||||
private static BASE_CLASS = 'color-picker';
|
||||
public container: HTMLElement;
|
||||
|
||||
private boxRect: DOMRect;
|
||||
//private boxDraggerRect: DOMRect;
|
||||
private hueRect: DOMRect;
|
||||
//private hueDraggerRect: DOMRect;
|
||||
|
||||
private hue = 0;
|
||||
private saturation = 100;
|
||||
private lightness = 50;
|
||||
private alpha = 1;
|
||||
private elements: {
|
||||
box: SVGSVGElement,
|
||||
boxDragger: SVGSVGElement,
|
||||
sliders: HTMLElement,
|
||||
hue: SVGSVGElement,
|
||||
hueDragger: SVGSVGElement,
|
||||
saturation: SVGLinearGradientElement,
|
||||
} = {} as any;
|
||||
private hexInputField: InputField;
|
||||
private rgbInputField: InputField;
|
||||
public onChange: (color: ReturnType<ColorPicker['getCurrentColor']>) => void;
|
||||
|
||||
constructor() {
|
||||
this.container = document.createElement('div');
|
||||
this.container.classList.add(ColorPicker.BASE_CLASS);
|
||||
|
||||
const html = `
|
||||
<svg class="${ColorPicker.BASE_CLASS + '-box'}" viewBox="0 0 380 198">
|
||||
<defs>
|
||||
<linearGradient id="color-picker-saturation" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" stop-color="#fff"></stop>
|
||||
<stop offset="100%" stop-color="hsl(0,100%,50%)"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient id="color-picker-brightness" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="rgba(0,0,0,0)"></stop>
|
||||
<stop offset="100%" stop-color="#000"></stop>
|
||||
</linearGradient>
|
||||
<pattern id="color-picker-pattern" width="100%" height="100%">
|
||||
<rect x="0" y="0" width="100%" height="100%" fill="url(#color-picker-saturation)"></rect>
|
||||
<rect x="0" y="0" width="100%" height="100%" fill="url(#color-picker-brightness)"></rect>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect rx="10" ry="10" x="0" y="0" width="380" height="198" fill="url(#color-picker-pattern)"></rect>
|
||||
<svg class="${ColorPicker.BASE_CLASS + '-dragger'} ${ColorPicker.BASE_CLASS + '-box-dragger'}" x="0" y="0">
|
||||
<circle r="11" fill="inherit" stroke="#fff" stroke-width="2"></circle>
|
||||
</svg>
|
||||
</svg>
|
||||
<div class="${ColorPicker.BASE_CLASS + '-sliders'}">
|
||||
<svg class="${ColorPicker.BASE_CLASS + '-color-slider'}" viewBox="0 0 380 24">
|
||||
<defs>
|
||||
<linearGradient id="hue" x1="100%" y1="0%" x2="0%" y2="0%">
|
||||
<stop offset="0%" stop-color="#f00"></stop>
|
||||
<stop offset="16.666%" stop-color="#f0f"></stop>
|
||||
<stop offset="33.333%" stop-color="#00f"></stop>
|
||||
<stop offset="50%" stop-color="#0ff"></stop>
|
||||
<stop offset="66.666%" stop-color="#0f0"></stop>
|
||||
<stop offset="83.333%" stop-color="#ff0"></stop>
|
||||
<stop offset="100%" stop-color="#f00"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect rx="4" ry="4" x="0" y="9" width="380" height="8" fill="url(#hue)"></rect>
|
||||
<svg class="${ColorPicker.BASE_CLASS + '-dragger'} ${ColorPicker.BASE_CLASS + '-color-slider-dragger'}" x="0" y="13">
|
||||
<circle r="11" fill="inherit" stroke="#fff" stroke-width="2"></circle>
|
||||
</svg>
|
||||
</svg>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.container.innerHTML = html;
|
||||
|
||||
this.elements.box = this.container.firstElementChild as any;
|
||||
this.elements.boxDragger = this.elements.box.lastElementChild as any;
|
||||
this.elements.saturation = this.elements.box.firstElementChild.firstElementChild as any;
|
||||
|
||||
this.elements.sliders = this.elements.box.nextElementSibling as any;
|
||||
|
||||
this.elements.hue = this.elements.sliders.firstElementChild as any;
|
||||
this.elements.hueDragger = this.elements.hue.lastElementChild as any;
|
||||
|
||||
this.hexInputField = new InputField({plainText: true, label: 'Appearance.Color.Hex'});
|
||||
this.rgbInputField = new InputField({plainText: true, label: 'Appearance.Color.RGB'});
|
||||
|
||||
const inputs = document.createElement('div');
|
||||
inputs.className = ColorPicker.BASE_CLASS + '-inputs';
|
||||
inputs.append(this.hexInputField.container, this.rgbInputField.container);
|
||||
this.container.append(inputs);
|
||||
|
||||
this.hexInputField.input.addEventListener('input', () => {
|
||||
let value = this.hexInputField.value.replace(/#/g, '').slice(0, 6);
|
||||
|
||||
const match = value.match(/([a-fA-F\d]+)/);
|
||||
const valid = match && match[0].length === value.length && [/* 3, 4, */6].includes(value.length);
|
||||
this.hexInputField.setState(valid ? InputState.Neutral : InputState.Error);
|
||||
|
||||
value = '#' + value;
|
||||
this.hexInputField.setValueSilently(value);
|
||||
|
||||
if(valid) {
|
||||
this.setColor(value, false, true);
|
||||
}
|
||||
});
|
||||
|
||||
// patched https://stackoverflow.com/a/34029238/6758968
|
||||
const rgbRegExp = /^(?:rgb)?\(?([01]?\d\d?|2[0-4]\d|25[0-5])(?:\W+)([01]?\d\d?|2[0-4]\d|25[0-5])\W+(?:([01]?\d\d?|2[0-4]\d|25[0-5])\)?)$/;
|
||||
this.rgbInputField.input.addEventListener('input', () => {
|
||||
const match = this.rgbInputField.value.match(rgbRegExp);
|
||||
this.rgbInputField.setState(match ? InputState.Neutral : InputState.Error);
|
||||
|
||||
if(match) {
|
||||
this.setColor(rgbaToHsla(+match[1], +match[2], +match[3]), true, false);
|
||||
}
|
||||
});
|
||||
|
||||
this.attachBoxListeners();
|
||||
this.attachHueListeners();
|
||||
}
|
||||
|
||||
private attachGrabListeners(element: SVGSVGElement, onStart: (position: EventPosition) => void, onMove: (position: EventPosition) => void, onEnd: (position: EventPosition) => void) {
|
||||
// * Mouse
|
||||
const onMouseMove = (event: MouseEvent) => {
|
||||
onMove({x: event.pageX, y: event.pageY});
|
||||
};
|
||||
|
||||
const onMouseDown = (event: MouseEvent) => {
|
||||
if(event.button !== 0) {
|
||||
element.addEventListener('mousedown', onMouseDown, {once: true});
|
||||
return;
|
||||
}
|
||||
|
||||
this.onGrabStart();
|
||||
onStart({x: event.pageX, y: event.pageY});
|
||||
onMouseMove(event);
|
||||
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
document.addEventListener('mouseup', () => {
|
||||
document.removeEventListener('mousemove', onMouseMove);
|
||||
element.addEventListener('mousedown', onMouseDown, {once: true});
|
||||
this.onGrabEnd();
|
||||
onEnd && onEnd({x: event.pageX, y: event.pageY});
|
||||
}, {once: true});
|
||||
};
|
||||
|
||||
element.addEventListener('mousedown', onMouseDown, {once: true});
|
||||
|
||||
// * Touch
|
||||
const onTouchMove = (event: TouchEvent) => {
|
||||
event.preventDefault();
|
||||
onMove({x: event.touches[0].clientX, y: event.touches[0].clientY, isTouch: true});
|
||||
};
|
||||
|
||||
const onTouchStart = (event: TouchEvent) => {
|
||||
this.onGrabStart();
|
||||
onStart({x: event.touches[0].clientX, y: event.touches[0].clientY, isTouch: true});
|
||||
onTouchMove(event);
|
||||
|
||||
document.addEventListener('touchmove', onTouchMove, {passive: false});
|
||||
document.addEventListener('touchend', (event) => {
|
||||
document.removeEventListener('touchmove', onTouchMove);
|
||||
element.addEventListener('touchstart', onTouchStart, {passive: true, once: true});
|
||||
this.onGrabEnd();
|
||||
onEnd && onEnd({x: event.touches[0].clientX, y: event.touches[0].clientY, isTouch: true});
|
||||
}, {passive: true, once: true});
|
||||
};
|
||||
|
||||
element.addEventListener('touchstart', onTouchStart, {passive: true, once: true});
|
||||
}
|
||||
|
||||
private onGrabStart = () => {
|
||||
document.documentElement.style.cursor = this.elements.boxDragger.style.cursor = 'grabbing';
|
||||
};
|
||||
|
||||
private onGrabEnd = () => {
|
||||
document.documentElement.style.cursor = this.elements.boxDragger.style.cursor = '';
|
||||
};
|
||||
|
||||
private attachBoxListeners() {
|
||||
this.attachGrabListeners(this.elements.box, () => {
|
||||
this.boxRect = this.elements.box.getBoundingClientRect();
|
||||
//this.boxDraggerRect = this.elements.boxDragger.getBoundingClientRect();
|
||||
}, (pos) => {
|
||||
this.saturationHandler(pos.x, pos.y);
|
||||
}, null);
|
||||
}
|
||||
|
||||
private attachHueListeners() {
|
||||
this.attachGrabListeners(this.elements.hue, () => {
|
||||
this.hueRect = this.elements.hue.getBoundingClientRect();
|
||||
//this.hueDraggerRect = this.elements.hueDragger.getBoundingClientRect();
|
||||
}, (pos) => {
|
||||
this.hueHandler(pos.x);
|
||||
}, null);
|
||||
}
|
||||
|
||||
public setColor(color: ColorHsla | string, updateHexInput = true, updateRgbInput = true) {
|
||||
if(color === undefined) { // * set to red
|
||||
color = {
|
||||
h: 0,
|
||||
s: 100,
|
||||
l: 50,
|
||||
a: 1
|
||||
};
|
||||
} else if(typeof(color) === 'string') {
|
||||
if(color[0] === '#') {
|
||||
color = hexaToHsla(color);
|
||||
} else {
|
||||
const rgb = color.match(/[.?\d]+/g);
|
||||
color = rgbaToHsla(+rgb[0], +rgb[1], +rgb[2], rgb[3] === undefined ? 1 : +rgb[3]);
|
||||
}
|
||||
}
|
||||
|
||||
// Set box
|
||||
this.boxRect = this.elements.box.getBoundingClientRect();
|
||||
|
||||
const boxX = this.boxRect.width / 100 * color.s;
|
||||
const percentY = 100 - (color.l / (100 - color.s / 2)) * 100;
|
||||
const boxY = this.boxRect.height / 100 * percentY;
|
||||
|
||||
this.saturationHandler(this.boxRect.left + boxX, this.boxRect.top + boxY, false);
|
||||
|
||||
// Set hue
|
||||
this.hueRect = this.elements.hue.getBoundingClientRect();
|
||||
|
||||
const percentHue = color.h / 360;
|
||||
const hueX = this.hueRect.left + this.hueRect.width * percentHue;
|
||||
|
||||
this.hueHandler(hueX, false);
|
||||
|
||||
// Set values
|
||||
this.hue = color.h;
|
||||
this.saturation = color.s;
|
||||
this.lightness = color.l;
|
||||
this.alpha = color.a;
|
||||
|
||||
this.updatePicker(updateHexInput, updateRgbInput);
|
||||
};
|
||||
|
||||
public getCurrentColor(): ColorPickerColor {
|
||||
const rgbaArray = hslaToRgba(this.hue, this.saturation, this.lightness, this.alpha);
|
||||
const hexa = rgbaToHexa(rgbaArray);
|
||||
const hex = hexa.slice(0, -2);
|
||||
|
||||
return {
|
||||
hsl: `hsl(${this.hue}, ${this.saturation}%, ${this.lightness}%)`,
|
||||
rgb: `rgb(${rgbaArray[0]}, ${rgbaArray[1]}, ${rgbaArray[2]})`,
|
||||
hex: hex,
|
||||
hsla: `hsla(${this.hue}, ${this.saturation}%, ${this.lightness}%, ${this.alpha})`,
|
||||
rgba: `rgba(${rgbaArray[0]}, ${rgbaArray[1]}, ${rgbaArray[2]}, ${rgbaArray[3]})`,
|
||||
hexa: hexa,
|
||||
rgbaArray: rgbaArray
|
||||
};
|
||||
}
|
||||
|
||||
public updatePicker(updateHexInput = true, updateRgbInput = true) {
|
||||
const color = this.getCurrentColor();
|
||||
this.elements.boxDragger.setAttributeNS(null, 'fill', color.hex);
|
||||
|
||||
if(updateHexInput) {
|
||||
this.hexInputField.setValueSilently(color.hex);
|
||||
this.hexInputField.setState(InputState.Neutral);
|
||||
}
|
||||
|
||||
if(updateRgbInput) {
|
||||
this.rgbInputField.setValueSilently(color.rgbaArray.slice(0, -1).join(', '));
|
||||
this.rgbInputField.setState(InputState.Neutral);
|
||||
}
|
||||
|
||||
if(this.onChange) {
|
||||
this.onChange(color);
|
||||
}
|
||||
}
|
||||
|
||||
private hueHandler(pageX: number, update = true) {
|
||||
const eventX = clamp(pageX - this.hueRect.left, 0, this.hueRect.width);
|
||||
|
||||
const percents = eventX / this.hueRect.width;
|
||||
this.hue = Math.round(360 * percents);
|
||||
|
||||
const hsla = `hsla(${this.hue}, 100%, 50%, ${this.alpha})`;
|
||||
|
||||
this.elements.hueDragger.setAttributeNS(null, 'x', (percents * 100) + '%');
|
||||
this.elements.hueDragger.setAttributeNS(null, 'fill', hsla);
|
||||
|
||||
this.elements.saturation.lastElementChild.setAttributeNS(null, 'stop-color', hsla);
|
||||
|
||||
if(update) {
|
||||
this.updatePicker();
|
||||
}
|
||||
}
|
||||
|
||||
private saturationHandler(pageX: number, pageY: number, update = true) {
|
||||
const maxX = this.boxRect.width;
|
||||
const maxY = this.boxRect.height;
|
||||
|
||||
const eventX = clamp(pageX - this.boxRect.left, 0, maxX);
|
||||
const eventY = clamp(pageY - this.boxRect.top, 0, maxY);
|
||||
|
||||
const posX = eventX / maxX * 100;
|
||||
const posY = eventY / maxY * 100;
|
||||
|
||||
const boxDragger = this.elements.boxDragger;
|
||||
boxDragger.setAttributeNS(null, 'x', posX + '%');
|
||||
boxDragger.setAttributeNS(null, 'y', posY + '%');
|
||||
|
||||
const saturation = clamp(posX, 0, 100);
|
||||
|
||||
const lightnessX = 100 - saturation / 2;
|
||||
const lightnessY = 100 - clamp(posY, 0, 100);
|
||||
|
||||
const lightness = clamp(lightnessY / 100 * lightnessX, 0, 100);
|
||||
|
||||
this.saturation = saturation;
|
||||
this.lightness = lightness;
|
||||
|
||||
if(update) {
|
||||
this.updatePicker();
|
||||
}
|
||||
};
|
||||
}
|
@ -8,31 +8,49 @@ import { generateSection } from "..";
|
||||
import { averageColor } from "../../../helpers/averageColor";
|
||||
import blur from "../../../helpers/blur";
|
||||
import { deferredPromise } from "../../../helpers/cancellablePromise";
|
||||
import { highlightningColor } from "../../../helpers/color";
|
||||
import { attachClickEvent } from "../../../helpers/dom";
|
||||
import findUpClassName from "../../../helpers/dom/findUpClassName";
|
||||
import highlightningColor from "../../../helpers/highlightningColor";
|
||||
import { copy } from "../../../helpers/object";
|
||||
import { AccountWallPapers, WallPaper } from "../../../layer";
|
||||
import appDocsManager, { MyDocument } from "../../../lib/appManagers/appDocsManager";
|
||||
import appDownloadManager from "../../../lib/appManagers/appDownloadManager";
|
||||
import appImManager from "../../../lib/appManagers/appImManager";
|
||||
import appStateManager from "../../../lib/appManagers/appStateManager";
|
||||
import appStateManager, { STATE_INIT } from "../../../lib/appManagers/appStateManager";
|
||||
import apiManager from "../../../lib/mtproto/mtprotoworker";
|
||||
import rootScope from "../../../lib/rootScope";
|
||||
import Button from "../../button";
|
||||
import CheckboxField from "../../checkboxField";
|
||||
import ProgressivePreloader from "../../preloader";
|
||||
import { SliderSuperTab } from "../../slider";
|
||||
import { wrapPhoto } from "../../wrappers";
|
||||
import AppBackgroundColorTab from "./backgroundColor";
|
||||
|
||||
export default class AppBackgroundTab extends SliderSuperTab {
|
||||
init() {
|
||||
this.container.classList.add('background-container');
|
||||
this.container.classList.add('background-container', 'background-image-container');
|
||||
this.setTitle('ChatBackground');
|
||||
|
||||
{
|
||||
const container = generateSection(this.scrollable);
|
||||
|
||||
//const uploadButton = Button('btn-primary btn-transparent', {icon: 'cameraadd', text: 'ChatBackground.UploadWallpaper', disabled: true});
|
||||
//const colorButton = Button('btn-primary btn-transparent', {icon: 'colorize', text: 'ChatBackground.SetColor', disabled: true});
|
||||
const colorButton = Button('btn-primary btn-transparent', {icon: 'colorize', text: 'SetColor'});
|
||||
const resetButton = Button('btn-primary btn-transparent', {icon: 'favourites', text: 'Appearance.Reset'});
|
||||
|
||||
attachClickEvent(colorButton, () => {
|
||||
new AppBackgroundColorTab(this.slider).open();
|
||||
}, {listenerSetter: this.listenerSetter});
|
||||
|
||||
attachClickEvent(resetButton, () => {
|
||||
const defaultTheme = STATE_INIT.settings.themes.find(t => t.name === theme.name);
|
||||
if(defaultTheme) {
|
||||
++tempId;
|
||||
theme.background = copy(defaultTheme.background);
|
||||
appStateManager.pushToState('settings', rootScope.settings);
|
||||
appImManager.applyCurrentTheme(undefined, undefined, true);
|
||||
}
|
||||
}, {listenerSetter: this.listenerSetter});
|
||||
|
||||
const theme = rootScope.settings.themes.find(t => t.name === rootScope.settings.theme);
|
||||
const blurCheckboxField = new CheckboxField({
|
||||
@ -41,7 +59,8 @@ export default class AppBackgroundTab extends SliderSuperTab {
|
||||
checked: theme.background.blur,
|
||||
withRipple: true
|
||||
});
|
||||
blurCheckboxField.input.addEventListener('change', () => {
|
||||
|
||||
this.listenerSetter.add(blurCheckboxField.input, 'change', () => {
|
||||
const active = grid.querySelector('.active') as HTMLElement;
|
||||
if(!active) return;
|
||||
|
||||
@ -54,7 +73,7 @@ export default class AppBackgroundTab extends SliderSuperTab {
|
||||
}, 100);
|
||||
});
|
||||
|
||||
container.append(/* uploadButton, colorButton, */blurCheckboxField.label);
|
||||
container.append(/* uploadButton, */colorButton, resetButton, blurCheckboxField.label);
|
||||
}
|
||||
|
||||
const grid = document.createElement('div');
|
||||
@ -92,7 +111,7 @@ export default class AppBackgroundTab extends SliderSuperTab {
|
||||
return;
|
||||
}
|
||||
|
||||
const hsla = highlightningColor(pixel);
|
||||
const hsla = highlightningColor(Array.from(pixel) as any);
|
||||
//console.log(doc, hsla, performance.now() - perf);
|
||||
|
||||
background.slug = slug;
|
||||
@ -208,15 +227,16 @@ export default class AppBackgroundTab extends SliderSuperTab {
|
||||
attachClickEvent(target, (e) => {
|
||||
if(preloader.preloader.parentElement) {
|
||||
preloader.onClick(e);
|
||||
preloader.detach();
|
||||
} else {
|
||||
load();
|
||||
}
|
||||
});
|
||||
}, {listenerSetter: this.listenerSetter});
|
||||
|
||||
load();
|
||||
|
||||
//console.log(doc);
|
||||
});
|
||||
}, {listenerSetter: this.listenerSetter});
|
||||
|
||||
//console.log(accountWallpapers);
|
||||
});
|
||||
|
142
src/components/sidebarLeft/tabs/backgroundColor.ts
Normal file
142
src/components/sidebarLeft/tabs/backgroundColor.ts
Normal file
@ -0,0 +1,142 @@
|
||||
import { SettingSection } from "..";
|
||||
import { hexaToRgba } from "../../../helpers/color";
|
||||
import { attachClickEvent } from "../../../helpers/dom";
|
||||
import findUpClassName from "../../../helpers/dom/findUpClassName";
|
||||
import highlightningColor from "../../../helpers/highlightningColor";
|
||||
import { throttle } from "../../../helpers/schedulers";
|
||||
import appImManager from "../../../lib/appManagers/appImManager";
|
||||
import appStateManager from "../../../lib/appManagers/appStateManager";
|
||||
import rootScope from "../../../lib/rootScope";
|
||||
import ColorPicker, { ColorPickerColor } from "../../colorPicker";
|
||||
import { SliderSuperTab } from "../../slider";
|
||||
|
||||
export default class AppBackgroundColorTab extends SliderSuperTab {
|
||||
private colorPicker: ColorPicker;
|
||||
private grid: HTMLElement;
|
||||
private applyColor: (hex: string, updateColorPicker?: boolean) => void;
|
||||
|
||||
init() {
|
||||
this.container.classList.add('background-container', 'background-color-container');
|
||||
this.setTitle('SetColor');
|
||||
|
||||
const section = new SettingSection({});
|
||||
this.colorPicker = new ColorPicker();
|
||||
|
||||
section.content.append(this.colorPicker.container);
|
||||
|
||||
this.scrollable.append(section.container);
|
||||
|
||||
const grid = this.grid = document.createElement('div');
|
||||
grid.classList.add('grid');
|
||||
|
||||
const colors = [
|
||||
'#E6EBEE',
|
||||
'#B2CEE1',
|
||||
'#008DD0',
|
||||
'#C6E7CB',
|
||||
'#C4E1A6',
|
||||
'#60B16E',
|
||||
'#CCD0AF',
|
||||
'#A6A997',
|
||||
'#7A7072',
|
||||
'#FDD7AF',
|
||||
'#FDB76E',
|
||||
'#DD8851'
|
||||
];
|
||||
|
||||
colors.forEach(color => {
|
||||
const item = document.createElement('div');
|
||||
item.classList.add('grid-item');
|
||||
item.dataset.color = color.toLowerCase();
|
||||
|
||||
// * need for transform scale
|
||||
const media = document.createElement('div');
|
||||
media.classList.add('grid-item-media');
|
||||
media.style.backgroundColor = color;
|
||||
|
||||
item.append(media);
|
||||
grid.append(item);
|
||||
});
|
||||
|
||||
attachClickEvent(grid, (e) => {
|
||||
const target = findUpClassName(e.target, 'grid-item');
|
||||
if(!target || target.classList.contains('active')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const color = target.dataset.color;
|
||||
if(!color) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.applyColor(color);
|
||||
}, {listenerSetter: this.listenerSetter});
|
||||
|
||||
this.scrollable.append(grid);
|
||||
|
||||
this.applyColor = throttle(this._applyColor, 16, true);
|
||||
}
|
||||
|
||||
private setActive() {
|
||||
const active = this.grid.querySelector('.active');
|
||||
const background = rootScope.settings.themes.find(t => t.name === rootScope.settings.theme).background;
|
||||
const target = background.type === 'color' ? this.grid.querySelector(`.grid-item[data-color="${background.color}"]`) : null;
|
||||
if(active === target) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(active) {
|
||||
active.classList.remove('active');
|
||||
}
|
||||
|
||||
if(target) {
|
||||
target.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
private _applyColor = (hex: string, updateColorPicker = true) => {
|
||||
if(updateColorPicker) {
|
||||
this.colorPicker.setColor(hex);
|
||||
} else {
|
||||
const rgba = hexaToRgba(hex);
|
||||
const background = rootScope.settings.themes.find(t => t.name === rootScope.settings.theme).background;
|
||||
const hsla = highlightningColor(rgba);
|
||||
|
||||
background.color = hex.toLowerCase();
|
||||
background.type = 'color';
|
||||
background.highlightningColor = hsla;
|
||||
appStateManager.pushToState('settings', rootScope.settings);
|
||||
|
||||
appImManager.applyCurrentTheme(undefined, undefined, true);
|
||||
this.setActive();
|
||||
}
|
||||
};
|
||||
|
||||
private onColorChange = (color: ColorPickerColor) => {
|
||||
this.applyColor(color.hex, false);
|
||||
};
|
||||
|
||||
onOpen() {
|
||||
setTimeout(() => {
|
||||
const background = rootScope.settings.themes.find(t => t.name === rootScope.settings.theme).background;
|
||||
|
||||
// * set active if type is color
|
||||
if(background.type === 'color') {
|
||||
this.colorPicker.onChange = this.onColorChange;
|
||||
}
|
||||
|
||||
this.colorPicker.setColor(background.color || '#cccccc');
|
||||
|
||||
if(background.type !== 'color') {
|
||||
this.colorPicker.onChange = this.onColorChange;
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
onCloseAfterTimeout() {
|
||||
this.colorPicker.onChange = undefined;
|
||||
this.colorPicker = undefined;
|
||||
|
||||
return super.onCloseAfterTimeout();
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { SliderSuperTab } from "../../slider";
|
||||
import { generateSection, SettingSection } from "..";
|
||||
import { SettingSection } from "..";
|
||||
import Row from "../../row";
|
||||
import { AccountPassword, Authorization, InputPrivacyKey } from "../../../layer";
|
||||
import appPrivacyManager, { PrivacyType } from "../../../lib/appManagers/appPrivacyManager";
|
||||
@ -26,6 +26,7 @@ import appUsersManager from "../../../lib/appManagers/appUsersManager";
|
||||
import rootScope from "../../../lib/rootScope";
|
||||
import { convertKeyToInputKey } from "../../../helpers/string";
|
||||
import { i18n, LangPackKey, _i18n } from "../../../lib/langPack";
|
||||
import { replaceContent } from "../../../helpers/dom";
|
||||
|
||||
export default class AppPrivacyAndSecurityTab extends SliderSuperTab {
|
||||
private activeSessionsRow: Row;
|
||||
@ -35,8 +36,6 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTab {
|
||||
this.container.classList.add('privacy-container');
|
||||
this.setTitle('PrivacySettings');
|
||||
|
||||
const section = generateSection.bind(null, this.scrollable);
|
||||
|
||||
const SUBTITLE: LangPackKey = 'Loading';
|
||||
|
||||
{
|
||||
@ -98,14 +97,11 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTab {
|
||||
section.content.append(blockedUsersRow.container, twoFactorRow.container, activeSessionsRow.container);
|
||||
this.scrollable.append(section.container);
|
||||
|
||||
let blockedCount: number;
|
||||
const setBlockedCount = (count: number) => {
|
||||
blockedCount = count;
|
||||
|
||||
if(count) {
|
||||
_i18n(blockedUsersRow.subtitle, 'PrivacySettingsController.UserCount', [count]);
|
||||
replaceContent(blockedUsersRow.subtitle, i18n('PrivacySettingsController.UserCount', [count]));
|
||||
} else {
|
||||
_i18n(blockedUsersRow.subtitle, 'BlockedEmpty');
|
||||
replaceContent(blockedUsersRow.subtitle, i18n('BlockedEmpty', [count]));
|
||||
}
|
||||
};
|
||||
|
||||
@ -130,7 +126,7 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTab {
|
||||
|
||||
passwordManager.getState().then(state => {
|
||||
passwordState = state;
|
||||
_i18n(twoFactorRow.subtitle, state.pFlags.has_password ? 'PrivacyAndSecurity.Item.On' : 'PrivacyAndSecurity.Item.Off');
|
||||
replaceContent(twoFactorRow.subtitle, i18n(state.pFlags.has_password ? 'PrivacyAndSecurity.Item.On' : 'PrivacyAndSecurity.Item.Off'));
|
||||
twoFactorRow.freezed = false;
|
||||
|
||||
//console.log('password state', state);
|
||||
@ -140,9 +136,9 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTab {
|
||||
}
|
||||
|
||||
{
|
||||
const container = section('PrivacyTitle');
|
||||
const section = new SettingSection({name: 'PrivacyTitle'});
|
||||
|
||||
container.classList.add('privacy-navigation-container');
|
||||
section.content.classList.add('privacy-navigation-container');
|
||||
|
||||
const rowsByKeys: Partial<{
|
||||
[key in InputPrivacyKey['_']]: Row
|
||||
@ -152,7 +148,7 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTab {
|
||||
titleLangKey: 'PrivacyPhoneTitle',
|
||||
subtitleLangKey: SUBTITLE,
|
||||
clickable: () => {
|
||||
new AppPrivacyPhoneNumberTab(this.slider).open()
|
||||
new AppPrivacyPhoneNumberTab(this.slider).open();
|
||||
}
|
||||
});
|
||||
|
||||
@ -160,7 +156,7 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTab {
|
||||
titleLangKey: 'LastSeenTitle',
|
||||
subtitleLangKey: SUBTITLE,
|
||||
clickable: () => {
|
||||
new AppPrivacyLastSeenTab(this.slider).open()
|
||||
new AppPrivacyLastSeenTab(this.slider).open();
|
||||
}
|
||||
});
|
||||
|
||||
@ -217,6 +213,9 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTab {
|
||||
});
|
||||
};
|
||||
|
||||
section.content.append(numberVisibilityRow.container, lastSeenTimeRow.container, photoVisibilityRow.container, callRow.container, linkAccountRow.container, groupChatsAddRow.container);
|
||||
this.scrollable.append(section.container);
|
||||
|
||||
for(const key in rowsByKeys) {
|
||||
updatePrivacyRow(key as keyof typeof rowsByKeys);
|
||||
}
|
||||
@ -224,8 +223,6 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTab {
|
||||
rootScope.on('privacy_update', (update) => {
|
||||
updatePrivacyRow(convertKeyToInputKey(update.key._) as any);
|
||||
});
|
||||
|
||||
container.append(numberVisibilityRow.container, lastSeenTimeRow.container, photoVisibilityRow.container, callRow.container, linkAccountRow.container, groupChatsAddRow.container);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -75,15 +75,15 @@ export default class SliderSuperTab implements SliderTab {
|
||||
|
||||
public async open(...args: any[]) {
|
||||
if(this.init) {
|
||||
const result = this.init();
|
||||
this.init = null;
|
||||
try {
|
||||
const result = this.init();
|
||||
this.init = null;
|
||||
|
||||
if(result instanceof Promise) {
|
||||
try {
|
||||
if(result instanceof Promise) {
|
||||
await result;
|
||||
} catch(err) {
|
||||
console.error('open tab error', err);
|
||||
}
|
||||
} catch(err) {
|
||||
console.error('open tab error', err);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,7 @@ const App = {
|
||||
id: 1025907,
|
||||
hash: '452b0359b988148995f22ff0f4229750',
|
||||
version: '0.4.3',
|
||||
langPackVersion: '0.1.5',
|
||||
langPackVersion: '0.1.6',
|
||||
langPack: 'macos',
|
||||
langPackCode: 'en',
|
||||
domains: [] as string[],
|
||||
|
@ -4,10 +4,22 @@
|
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
export function rgbToHsl(r: number, g: number, b: number) {
|
||||
export type ColorHsla = {
|
||||
h: number,
|
||||
s: number,
|
||||
l: number,
|
||||
a: number
|
||||
};
|
||||
|
||||
export type ColorRgba = [number, number, number, number];
|
||||
|
||||
/**
|
||||
* @returns h [0, 360], s [0, 100], l [0, 100], a [0, 1]
|
||||
*/
|
||||
export function rgbaToHsla(r: number, g: number, b: number, a: number = 1): ColorHsla {
|
||||
r /= 255, g /= 255, b /= 255;
|
||||
let max = Math.max(r, g, b),
|
||||
min = Math.min(r, g, b);
|
||||
const max = Math.max(r, g, b),
|
||||
min = Math.min(r, g, b);
|
||||
let h, s, l = (max + min) / 2;
|
||||
|
||||
if(max === min) {
|
||||
@ -29,18 +41,19 @@ export function rgbToHsl(r: number, g: number, b: number) {
|
||||
h /= 6;
|
||||
}
|
||||
|
||||
return ({
|
||||
h: h,
|
||||
s: s,
|
||||
l: l,
|
||||
});
|
||||
return {
|
||||
h: h * 360,
|
||||
s: s * 100,
|
||||
l: l * 100,
|
||||
a
|
||||
};
|
||||
}
|
||||
|
||||
// * https://stackoverflow.com/a/9493060/6758968
|
||||
/**
|
||||
* Converts an HSL color value to RGB. Conversion formula
|
||||
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
|
||||
* Assumes h, s, and l are contained in the set [0, 1] and
|
||||
* Assumes h in [0, 360], s, and l are contained in the set [0, 1], a in [0, 1] and
|
||||
* returns r, g, and b in the set [0, 255].
|
||||
*
|
||||
* @param {number} h The hue
|
||||
@ -48,7 +61,8 @@ export function rgbToHsl(r: number, g: number, b: number) {
|
||||
* @param {number} l The lightness
|
||||
* @return {Array} The RGB representation
|
||||
*/
|
||||
export function hslToRgba(h: number, s: number, l: number, a: number) {
|
||||
export function hslaToRgba(h: number, s: number, l: number, a: number): ColorRgba {
|
||||
h /= 360, s /= 100, l /= 100;
|
||||
let r: number, g: number, b: number;
|
||||
|
||||
if(s === 0) {
|
||||
@ -78,31 +92,50 @@ export function hslaStringToRgba(hsla: string) {
|
||||
const alpha = +splitted.pop();
|
||||
const arr = splitted.map((val) => {
|
||||
if(val.endsWith('%')) {
|
||||
return +val.slice(0, -1) / 100;
|
||||
return +val.slice(0, -1);
|
||||
}
|
||||
|
||||
return +val / 360;
|
||||
return +val;
|
||||
});
|
||||
|
||||
return hslToRgba(arr[0], arr[1], arr[2], alpha);
|
||||
return hslaToRgba(arr[0], arr[1], arr[2], alpha);
|
||||
}
|
||||
|
||||
export function hslaStringToRgbaString(hsla: string) {
|
||||
return '#' + hslaStringToRgba(hsla).map(v => ('0' + v.toString(16)).slice(-2)).join('');
|
||||
}
|
||||
export function hexaToRgba(hexa: string) {
|
||||
const arr: ColorRgba = [] as any;
|
||||
const offset = 1;
|
||||
if(hexa.length === (3 + offset)) {
|
||||
for(let i = offset; i < hexa.length; ++i) {
|
||||
arr.push(parseInt(hexa[i] + hexa[i], 16));
|
||||
}
|
||||
} else if(hexa.length === (4 + offset)) {
|
||||
for(let i = offset; i < (hexa.length - 1); ++i) {
|
||||
arr.push(parseInt(hexa[i] + hexa[i], 16));
|
||||
}
|
||||
|
||||
export function hslaStringToRgbString(hsla: string) {
|
||||
return hslaStringToRgbaString(hsla).slice(0, -2);
|
||||
}
|
||||
|
||||
// * https://github.com/TelegramMessenger/Telegram-iOS/blob/3d062fff78cc6b287c74e6171f855a3500c0156d/submodules/TelegramPresentationData/Sources/PresentationData.swift#L453
|
||||
export function highlightningColor(pixel: Uint8ClampedArray) {
|
||||
let {h, s, l} = rgbToHsl(pixel[0], pixel[1], pixel[2]);
|
||||
if(s > 0.0) {
|
||||
s = Math.min(1.0, s + 0.05 + 0.1 * (1.0 - s));
|
||||
arr.push(parseInt(hexa[hexa.length - 1], 16));
|
||||
} else {
|
||||
for(let i = offset; i < hexa.length; i += 2) {
|
||||
arr.push(parseInt(hexa.slice(i, i + 2), 16));
|
||||
}
|
||||
}
|
||||
l = Math.max(0.0, l * 0.65);
|
||||
|
||||
const hsla = `hsla(${h * 360}, ${s * 100}%, ${l * 100}%, .4)`;
|
||||
return hsla;
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
export function hexaToHsla(hexa: string) {
|
||||
const rgba = hexaToRgba(hexa);
|
||||
return rgbaToHsla(rgba[0], rgba[1], rgba[2], rgba[3]);
|
||||
}
|
||||
|
||||
export function rgbaToHexa(rgba: ColorRgba) {
|
||||
return '#' + rgba.map(v => ('0' + v.toString(16)).slice(-2)).join('');
|
||||
}
|
||||
|
||||
export function hslaStringToHexa(hsla: string) {
|
||||
return rgbaToHexa(hslaStringToRgba(hsla));
|
||||
}
|
||||
|
||||
export function hslaStringToHex(hsla: string) {
|
||||
return hslaStringToHexa(hsla).slice(0, -2);
|
||||
}
|
||||
|
13
src/helpers/highlightningColor.ts
Normal file
13
src/helpers/highlightningColor.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { rgbaToHsla } from "./color";
|
||||
|
||||
// * https://github.com/TelegramMessenger/Telegram-iOS/blob/3d062fff78cc6b287c74e6171f855a3500c0156d/submodules/TelegramPresentationData/Sources/PresentationData.swift#L453
|
||||
export default function highlightningColor(rgba: [number, number, number, number?]) {
|
||||
let {h, s, l} = rgbaToHsla(rgba[0], rgba[1], rgba[2]);
|
||||
if(s > 0) {
|
||||
s = Math.min(100, s + 5 + 0.1 * (100 - s));
|
||||
}
|
||||
l = Math.max(0, l * .65);
|
||||
|
||||
const hsla = `hsla(${h}, ${s}%, ${l}%, .4)`;
|
||||
return hsla;
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
const lang = {
|
||||
"Animations": "Animations",
|
||||
"AttachAlbum": "Album",
|
||||
"Appearance.Color.Hex": "HEX",
|
||||
"Appearance.Color.RGB": "RGB",
|
||||
"BlockModal.Search.Placeholder": "Block user...",
|
||||
"DarkMode": "Dark Mode",
|
||||
"FilterIncludeExcludeInfo": "Choose chats and types of chats that will\nappear and never appear in this folder.",
|
||||
@ -43,7 +45,6 @@ const lang = {
|
||||
"General.SendShortcut.NewLine.Enter": "New line by Enter",
|
||||
"General.AutoplayMedia": "Auto-Play Media",
|
||||
"ChatBackground.UploadWallpaper": "Upload Wallpaper",
|
||||
"ChatBackground.SetColor": "Set a Color",
|
||||
"ChatBackground.Blur": "Blur Wallpaper Image",
|
||||
"Notifications.Sound": "Notification Sound",
|
||||
"Notifications.MessagePreview": "Message preview",
|
||||
@ -401,12 +402,14 @@ const lang = {
|
||||
},
|
||||
"HidAccount": "The account was hidden by the user",
|
||||
"TelegramFeatures": "Telegram Features",
|
||||
"SetColor": "Set a color",
|
||||
|
||||
// * macos
|
||||
"AccountSettings.Filters": "Chat Folders",
|
||||
"AccountSettings.Notifications": "Notifications and Sounds",
|
||||
"AccountSettings.PrivacyAndSecurity": "Privacy and Security",
|
||||
"AccountSettings.Language": "Language",
|
||||
"Appearance.Reset": "Reset to Defaults",
|
||||
"Bio.Description": "Any details such as age, occupation or city.\nExample: 23 y.o. designer from San Francisco",
|
||||
"Channel.UsernameAboutChannel": "People can share this link with others and can find your channel using Telegram search.",
|
||||
"Channel.UsernameAboutGroup": "People can share this link with others and find your group using Telegram search.",
|
||||
|
@ -45,7 +45,7 @@ import appNotificationsManager from './appNotificationsManager';
|
||||
import AppPrivateSearchTab from '../../components/sidebarRight/tabs/search';
|
||||
import { i18n, LangPackKey } from '../langPack';
|
||||
import { SendMessageAction } from '../../layer';
|
||||
import { hslaStringToRgbString } from '../../helpers/color';
|
||||
import { hslaStringToHex } from '../../helpers/color';
|
||||
import { copy, getObjectKeysAndSort } from '../../helpers/object';
|
||||
import { getFilesFromEvent } from '../../helpers/files';
|
||||
import PeerTitle from '../../components/peerTitle';
|
||||
@ -224,7 +224,7 @@ export class AppImManager {
|
||||
public setCurrentBackground(broadcastEvent = false) {
|
||||
const theme = rootScope.settings.themes.find(t => t.name === rootScope.settings.theme);
|
||||
|
||||
if(theme.background.slug) {
|
||||
if(theme.background.type === 'image' || (theme.background.type === 'default' && theme.background.slug)) {
|
||||
const defaultTheme = AppStateManager.STATE_INIT.settings.themes.find(t => t.name === theme.name);
|
||||
const isDefaultBackground = theme.background.blur === defaultTheme.background.blur &&
|
||||
theme.background.slug === defaultTheme.background.slug;
|
||||
@ -312,7 +312,7 @@ export class AppImManager {
|
||||
|
||||
let themeColor = '#ffffff';
|
||||
if(hsla) {
|
||||
themeColor = hslaStringToRgbString(hsla);
|
||||
themeColor = hslaStringToHex(hsla);
|
||||
}
|
||||
|
||||
if(this.themeColorElem === undefined) {
|
||||
@ -324,7 +324,7 @@ export class AppImManager {
|
||||
}
|
||||
}
|
||||
|
||||
public applyCurrentTheme(slug?: string, backgroundUrl?: string) {
|
||||
public applyCurrentTheme(slug?: string, backgroundUrl?: string, broadcastEvent?: boolean) {
|
||||
this.applyHighlightningColor();
|
||||
|
||||
document.documentElement.classList.toggle('night', rootScope.settings.theme === 'night');
|
||||
@ -333,7 +333,7 @@ export class AppImManager {
|
||||
this.backgroundPromises[slug] = Promise.resolve(backgroundUrl);
|
||||
}
|
||||
|
||||
return this.setCurrentBackground(!!slug);
|
||||
return this.setCurrentBackground(broadcastEvent === undefined ? !!slug : broadcastEvent);
|
||||
}
|
||||
|
||||
private setSettings = () => {
|
||||
|
35
src/scss/partials/_colorPicker.scss
Normal file
35
src/scss/partials/_colorPicker.scss
Normal file
@ -0,0 +1,35 @@
|
||||
.color-picker {
|
||||
width: 380px;
|
||||
max-width: 100%;
|
||||
margin: 1.1875rem auto 1rem;
|
||||
user-select: none;
|
||||
|
||||
&-box {
|
||||
width: 100%;
|
||||
height: 198px;
|
||||
}
|
||||
|
||||
&-box, &-color-slider, &-dragger {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
&-sliders {
|
||||
margin: 1rem 0 1.125rem;
|
||||
}
|
||||
|
||||
&-dragger {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
&-inputs {
|
||||
display: flex;
|
||||
|
||||
.input-field {
|
||||
flex: 1 1 auto;
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-left: 1.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1108,7 +1108,7 @@
|
||||
|
||||
.background-container {
|
||||
.grid {
|
||||
padding: 0 .5rem;
|
||||
padding: 0 .5rem .5rem;
|
||||
|
||||
&-item {
|
||||
&:after {
|
||||
@ -1141,3 +1141,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.background-image-container {
|
||||
.sidebar-left-section {
|
||||
padding-bottom: .5rem;
|
||||
}
|
||||
}
|
||||
|
@ -256,6 +256,7 @@ html.night {
|
||||
@import "partials/poll";
|
||||
@import "partials/transition";
|
||||
@import "partials/row";
|
||||
@import "partials/colorPicker";
|
||||
|
||||
@import "partials/popups/popup";
|
||||
@import "partials/popups/editAvatar";
|
||||
|
Loading…
x
Reference in New Issue
Block a user