diff --git a/src/components/sidebarLeft/tabs/background.ts b/src/components/sidebarLeft/tabs/background.ts index 7a965fb6..71b146e9 100644 --- a/src/components/sidebarLeft/tabs/background.ts +++ b/src/components/sidebarLeft/tabs/background.ts @@ -10,13 +10,16 @@ import blur from "../../../helpers/blur"; import { deferredPromise } from "../../../helpers/cancellablePromise"; import { attachClickEvent } from "../../../helpers/dom"; import findUpClassName from "../../../helpers/dom/findUpClassName"; +import { requestFile } from "../../../helpers/files"; import highlightningColor from "../../../helpers/highlightningColor"; import { copy } from "../../../helpers/object"; -import { AccountWallPapers, WallPaper } from "../../../layer"; +import sequentialDom from "../../../helpers/sequentialDom"; +import { AccountWallPapers, PhotoSize, WallPaper } from "../../../layer"; import appDocsManager, { MyDocument } from "../../../lib/appManagers/appDocsManager"; import appDownloadManager from "../../../lib/appManagers/appDownloadManager"; import appImManager from "../../../lib/appManagers/appImManager"; -import appStateManager, { STATE_INIT } from "../../../lib/appManagers/appStateManager"; +import appPhotosManager from "../../../lib/appManagers/appPhotosManager"; +import appStateManager, { Theme, STATE_INIT } from "../../../lib/appManagers/appStateManager"; import apiManager from "../../../lib/mtproto/mtprotoworker"; import rootScope from "../../../lib/rootScope"; import Button from "../../button"; @@ -26,43 +29,45 @@ import { SliderSuperTab } from "../../slider"; import { wrapPhoto } from "../../wrappers"; import AppBackgroundColorTab from "./backgroundColor"; +let uploadTempId = 0; + export default class AppBackgroundTab extends SliderSuperTab { + private grid: HTMLElement; + private tempId = 0; + private theme: Theme; + private clicked: Set = new Set(); + private blurCheckboxField: CheckboxField; + init() { this.container.classList.add('background-container', 'background-image-container'); this.setTitle('ChatBackground'); + this.theme = rootScope.settings.themes.find(t => t.name === rootScope.settings.theme); + { const container = generateSection(this.scrollable); - //const uploadButton = Button('btn-primary btn-transparent', {icon: 'cameraadd', text: 'ChatBackground.UploadWallpaper', disabled: true}); + 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, () => { 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); - blurCheckboxField.setValueSilently(theme.background.blur); - } - }, {listenerSetter: this.listenerSetter}); + attachClickEvent(resetButton, this.onResetClick, {listenerSetter: this.listenerSetter}); - const theme = rootScope.settings.themes.find(t => t.name === rootScope.settings.theme); - const blurCheckboxField = new CheckboxField({ + const blurCheckboxField = this.blurCheckboxField = new CheckboxField({ text: 'ChatBackground.Blur', name: 'blur', - checked: theme.background.blur, + checked: this.theme.background.blur, withRipple: true }); this.listenerSetter.add(blurCheckboxField.input, 'change', () => { - theme.background.blur = blurCheckboxField.input.checked; + this.theme.background.blur = blurCheckboxField.input.checked; appStateManager.pushToState('settings', rootScope.settings); const active = grid.querySelector('.active') as HTMLElement; @@ -70,178 +75,290 @@ export default class AppBackgroundTab extends SliderSuperTab { // * wait for animation end setTimeout(() => { - setBackgroundDocument(active.dataset.slug, appDocsManager.getDoc(active.dataset.docId)); + this.setBackgroundDocument(active.dataset.slug, appDocsManager.getDoc(active.dataset.docId)); }, 100); }); - container.append(/* uploadButton, */colorButton, resetButton, blurCheckboxField.label); + container.append(uploadButton, colorButton, resetButton, blurCheckboxField.label); } - const grid = document.createElement('div'); - grid.classList.add('grid'); + rootScope.on('background_change', this.setActive); - const saveToCache = (slug: string, url: string) => { - fetch(url).then(response => { - appDownloadManager.cacheStorage.save('backgrounds/' + slug, response); + apiManager.invokeApiHashable('account.getWallPapers').then((accountWallpapers) => { + const wallpapers = (accountWallpapers as AccountWallPapers.accountWallPapers).wallpapers as WallPaper.wallPaper[]; + wallpapers.forEach((wallpaper) => { + this.addWallPaper(wallpaper); }); - }; - let tempId = 0; - const setBackgroundDocument = (slug: string, doc: MyDocument) => { - let _tempId = ++tempId; - const middleware = () => _tempId === tempId; + //console.log(accountWallpapers); + }); - const download = appDocsManager.downloadDoc(doc, appImManager.chat.bubbles ? appImManager.chat.bubbles.lazyLoadQueue.queueId : 0); + const grid = this.grid = document.createElement('div'); + grid.classList.add('grid'); + attachClickEvent(grid, this.onGridClick, {listenerSetter: this.listenerSetter}); + this.scrollable.append(grid); + } - const deferred = deferredPromise(); - deferred.addNotifyListener = download.addNotifyListener; - deferred.cancel = download.cancel; - - download.then(() => { - if(!middleware()) { - deferred.resolve(); - return; - } - - const background = rootScope.settings.themes.find(t => t.name === rootScope.settings.theme).background; - const onReady = (url: string) => { - //const perf = performance.now(); - averageColor(url).then(pixel => { - if(!middleware()) { - deferred.resolve(); - return; - } - - const hsla = highlightningColor(Array.from(pixel) as any); - //console.log(doc, hsla, performance.now() - perf); + private onUploadClick = () => { + requestFile('image/x-png,image/png,image/jpeg').then(file => { + const id = 'wallpaper-upload-' + ++uploadTempId; + + const thumb = { + _: 'photoSize', + h: 0, + w: 0, + location: {} as any, + size: file.size, + type: 'full', + url: URL.createObjectURL(file) + } as PhotoSize.photoSize; + let document: MyDocument = { + _: 'document', + access_hash: '', + attributes: [], + dc_id: 0, + file_reference: [], + id, + mime_type: file.type, + size: file.size, + downloaded: true, + date: Date.now() / 1000, + url: thumb.url, + pFlags: {}, + thumbs: [thumb], + file_name: file.name + }; + + document = appDocsManager.saveDoc(document); + + const docThumb = appPhotosManager.getDocumentCachedThumb(document.id); + docThumb.downloaded = thumb.size; + docThumb.url = thumb.url; + + let wallpaper: WallPaper.wallPaper = { + _: 'wallPaper', + access_hash: '', + document: document, + id, + slug: id, + pFlags: {} + }; + + const upload = appDownloadManager.upload(file, file.name); - background.slug = slug; - background.type = 'image'; - background.highlightningColor = hsla; - appStateManager.pushToState('settings', rootScope.settings); + const deferred = deferredPromise(); + deferred.addNotifyListener = upload.addNotifyListener; + deferred.cancel = upload.cancel; + + upload.then(inputFile => { + apiManager.invokeApi('account.uploadWallPaper', { + file: inputFile, + mime_type: file.type, + settings: { + _: 'wallPaperSettings' + } + }).then(_wallpaper => { + const newDoc = (_wallpaper as WallPaper.wallPaper).document as MyDocument; + newDoc.downloaded = document.downloaded; + newDoc.url = document.url; + + wallpaper = _wallpaper as WallPaper.wallPaper; + wallpaper.document = appDocsManager.saveDoc(wallpaper.document); + + container.dataset.docId = wallpaper.document.id; + container.dataset.slug = wallpaper.slug; + + this.setBackgroundDocument(wallpaper.slug, wallpaper.document).then(deferred.resolve, deferred.reject); + }, deferred.reject); + }, deferred.reject); + + deferred.then(() => { + this.clicked.delete(wallpaper.document.id); + }, (err) => { + container.remove(); + //console.error('i saw the body drop', err); + }); - saveToCache(slug, url); - appImManager.applyCurrentTheme(slug, url).then(deferred.resolve); - }); - }; - - if(background.blur) { - setTimeout(() => { - blur(doc.url, 12, 4) - .then(url => { - if(!middleware()) { - deferred.resolve(); - return; - } - - onReady(url); - }); - }, 200); - } else { - onReady(doc.url); - } + const preloader = new ProgressivePreloader({ + isUpload: true, + cancelable: true, + tryAgainOnFail: false }); - return deferred; - }; + const container = this.addWallPaper(wallpaper, false); + this.clicked.add(wallpaper.document.id); - const setActive = () => { - const active = grid.querySelector('.active'); - const background = rootScope.settings.themes.find(t => t.name === rootScope.settings.theme).background; - const target = background.type === 'image' ? grid.querySelector(`.grid-item[data-slug="${background.slug}"]`) : null; - if(active === target) { - return; - } + preloader.attach(container, false, deferred); + }); + }; + + private onResetClick = () => { + const defaultTheme = STATE_INIT.settings.themes.find(t => t.name === this.theme.name); + if(defaultTheme) { + ++this.tempId; + this.theme.background = copy(defaultTheme.background); + appStateManager.pushToState('settings', rootScope.settings); + appImManager.applyCurrentTheme(undefined, undefined, true); + this.blurCheckboxField.setValueSilently(this.theme.background.blur); + } + }; - if(active) { - active.classList.remove('active'); - } + private addWallPaper(wallpaper: WallPaper.wallPaper, append = true) { + if(wallpaper.pFlags.pattern || (wallpaper.document as MyDocument).mime_type.indexOf('application/') === 0) { + return; + } - if(target) { - target.classList.add('active'); - } - }; + wallpaper.document = appDocsManager.saveDoc(wallpaper.document); - rootScope.on('background_change', setActive); + const container = document.createElement('div'); + container.classList.add('grid-item'); - apiManager.invokeApiHashable('account.getWallPapers').then((accountWallpapers) => { - const background = rootScope.settings.themes.find(t => t.name === rootScope.settings.theme).background; - const wallpapers = (accountWallpapers as AccountWallPapers.accountWallPapers).wallpapers as WallPaper.wallPaper[]; - wallpapers.forEach((wallpaper) => { - if(wallpaper.pFlags.pattern || (wallpaper.document as MyDocument).mime_type.indexOf('application/') === 0) { - return; - } - - wallpaper.document = appDocsManager.saveDoc(wallpaper.document); - - const container = document.createElement('div'); - container.classList.add('grid-item'); - - const wrapped = wrapPhoto({ - photo: wallpaper.document, - message: null, - container: container, - boxWidth: 0, - boxHeight: 0, - withoutPreloader: true - }); + const media = document.createElement('div'); + media.classList.add('grid-item-media'); - [wrapped.images.thumb, wrapped.images.full].filter(Boolean).forEach(image => { - image.classList.add('grid-item-media'); - }); + const wrapped = wrapPhoto({ + photo: wallpaper.document, + message: null, + container: media, + boxWidth: 0, + boxHeight: 0, + withoutPreloader: true + }); - container.dataset.docId = wallpaper.document.id; - container.dataset.slug = wallpaper.slug; + container.dataset.docId = wallpaper.document.id; + container.dataset.slug = wallpaper.slug; - if(background.type === 'image' && background.slug === wallpaper.slug) { - container.classList.add('active'); - } + if(this.theme.background.type === 'image' && this.theme.background.slug === wallpaper.slug) { + container.classList.add('active'); + } - grid.append(container); + (wrapped.loadPromises.thumb || wrapped.loadPromises.full).then(() => { + sequentialDom.mutate(() => { + container.append(media); }); + }); - let clicked: Set = new Set(); - attachClickEvent(grid, (e) => { - const target = findUpClassName(e.target, 'grid-item') as HTMLElement; - if(!target) return; + this.grid[append ? 'append' : 'prepend'](container); - const {docId, slug} = target.dataset; - if(clicked.has(docId)) return; - clicked.add(docId); + return container; + } - const preloader = new ProgressivePreloader({ - cancelable: true, - tryAgainOnFail: false - }); + private onGridClick = (e: MouseEvent | TouchEvent) => { + const target = findUpClassName(e.target, 'grid-item') as HTMLElement; + if(!target) return; - const doc = appDocsManager.getDoc(docId); + const {docId, slug} = target.dataset; + if(this.clicked.has(docId)) return; + this.clicked.add(docId); - const load = () => { - const promise = setBackgroundDocument(slug, doc); - if(!doc.url || background.blur) { - preloader.attach(target, true, promise); - } - }; + const preloader = new ProgressivePreloader({ + cancelable: true, + tryAgainOnFail: false + }); - preloader.construct(); + const doc = appDocsManager.getDoc(docId); - attachClickEvent(target, (e) => { - if(preloader.preloader.parentElement) { - preloader.onClick(e); - preloader.detach(); - } else { - load(); - } - }, {listenerSetter: this.listenerSetter}); + const load = () => { + const promise = this.setBackgroundDocument(slug, doc); + if(!doc.url || this.theme.background.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}); - //console.log(doc); - }, {listenerSetter: this.listenerSetter}); + load(); - //console.log(accountWallpapers); + //console.log(doc); + }; + + private saveToCache = (slug: string, url: string) => { + fetch(url).then(response => { + appDownloadManager.cacheStorage.save('backgrounds/' + slug, response); }); + }; - this.scrollable.append(grid); - } + private setBackgroundDocument = (slug: string, doc: MyDocument) => { + let _tempId = ++this.tempId; + const middleware = () => _tempId === this.tempId; + + const download = appDocsManager.downloadDoc(doc, appImManager.chat.bubbles ? appImManager.chat.bubbles.lazyLoadQueue.queueId : 0); + + const deferred = deferredPromise(); + deferred.addNotifyListener = download.addNotifyListener; + deferred.cancel = download.cancel; + + download.then(() => { + if(!middleware()) { + deferred.resolve(); + return; + } + + const background = this.theme.background; + const onReady = (url: string) => { + //const perf = performance.now(); + averageColor(url).then(pixel => { + if(!middleware()) { + deferred.resolve(); + return; + } + + const hsla = highlightningColor(Array.from(pixel) as any); + //console.log(doc, hsla, performance.now() - perf); + + background.slug = slug; + background.type = 'image'; + background.highlightningColor = hsla; + appStateManager.pushToState('settings', rootScope.settings); + + this.saveToCache(slug, url); + appImManager.applyCurrentTheme(slug, url).then(deferred.resolve); + }); + }; + + if(background.blur) { + setTimeout(() => { + blur(doc.url, 12, 4) + .then(url => { + if(!middleware()) { + deferred.resolve(); + return; + } + + onReady(url); + }); + }, 200); + } else { + onReady(doc.url); + } + }); + + return deferred; + }; + + 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 === 'image' ? this.grid.querySelector(`.grid-item[data-slug="${background.slug}"]`) : null; + if(active === target) { + return; + } + + if(active) { + active.classList.remove('active'); + } + + if(target) { + target.classList.add('active'); + } + }; } diff --git a/src/helpers/files.ts b/src/helpers/files.ts index 5ab4da40..618b34d9 100644 --- a/src/helpers/files.ts +++ b/src/helpers/files.ts @@ -120,3 +120,33 @@ export async function getFilesFromEvent(e: ClipboardEvent | DragEvent, onlyTypes return files; } + +export function requestFile(accept?: string) { + const input = document.createElement('input'); + input.type = 'file'; + input.style.display = 'none'; + + if(accept) { + input.accept = accept; + } + + document.body.append(input); + + const promise = new Promise((resolve, reject) => { + input.addEventListener('change', (e: any) => { + const file: File = e.target.files[0]; + if(!file) { + reject('NO_FILE_SELECTED'); + return; + } + + resolve(file); + }, {once: true}); + }).finally(() => { + input.remove(); + }); + + input.click(); + + return promise; +} diff --git a/src/lib/appManagers/appStateManager.ts b/src/lib/appManagers/appStateManager.ts index 34c118d7..46ed0229 100644 --- a/src/lib/appManagers/appStateManager.ts +++ b/src/lib/appManagers/appStateManager.ts @@ -24,7 +24,7 @@ import DEBUG, { MOUNT_CLASS_TO } from '../../config/debug'; const REFRESH_EVERY = 24 * 60 * 60 * 1000; // 1 day const STATE_VERSION = App.version; -type Background = { +export type Background = { type: 'color' | 'image' | 'default', blur: boolean, highlightningColor?: string, @@ -32,7 +32,7 @@ type Background = { slug?: string, }; -type Theme = { +export type Theme = { name: 'day' | 'night', background: Background }; diff --git a/src/scss/partials/_leftSidebar.scss b/src/scss/partials/_leftSidebar.scss index 75c34ba3..5361cf78 100644 --- a/src/scss/partials/_leftSidebar.scss +++ b/src/scss/partials/_leftSidebar.scss @@ -1139,6 +1139,16 @@ transform: scale(1); } } + + .media-photo { + width: 100%; + height: 100%; + object-fit: cover; + } + + .preloader-container { + z-index: 1; + } } }