diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 893edcb9c..dae354861 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -50,6 +50,7 @@ import { CoreUpdateManagerProvider } from '@providers/update-manager'; import { CorePluginFileDelegate } from '@providers/plugin-file-delegate'; import { CoreSyncProvider } from '@providers/sync'; import { CoreAddonManagerProvider } from '@providers/addonmanager'; +import { CoreFileHelperProvider } from '@providers/file-helper'; // Core modules. import { CoreComponentsModule } from '@components/components.module'; @@ -108,7 +109,8 @@ export const CORE_PROVIDERS: any[] = [ CoreUpdateManagerProvider, CorePluginFileDelegate, CoreSyncProvider, - CoreAddonManagerProvider + CoreAddonManagerProvider, + CoreFileHelperProvider ]; @NgModule({ diff --git a/src/components/file/file.ts b/src/components/file/file.ts index 20023d454..7ed4d61c6 100644 --- a/src/components/file/file.ts +++ b/src/components/file/file.ts @@ -13,11 +13,10 @@ // limitations under the License. import { Component, Input, Output, OnInit, OnDestroy, EventEmitter } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreEventsProvider } from '@providers/events'; -import { CoreFileProvider } from '@providers/file'; import { CoreFilepoolProvider } from '@providers/filepool'; +import { CoreFileHelperProvider } from '@providers/file-helper'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype'; @@ -36,7 +35,6 @@ export class CoreFileComponent implements OnInit, OnDestroy { @Input() file: any; // The file. Must have a property 'filename' and a 'fileurl' or 'url' @Input() component?: string; // Component the file belongs to. @Input() componentId?: string | number; // Component ID. - @Input() timemodified?: number; // If set, the value will be used to check if the file is outdated. @Input() canDelete?: boolean | string; // Whether file can be deleted. @Input() alwaysDownload?: boolean | string; // Whether it should always display the refresh button when the file is downloaded. // Use it for files that you cannot determine if they're outdated or not. @@ -52,12 +50,14 @@ export class CoreFileComponent implements OnInit, OnDestroy { protected fileUrl: string; protected siteId: string; protected fileSize: number; + protected state: string; + protected timemodified: number; protected observer; - constructor(private translate: TranslateService, private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider, - private domUtils: CoreDomUtilsProvider, private filepoolProvider: CoreFilepoolProvider, - private fileProvider: CoreFileProvider, private appProvider: CoreAppProvider, - private mimeUtils: CoreMimetypeUtilsProvider, private eventsProvider: CoreEventsProvider) { + constructor(private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider, private domUtils: CoreDomUtilsProvider, + private filepoolProvider: CoreFilepoolProvider, private appProvider: CoreAppProvider, + private fileHelper: CoreFileHelperProvider, private mimeUtils: CoreMimetypeUtilsProvider, + private eventsProvider: CoreEventsProvider) { this.onDelete = new EventEmitter(); } @@ -68,9 +68,9 @@ export class CoreFileComponent implements OnInit, OnDestroy { this.canDelete = this.utils.isTrueOrOne(this.canDelete); this.alwaysDownload = this.utils.isTrueOrOne(this.alwaysDownload); this.canDownload = this.utils.isTrueOrOne(this.canDownload); - this.timemodified = this.timemodified || 0; - this.fileUrl = this.file.fileurl || this.file.url; + this.fileUrl = this.fileHelper.getFileUrl(this.file); + this.timemodified = this.fileHelper.getFileTimemodified(this.file); this.siteId = this.sitesProvider.getCurrentSiteId(); this.fileSize = this.file.filesize; this.fileName = this.file.filename; @@ -102,6 +102,7 @@ export class CoreFileComponent implements OnInit, OnDestroy { return this.filepoolProvider.getFileStateByUrl(this.siteId, this.fileUrl, this.timemodified).then((state) => { const canDownload = this.sitesProvider.getCurrentSite().canDownloadFiles(); + this.state = state; this.isDownloaded = state === CoreConstants.DOWNLOADED || state === CoreConstants.OUTDATED; this.isDownloading = canDownload && state === CoreConstants.DOWNLOADING; this.showDownload = canDownload && (state === CoreConstants.NOT_DOWNLOADED || state === CoreConstants.OUTDATED || @@ -109,123 +110,19 @@ export class CoreFileComponent implements OnInit, OnDestroy { }); } - /** - * Download the file. - * - * @return {Promise} Promise resolved when file is downloaded. - */ - protected downloadFile(): Promise { - if (!this.sitesProvider.getCurrentSite().canDownloadFiles()) { - this.domUtils.showErrorModal('core.cannotdownloadfiles', true); - - return Promise.reject(null); - } - - this.isDownloading = true; - - return this.filepoolProvider.downloadUrl(this.siteId, this.fileUrl, false, this.component, this.componentId, - this.timemodified, undefined, undefined, this.file).catch(() => { - - // Call calculateState to make sure we have the right state. - return this.calculateState().then(() => { - if (this.isDownloaded) { - return this.filepoolProvider.getInternalUrlByUrl(this.siteId, this.fileUrl); - } else { - return Promise.reject(null); - } - }); - }); - } - /** * Convenience function to open a file, downloading it if needed. * * @return {Promise} Promise resolved when file is opened. */ protected openFile(): Promise { - const fixedUrl = this.sitesProvider.getCurrentSite().fixPluginfileURL(this.fileUrl); - let promise; - - if (this.fileProvider.isAvailable()) { - promise = Promise.resolve().then(() => { - // The file system is available. - const isWifi = !this.appProvider.isNetworkAccessLimited(), - isOnline = this.appProvider.isOnline(); - - if (this.isDownloaded && !this.showDownload) { - // File is downloaded, get the local file URL. - return this.filepoolProvider.getUrlByUrl(this.siteId, this.fileUrl, - this.component, this.componentId, this.timemodified, false, false, this.file); - } else { - if (!isOnline && !this.isDownloaded) { - // Not downloaded and user is offline, reject. - return Promise.reject(this.translate.instant('core.networkerrormsg')); - } - - const isDownloading = this.isDownloading; - this.isDownloading = true; // This check could take a while, show spinner. - - return this.filepoolProvider.shouldDownloadBeforeOpen(fixedUrl, this.fileSize).then(() => { - if (isDownloading) { - // It's already downloading, stop. - return; - } - - // Download and then return the local URL. - return this.downloadFile(); - }, () => { - // Start the download if in wifi, but return the URL right away so the file is opened. - if (isWifi && isOnline) { - this.downloadFile(); - } - - if (isDownloading || !this.isDownloaded || isOnline) { - // Not downloaded or outdated and online, return the online URL. - return fixedUrl; - } else { - // Outdated but offline, so we return the local URL. - return this.filepoolProvider.getUrlByUrl(this.siteId, this.fileUrl, - this.component, this.componentId, this.timemodified, false, false, this.file); - } - }); - } - }); - } else { - // Use the online URL. - promise = Promise.resolve(fixedUrl); - } - - return promise.then((url) => { - if (!url) { - return; - } - - if (url.indexOf('http') === 0) { - return this.utils.openOnlineFile(url).catch((error) => { - // Error opening the file, some apps don't allow opening online files. - if (!this.fileProvider.isAvailable()) { - return Promise.reject(error); - } else if (this.isDownloading) { - return Promise.reject(this.translate.instant('core.erroropenfiledownloading')); - } - - let subPromise; - - if (status === CoreConstants.NOT_DOWNLOADED) { - // File is not downloaded, download and then return the local URL. - subPromise = this.downloadFile(); - } else { - // File is outdated and can't be opened in online, return the local URL. - subPromise = this.filepoolProvider.getInternalUrlByUrl(this.siteId, this.fileUrl); - } - - return subPromise.then((url) => { - return this.utils.openFile(url); - }); - }); - } else { - return this.utils.openFile(url); + return this.fileHelper.downloadAndOpenFile(this.file, this.component, this.componentId, this.state, (event) => { + if (event && event.calculating) { + // The process is calculating some data required for the download, show the spinner. + this.isDownloading = true; } + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true); }); } diff --git a/src/directives/directives.module.ts b/src/directives/directives.module.ts index f28c00562..916710a90 100644 --- a/src/directives/directives.module.ts +++ b/src/directives/directives.module.ts @@ -14,6 +14,7 @@ import { NgModule } from '@angular/core'; import { CoreAutoFocusDirective } from './auto-focus'; +import { CoreDownloadFileDirective } from './download-file'; import { CoreExternalContentDirective } from './external-content'; import { CoreFormatTextDirective } from './format-text'; import { CoreLinkDirective } from './link'; @@ -25,6 +26,7 @@ import { CoreLongPressDirective } from './long-press'; @NgModule({ declarations: [ CoreAutoFocusDirective, + CoreDownloadFileDirective, CoreExternalContentDirective, CoreFormatTextDirective, CoreKeepKeyboardDirective, @@ -36,6 +38,7 @@ import { CoreLongPressDirective } from './long-press'; imports: [], exports: [ CoreAutoFocusDirective, + CoreDownloadFileDirective, CoreExternalContentDirective, CoreFormatTextDirective, CoreKeepKeyboardDirective, diff --git a/src/directives/download-file.ts b/src/directives/download-file.ts new file mode 100644 index 000000000..f715d15ee --- /dev/null +++ b/src/directives/download-file.ts @@ -0,0 +1,60 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Directive, Input, OnInit, ElementRef } from '@angular/core'; +import { NavController } from 'ionic-angular'; +import { CoreFileHelperProvider } from '../providers/file-helper'; +import { CoreDomUtilsProvider } from '../providers/utils/dom'; +import { CoreUtilsProvider } from '../providers/utils/utils'; + +/** + * Directive to allow downloading and open a file. When the item with this directive is clicked, the file will be + * downloaded (if needed) and opened. + */ +@Directive({ + selector: '[core-download-file]' +}) +export class CoreDownloadFileDirective implements OnInit { + @Input('core-download-file') file: any; // The file to download. + @Input() component?: string; // Component to link the file to. + @Input() componentId?: string | number; // Component ID to use in conjunction with the component. + + protected element: HTMLElement; + + constructor(element: ElementRef, protected domUtils: CoreDomUtilsProvider, protected fileHelper: CoreFileHelperProvider) { + this.element = element.nativeElement || element; + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.element.addEventListener('click', (ev: Event): void => { + if (!this.file) { + return; + } + + ev.preventDefault(); + ev.stopPropagation(); + + const modal = this.domUtils.showModalLoading(); + + this.fileHelper.downloadAndOpenFile(this.file, this.component, this.componentId).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true); + }).finally(() => { + modal.dismiss(); + }); + }); + } +} diff --git a/src/providers/file-helper.ts b/src/providers/file-helper.ts new file mode 100644 index 000000000..a7a936747 --- /dev/null +++ b/src/providers/file-helper.ts @@ -0,0 +1,240 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreAppProvider } from './app'; +import { CoreFileProvider } from './file'; +import { CoreFilepoolProvider } from './filepool'; +import { CoreSitesProvider } from './sites'; +import { CoreUtilsProvider } from './utils/utils'; +import { CoreConstants } from '../core/constants'; + +/** + * Provider to provide some helper functions regarding files and packages. + */ +@Injectable() +export class CoreFileHelperProvider { + + constructor(private fileProvider: CoreFileProvider, private filepoolProvider: CoreFilepoolProvider, + private sitesProvider: CoreSitesProvider, private appProvider: CoreAppProvider, private translate: TranslateService, + private utils: CoreUtilsProvider) { } + + /** + * Convenience function to open a file, downloading it if needed. + * + * @param {any} file The file to download. + * @param {string} [component] The component to link the file to. + * @param {string|number} [componentId] An ID to use in conjunction with the component. + * @param {string} [state] The file's state. If not provided, it will be calculated. + * @param {Function} [onProgress] Function to call on progress. + * @param {string} [siteId] The site ID. If not defined, current site. + * @return {Promise} Resolved on success. + */ + downloadAndOpenFile(file: any, component: string, componentId: string | number, state?: string, + onProgress?: (event: any) => any, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + const fileUrl = this.getFileUrl(file), + timemodified = this.getFileTimemodified(file); + + return this.downloadFileIfNeeded(file, fileUrl, component, componentId, timemodified, state, onProgress, siteId) + .then((url) => { + if (!url) { + return; + } + + if (url.indexOf('http') === 0) { + return this.utils.openOnlineFile(url).catch((error) => { + // Error opening the file, some apps don't allow opening online files. + if (!this.fileProvider.isAvailable()) { + return Promise.reject(error); + } + + // Get the state. + if (state) { + return state; + } else { + return this.filepoolProvider.getFileStateByUrl(siteId, fileUrl, timemodified); + } + }).then((state) => { + if (state == CoreConstants.DOWNLOADING) { + return Promise.reject(this.translate.instant('core.erroropenfiledownloading')); + } + + let promise; + + if (state === CoreConstants.NOT_DOWNLOADED) { + // File is not downloaded, download and then return the local URL. + promise = this.downloadFile(fileUrl, component, componentId, timemodified, onProgress, file, siteId); + } else { + // File is outdated and can't be opened in online, return the local URL. + promise = this.filepoolProvider.getInternalUrlByUrl(siteId, fileUrl); + } + + return promise.then((url) => { + return this.utils.openFile(url); + }); + }); + } else { + return this.utils.openFile(url); + } + }); + } + + /** + * Download a file if it needs to be downloaded. + * + * @param {any} file The file to download. + * @param {string} fileUrl The file URL. + * @param {string} [component] The component to link the file to. + * @param {string|number} [componentId] An ID to use in conjunction with the component. + * @param {number} [timemodified] The time this file was modified. + * @param {string} [state] The file's state. If not provided, it will be calculated. + * @param {Function} [onProgress] Function to call on progress. + * @param {string} [siteId] The site ID. If not defined, current site. + * @return {Promise} Resolved with the URL to use on success. + */ + protected downloadFileIfNeeded(file: any, fileUrl: string, component?: string, componentId?: string | number, + timemodified?: number, state?: string, onProgress?: (event: any) => any, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + return this.sitesProvider.getSite(siteId).then((site) => { + const fixedUrl = site.fixPluginfileURL(fileUrl); + + if (this.fileProvider.isAvailable()) { + let promise; + if (state) { + promise = Promise.resolve(state); + } else { + // Calculate the state. + promise = this.filepoolProvider.getFileStateByUrl(siteId, fileUrl, timemodified); + } + + return promise.then((state) => { + // The file system is available. + const isWifi = !this.appProvider.isNetworkAccessLimited(), + isOnline = this.appProvider.isOnline(); + + if (state == CoreConstants.DOWNLOADED) { + // File is downloaded, get the local file URL. + return this.filepoolProvider.getUrlByUrl( + siteId, fileUrl, component, componentId, timemodified, false, false, file); + } else { + if (!isOnline && !this.isStateDownloaded(state)) { + // Not downloaded and user is offline, reject. + return Promise.reject(this.translate.instant('core.networkerrormsg')); + } + + if (onProgress) { + // This call can take a while. Send a fake event to notify that we're doing some calculations. + onProgress({calculating: true}); + } + + return this.filepoolProvider.shouldDownloadBeforeOpen(fixedUrl, file.filesize).then(() => { + if (state == CoreConstants.DOWNLOADING) { + // It's already downloading, stop. + return; + } + + // Download and then return the local URL. + return this.downloadFile(fileUrl, component, componentId, timemodified, onProgress, file, siteId); + }, () => { + // Start the download if in wifi, but return the URL right away so the file is opened. + if (isWifi && isOnline) { + this.downloadFile(fileUrl, component, componentId, timemodified, onProgress, file, siteId); + } + + if (!this.isStateDownloaded(state) || isOnline) { + // Not downloaded or online, return the online URL. + return fixedUrl; + } else { + // Outdated but offline, so we return the local URL. + return this.filepoolProvider.getUrlByUrl( + siteId, fileUrl, component, componentId, timemodified, false, false, file); + } + }); + } + }); + } else { + // Use the online URL. + return fixedUrl; + } + }); + } + + /** + * Download the file. + * + * @param {string} fileUrl The file URL. + * @param {string} [component] The component to link the file to. + * @param {string|number} [componentId] An ID to use in conjunction with the component. + * @param {number} [timemodified] The time this file was modified. + * @param {Function} [onProgress] Function to call on progress. + * @param {any} [file] The file to download. + * @param {string} [siteId] The site ID. If not defined, current site. + * @return {Promise} Resolved with internal URL on success, rejected otherwise. + */ + downloadFile(fileUrl: string, component?: string, componentId?: string | number, timemodified?: number, + onProgress?: (event: any) => any, file?: any, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + // Get the site and check if it can download files. + return this.sitesProvider.getSite(siteId).then((site) => { + if (!site.canDownloadFiles()) { + return Promise.reject(this.translate.instant('core.cannotdownloadfiles')); + } + + return this.filepoolProvider.downloadUrl(siteId, fileUrl, false, component, componentId, + timemodified, onProgress, undefined, file).catch((error) => { + + // Download failed, check the state again to see if the file was downloaded before. + return this.filepoolProvider.getFileStateByUrl(siteId, fileUrl, timemodified).then((state) => { + if (this.isStateDownloaded(state)) { + return this.filepoolProvider.getInternalUrlByUrl(siteId, fileUrl); + } else { + return Promise.reject(error); + } + }); + }); + }); + } + + /** + * Get the file's URL. + * + * @param {any} file The file. + */ + getFileUrl(file: any): string { + return file.fileurl || file.url; + } + + /** + * Get the file's timemodified. + * + * @param {any} file The file. + */ + getFileTimemodified(file: any): number { + return file.timemodified || 0; + } + + /** + * Check if a state is downloaded or outdated. + * + * @param {string} state The state to check. + */ + isStateDownloaded(state: string): boolean { + return state === CoreConstants.DOWNLOADED || state === CoreConstants.OUTDATED; + } +}