/** * This file is part of Threema Web. * * Threema Web is free software: you can redistribute it and/or modify it * under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or (at * your option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero * General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Threema Web. If not, see . */ // tslint:disable:max-line-length import {bufferToUrl, logAdapter} from '../helpers'; /** * Support uploading and resizing avatar */ export default [ '$log', function($log: ng.ILogService) { return { restrict: 'EA', scope: { onChange: '=', }, link(scope: any, element, attrs, controller) { const logTag: string = '[AvatarEditorDirective]'; // Constants const DRAGOVER_CSS_CLASS = 'is-dragover'; const VIEWPORT_SIZE = 220; // Elements const editorArea: any = angular.element(element[0].querySelector('.avatar-editor')); const fileTrigger: any = angular.element(element[0].querySelector('.file-trigger')); const fileInput: any = angular.element(element[0].querySelector('input.file-input')); // const avatarRemove: any = angular.element(element[0].querySelector('.avatar-remove')); // const navigation: any = angular.element(element[0].querySelector('.avatar-area-navigation')); const enabled = scope.enabled === undefined || scope.enabled === true; let croppieInstance = null; const initCroppie = () => { if (croppieInstance !== null) { return croppieInstance; } croppieInstance = new Croppie(element[0].querySelector('.croppie-target'), { viewport: { type: 'square', width: VIEWPORT_SIZE, height: VIEWPORT_SIZE, }, customClass: 'has-image', showZoomer: true, update() { if (updateTimeout !== undefined) { clearTimeout(updateTimeout); } updateTimeout = self.setTimeout(() => { croppieInstance.result({ type: 'blob', // max allowed size on device size: [512, 512], circle: false, format: 'png', }) .then((blob: Blob) => { const fileReader = new FileReader(); fileReader.onload = function() { scope.onChange(this.result); }; fileReader.readAsArrayBuffer(blob); }); }, 500); }, }); return croppieInstance; }; function loading(show: boolean): void { if (show) { editorArea.addClass('loading'); } else { editorArea.removeClass('loading'); } } // set after default avatar is set let updateTimeout; // Function to fetch file contents // Resolve to ArrayBuffer or reject to ErrorEvent. function fetchFileContent(file: File): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = function(ev: ProgressEvent) { // The result will be an ArrayBuffer because we call // the `FileReader.readAsArrayBuffer` method. resolve(this.result as ArrayBuffer); }; reader.onerror = function(ev: ProgressEvent) { // set a null object reject(ev); }; reader.onprogress = function(ev: ProgressEvent) { if (ev.lengthComputable) { // TODO implement progress? // let progress = ((data.loaded / data.total) * 100); } }; reader.readAsArrayBuffer(file); }); } function uploadFiles(fileList: FileList): void { if (fileList.length < 1) { return; } // get first fetchFileContent(fileList[0]).then((data: ArrayBuffer) => { const image = bufferToUrl(data, 'image/jpeg', logAdapter($log.warn, logTag)); setImage(image); }).catch((ev: ErrorEvent) => { $log.error(logTag, 'Could not load file:', ev.message); }); } // Handle the drop effect function onDrop(ev: DragEvent): void { ev.stopPropagation(); ev.preventDefault(); // simulate a leave to reset styles onDragleave(ev); // Upload files uploadFiles(ev.dataTransfer.files); } // File is dragged over compose area function onDragover(ev: DragEvent): void { ev.stopPropagation(); ev.preventDefault(); editorArea.addClass(DRAGOVER_CSS_CLASS); } // File is dragged out of compose area function onDragleave(ev: DragEvent): void { ev.stopPropagation(); ev.preventDefault(); editorArea.removeClass(DRAGOVER_CSS_CLASS); } // File trigger is clicked function onFileTrigger(ev: MouseEvent): void { ev.preventDefault(); ev.stopPropagation(); const input = element[0].querySelector('.file-input') as HTMLInputElement; input.click(); } // File(s) are uploaded via input field function onFileUploaded() { uploadFiles(this.files); } function setImage(newImage: any) { const croppie = initCroppie(); if (newImage === null) { // set a none image // TODO croppie.bind({ url: null, }); scope.onChange(null); return; } loading(true); // load image to calculate size const img = new Image(); img.addEventListener('load', function() { $log.debug(logTag, 'Image loaded'); const w = this.naturalWidth; const h = this.naturalHeight; const size = Math.min(w, h); // set to center const imageSize = [ (w - size) / 2, (h - size) / 2, size, size]; croppie.bind({ url: newImage, points: imageSize, }).then(() => { loading(false); }).catch((e) => { $log.error(logTag, 'Could not bind avatar preview:', e); loading(false); }); if (newImage === null) { scope.onChange(null); } }); img.addEventListener('error', function(e) { // this is not a valid image $log.error(logTag, 'Could not load image:', e); loading(false); }); img.src = newImage; } // Handle dragover / dragleave events editorArea.on('dragover dragenter', onDragover); editorArea.on('dragleave dragend', onDragleave); // Handle drop event editorArea.on('drop', onDrop); // Handle click on file trigger fileTrigger.on('click', onFileTrigger); // Handle file uploads fileInput.on('change', onFileUploaded); // Handle remove if (scope.enableClear !== undefined && scope.enableClear === true) { // avatarRemove.on('click', () => { // setImage(null); // }); } else { // remove element if clear disabled // avatarRemove.remove(); } }, template: `
messenger.UPLOAD_AVATAR
`, }; }, ];