123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268 |
- /**
- * 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 <http://www.gnu.org/licenses/>.
- */
- // tslint:disable:max-line-length
- /**
- * Support uploading and resizing avatar
- */
- export default [
- '$window',
- '$timeout',
- '$translate',
- '$filter',
- '$log',
- '$mdDialog',
- function($window, $timeout: ng.ITimeoutService, $translate, $filter: any, $log: ng.ILogService, $mdDialog) {
- 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-container'), {
- viewport: {
- type: 'square',
- width: VIEWPORT_SIZE,
- height: VIEWPORT_SIZE,
- },
- customClass: 'has-image',
- showZoomer: true,
- update() {
- if (updateTimeout !== undefined) {
- clearTimeout(updateTimeout);
- }
- updateTimeout = setTimeout(() => {
- croppieInstance.result({
- type: 'blob',
- // max allowed size on device
- size: [512, 512],
- circle: 'false',
- })
- .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<ArrayBuffer> {
- return new Promise((resolve, reject) => {
- const reader = new FileReader();
- reader.onload = (ev: Event) => {
- resolve((ev.target as FileReader).result);
- };
- reader.onerror = (ev: ErrorEvent) => {
- // set a null object
- reject(ev);
- };
- reader.onprogress = function(data) {
- if (data.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 = $filter('bufferToUrl')(data, 'image/jpg', false);
- 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: `
- <div class="avatar-editor">
- <div class="avatar-editor-drag croppie-container"></div>
- <div class="avatar-editor-navigation" layout="column" layout-wrap layout-margin layout-align="center center">
- <input class="file-input" type="file" style="visibility: hidden" multiple/>
- <md-button type="submit" class="file-trigger md-raised">
- <span translate>messenger.UPLOAD_AVATAR</span>
- </md-button>
- </div>
- </div>
- `,
- };
- },
- ];
|