tweb/src/components/sidebarLeft/tabs/background.ts

471 lines
16 KiB
TypeScript

/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import {averageColor, averageColorFromCanvas} from '../../../helpers/averageColor';
import blur from '../../../helpers/blur';
import deferredPromise, {CancellablePromise} from '../../../helpers/cancellablePromise';
import {attachClickEvent} from '../../../helpers/dom/clickEvent';
import findUpClassName from '../../../helpers/dom/findUpClassName';
import highlightningColor from '../../../helpers/highlightningColor';
import copy from '../../../helpers/object/copy';
import sequentialDom from '../../../helpers/sequentialDom';
import ChatBackgroundGradientRenderer from '../../chat/gradientRenderer';
import {Document, PhotoSize, WallPaper} from '../../../layer';
import {MyDocument} from '../../../lib/appManagers/appDocsManager';
import appDownloadManager, {AppDownloadManager, DownloadBlob} from '../../../lib/appManagers/appDownloadManager';
import appImManager from '../../../lib/appManagers/appImManager';
import rootScope from '../../../lib/rootScope';
import Button from '../../button';
import CheckboxField from '../../checkboxField';
import ProgressivePreloader from '../../preloader';
import {SliderSuperTab} from '../../slider';
import AppBackgroundColorTab from './backgroundColor';
import choosePhotoSize from '../../../lib/appManagers/utils/photos/choosePhotoSize';
import {STATE_INIT, AppTheme} from '../../../config/state';
import themeController from '../../../helpers/themeController';
import requestFile from '../../../helpers/files/requestFile';
import {renderImageFromUrlPromise} from '../../../helpers/dom/renderImageFromUrl';
import scaleMediaElement from '../../../helpers/canvas/scaleMediaElement';
import {MediaSize} from '../../../helpers/mediaSize';
import wrapPhoto from '../../wrappers/photo';
import {CreateRowFromCheckboxField} from '../../row';
import {generateSection} from '../../settingSection';
import {hexToRgb} from '../../../helpers/color';
export function getHexColorFromTelegramColor(color: number) {
const hex = (color < 0 ? 0xFFFFFF + color : color).toString(16);
return '#' + (hex.length >= 6 ? hex : '0'.repeat(6 - hex.length) + hex);
}
export function getRgbColorFromTelegramColor(color: number) {
return hexToRgb(getHexColorFromTelegramColor(color));
}
export function getColorsFromWallPaper(wallPaper: WallPaper) {
return wallPaper.settings ? [
wallPaper.settings.background_color,
wallPaper.settings.second_background_color,
wallPaper.settings.third_background_color,
wallPaper.settings.fourth_background_color
].filter(Boolean).map(getHexColorFromTelegramColor).join(',') : '';
}
export default class AppBackgroundTab extends SliderSuperTab {
public static tempId = 0;
private grid: HTMLElement;
private clicked: Set<DocId> = new Set();
private blurCheckboxField: CheckboxField;
private wallPapersByElement: Map<HTMLElement, WallPaper> = new Map();
private elementsByKey: Map<string, HTMLElement> = new Map();
public static getInitArgs() {
return {
backgrounds: rootScope.managers.appDocsManager.getWallPapers()
};
}
private get theme() {
return themeController.getTheme();
}
public init(p: ReturnType<typeof AppBackgroundTab['getInitArgs']> = AppBackgroundTab.getInitArgs()) {
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'});
const colorButton = Button('btn-primary btn-transparent', {icon: 'colorize', text: 'SetColor'});
const resetButton = Button('btn-primary btn-transparent', {icon: 'favourites', text: 'Appearance.Reset'});
attachClickEvent(uploadButton, this.onUploadClick, {listenerSetter: this.listenerSetter});
attachClickEvent(colorButton, () => {
this.slider.createTab(AppBackgroundColorTab).open();
}, {listenerSetter: this.listenerSetter});
attachClickEvent(resetButton, this.onResetClick, {listenerSetter: this.listenerSetter});
const wallPaper = this.theme.settings?.wallpaper;
const blurCheckboxField = this.blurCheckboxField = new CheckboxField({
text: 'ChatBackground.Blur',
name: 'blur',
checked: (wallPaper as WallPaper.wallPaper)?.settings?.pFlags?.blur
});
this.listenerSetter.add(blurCheckboxField.input)('change', async() => {
this.theme.settings.wallpaper.settings.pFlags.blur = blurCheckboxField.input.checked || undefined;
await this.managers.appStateManager.pushToState('settings', rootScope.settings);
// * wait for animation end
setTimeout(() => {
const active = grid.querySelector('.active') as HTMLElement;
if(!active) return;
const wallpaper = this.wallPapersByElement.get(active);
if((wallpaper as WallPaper.wallPaper).pFlags.pattern || wallpaper._ === 'wallPaperNoFile') {
return;
}
AppBackgroundTab.setBackgroundDocument(wallpaper);
}, 100);
});
container.append(
uploadButton,
colorButton,
resetButton,
CreateRowFromCheckboxField(blurCheckboxField).container
);
}
rootScope.addEventListener('background_change', this.setActive);
const promise = p.backgrounds.then((wallPapers) => {
const promises = wallPapers.map((wallPaper) => {
return this.addWallPaper(wallPaper);
});
return Promise.all(promises);
});
const gridContainer = generateSection(this.scrollable);
const grid = this.grid = document.createElement('div');
grid.classList.add('grid');
attachClickEvent(grid, this.onGridClick, {listenerSetter: this.listenerSetter});
gridContainer.append(grid);
return promise;
}
private onUploadClick = () => {
requestFile('image/x-png,image/png,image/jpeg').then(async(file) => {
if(file.name.endsWith('.png')) {
const img = document.createElement('img');
const url = URL.createObjectURL(file);
await renderImageFromUrlPromise(img, url, false);
const mimeType = 'image/jpeg';
const {blob} = await scaleMediaElement({media: img, size: new MediaSize(img.naturalWidth, img.naturalHeight), mimeType});
file = new File([blob], file.name.replace(/\.png$/, '.jpg'), {type: mimeType});
}
const wallPaper = await this.managers.appDocsManager.prepareWallPaperUpload(file);
const uploadPromise = this.managers.appDocsManager.uploadWallPaper(wallPaper.id);
const uploadDeferred: CancellablePromise<any> = appDownloadManager.getNewDeferredForUpload(file.name, uploadPromise);
const deferred = deferredPromise<void>();
deferred.addNotifyListener = uploadDeferred.addNotifyListener;
deferred.cancel = uploadDeferred.cancel;
uploadDeferred.then((wallPaper) => {
this.clicked.delete(key);
this.elementsByKey.delete(key);
this.wallPapersByElement.set(container, wallPaper);
const newKey = this.getWallPaperKey(wallPaper);
this.elementsByKey.set(newKey, container);
AppBackgroundTab.setBackgroundDocument(wallPaper).then(deferred.resolve, deferred.reject);
}, deferred.reject);
const key = this.getWallPaperKey(wallPaper);
deferred.catch(() => {
container.remove();
});
const preloader = new ProgressivePreloader({
isUpload: true,
cancelable: true,
tryAgainOnFail: false
});
const {container} = await this.addWallPaper(wallPaper, false);
this.clicked.add(key);
preloader.attach(container, false, deferred);
});
};
private onResetClick = () => {
const defaultTheme = STATE_INIT.settings.themes.find((t) => t.name === this.theme.name);
if(defaultTheme) {
++AppBackgroundTab.tempId;
this.theme.settings = copy(defaultTheme.settings);
this.managers.appStateManager.pushToState('settings', rootScope.settings);
appImManager.applyCurrentTheme(undefined, undefined, true);
this.blurCheckboxField.setValueSilently(this.theme.settings?.wallpaper?.settings?.pFlags?.blur);
}
};
private getWallPaperKey(wallPaper: WallPaper) {
return '' + wallPaper.id;
}
private getWallPaperKeyFromTheme(theme: AppTheme) {
return '' + (this.getWallPaperKey(theme.settings?.wallpaper) || '');
}
public static addWallPaper(
wallPaper: WallPaper,
container = document.createElement('div')
) {
const colors = getColorsFromWallPaper(wallPaper);
const hasFile = wallPaper._ === 'wallPaper';
if((hasFile && wallPaper.pFlags.pattern && !colors)/* ||
(wallpaper.document as MyDocument).mime_type.indexOf('application/') === 0 */) {
return;
}
const isDark = !!wallPaper.pFlags.dark;
const doc = hasFile ? wallPaper.document as Document.document : undefined;
container.classList.add('background-item');
container.dataset.id = '' + wallPaper.id;
const media = document.createElement('div');
media.classList.add('background-item-media');
const loadPromises: Promise<any>[] = [];
let wrapped: ReturnType<typeof wrapPhoto>, size: ReturnType<typeof choosePhotoSize>;
if(hasFile) {
size = choosePhotoSize(doc, 200, 200);
wrapped = wrapPhoto({
photo: doc,
message: null,
container: media,
withoutPreloader: true,
size: size,
noFadeIn: wallPaper.pFlags.pattern
});
if(wallPaper.pFlags.pattern) {
media.classList.add('is-pattern');
}
const promise = wrapped.then(async({loadPromises, images}) => {
await loadPromises.thumb || loadPromises.full;
return images;
}).then((images) => {
if(wallPaper.pFlags.pattern) {
if(isDark) {
images.full.style.display = 'none';
if(images.thumb) {
images.thumb.style.display = 'none';
}
} else if(wallPaper.settings?.intensity) {
images.full.style.opacity = '' + Math.abs(wallPaper.settings.intensity) / 100;
}
}
return sequentialDom.mutate(() => {
container.append(media);
});
});
loadPromises.push(promise);
} else {
container.append(media);
}
if(wallPaper.settings && wallPaper.settings.background_color !== undefined) {
const {canvas} = ChatBackgroundGradientRenderer.create(colors);
canvas.classList.add('background-colors-canvas');
if(isDark && hasFile) {
const promise = wrapped.then(({loadPromises}) => {
return loadPromises.full.then(async() => {
const cacheContext = await rootScope.managers.thumbsStorage.getCacheContext(doc, size.type);
canvas.style.webkitMaskImage = `url(${cacheContext.url})`;
canvas.style.opacity = '' + (wallPaper.pFlags.dark ? 100 + wallPaper.settings.intensity : wallPaper.settings.intensity) / 100;
media.append(canvas);
});
});
loadPromises.push(promise);
} else {
media.append(canvas);
}
}
return {
container,
media,
loadPromise: Promise.all(loadPromises)
};
}
private addWallPaper(wallPaper: WallPaper, append = true) {
const result = AppBackgroundTab.addWallPaper(wallPaper);
if(result) {
const {container, media} = result;
container.classList.add('grid-item');
media.classList.add('grid-item-media');
const key = this.getWallPaperKey(wallPaper);
this.wallPapersByElement.set(container, wallPaper);
this.elementsByKey.set(key, container);
if(this.getWallPaperKeyFromTheme(this.theme) === key) {
container.classList.add('active');
}
this.grid[append ? 'append' : 'prepend'](container);
}
return result && result.loadPromise.then(() => result);
}
private onGridClick = (e: MouseEvent | TouchEvent) => {
const target = findUpClassName(e.target, 'grid-item') as HTMLElement;
if(!target) return;
const wallpaper = this.wallPapersByElement.get(target);
if(wallpaper._ === 'wallPaperNoFile') {
AppBackgroundTab.setBackgroundDocument(wallpaper);
return;
}
const key = this.getWallPaperKey(wallpaper);
if(this.clicked.has(key)) return;
this.clicked.add(key);
const doc = wallpaper.document as MyDocument;
const preloader = new ProgressivePreloader({
cancelable: true,
tryAgainOnFail: false
});
const load = async() => {
const promise = AppBackgroundTab.setBackgroundDocument(wallpaper);
const cacheContext = await this.managers.thumbsStorage.getCacheContext(doc);
if(!cacheContext.url || this.theme.settings?.wallpaper?.settings?.pFlags?.blur) {
preloader.attach(target, true, promise);
}
};
preloader.construct();
attachClickEvent(target, (e) => {
if(preloader.preloader.parentElement) {
preloader.onClick(e);
preloader.detach();
} else {
load();
}
}, {listenerSetter: this.listenerSetter});
load();
// console.log(doc);
};
public static setBackgroundDocument = (wallPaper: WallPaper) => {
const _tempId = ++this.tempId;
const middleware = () => _tempId === this.tempId;
const doc = (wallPaper as WallPaper.wallPaper).document as MyDocument;
const deferred = deferredPromise<void>();
let download: Promise<void> | ReturnType<AppDownloadManager['downloadMediaURL']>;
if(doc) {
download = appDownloadManager.downloadMediaURL({
media: doc,
queueId: appImManager.chat.bubbles ? appImManager.chat.bubbles.lazyLoadQueue.queueId : 0
});
deferred.addNotifyListener = download.addNotifyListener;
deferred.cancel = download.cancel;
} else {
download = Promise.resolve();
}
const saveToCache = (slug: string, url: string) => {
fetch(url).then((response) => {
appImManager.cacheStorage.save('backgrounds/' + slug, response);
});
};
download.then(async() => {
if(!middleware()) {
deferred.resolve();
return;
}
const themeSettings = themeController.getTheme().settings;
const onReady = (url?: string) => {
let getPixelPromise: Promise<Uint8ClampedArray>;
const backgroundColor = getColorsFromWallPaper(wallPaper);
if(url && !backgroundColor) {
getPixelPromise = averageColor(url);
} else {
const {canvas} = ChatBackgroundGradientRenderer.create(backgroundColor);
getPixelPromise = Promise.resolve(averageColorFromCanvas(canvas));
}
getPixelPromise.then((pixel) => {
if(!middleware()) {
deferred.resolve();
return;
}
const hsla = highlightningColor(Array.from(pixel) as any);
const slug = (wallPaper as WallPaper.wallPaper).slug ?? '';
themeSettings.wallpaper = wallPaper;
themeSettings.highlightningColor = hsla;
rootScope.managers.appStateManager.pushToState('settings', rootScope.settings);
if(slug) {
saveToCache(slug, url);
}
appImManager.applyCurrentTheme(slug, url, true).then(deferred.resolve);
});
};
if(!doc) {
onReady();
return;
}
const cacheContext = await rootScope.managers.thumbsStorage.getCacheContext(doc);
if(themeSettings.wallpaper?.settings?.pFlags?.blur) {
setTimeout(() => {
const {canvas, promise} = blur(cacheContext.url, 12, 4);
promise.then(() => {
if(!middleware()) {
deferred.resolve();
return;
}
onReady(canvas.toDataURL());
});
}, 200);
} else {
onReady(cacheContext.url);
}
});
return deferred;
};
private setActive = () => {
const active = this.grid.querySelector('.active');
const target = this.elementsByKey.get(this.getWallPaperKeyFromTheme(this.theme));
if(active === target) {
return;
}
if(active) {
active.classList.remove('active');
}
if(target) {
target.classList.add('active');
}
};
}