From 10c6d376569ef6f6cf29733bea31dae7f5f3e012 Mon Sep 17 00:00:00 2001 From: Igor Zhukov Date: Wed, 19 Nov 2014 17:09:24 +0300 Subject: [PATCH] Improved Safari support Added no-blob IndexedDB support. Disabled for Safari, because IDB is very slow here. Added FileManager.getFileCorrectUrl method for Safari --- app/js/directives.js | 12 +-- app/js/lib/bin_utils.js | 46 +++++++++ app/js/lib/mtproto_wrapper.js | 6 +- app/js/lib/ng_utils.js | 178 ++++++++++++++++++++++++---------- app/js/services.js | 16 +-- 5 files changed, 186 insertions(+), 72 deletions(-) diff --git a/app/js/directives.js b/app/js/directives.js index 2d090b64..84d5f553 100644 --- a/app/js/directives.js +++ b/app/js/directives.js @@ -1233,17 +1233,7 @@ angular.module('myApp.directives', ['myApp.filters']) if (src.substr(0, 5) == 'data:') { remove = true; - src = src.substr(5).split(';'); - var contentType = src[0]; - var base64 = atob(src[1].split(',')[1]); - var array = new Uint8Array(base64.length); - - for (var i = 0; i < base64.length; i++) { - array[i] = base64.charCodeAt(i); - } - - var blob = new Blob([array], {type: contentType}); - + var blob = dataUrlToBlob(src); ErrorService.confirm({type: 'FILE_CLIPBOARD_PASTE'}).then(function () { $scope.draftMessage.files = [blob]; $scope.draftMessage.isMedia = true; diff --git a/app/js/lib/bin_utils.js b/app/js/lib/bin_utils.js index 601fb21c..82ad9d45 100644 --- a/app/js/lib/bin_utils.js +++ b/app/js/lib/bin_utils.js @@ -92,6 +92,52 @@ function uint6ToBase64 (nUint6) { : 65; } +function base64ToBlob(base64str, mimeType) { + var sliceSize = 1024; + var byteCharacters = atob(base64str); + var bytesLength = byteCharacters.length; + var slicesCount = Math.ceil(bytesLength / sliceSize); + var byteArrays = new Array(slicesCount); + + for (var sliceIndex = 0; sliceIndex < slicesCount; ++sliceIndex) { + var begin = sliceIndex * sliceSize; + var end = Math.min(begin + sliceSize, bytesLength); + + var bytes = new Array(end - begin); + for (var offset = begin, i = 0 ; offset < end; ++i, ++offset) { + bytes[i] = byteCharacters[offset].charCodeAt(0); + } + byteArrays[sliceIndex] = new Uint8Array(bytes); + } + + return blobConstruct(byteArrays, mimeType); +} + +function dataUrlToBlob(url) { + // var name = 'b64blob ' + url.length; + // console.time(name); + var urlParts = url.split(','); + var base64str = urlParts[1]; + var mimeType = urlParts[0].split(':')[1].split(';')[0]; + var blob = base64ToBlob(base64str, mimeType); + // console.timeEnd(name); + return blob; +} + +function blobConstruct (blobParts, mimeType) { + var blob; + try { + blob = new Blob(blobParts, {type: mimeType}); + } catch (e) { + var bb = new BlobBuilder; + angular.forEach(blobParts, function(blobPart) { + bb.append(blobPart); + }); + blob = bb.getBlob(mimeType); + } + return blob; +} + function bytesCmp (bytes1, bytes2) { var len = bytes1.length; if (len != bytes2.length) { diff --git a/app/js/lib/mtproto_wrapper.js b/app/js/lib/mtproto_wrapper.js index 4dda4a3b..97f0a168 100644 --- a/app/js/lib/mtproto_wrapper.js +++ b/app/js/lib/mtproto_wrapper.js @@ -7,7 +7,7 @@ angular.module('izhukov.mtproto.wrapper', ['izhukov.utils', 'izhukov.mtproto']) -.factory('MtpApiManager', function (Storage, MtpAuthorizer, MtpNetworkerFactory, MtpSingleInstanceService, ErrorService, $q) { +.factory('MtpApiManager', function (Storage, MtpAuthorizer, MtpNetworkerFactory, MtpSingleInstanceService, ErrorService, qSync, $q) { var cachedNetworkers = {}, cachedUploadNetworkers = {}, cachedExportPromise = {}, @@ -65,9 +65,7 @@ angular.module('izhukov.mtproto.wrapper', ['izhukov.utils', 'izhukov.mtproto']) } if (cache[dcID] !== undefined) { - return {then: function (cb) { - cb(cache[dcID]); - }}; + return qSync.when(cache[dcID]); } var akk = 'dc' + dcID + '_auth_key', diff --git a/app/js/lib/ng_utils.js b/app/js/lib/ng_utils.js index 41384701..db20de35 100644 --- a/app/js/lib/ng_utils.js +++ b/app/js/lib/ng_utils.js @@ -33,10 +33,23 @@ angular.module('izhukov.utils', []) }) -.service('FileManager', function ($window, $q, $timeout) { +.service('qSync', function () { + + return { + when: function (result) { + return {then: function (cb) { + return cb(result); + }}; + } + } + +}) + +.service('FileManager', function ($window, $q, $timeout, qSync) { $window.URL = $window.URL || $window.webkitURL; $window.BlobBuilder = $window.BlobBuilder || $window.WebKitBlobBuilder || $window.MozBlobBuilder; + var buggyUnknownBlob = navigator.userAgent.indexOf('Safari') != -1; var blobSupported = true; @@ -61,20 +74,6 @@ angular.module('izhukov.utils', []) }); } - function blobConstruct (blobParts, mimeType) { - var blob; - try { - blob = new Blob(blobParts, {type: mimeType}); - } catch (e) { - var bb = new BlobBuilder; - angular.forEach(blobParts, function(blobPart) { - bb.append(blobPart); - }); - blob = bb.getBlob(mimeType); - } - return blob; - } - function fileWriteData(fileWriter, bytes) { var deferred = $q.defer(); @@ -182,42 +181,68 @@ angular.module('izhukov.utils', []) return 'data:' + mimeType + ';base64,' + bytesToBase64(fileData); } + function getDataUrl(blob) { + var deferred; + try { + var reader = new FileReader(); + reader.onloadend = function() { + deferred.resolve(reader.result); + } + reader.readAsDataURL(blob); + } catch (e) { + return $q.reject(e); + } + + deferred = $q.defer(); + + return deferred.promise; + } + + function getFileCorrectUrl(blob, mimeType) { + if (buggyUnknownBlob) { + var mimeType = blob.type || blob.mimeType || mimeType || ''; + if (!mimeType.match(/image\/(jpeg|gif|png|bmp)|video\/quicktime/)) { + return getDataUrl(blob); + } + } + return qSync.when(getUrl(blob, mimeType)); + } + function downloadFile (blob, mimeType, fileName) { if (window.navigator && navigator.msSaveBlob !== undefined) { window.navigator.msSaveBlob(blob, fileName); return false; } + getFileCorrectUrl(blob, mimeType).then(function (url) { + var anchor = document.createElementNS('http://www.w3.org/1999/xhtml', 'a'); + anchor.href = url; + anchor.target = '_blank'; + anchor.download = fileName; + if (anchor.dataset) { + anchor.dataset.downloadurl = ["video/quicktime", fileName, url].join(':'); + } + $(anchor).css({position: 'absolute', top: 1, left: 1}).appendTo('body'); - var url = getUrl(blob, mimeType); - - var anchor = document.createElementNS('http://www.w3.org/1999/xhtml', 'a'); - anchor.href = url; - anchor.target = '_blank'; - anchor.download = fileName; - if (anchor.dataset) { - anchor.dataset.downloadurl = ["video/quicktime", fileName, url].join(':'); - } - $(anchor).css({position: 'absolute', top: 1, left: 1}).appendTo('body'); - - try { - var clickEvent = document.createEvent('MouseEvents'); - clickEvent.initMouseEvent( - 'click', true, false, window, 0, 0, 0, 0, 0 - , false, false, false, false, 0, null - ); - anchor.dispatchEvent(clickEvent); - } catch (e) { - console.error('Download click error', e); try { - console.error('Download click error', e); - anchor[0].click(); + var clickEvent = document.createEvent('MouseEvents'); + clickEvent.initMouseEvent( + 'click', true, false, window, 0, 0, 0, 0, 0 + , false, false, false, false, 0, null + ); + anchor.dispatchEvent(clickEvent); } catch (e) { - window.open(url, '_blank'); + console.error('Download click error', e); + try { + console.error('Download click error', e); + anchor[0].click(); + } catch (e) { + window.open(url, '_blank'); + } } - } - $timeout(function () { - $(anchor).remove(); - }, 100); + $timeout(function () { + $(anchor).remove(); + }, 100); + }); } return { @@ -228,6 +253,8 @@ angular.module('izhukov.utils', []) getFakeFileWriter: getFakeFileWriter, chooseSave: chooseSaveFile, getUrl: getUrl, + getDataUrl: getDataUrl, + getFileCorrectUrl: getFileCorrectUrl, download: downloadFile }; }) @@ -237,11 +264,15 @@ angular.module('izhukov.utils', []) $window.indexedDB = $window.indexedDB || $window.webkitIndexedDB || $window.mozIndexedDB || $window.OIndexedDB || $window.msIndexedDB; $window.IDBTransaction = $window.IDBTransaction || $window.webkitIDBTransaction || $window.OIDBTransaction || $window.msIDBTransaction; - var dbName = 'cachedFiles', - dbStoreName = 'files', - dbVersion = 1, - openDbPromise, - storageIsAvailable = $window.indexedDB !== undefined && $window.IDBTransaction !== undefined; + var dbName = 'cachedFiles'; + var dbStoreName = 'files'; + var dbVersion = 1; + var openDbPromise; + var storageIsAvailable = $window.indexedDB !== undefined && + $window.IDBTransaction !== undefined && + navigator.userAgent.indexOf('Safari') == -1; + // As of Safari 8.0 IndexedDB is REALLY slow, no point in it + var storeBlobsAvailable = storageIsAvailable || false; function isAvailable () { return storageIsAvailable; @@ -307,15 +338,24 @@ angular.module('izhukov.utils', []) function saveFile (fileName, blob) { return openDatabase().then(function (db) { + if (!storeBlobsAvailable) { + return saveFileBase64(db, fileName, blob); + } + try { - var deferred = $q.defer(), - objectStore = db.transaction([dbStoreName], IDBTransaction.READ_WRITE || 'readwrite').objectStore(dbStoreName), + var objectStore = db.transaction([dbStoreName], IDBTransaction.READ_WRITE || 'readwrite').objectStore(dbStoreName), request = objectStore.put(blob, fileName); } catch (error) { + if (storeBlobsAvailable) { + storeBlobsAvailable = false; + return saveFileBase64(db, fileName, blob); + } storageIsAvailable = false; return $q.reject(error); } + var deferred = $q.defer(); + request.onsuccess = function (event) { deferred.resolve(blob); }; @@ -328,6 +368,38 @@ angular.module('izhukov.utils', []) }); }; + function saveFileBase64(db, fileName, blob) { + try { + var reader = new FileReader(); + reader.readAsDataURL(blob); + } catch (e) { + storageIsAvailable = false; + return $q.reject(); + } + + var deferred = $q.defer(); + + reader.onloadend = function() { + try { + var objectStore = db.transaction([dbStoreName], IDBTransaction.READ_WRITE || 'readwrite').objectStore(dbStoreName), + request = objectStore.put(reader.result, fileName); + } catch (error) { + storageIsAvailable = false; + deferred.reject(error); + return; + }; + request.onsuccess = function (event) { + deferred.resolve(blob); + }; + + request.onerror = function (error) { + deferred.reject(error); + }; + } + + return deferred.promise; + } + function getFile (fileName) { return openDatabase().then(function (db) { var deferred = $q.defer(), @@ -335,10 +407,14 @@ angular.module('izhukov.utils', []) request = objectStore.get(fileName); request.onsuccess = function (event) { - if (event.target.result === undefined) { + var result = event.target.result; + if (result === undefined) { deferred.reject(); + } else if (typeof result === 'string' && + result.substr(0, 5) === 'data:') { + deferred.resolve(dataUrlToBlob(result)); } else { - deferred.resolve(event.target.result); + deferred.resolve(result); } }; diff --git a/app/js/services.js b/app/js/services.js index 913c07ce..0a9e3db4 100644 --- a/app/js/services.js +++ b/app/js/services.js @@ -2500,9 +2500,11 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils']) }); downloadPromise.then(function (blob) { - var url = FileManager.getUrl(blob, mimeType); + FileManager.getFileCorrectUrl(blob, mimeType).then(function (url) { + historyVideo.url = $sce.trustAsResourceUrl(url); + }); + delete historyVideo.progress; - historyVideo.url = $sce.trustAsResourceUrl(url); historyVideo.downloaded = true; console.log('video save done'); }, function (e) { @@ -2655,9 +2657,10 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils']) }); downloadPromise.then(function (blob) { - var url = FileManager.getUrl(blob, doc.mime_type); + FileManager.getFileCorrectUrl(blob, doc.mime_type).then(function (url) { + historyDoc.url = $sce.trustAsResourceUrl(url); + }) delete historyDoc.progress; - historyDoc.url = $sce.trustAsResourceUrl(url); historyDoc.downloaded = true; console.log('file save done'); }, function (e) { @@ -2771,9 +2774,10 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils']) }); downloadPromise.then(function (blob) { - var url = FileManager.getUrl(blob, mimeType); + FileManager.getFileCorrectUrl(blob, mimeType).then(function (url) { + historyAudio.url = $sce.trustAsResourceUrl(url); + }); delete historyAudio.progress; - historyAudio.url = $sce.trustAsResourceUrl(url); historyAudio.downloaded = true; console.log('audio save done'); }, function (e) {