From 57790f3d2e563387001fec71f22503c7bf4b51f0 Mon Sep 17 00:00:00 2001 From: Eduard Kuzmenko Date: Thu, 22 Apr 2021 20:58:31 +0400 Subject: [PATCH] Background color --- src/components/chat/chat.ts | 26 +- src/components/colorPicker.ts | 336 ++++++++++++++++++ src/components/sidebarLeft/tabs/background.ts | 38 +- .../sidebarLeft/tabs/backgroundColor.ts | 142 ++++++++ .../sidebarLeft/tabs/privacyAndSecurity.ts | 27 +- src/components/sliderTab.ts | 12 +- src/config/app.ts | 2 +- src/helpers/color.ts | 87 +++-- src/helpers/highlightningColor.ts | 13 + src/lang.ts | 5 +- src/lib/appManagers/appImManager.ts | 10 +- src/scss/partials/_colorPicker.scss | 35 ++ src/scss/partials/_leftSidebar.scss | 8 +- src/scss/style.scss | 1 + 14 files changed, 673 insertions(+), 69 deletions(-) create mode 100644 src/components/colorPicker.ts create mode 100644 src/components/sidebarLeft/tabs/backgroundColor.ts create mode 100644 src/helpers/highlightningColor.ts create mode 100644 src/scss/partials/_colorPicker.scss diff --git a/src/components/chat/chat.ts b/src/components/chat/chat.ts index 77a97961..71f737ca 100644 --- a/src/components/chat/chat.ts +++ b/src/components/chat/chat.ts @@ -83,10 +83,22 @@ export default class Chat extends EventListenerBase<{ } public setBackground(url: string): Promise { - 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((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); // * одного недостаточно, при обновлении страницы все равно фон появляется неплавно diff --git a/src/components/colorPicker.ts b/src/components/colorPicker.ts new file mode 100644 index 00000000..506933f2 --- /dev/null +++ b/src/components/colorPicker.ts @@ -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) => void; + + constructor() { + this.container = document.createElement('div'); + this.container.classList.add(ColorPicker.BASE_CLASS); + + const html = ` + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + +
+ `; + + 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(); + } + }; +} diff --git a/src/components/sidebarLeft/tabs/background.ts b/src/components/sidebarLeft/tabs/background.ts index 4f166678..34a3a08b 100644 --- a/src/components/sidebarLeft/tabs/background.ts +++ b/src/components/sidebarLeft/tabs/background.ts @@ -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); }); diff --git a/src/components/sidebarLeft/tabs/backgroundColor.ts b/src/components/sidebarLeft/tabs/backgroundColor.ts new file mode 100644 index 00000000..bd9bd914 --- /dev/null +++ b/src/components/sidebarLeft/tabs/backgroundColor.ts @@ -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(); + } +} diff --git a/src/components/sidebarLeft/tabs/privacyAndSecurity.ts b/src/components/sidebarLeft/tabs/privacyAndSecurity.ts index b80536ad..afb29196 100644 --- a/src/components/sidebarLeft/tabs/privacyAndSecurity.ts +++ b/src/components/sidebarLeft/tabs/privacyAndSecurity.ts @@ -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); } } diff --git a/src/components/sliderTab.ts b/src/components/sliderTab.ts index efa93aa5..31a68f2f 100644 --- a/src/components/sliderTab.ts +++ b/src/components/sliderTab.ts @@ -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); } } diff --git a/src/config/app.ts b/src/config/app.ts index a5137496..7a1cde86 100644 --- a/src/config/app.ts +++ b/src/config/app.ts @@ -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[], diff --git a/src/helpers/color.ts b/src/helpers/color.ts index 1d7d9c9a..a87d5794 100644 --- a/src/helpers/color.ts +++ b/src/helpers/color.ts @@ -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)); + } + + 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)); + } + } + + return arr; } -export function hslaStringToRgbString(hsla: string) { - return hslaStringToRgbaString(hsla).slice(0, -2); +export function hexaToHsla(hexa: string) { + const rgba = hexaToRgba(hexa); + return rgbaToHsla(rgba[0], rgba[1], rgba[2], rgba[3]); } -// * 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)); - } - l = Math.max(0.0, l * 0.65); - - const hsla = `hsla(${h * 360}, ${s * 100}%, ${l * 100}%, .4)`; - return hsla; +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); } diff --git a/src/helpers/highlightningColor.ts b/src/helpers/highlightningColor.ts new file mode 100644 index 00000000..ee3f2ebf --- /dev/null +++ b/src/helpers/highlightningColor.ts @@ -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; +} diff --git a/src/lang.ts b/src/lang.ts index 373fa0dd..90190df4 100644 --- a/src/lang.ts +++ b/src/lang.ts @@ -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.", diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index fa78c63f..56a42acc 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -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 = () => { diff --git a/src/scss/partials/_colorPicker.scss b/src/scss/partials/_colorPicker.scss new file mode 100644 index 00000000..029e778a --- /dev/null +++ b/src/scss/partials/_colorPicker.scss @@ -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; + } + } + } +} diff --git a/src/scss/partials/_leftSidebar.scss b/src/scss/partials/_leftSidebar.scss index 6d5b6fa9..75c34ba3 100644 --- a/src/scss/partials/_leftSidebar.scss +++ b/src/scss/partials/_leftSidebar.scss @@ -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; + } +} diff --git a/src/scss/style.scss b/src/scss/style.scss index 126f512f..3a1cd570 100644 --- a/src/scss/style.scss +++ b/src/scss/style.scss @@ -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";