Merge pull request #1221 from dpalou/MOBILE-2312

Mobile 2312
main
Juan Leyva 2018-01-15 09:42:07 +01:00 committed by GitHub
commit 6e899ad5a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 4643 additions and 452 deletions

731
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -34,6 +34,7 @@
"@angular/http": "5.0.0",
"@angular/platform-browser": "5.0.0",
"@angular/platform-browser-dynamic": "5.0.0",
"@ionic-native/camera": "^4.5.2",
"@ionic-native/clipboard": "^4.3.2",
"@ionic-native/core": "4.3.0",
"@ionic-native/file": "^4.3.3",
@ -42,6 +43,7 @@
"@ionic-native/in-app-browser": "^4.3.3",
"@ionic-native/keyboard": "^4.3.2",
"@ionic-native/local-notifications": "^4.4.0",
"@ionic-native/media-capture": "^4.5.2",
"@ionic-native/network": "^4.3.2",
"@ionic-native/splash-screen": "4.3.0",
"@ionic-native/sqlite": "^4.3.2",

View File

@ -53,6 +53,8 @@ import { CoreEmulatorModule } from '../core/emulator/emulator.module';
import { CoreLoginModule } from '../core/login/login.module';
import { CoreMainMenuModule } from '../core/mainmenu/mainmenu.module';
import { CoreCoursesModule } from '../core/courses/courses.module';
import { CoreFileUploaderModule } from '../core/fileuploader/fileuploader.module';
import { CoreSharedFilesModule } from '../core/sharedfiles/sharedfiles.module';
// For translate loader. AoT requires an exported function for factories.
@ -82,6 +84,8 @@ export function createTranslateLoader(http: HttpClient) {
CoreLoginModule,
CoreMainMenuModule,
CoreCoursesModule,
CoreFileUploaderModule,
CoreSharedFilesModule,
CoreComponentsModule
],
bootstrap: [IonicApp],

View File

@ -274,3 +274,22 @@ ion-select {
}
}
}
// File uploader.
// -------------------------
.core-fileuploader-file-handler {
position: relative;
input {
position: absolute;
top: 0;
right: 0;
min-width: 100%;
min-height: 100%;
opacity: 0;
outline: none;
z-index: 100;
cursor: pointer;
}
}

View File

@ -717,9 +717,10 @@ export class CoreSite {
*
* @param {string} filePath File path.
* @param {CoreWSFileUploadOptions} options File upload options.
* @param {Function} [onProgress] Function to call on progress.
* @return {Promise<any>} Promise resolved when uploaded.
*/
uploadFile(filePath: string, options: CoreWSFileUploadOptions) : Promise<any> {
uploadFile(filePath: string, options: CoreWSFileUploadOptions, onProgress?: (event: ProgressEvent) => any) : Promise<any> {
if (!options.fileArea) {
options.fileArea = 'draft';
}
@ -727,7 +728,7 @@ export class CoreSite {
return this.wsProvider.uploadFile(filePath, options, {
siteUrl: this.siteUrl,
wsToken: this.token
});
}, onProgress);
}
/**
@ -1061,7 +1062,7 @@ export class CoreSite {
}
if (alertMessage) {
let alert = this.domUtils.showAlert('core.notice', alertMessage, undefined, 3000);
let alert = this.domUtils.showAlert(this.translate.instant('core.notice'), alertMessage, undefined, 3000);
alert.onDidDismiss(() => {
if (inApp) {
resolve(this.utils.openInApp(url, options));

View File

@ -0,0 +1,116 @@
// (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 { Component, Input, OnChanges, OnDestroy, Output, EventEmitter, SimpleChange, ChangeDetectorRef } from '@angular/core';
/**
* This component shows a chronometer in format HH:MM:SS.
*
* If no startTime is provided, it will start at 00:00:00.
* If an endTime is provided, the chrono will stop and emit an event in the onEnd output when that number of milliseconds is
* reached. E.g. if startTime=60000 and endTime=120000, the chrono will start at 00:01:00 and end when it reaches 00:02:00.
*
* This component has 2 boolean inputs to control the timer: running (to start and stop it) and reset.
*
* Example usage:
* <core-chrono [running]="running" [reset]="reset" [endTime]="maxTime" (onEnd)="stopCapturing()"></core-chrono>
*/
@Component({
selector: 'core-chrono',
template: '<span>{{ time / 1000 | coreSecondsToHMS }}</span>'
})
export class CoreChronoComponent implements OnChanges, OnDestroy {
@Input() running: boolean; // Set it to true to start the chrono. Set it to false to stop it.
@Input() startTime?: number = 0; // Number of milliseconds to put in the chrono before starting.
@Input() endTime?: number; // Number of milliseconds to stop the chrono.
@Input() reset?: boolean; // Set it to true to reset the chrono.
@Output() onEnd?: EventEmitter<void>; // Will emit an event when the endTime is reached.
time: number = 0;
protected interval;
constructor(private cdr: ChangeDetectorRef) {
this.onEnd = new EventEmitter();
}
/**
* Component being initialized.
*/
ngOnInit() {
this.time = this.startTime || 0;
}
/**
* Component being initialized.
*/
ngOnChanges(changes: {[name: string]: SimpleChange}) {
if (changes && changes.running) {
if (changes.running.currentValue) {
this.start();
} else {
this.stop();
}
}
if (changes && changes.reset && changes.reset.currentValue) {
this.resetChrono();
}
}
/**
* Reset the chrono, stopping it and setting it to startTime.
*/
protected resetChrono() : void {
this.stop();
this.time = this.startTime || 0;
}
/**
* Start the chrono if it isn't running.
*/
protected start() : void {
if (this.interval) {
// Already setup.
return;
}
let lastExecTime = Date.now();
this.interval = setInterval(() => {
// Increase the chrono.
this.time += Date.now() - lastExecTime;
lastExecTime = Date.now();
if (typeof this.endTime != 'undefined' && this.time > this.endTime) {
// End time reached, stop the timer and call the end function.
this.stop();
this.onEnd.emit();
}
// Force change detection. Angular doesn't detect these async operations.
this.cdr.detectChanges();
}, 200);
}
/**
* Stop the chrono, leaving the same time it has.
*/
protected stop() : void {
clearInterval(this.interval);
delete this.interval;
}
ngOnDestroy() {
this.stop();
}
}

View File

@ -16,6 +16,7 @@ import { NgModule } from '@angular/core';
import { IonicModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreDirectivesModule } from '../directives/directives.module';
import { CorePipesModule } from '../pipes/pipes.module';
import { CoreLoadingComponent } from './loading/loading';
import { CoreMarkRequiredComponent } from './mark-required/mark-required';
import { CoreInputErrorsComponent } from './input-errors/input-errors';
@ -28,6 +29,9 @@ import { CoreFileComponent } from './file/file';
import { CoreContextMenuComponent } from './context-menu/context-menu';
import { CoreContextMenuItemComponent } from './context-menu/context-menu-item';
import { CoreContextMenuPopoverComponent } from './context-menu/context-menu-popover';
import { CoreChronoComponent } from './chrono/chrono';
import { CoreLocalFileComponent } from './local-file/local-file';
import { CoreSitePickerComponent } from './site-picker/site-picker';
@NgModule({
declarations: [
@ -42,7 +46,10 @@ import { CoreContextMenuPopoverComponent } from './context-menu/context-menu-pop
CoreFileComponent,
CoreContextMenuComponent,
CoreContextMenuItemComponent,
CoreContextMenuPopoverComponent
CoreContextMenuPopoverComponent,
CoreChronoComponent,
CoreLocalFileComponent,
CoreSitePickerComponent
],
entryComponents: [
CoreContextMenuPopoverComponent
@ -50,7 +57,8 @@ import { CoreContextMenuPopoverComponent } from './context-menu/context-menu-pop
imports: [
IonicModule,
TranslateModule.forChild(),
CoreDirectivesModule
CoreDirectivesModule,
CorePipesModule
],
exports: [
CoreLoadingComponent,
@ -63,7 +71,10 @@ import { CoreContextMenuPopoverComponent } from './context-menu/context-menu-pop
CoreSearchBoxComponent,
CoreFileComponent,
CoreContextMenuComponent,
CoreContextMenuItemComponent
CoreContextMenuItemComponent,
CoreChronoComponent,
CoreLocalFileComponent,
CoreSitePickerComponent
]
})
export class CoreComponentsModule {}

View File

@ -1,4 +1,4 @@
<a ion-item text-wrap class="item-media" (click)="download($event, true)" [class.item-2-button-right]="canDelete">
<a ion-item text-wrap class="item-media" (click)="download($event, true)" [class.item-2-button-right]="canDelete" detail-none>
<img [src]="fileIcon" alt="" role="presentation" item-start />
<p>{{fileName}}</p>
<div class="buttons" item-end>

View File

@ -0,0 +1,29 @@
<a ion-item text-wrap class="item-media" (click)="fileClicked($event)" detail-none>
<img [src]="fileIcon" alt="{{fileExtension}}" role="presentation" item-start />
<!-- File name and edit button (if editable). -->
<p *ngIf="!editMode" class="core-text-with-icon-right">
{{fileName}}
<a ion-button icon-only clear *ngIf="manage" (click)="activateEdit($event)" [attr.aria-label]="'core.edit' | translate">
<ion-icon name="create" ios="md-create"></ion-icon>
</a>
</p>
<!-- Form to edit the file's name. -->
<form *ngIf="editMode" (ngSubmit)="changeName(newFileName)">
<ion-input type="text" name="filename" [(ngModel)]="newFileName" [placeholder]="'core.filename' | translate" autocapitalize="none" autocorrect="off" (click)="$event.stopPropagation()" [core-auto-focus]></ion-input>
<button type="submit" ion-button icon-only clear class="core-button-icon-small" [attr.aria-label]="'core.save' | translate">
<ion-icon name="checkmark"></ion-icon>
</button>
</form>
<!-- More data about the file. -->
<p *ngIf="size">{{ size }}</p>
<p *ngIf="timemodified">{{ timemodified }}</p>
<div class="buttons" item-end *ngIf="manage">
<button ion-button clear icon-only (click)="deleteFile($event)" [attr.aria-label]="'core.delete' | translate" color="danger">
<ion-icon name="trash"></ion-icon>
</button>
</div>
</a>

View File

@ -0,0 +1,187 @@
// (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 { Component, Input, Output, OnInit, EventEmitter } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { CoreFileProvider } from '../../providers/file';
import { CoreDomUtilsProvider } from '../../providers/utils/dom';
import { CoreMimetypeUtilsProvider } from '../../providers/utils/mimetype';
import { CoreTextUtilsProvider } from '../../providers/utils/text';
import { CoreUtilsProvider } from '../../providers/utils/utils';
import * as moment from 'moment';
/**
* Component to handle a local file. Only files inside the app folder can be managed.
*
* Shows the file name, icon (depending on extension), size and time modified.
* Also, if managing is enabled it will also show buttons to rename and delete the file.
*/
@Component({
selector: 'core-local-file',
templateUrl: 'local-file.html'
})
export class CoreLocalFileComponent implements OnInit {
@Input() file: any; // A fileEntry retrieved using CoreFileProvider.getFile or similar.
@Input() manage?: boolean|string; // Whether the user can manage the file (edit and delete).
@Input() overrideClick?: boolean|string; // Whether the default item click should be overridden.
@Output() onDelete?: EventEmitter<void>; // Will notify when the file is deleted.
@Output() onRename?: EventEmitter<any>; // Will notify when the file is renamed. Receives the FileEntry as the param.
@Output() onClick?: EventEmitter<void>; // Will notify when the file is clicked. Only if overrideClick is true.
fileName: string;
fileIcon: string;
fileExtension: string;
size: string;
timemodified: string;
newFileName: string = '';
editMode: boolean;
relativePath: string;
constructor(private mimeUtils: CoreMimetypeUtilsProvider, private utils: CoreUtilsProvider, private translate: TranslateService,
private textUtils: CoreTextUtilsProvider, private fileProvider: CoreFileProvider,
private domUtils: CoreDomUtilsProvider) {
this.onDelete = new EventEmitter();
this.onRename = new EventEmitter();
this.onClick = new EventEmitter();
}
/**
* Component being initialized.
*/
ngOnInit() {
this.manage = this.utils.isTrueOrOne(this.manage);
// Let's calculate the relative path for the file.
this.relativePath = this.fileProvider.removeBasePath(this.file.toURL());
if (!this.relativePath) {
// Didn't find basePath, use fullPath but if the user tries to manage the file it'll probably fail.
this.relativePath = this.file.fullPath;
}
this.loadFileBasicData();
// Get the size and timemodified.
this.fileProvider.getMetadata(this.file).then((metadata) => {
if (metadata.size >= 0) {
this.size = this.textUtils.bytesToSize(metadata.size, 2);
}
this.timemodified = moment(metadata.modificationTime).format('LLL');
});
}
/**
* Load the basic data for the file.
*
* @param {[type]} scope [description]
* @param {[type]} file [description]
*/
protected loadFileBasicData() {
this.fileName = this.file.name;
this.fileIcon = this.mimeUtils.getFileIcon(this.file.name);
this.fileExtension = this.mimeUtils.getFileExtension(this.file.name);
}
/**
* File clicked.
*
* @param {Event} e Click event.
*/
fileClicked(e: Event) : void {
e.preventDefault();
e.stopPropagation();
if (this.utils.isTrueOrOne(this.overrideClick) && this.onClick.observers.length) {
this.onClick.emit();
} else {
this.utils.openFile(this.file.toURL());
}
};
/**
* Activate the edit mode.
*
* @param {Event} e Click event.
*/
activateEdit(e: Event) : void {
e.preventDefault();
e.stopPropagation();
this.editMode = true;
this.newFileName = this.file.name;
// @todo For some reason core-auto-focus isn't working right. Focus the input manually.
// $timeout(function() {
// $mmUtil.focusElement(element[0].querySelector('input'));
// });
};
/**
* Rename the file.
*
* @param {string} newName New name.
*/
changeName(newName: string) : void {
if (newName == this.file.name) {
// Name hasn't changed, stop.
this.editMode = false;
return;
}
let modal = this.domUtils.showModalLoading(),
fileAndDir = this.fileProvider.getFileAndDirectoryFromPath(this.relativePath),
newPath = this.textUtils.concatenatePaths(fileAndDir.directory, newName);
// Check if there's a file with this name.
this.fileProvider.getFile(newPath).then(() => {
// There's a file with this name, show error and stop.
this.domUtils.showErrorModal('core.errorfileexistssamename', true);
}).catch(() => {
// File doesn't exist, move it.
return this.fileProvider.moveFile(this.relativePath, newPath).then((fileEntry) => {
this.editMode = false;
this.file = fileEntry;
this.loadFileBasicData();
this.onRename.emit({file: this.file});
}).catch(() => {
this.domUtils.showErrorModal('core.errorrenamefile', true);
});
}).finally(() => {
modal.dismiss();
});
};
/**
* Delete the file.
*
* @param {Event} e Click event.
*/
deleteFile(e: Event) : void {
e.preventDefault();
e.stopPropagation();
// Ask confirmation.
this.domUtils.showConfirm(this.translate.instant('core.confirmdeletefile')).then(() => {
let modal = this.domUtils.showModalLoading();
this.fileProvider.removeFile(this.relativePath).then(() => {
this.onDelete.emit();
}).catch(() => {
this.domUtils.showErrorModal('core.errordeletefile', true);
}).finally(() => {
modal.dismiss();
});
}).catch(() => {
// User cancelled.
});
}
}

View File

@ -0,0 +1,6 @@
<ion-item>
<ion-label>{{ 'core.site' | translate }}</ion-label>
<ion-select [(ngModel)]="selectedSite" (ngModelChange)="siteSelected.emit(selectedSite)">
<ion-option *ngFor="let site of sites" [value]="site.id">{{ site.fullNameAndSiteName }}</ion-option>
</ion-select>
</ion-item>

View File

@ -0,0 +1,66 @@
// (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 { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { CoreSitesProvider } from '../../providers/sites';
import { CoreTextUtilsProvider } from '../../providers/utils/text';
/**
* Component to display a site selector. It will display a select with the list of sites. If the selected site changes,
* an output will be emitted with the site ID.
*
* Example usage:
* <core-site-picker (siteSelected)="changeSite($event)"></core-site-picker>
*/
@Component({
selector: 'core-site-picker',
templateUrl: 'site-picker.html'
})
export class CoreSitePickerComponent implements OnInit {
@Input() initialSite?: string; // Initial site. If not provided, current site.
@Output() siteSelected: EventEmitter<string>; // Emit an event when a site is selected. Sends the siteId as parameter.
selectedSite: string;
sites: any[];
constructor(private translate: TranslateService, private sitesProvider: CoreSitesProvider,
private textUtils: CoreTextUtilsProvider) {
this.siteSelected = new EventEmitter();
}
ngOnInit() {
this.selectedSite = this.initialSite || this.sitesProvider.getCurrentSiteId();
// Load the sites.
this.sitesProvider.getSites().then((sites) => {
let promises = [];
sites.forEach((site: any) => {
// Format the site name.
promises.push(this.textUtils.formatText(site.siteName, true, true).catch(() => {
return site.siteName;
}).then((formatted) => {
site.fullNameAndSiteName = this.translate.instant('core.fullnameandsitename',
{fullname: site.fullName, sitename: formatted});
}));
});
return Promise.all(promises).then(() => {
this.sites = sites;
});
});
}
}

View File

@ -15,6 +15,8 @@
import { NgModule } from '@angular/core';
import { Platform } from 'ionic-angular';
// Ionic Native services.
import { Camera } from '@ionic-native/camera';
import { Clipboard } from '@ionic-native/clipboard';
import { File } from '@ionic-native/file';
import { FileTransfer } from '@ionic-native/file-transfer';
@ -22,22 +24,27 @@ import { Globalization } from '@ionic-native/globalization';
import { InAppBrowser } from '@ionic-native/in-app-browser';
import { Keyboard } from '@ionic-native/keyboard';
import { LocalNotifications } from '@ionic-native/local-notifications';
import { MediaCapture } from '@ionic-native/media-capture';
import { Network } from '@ionic-native/network';
import { SplashScreen } from '@ionic-native/splash-screen';
import { StatusBar } from '@ionic-native/status-bar';
import { SQLite } from '@ionic-native/sqlite';
import { Zip } from '@ionic-native/zip';
// Services that Mock Ionic Native in browser an desktop.
import { CameraMock } from './providers/camera';
import { ClipboardMock } from './providers/clipboard';
import { FileMock } from './providers/file';
import { FileTransferMock } from './providers/file-transfer';
import { GlobalizationMock } from './providers/globalization';
import { InAppBrowserMock } from './providers/inappbrowser';
import { LocalNotificationsMock } from './providers/local-notifications';
import { MediaCaptureMock } from './providers/media-capture';
import { NetworkMock } from './providers/network';
import { ZipMock } from './providers/zip';
import { CoreEmulatorHelperProvider } from './providers/helper';
import { CoreEmulatorCaptureHelperProvider } from './providers/capture-helper';
import { CoreAppProvider } from '../../providers/app';
import { CoreFileProvider } from '../../providers/file';
import { CoreTextUtilsProvider } from '../../providers/utils/text';
@ -46,6 +53,15 @@ import { CoreUrlUtilsProvider } from '../../providers/utils/url';
import { CoreUtilsProvider } from '../../providers/utils/utils';
import { CoreInitDelegate } from '../../providers/init';
/**
* This module handles the emulation of Cordova plugins in browser and desktop.
*
* It includes the "mock" of all the Ionic Native services that should be supported in browser and desktop,
* otherwise those features would only work in a Cordova environment.
*
* This module also determines if the app should use the original service or the mock. In each of the "useFactory"
* functions we check if the app is running in mobile or not, and then provide the right service to use.
*/
@NgModule({
declarations: [
],
@ -53,6 +69,14 @@ import { CoreInitDelegate } from '../../providers/init';
],
providers: [
CoreEmulatorHelperProvider,
CoreEmulatorCaptureHelperProvider,
{
provide: Camera,
deps: [CoreAppProvider, CoreEmulatorCaptureHelperProvider],
useFactory: (appProvider: CoreAppProvider, captureHelper: CoreEmulatorCaptureHelperProvider) => {
return appProvider.isMobile() ? new Camera() : new CameraMock(captureHelper);
}
},
{
provide: Clipboard,
deps: [CoreAppProvider],
@ -99,6 +123,13 @@ import { CoreInitDelegate } from '../../providers/init';
return appProvider.isMobile() ? new LocalNotifications() : new LocalNotificationsMock(appProvider, utils);
}
},
{
provide: MediaCapture,
deps: [CoreAppProvider, CoreEmulatorCaptureHelperProvider],
useFactory: (appProvider: CoreAppProvider, captureHelper: CoreEmulatorCaptureHelperProvider) => {
return appProvider.isMobile() ? new MediaCapture() : new MediaCaptureMock(captureHelper);
}
},
{
provide: Network,
deps: [Platform],

View File

@ -0,0 +1,55 @@
<ion-header>
<ion-navbar>
<ion-buttons start>
<button ion-button (click)="cancel()">{{ 'core.cancel' | translate }}</button>
</ion-buttons>
<ion-title>{{ title }}</ion-title>
<ion-buttons end>
<button ion-button *ngIf="hasCaptured" (click)="done()">{{ 'core.done' | translate }}</button>
</ion-buttons>
</ion-navbar>
</ion-header>
<ion-content>
<core-loading [hideUntil]="readyToCapture">
<div class="core-av-wrapper">
<!-- Video stream for image and video. -->
<video *ngIf="!isAudio" [hidden]="hasCaptured" class="core-webcam-stream" autoplay #streamVideo></video>
<!-- For video recording, use 2 videos and show/hide them because a CSS rule caused problems with the controls. -->
<video *ngIf="isVideo" [hidden]="!hasCaptured" class="core-webcam-video-captured" controls #previewVideo></video>
<!-- Canvas to treat the image and an img to show the result. -->
<canvas *ngIf="isImage" class="core-webcam-image-canvas" #imgCanvas></canvas>
<img *ngIf="isImage" [hidden]="!hasCaptured" class="core-webcam-image" alt="{{ 'core.capturedimage' | translate }}" #previewImage>
<!-- Canvas to show audio waves when recording audio and audio player to listen to the result. -->
<div *ngIf="isAudio" class="core-audio-record-container">
<canvas [hidden]="hasCaptured" class="core-audio-canvas" #streamAudio></canvas>
<audio [hidden]="!hasCaptured" class="core-audio-captured" controls #previewAudio></audio>
</div>
</div>
</core-loading>
</ion-content>
<ion-footer>
<ion-row *ngIf="readyToCapture">
<ion-col></ion-col>
<ion-col text-center>
<button ion-button icon-only clear *ngIf="!hasCaptured" (click)="actionClicked()" [attr.aria-label]="title">
<ion-icon *ngIf="!isCapturing && isAudio" name="microphone"></ion-icon>
<ion-icon *ngIf="!isCapturing && isVideo" name="videocam"></ion-icon>
<ion-icon *ngIf="isImage" name="camera"></ion-icon>
<ion-icon *ngIf="isCapturing" name="square"></ion-icon>
</button>
<button ion-button icon-only clear *ngIf="hasCaptured" (click)="discard()" [attr.aria-label]="'core.discard' | translate">
<ion-icon name="trash"></ion-icon>
</button>
</ion-col>
<ion-col padding text-right class="chrono-container">
<core-chrono *ngIf="!isImage" [hidden]="hasCaptured" [running]="isCapturing" [reset]="resetChrono" [endTime]="maxTime" (onEnd)="stopCapturing()"></core-chrono>
</ion-col>
</ion-row>
</ion-footer>

View File

@ -0,0 +1,31 @@
// (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 { NgModule } from '@angular/core';
import { IonicPageModule } from 'ionic-angular';
import { CoreEmulatorCaptureMediaPage } from './capture-media';
import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '../../../../components/components.module';
@NgModule({
declarations: [
CoreEmulatorCaptureMediaPage
],
imports: [
CoreComponentsModule,
IonicPageModule.forChild(CoreEmulatorCaptureMediaPage),
TranslateModule.forChild()
]
})
export class CoreEmulatorCaptureMediaPageModule {}

View File

@ -0,0 +1,68 @@
page-core-emulator-capture-media {
ion-content {
.core-av-wrapper {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: 0;
padding: 0;
clear: both;
.core-webcam-image-canvas {
display: none;
}
.core-audio-record-container {
width: 100%;
height: 100%;
.core-audio-canvas {
width: 100%;
height: 100%;
}
.core-audio-captured {
width: 100%;
}
}
audio, video, img {
width: 100%;
height: 100%;
display: table-cell;
text-align: center;
vertical-align: middle;
object-fit: contain;
&.core-webcam-stream {
-webkit-transform: scaleX(-1);
transform: scaleX(-1);
}
}
}
.scroll-content {
// We're modifying the height of the footer, the padding-bottom of the scroll needs to change too.
margin-bottom: 44px !important;
}
}
ion-footer {
background-color: $gray;
border-top: 1px solid $gray-dark;
.col {
padding: 0;
.icon.ion-md-trash, .icon.ion-ios-trash {
color: $red;
}
}
.chrono-container {
line-height: 24px;
}
}
}

View File

@ -0,0 +1,402 @@
// (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 { Component, OnInit, OnDestroy, ViewChild, ElementRef, ChangeDetectorRef } from '@angular/core';
import { IonicPage, ViewController, NavParams } from 'ionic-angular';
import { CoreFileProvider } from '../../../../providers/file';
import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
import { CoreTextUtilsProvider } from '../../../../providers/utils/text';
import { CoreTimeUtilsProvider } from '../../../../providers/utils/time';
/**
* Page to capture media in browser or desktop.
*/
@IonicPage()
@Component({
selector: 'page-core-emulator-capture-media',
templateUrl: 'capture-media.html',
})
export class CoreEmulatorCaptureMediaPage implements OnInit, OnDestroy {
@ViewChild('streamVideo') streamVideo: ElementRef;
@ViewChild('previewVideo') previewVideo: ElementRef;
@ViewChild('imgCanvas') imgCanvas: ElementRef;
@ViewChild('previewImage') previewImage: ElementRef;
@ViewChild('streamAudio') streamAudio: ElementRef;
@ViewChild('previewAudio') previewAudio: ElementRef;
title: string; // The title of the page.
isAudio: boolean; // Whether it should capture audio.
isVideo: boolean; // Whether it should capture video.
isImage: boolean; // Whether it should capture image.
readyToCapture: boolean; // Whether it's ready to capture.
hasCaptured: boolean; // Whether it has captured something.
isCapturing: boolean; // Whether it's capturing.
maxTime: number; // The max time to capture.
resetChrono: boolean; // Boolean to reset the chrono.
protected type: string; // The type to capture: audio, video, image, captureimage.
protected isCaptureImage: boolean; // To identify if it's capturing an image using media capture plugin (instead of camera).
protected returnDataUrl: boolean; // Whether it should return a data img. Only if isImage.
protected facingMode: string; // Camera facing mode.
protected mimetype: string;
protected extension: string;
protected window: any; // Cast window to "any" because some of the properties used aren't in the window spec.
protected mediaRecorder; // To record video/audio.
protected audioDrawer; // To start/stop the display of audio sound.
protected quality; // Image only.
protected previewMedia: HTMLAudioElement|HTMLVideoElement; // The element to preview the audio/video captured.
protected mediaBlob: Blob; // A Blob where the captured data is stored.
protected localMediaStream: MediaStream;
constructor(private viewCtrl: ViewController, params: NavParams, private domUtils: CoreDomUtilsProvider,
private timeUtils: CoreTimeUtilsProvider, private fileProvider: CoreFileProvider,
private textUtils: CoreTextUtilsProvider, private cdr: ChangeDetectorRef) {
this.window = window;
this.type = params.get('type');
this.maxTime = params.get('maxTime');
this.facingMode = params.get('facingMode') || 'environment';
this.mimetype = params.get('mimetype');
this.extension = params.get('extension');
this.quality = params.get('quality') || 0.92;
this.returnDataUrl = !!params.get('returnDataUrl');
}
/**
* Component being initialized.
*/
ngOnInit() {
this.initVariables();
let constraints = {
video: this.isAudio ? false : {facingMode: this.facingMode},
audio: !this.isImage
};
navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
let chunks = [];
this.localMediaStream = stream;
if (!this.isImage) {
if (this.isVideo) {
this.previewMedia = this.previewVideo.nativeElement;
} else {
this.previewMedia = this.previewAudio.nativeElement;
this.initAudioDrawer(this.localMediaStream);
this.audioDrawer.start();
}
this.mediaRecorder = new this.window.MediaRecorder(this.localMediaStream, {mimeType: this.mimetype});
// When video or audio is recorded, add it to the list of chunks.
this.mediaRecorder.ondataavailable = (e) => {
if (e.data.size > 0) {
chunks.push(e.data);
}
};
// When recording stops, create a Blob element with the recording and set it to the video or audio.
this.mediaRecorder.onstop = () => {
this.mediaBlob = new Blob(chunks);
chunks = [];
this.previewMedia.src = window.URL.createObjectURL(this.mediaBlob);
};
}
if (this.isImage || this.isVideo) {
let hasLoaded = false,
waitTimeout;
// Listen for stream ready to display the stream.
this.streamVideo.nativeElement.onloadedmetadata = () => {
if (hasLoaded) {
// Already loaded or timeout triggered, stop.
return;
}
hasLoaded = true;
clearTimeout(waitTimeout);
this.readyToCapture = true;
this.streamVideo.nativeElement.onloadedmetadata = null;
// Force change detection. Angular doesn't detect these async operations.
this.cdr.detectChanges();
};
// Set the stream as the source of the video.
this.streamVideo.nativeElement.src = window.URL.createObjectURL(this.localMediaStream);
// If stream isn't ready in a while, show error.
waitTimeout = setTimeout(() => {
if (!hasLoaded) {
// Show error.
hasLoaded = true;
this.dismissWithError(-1, 'Cannot connect to webcam.');
}
}, 10000);
} else {
// It's ready to capture.
this.readyToCapture = true;
}
}).catch((error) => {
this.dismissWithError(-1, error.message || error);
});
}
/**
* Initialize the audio drawer. This code has been extracted from MDN's example on MediaStream Recording:
* https://github.com/mdn/web-dictaphone
*
* @param {MediaStream} stream Stream returned by getUserMedia.
*/
protected initAudioDrawer(stream: MediaStream) : void {
let audioCtx = new (this.window.AudioContext || this.window.webkitAudioContext)(),
canvasCtx = this.streamAudio.nativeElement.getContext('2d'),
source = audioCtx.createMediaStreamSource(stream),
analyser = audioCtx.createAnalyser(),
bufferLength = analyser.frequencyBinCount,
dataArray = new Uint8Array(bufferLength),
width = this.streamAudio.nativeElement.width,
height = this.streamAudio.nativeElement.height,
running = false,
skip = true,
drawAudio = () => {
if (!running) {
return;
}
// Update the draw every animation frame.
requestAnimationFrame(drawAudio);
// Skip half of the frames to improve performance, shouldn't affect the smoothness.
skip = !skip;
if (skip) {
return;
}
let sliceWidth = width / bufferLength,
x = 0;
analyser.getByteTimeDomainData(dataArray);
canvasCtx.fillStyle = 'rgb(200, 200, 200)';
canvasCtx.fillRect(0, 0, width, height);
canvasCtx.lineWidth = 1;
canvasCtx.strokeStyle = 'rgb(0, 0, 0)';
canvasCtx.beginPath();
for (let i = 0; i < bufferLength; i++) {
let v = dataArray[i] / 128.0,
y = v * height / 2;
if (i === 0) {
canvasCtx.moveTo(x, y);
} else {
canvasCtx.lineTo(x, y);
}
x += sliceWidth;
}
canvasCtx.lineTo(width, height / 2);
canvasCtx.stroke();
};
analyser.fftSize = 2048;
source.connect(analyser);
this.audioDrawer = {
start: () => {
if (running) {
return;
}
running = true;
drawAudio();
},
stop: () => {
running = false;
}
};
}
/**
* Initialize some variables based on the params.
*/
protected initVariables() {
if (this.type == 'captureimage') {
this.isCaptureImage = true;
this.type = 'image';
}
// Initialize some data based on the type of media to capture.
if (this.type == 'video') {
this.isVideo = true;
this.title = 'core.capturevideo';
} else if (this.type == 'audio') {
this.isAudio = true;
this.title = 'core.captureaudio';
} else if (this.type == 'image') {
this.isImage = true;
this.title = 'core.captureimage';
}
}
/**
* Main action clicked: record or stop recording.
*/
actionClicked() : void {
if (this.isCapturing) {
// It's capturing, stop.
this.stopCapturing();
this.cdr.detectChanges();
} else {
if (!this.isImage) {
// Start the capture.
this.isCapturing = true;
this.resetChrono = false;
this.mediaRecorder.start();
this.cdr.detectChanges();
} else {
// Get the image from the video and set it to the canvas, using video width/height.
let width = this.streamVideo.nativeElement.videoWidth,
height = this.streamVideo.nativeElement.videoHeight;
this.imgCanvas.nativeElement.width = width;
this.imgCanvas.nativeElement.height = height;
this.imgCanvas.nativeElement.getContext('2d').drawImage(this.streamVideo.nativeElement, 0, 0, width, height);
// Convert the image to blob and show it in an image element.
let loadingModal = this.domUtils.showModalLoading();
this.imgCanvas.nativeElement.toBlob((blob) => {
loadingModal.dismiss();
this.mediaBlob = blob;
this.previewImage.nativeElement.setAttribute('src', window.URL.createObjectURL(this.mediaBlob));
this.hasCaptured = true;
}, this.mimetype, this.quality);
}
}
}
/**
* User cancelled.
*/
cancel() : void {
// Send a "cancelled" error like the Cordova plugin does.
this.dismissWithError(3, 'Canceled.', 'Camera cancelled');
}
/**
* Discard the captured media.
*/
discard() : void {
this.previewMedia && this.previewMedia.pause();
this.streamVideo && this.streamVideo.nativeElement.play();
this.audioDrawer && this.audioDrawer.start();
this.hasCaptured = false;
this.isCapturing = false;
this.resetChrono = true;
delete this.mediaBlob;
this.cdr.detectChanges();
};
/**
* Close the modal, returning some data (success).
*
* @param {any} data Data to return.
*/
dismissWithData(data: any) : void {
this.viewCtrl.dismiss(data, 'success');
}
/**
* Close the modal, returning an error.
*
* @param {number} code Error code. Will not be used if it's a Camera capture.
* @param {string} message Error message.
* @param {string} [cameraMessage] A specific message to use if it's a Camera capture. If not set, message will be used.
*/
dismissWithError(code: number, message: string, cameraMessage?: string) : void {
let isCamera = this.isImage && !this.isCaptureImage,
error = isCamera ? (cameraMessage || message) : {code: code, message: message};
this.viewCtrl.dismiss(error, 'error');
}
/**
* Done capturing, write the file.
*/
done() : void {
if (this.returnDataUrl) {
// Return the image as a base64 string.
this.dismissWithData(this.imgCanvas.nativeElement.toDataURL(this.mimetype, this.quality));
return;
}
if (!this.mediaBlob) {
// Shouldn't happen.
this.domUtils.showErrorModal('Please capture the media first.');
return;
}
// Create the file and return it.
let fileName = this.type + '_' + this.timeUtils.readableTimestamp() + '.' + this.extension,
path = this.textUtils.concatenatePaths(this.fileProvider.TMPFOLDER, 'media/' + fileName);
let loadingModal = this.domUtils.showModalLoading();
this.fileProvider.writeFile(path, this.mediaBlob).then((fileEntry) => {
if (this.isImage && !this.isCaptureImage) {
this.dismissWithData(fileEntry.toURL());
} else {
// The capture plugin returns a MediaFile, not a FileEntry. The only difference is that
// it supports a new function that won't be supported in desktop.
fileEntry.getFormatData = (successFn, errorFn) => {};
this.dismissWithData([fileEntry]);
}
}).catch((err) => {
this.domUtils.showErrorModal(err);
}).finally(() => {
loadingModal.dismiss();
});
};
/**
* Stop capturing. Only for video and audio.
*/
stopCapturing() : void {
this.streamVideo && this.streamVideo.nativeElement.pause();
this.audioDrawer && this.audioDrawer.stop();
this.mediaRecorder && this.mediaRecorder.stop();
this.isCapturing = false;
this.hasCaptured = true;
};
/**
* Page destroyed.
*/
ngOnDestroy() : void {
const tracks = this.localMediaStream.getTracks();
tracks.forEach((track) => {
track.stop();
});
this.streamVideo && this.streamVideo.nativeElement.pause();
this.previewMedia && this.previewMedia.pause();
this.audioDrawer && this.audioDrawer.stop();
delete this.mediaBlob;
}
}

View File

@ -0,0 +1,48 @@
// (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 { Camera, CameraOptions } from '@ionic-native/camera';
import { CoreEmulatorCaptureHelperProvider } from './capture-helper';
/**
* Emulates the Cordova Camera plugin in desktop apps and in browser.
*/
@Injectable()
export class CameraMock extends Camera {
constructor(private captureHelper: CoreEmulatorCaptureHelperProvider) {
super();
}
/**
* Remove intermediate image files that are kept in temporary storage after calling camera.getPicture.
*
* @return {Promise<any>} Promise resolved when done.
*/
cleanup() : Promise<any> {
// iOS only, nothing to do.
return Promise.resolve();
}
/**
* Take a picture.
*
* @param {CameraOptions} options Options that you want to pass to the camera.
* @return {Promise<any>} Promise resolved when captured.
*/
getPicture(options: CameraOptions) : Promise<any> {
return this.captureHelper.captureMedia('image', options);
}
}

View File

@ -0,0 +1,222 @@
// (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 { ModalController, Modal } from 'ionic-angular';
import { CoreMimetypeUtilsProvider } from '../../../providers/utils/mimetype';
import { CoreUtilsProvider } from '../../../providers/utils/utils';
/**
* Helper service with some features to capture media (image, audio, video).
*/
@Injectable()
export class CoreEmulatorCaptureHelperProvider {
protected possibleAudioMimeTypes = {
'audio/webm': 'weba',
'audio/ogg': 'ogg'
};
protected possibleVideoMimeTypes = {
'video/webm;codecs=vp9': 'webm',
'video/webm;codecs=vp8': 'webm',
'video/ogg': 'ogv'
};
protected win: any;
videoMimeType: string;
audioMimeType: string;
constructor(private utils: CoreUtilsProvider, private mimeUtils: CoreMimetypeUtilsProvider,
private modalCtrl: ModalController) {
// Convert the window to "any" type because some of the variables used (like MediaRecorder) aren't in the window spec.
this.win = <any>window;
}
/**
* Capture media (image, audio, video).
*
* @param {String} type Type of media: image, audio, video.
* @param {Function} successCallback Function called when media taken.
* @param {Function} errorCallback Function called when error or cancel.
* @param {Object} [options] Optional options.
* @return {Void}
*/
captureMedia(type: string, options: any) : Promise<any> {
options = options || {};
try {
// Build the params to send to the modal.
let deferred = this.utils.promiseDefer(),
params: any = {
type: type
},
mimeAndExt,
modal: Modal;
// Initialize some data based on the type of media to capture.
if (type == 'video') {
mimeAndExt = this.getMimeTypeAndExtension(type, options.mimetypes);
params.mimetype = mimeAndExt.mimetype;
params.extension = mimeAndExt.extension;
} else if (type == 'audio') {
mimeAndExt = this.getMimeTypeAndExtension(type, options.mimetypes);
params.mimetype = mimeAndExt.mimetype;
params.extension = mimeAndExt.extension;
} else if (type == 'image') {
if (typeof options.sourceType != 'undefined' && options.sourceType != 1) {
return Promise.reject('This source type is not supported in desktop.');
}
if (options.cameraDirection == 1) {
params.facingMode = 'user';
}
if (options.encodingType == 1) {
params.mimetype = 'image/png';
params.extension = 'png';
} else {
params.mimetype = 'image/jpeg';
params.extension = 'jpeg';
}
if (options.quality >= 0 && options.quality <= 100) {
params.quality = options.quality / 100;
}
if (options.destinationType == 0) {
params.returnDataUrl = true;
}
}
if (options.duration) {
params.maxTime = options.duration * 1000;
}
modal = this.modalCtrl.create('CoreEmulatorCaptureMediaPage', params);
modal.present();
modal.onDidDismiss((data: any, role: string) => {
if (role == 'success') {
deferred.resolve(data);
} else {
deferred.reject(data);
}
});
return deferred.promise;
} catch(ex) {
return Promise.reject(ex.toString());
}
}
/**
* Get the mimetype and extension to capture media.
*
* @param {string} type Type of media: image, audio, video.
* @param {string[]} [mimetypes] List of supported mimetypes. If undefined, all mimetypes supported.
* @return {{extension: string, mimetype: string}} An object with mimetype and extension to use.
*/
protected getMimeTypeAndExtension(type: string, mimetypes) : {extension: string, mimetype: string} {
var result: any = {};
if (mimetypes && mimetypes.length) {
// Search for a supported mimetype.
for (let i = 0; i < mimetypes.length; i++) {
let mimetype = mimetypes[i],
matches = mimetype.match(new RegExp('^' + type + '/'));
if (matches && matches.length && this.win.MediaRecorder.isTypeSupported(mimetype)) {
result.mimetype = mimetype;
break;
}
}
}
if (result.mimetype) {
// Found a supported mimetype in the mimetypes array, get the extension.
result.extension = this.mimeUtils.getExtension(result.mimetype);
} else if (type == 'video') {
// No mimetype found, use default extension.
result.mimetype = this.videoMimeType;
result.extension = this.possibleVideoMimeTypes[result.mimetype];
} else if (type == 'audio') {
// No mimetype found, use default extension.
result.mimetype = this.audioMimeType;
result.extension = this.possibleAudioMimeTypes[result.mimetype];
}
return result;
}
/**
* Init the getUserMedia function, using a deprecated function as fallback if the new one doesn't exist.
*
* @return {boolean} Whether the function is supported.
*/
protected initGetUserMedia() : boolean {
let nav = <any>navigator;
// Check if there is a function to get user media.
if (typeof nav.mediaDevices == 'undefined') {
nav.mediaDevices = {};
}
if (!nav.mediaDevices.getUserMedia) {
// New function doesn't exist, check if the deprecated function is supported.
nav.getUserMedia = nav.getUserMedia || nav.webkitGetUserMedia || nav.mozGetUserMedia || nav.msGetUserMedia;
if (nav.getUserMedia) {
// Deprecated function exists, support the new function using the deprecated one.
navigator.mediaDevices.getUserMedia = (constraints) => {
let deferred = this.utils.promiseDefer();
nav.getUserMedia(constraints, deferred.resolve, deferred.reject);
return deferred.promise;
};
} else {
return false;
}
}
return true;
}
/**
* Initialize the mimetypes to use when capturing.
*/
protected initMimeTypes() : void {
// Determine video and audio mimetype to use.
for (let mimeType in this.possibleVideoMimeTypes) {
if (this.win.MediaRecorder.isTypeSupported(mimeType)) {
this.videoMimeType = mimeType;
break;
}
}
for (let mimeType in this.possibleAudioMimeTypes) {
if (this.win.MediaRecorder.isTypeSupported(mimeType)) {
this.audioMimeType = mimeType;
break;
}
}
}
/**
* Load the Mocks that need it.
*
* @return {Promise<void>} Promise resolved when loaded.
*/
load() : Promise<void> {
if (typeof this.win.MediaRecorder != 'undefined' && this.initGetUserMedia()) {
this.initMimeTypes();
}
return Promise.resolve();
}
}

View File

@ -296,7 +296,7 @@ export class FileTransferObjectMock extends FileTransferObject {
}
}
(<any>xhr).onprogress = (xhr, ev) => {
xhr.onprogress = (ev: ProgressEvent) : any => {
if (this.progressListener) {
this.progressListener(ev);
}

View File

@ -19,9 +19,10 @@ import { File } from '@ionic-native/file';
import { LocalNotifications } from '@ionic-native/local-notifications';
import { CoreInitDelegate, CoreInitHandler } from '../../../providers/init';
import { FileTransferErrorMock } from './file-transfer';
import { CoreEmulatorCaptureHelperProvider } from './capture-helper';
/**
* Emulates the Cordova Zip plugin in desktop apps and in browser.
* Helper service for the emulator feature. It also acts as an init handler.
*/
@Injectable()
export class CoreEmulatorHelperProvider implements CoreInitHandler {
@ -30,7 +31,8 @@ export class CoreEmulatorHelperProvider implements CoreInitHandler {
blocking = true;
constructor(private file: File, private fileProvider: CoreFileProvider, private utils: CoreUtilsProvider,
initDelegate: CoreInitDelegate, private localNotif: LocalNotifications) {}
initDelegate: CoreInitDelegate, private localNotif: LocalNotifications,
private captureHelper: CoreEmulatorCaptureHelperProvider) {}
/**
* Load the Mocks that need it.
@ -44,6 +46,7 @@ export class CoreEmulatorHelperProvider implements CoreInitHandler {
this.fileProvider.setHTMLBasePath(basePath);
}));
promises.push((<any>this.localNotif).load());
promises.push(this.captureHelper.load());
(<any>window).FileTransferError = FileTransferErrorMock;

View File

@ -0,0 +1,58 @@
// (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 { MediaCapture, CaptureAudioOptions, CaptureImageOptions, CaptureVideoOptions } from '@ionic-native/media-capture';
import { CoreEmulatorCaptureHelperProvider } from './capture-helper';
/**
* Emulates the Cordova MediaCapture plugin in desktop apps and in browser.
*/
@Injectable()
export class MediaCaptureMock extends MediaCapture {
constructor(private captureHelper: CoreEmulatorCaptureHelperProvider) {
super();
}
/**
* Start the audio recorder application and return information about captured audio clip files.
*
* @param {CaptureAudioOptions} options Options.
* @return {Promise<any>} Promise resolved when captured.
*/
captureAudio(options: CaptureAudioOptions) : Promise<any> {
return this.captureHelper.captureMedia('audio', options);
}
/**
* Start the camera application and return information about captured image files.
*
* @param {CaptureImageOptions} options Options.
* @return {Promise<any>} Promise resolved when captured.
*/
captureImage(options: CaptureImageOptions) : Promise<any> {
return this.captureHelper.captureMedia('captureimage', options);
}
/**
* Start the video recorder application and return information about captured video clip files.
*
* @param {CaptureVideoOptions} options Options.
* @return {Promise<any>} Promise resolved when captured.
*/
captureVideo(options: CaptureVideoOptions) : Promise<any> {
return this.captureHelper.captureMedia('video', options);
}
}

View File

@ -0,0 +1,51 @@
// (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 { NgModule } from '@angular/core';
import { CoreFileUploaderProvider } from './providers/fileuploader';
import { CoreFileUploaderHelperProvider } from './providers/helper';
import { CoreFileUploaderDelegate } from './providers/delegate';
import { CoreFileUploaderAlbumHandler } from './providers/album-handler';
import { CoreFileUploaderAudioHandler } from './providers/audio-handler';
import { CoreFileUploaderCameraHandler } from './providers/camera-handler';
import { CoreFileUploaderFileHandler } from './providers/file-handler';
import { CoreFileUploaderVideoHandler } from './providers/video-handler';
@NgModule({
declarations: [
],
imports: [
],
providers: [
CoreFileUploaderProvider,
CoreFileUploaderHelperProvider,
CoreFileUploaderDelegate,
CoreFileUploaderAlbumHandler,
CoreFileUploaderAudioHandler,
CoreFileUploaderCameraHandler,
CoreFileUploaderFileHandler,
CoreFileUploaderVideoHandler
]
})
export class CoreFileUploaderModule {
constructor(delegate: CoreFileUploaderDelegate, albumHandler: CoreFileUploaderAlbumHandler,
audioHandler: CoreFileUploaderAudioHandler, cameraHandler: CoreFileUploaderCameraHandler,
videoHandler: CoreFileUploaderVideoHandler, fileHandler: CoreFileUploaderFileHandler) {
delegate.registerHandler(albumHandler);
delegate.registerHandler(audioHandler);
delegate.registerHandler(cameraHandler);
delegate.registerHandler(fileHandler);
delegate.registerHandler(videoHandler);
}
}

View File

@ -0,0 +1,28 @@
{
"addfiletext": "Add file",
"audio": "Audio",
"camera": "Camera",
"confirmuploadfile": "You are about to upload {{size}}. Are you sure you want to continue?",
"confirmuploadunknownsize": "It was not possible to calculate the size of the upload. Are you sure you want to continue?",
"errorcapturingaudio": "Error capturing audio.",
"errorcapturingimage": "Error capturing image.",
"errorcapturingvideo": "Error capturing video.",
"errorgettingimagealbum": "Error getting image from album.",
"errormustbeonlinetoupload": "You have to be online to upload files.",
"errornoapp": "You don't have an app installed to perform this action.",
"errorreadingfile": "Error reading file.",
"errorwhileuploading": "An error occurred during the file upload.",
"file": "File",
"fileuploaded": "The file was successfully uploaded.",
"filesofthesetypes": "Accepted file types:",
"invalidfiletype": "{{$a}} filetype cannot be accepted.",
"maxbytesfile": "The file {{$a.file}} is too large. The maximum size you can upload is {{$a.size}}.",
"more": "More",
"photoalbums": "Photo albums",
"readingfile": "Reading file",
"selectafile": "Select a file",
"uploadafile": "Upload a file",
"uploading": "Uploading",
"uploadingperc": "Uploading: {{$a}}%",
"video": "Video"
}

View File

@ -0,0 +1,71 @@
// (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 { CoreAppProvider } from '../../../providers/app';
import { CoreUtilsProvider } from '../../../providers/utils/utils';
import { CoreFileUploaderHandler, CoreFileUploaderHandlerData } from './delegate';
import { CoreFileUploaderHelperProvider } from './helper';
/**
* Handler to upload files from the album.
*/
@Injectable()
export class CoreFileUploaderAlbumHandler implements CoreFileUploaderHandler {
name = 'CoreFileUploaderAlbum';
priority = 2000;
constructor(private appProvider: CoreAppProvider, private utils: CoreUtilsProvider,
private uploaderHelper: CoreFileUploaderHelperProvider) {}
/**
* Whether or not the handler is enabled on a site level.
*
* @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
*/
isEnabled(): boolean|Promise<boolean> {
return this.appProvider.isMobile();
}
/**
* Given a list of mimetypes, return the ones that are supported by the handler.
*
* @param {string[]} [mimetypes] List of mimetypes.
* @return {string[]} Supported mimetypes.
*/
getSupportedMimetypes(mimetypes: string[]) : string[] {
// Album allows picking images and videos.
return this.utils.filterByRegexp(mimetypes, /^(image|video)\//);
}
/**
* Get the data to display the handler.
*
* @return {CoreFileUploaderHandlerData} Data.
*/
getData() : CoreFileUploaderHandlerData {
return {
title: 'core.fileuploader.photoalbums',
class: 'core-fileuploader-album-handler',
icon: 'images',
action: (maxSize?: number, upload?: boolean, allowOffline?: boolean, mimetypes?: string[]) => {
return this.uploaderHelper.uploadImage(true, maxSize, upload, mimetypes).then((result) => {
return {
treated: true,
result: result
};
});
}
};
}
}

View File

@ -0,0 +1,88 @@
// (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 { Platform } from 'ionic-angular';
import { CoreAppProvider } from '../../../providers/app';
import { CoreUtilsProvider } from '../../../providers/utils/utils';
import { CoreFileUploaderHandler, CoreFileUploaderHandlerData } from './delegate';
import { CoreFileUploaderHelperProvider } from './helper';
/**
* Handler to record an audio to upload it.
*/
@Injectable()
export class CoreFileUploaderAudioHandler implements CoreFileUploaderHandler {
name = 'CoreFileUploaderAudio';
priority = 1600;
constructor(private appProvider: CoreAppProvider, private utils: CoreUtilsProvider, private platform: Platform,
private uploaderHelper: CoreFileUploaderHelperProvider) {}
/**
* Whether or not the handler is enabled on a site level.
*
* @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
*/
isEnabled(): boolean|Promise<boolean> {
return this.appProvider.isMobile() || (this.appProvider.canGetUserMedia() && this.appProvider.canRecordMedia());
}
/**
* Given a list of mimetypes, return the ones that are supported by the handler.
*
* @param {string[]} [mimetypes] List of mimetypes.
* @return {string[]} Supported mimetypes.
*/
getSupportedMimetypes(mimetypes: string[]) : string[] {
if (this.platform.is('ios')) {
// iOS records as WAV.
return this.utils.filterByRegexp(mimetypes, /^audio\/wav$/);
} else if (this.platform.is('android')) {
// In Android we don't know the format the audio will be recorded, so accept any audio mimetype.
return this.utils.filterByRegexp(mimetypes, /^audio\//);
} else {
// In desktop, support audio formats that are supported by MediaRecorder.
let mediaRecorder = (<any>window).MediaRecorder;
if (mediaRecorder) {
return mimetypes.filter((type) => {
let matches = type.match(/^audio\//);
return matches && matches.length && mediaRecorder.isTypeSupported(type);
});
}
}
return [];
}
/**
* Get the data to display the handler.
*
* @return {CoreFileUploaderHandlerData} Data.
*/
getData() : CoreFileUploaderHandlerData {
return {
title: 'core.fileuploader.audio',
class: 'core-fileuploader-audio-handler',
icon: 'microphone',
action: (maxSize?: number, upload?: boolean, allowOffline?: boolean, mimetypes?: string[]) => {
return this.uploaderHelper.uploadAudioOrVideo(true, maxSize, upload, mimetypes).then((result) => {
return {
treated: true,
result: result
};
});
}
};
}
}

View File

@ -0,0 +1,71 @@
// (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 { CoreAppProvider } from '../../../providers/app';
import { CoreUtilsProvider } from '../../../providers/utils/utils';
import { CoreFileUploaderHandler, CoreFileUploaderHandlerData } from './delegate';
import { CoreFileUploaderHelperProvider } from './helper';
/**
* Handler to take a picture to upload it.
*/
@Injectable()
export class CoreFileUploaderCameraHandler implements CoreFileUploaderHandler {
name = 'CoreFileUploaderCamera';
priority = 1800;
constructor(private appProvider: CoreAppProvider, private utils: CoreUtilsProvider,
private uploaderHelper: CoreFileUploaderHelperProvider) {}
/**
* Whether or not the handler is enabled on a site level.
*
* @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
*/
isEnabled(): boolean|Promise<boolean> {
return this.appProvider.isMobile() || this.appProvider.canGetUserMedia();
}
/**
* Given a list of mimetypes, return the ones that are supported by the handler.
*
* @param {string[]} [mimetypes] List of mimetypes.
* @return {string[]} Supported mimetypes.
*/
getSupportedMimetypes(mimetypes: string[]) : string[] {
// Camera only supports JPEG and PNG.
return this.utils.filterByRegexp(mimetypes, /^image\/(jpeg|png)$/);
}
/**
* Get the data to display the handler.
*
* @return {CoreFileUploaderHandlerData} Data.
*/
getData() : CoreFileUploaderHandlerData {
return {
title: 'core.fileuploader.camera',
class: 'core-fileuploader-camera-handler',
icon: 'camera',
action: (maxSize?: number, upload?: boolean, allowOffline?: boolean, mimetypes?: string[]) => {
return this.uploaderHelper.uploadImage(false, maxSize, upload, mimetypes).then((result) => {
return {
treated: true,
result: result
};
});
}
};
}
}

View File

@ -0,0 +1,303 @@
// (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 { CoreEventsProvider } from '../../../providers/events';
import { CoreLoggerProvider } from '../../../providers/logger';
import { CoreSitesProvider } from '../../../providers/sites';
/**
* Interface that all handlers must implement.
*/
export interface CoreFileUploaderHandler {
/**
* A name to identify the addon.
* @type {string}
*/
name: string;
/**
* Handler's priority. The highest priority, the highest position.
* @type {string}
*/
priority?: number;
/**
* Whether or not the handler is enabled on a site level.
*
* @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
*/
isEnabled(): boolean|Promise<boolean>;
/**
* Given a list of mimetypes, return the ones that are supported by the handler.
*
* @param {string[]} [mimetypes] List of mimetypes.
* @return {string[]} Supported mimetypes.
*/
getSupportedMimetypes(mimetypes: string[]) : string[];
/**
* Get the data to display the handler.
*
* @return {CoreFileUploaderHandlerData} Data.
*/
getData() : CoreFileUploaderHandlerData;
};
/**
* Data needed to render the handler in the file picker. It must be returned by the handler.
*/
export interface CoreFileUploaderHandlerData {
/**
* The title to display in the handler.
* @type {string}
*/
title: string;
/**
* The icon to display in the handler.
* @type {string}
*/
icon?: string;
/**
* The class to assign to the handler item.
* @type {string}
*/
class?: string;
/**
* Action to perform when the handler is clicked.
*
* @param {number} [maxSize] Max size of the file. If not defined or -1, no max size.
* @param {boolean} [upload] Whether the file should be uploaded.
* @param {boolean} [allowOffline] True to allow selecting in offline, false to require connection.
* @param {string[]} [mimetypes] List of supported mimetypes. If undefined, all mimetypes supported.
* @return {Promise<CoreFileUploaderHandlerResult>} Promise resolved with the result of picking/uploading the file.
*/
action?(maxSize?: number, upload?: boolean, allowOffline?: boolean, mimetypes?: string[])
: Promise<CoreFileUploaderHandlerResult>;
/**
* Function called after the handler is rendered.
*
* @param {number} [maxSize] Max size of the file. If not defined or -1, no max size.
* @param {boolean} [upload] Whether the file should be uploaded.
* @param {boolean} [allowOffline] True to allow selecting in offline, false to require connection.
* @param {string[]} [mimetypes] List of supported mimetypes. If undefined, all mimetypes supported.
*/
afterRender?(maxSize: number, upload: boolean, allowOffline: boolean, mimetypes: string[]) : void;
};
/**
* The result of clicking a handler.
*/
export interface CoreFileUploaderHandlerResult {
/**
* Whether the file was treated (uploaded or copied to tmp folder).
* @type {boolean}
*/
treated: boolean;
/**
* The path of the file picked. Required if treated=false and fileEntry is not set.
* @type {string}
*/
path?: string;
/**
* The fileEntry of the file picked. Required if treated=false and path is not set.
* @type {any}
*/
fileEntry?: any;
/**
* Whether the file should be deleted after the upload. Ignored if treated=true.
* @type {boolean}
*/
delete?: boolean;
/**
* The result of picking/uploading the file. Ignored if treated=false.
* @type {any}
*/
result?: any;
};
/**
* Data returned by the delegate for each handler.
*/
export interface CoreFileUploaderHandlerDataToReturn extends CoreFileUploaderHandlerData {
/**
* Handler's priority.
* @type {number}
*/
priority?: number;
/**
* Supported mimetypes.
* @type {string[]}
*/
mimetypes?: string[];
};
/**
* Delegate to register handlers to be shown in the file picker.
*/
@Injectable()
export class CoreFileUploaderDelegate {
protected logger;
protected handlers: {[s: string]: CoreFileUploaderHandler} = {}; // All registered handlers.
protected enabledHandlers: {[s: string]: CoreFileUploaderHandler} = {}; // Handlers enabled for the current site.
protected lastUpdateHandlersStart: number;
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider) {
this.logger = logger.getInstance('CoreCourseModuleDelegate');
eventsProvider.on(CoreEventsProvider.LOGIN, this.updateHandlers.bind(this));
eventsProvider.on(CoreEventsProvider.SITE_UPDATED, this.updateHandlers.bind(this));
eventsProvider.on(CoreEventsProvider.REMOTE_ADDONS_LOADED, this.updateHandlers.bind(this));
eventsProvider.on(CoreEventsProvider.LOGOUT, this.clearSiteHandlers.bind(this));
}
/**
* Clear current site handlers. Reserved for core use.
*/
protected clearSiteHandlers() : void {
this.enabledHandlers = {};
}
/**
* Get the handlers for the current site.
*
* @param {string[]} [mimetypes] List of supported mimetypes. If undefined, all mimetypes supported.
* @return {CoreFileUploaderHandlerDataToReturn[]} List of handlers data.
*/
getHandlers(mimetypes: string[]) : CoreFileUploaderHandlerDataToReturn[] {
let handlers = [];
for (let name in this.enabledHandlers) {
let handler = this.enabledHandlers[name],
supportedMimetypes;
if (mimetypes) {
if (!handler.getSupportedMimetypes) {
// Handler doesn't implement a required function, don't add it.
return;
}
supportedMimetypes = handler.getSupportedMimetypes(mimetypes);
if (!supportedMimetypes.length) {
// Handler doesn't support any mimetype, don't add it.
return;
}
}
let data : CoreFileUploaderHandlerDataToReturn = handler.getData();
data.priority = handler.priority;
data.mimetypes = supportedMimetypes;
handlers.push(data);
}
return handlers;
}
/**
* Check if a time belongs to the last update handlers call.
* This is to handle the cases where updateHandlers don't finish in the same order as they're called.
*
* @param {number} time Time to check.
* @return {boolean} Whether it's the last call.
*/
isLastUpdateCall(time: number) : boolean {
if (!this.lastUpdateHandlersStart) {
return true;
}
return time == this.lastUpdateHandlersStart;
}
/**
* Register a handler.
*
* @param {CoreFileUploaderHandler} handler The handler to register.
* @return {boolean} True if registered successfully, false otherwise.
*/
registerHandler(handler: CoreFileUploaderHandler) : boolean {
if (typeof this.handlers[handler.name] !== 'undefined') {
this.logger.log(`Addon '${handler.name}' already registered`);
return false;
}
this.logger.log(`Registered addon '${handler.name}'`);
this.handlers[handler.name] = handler;
return true;
}
/**
* Update the handler for the current site.
*
* @param {CoreFileUploaderHandler} handler The handler to check.
* @param {number} time Time this update process started.
* @return {Promise<void>} Resolved when done.
*/
protected updateHandler(handler: CoreFileUploaderHandler, time: number) : Promise<void> {
let promise,
siteId = this.sitesProvider.getCurrentSiteId();
if (!this.sitesProvider.isLoggedIn()) {
promise = Promise.reject(null);
} else {
promise = Promise.resolve(handler.isEnabled());
}
// Checks if the handler is enabled.
return promise.catch(() => {
return false;
}).then((enabled: boolean) => {
// Verify that this call is the last one that was started.
if (this.isLastUpdateCall(time) && this.sitesProvider.getCurrentSiteId() === siteId) {
if (enabled) {
this.enabledHandlers[handler.name] = handler;
} else {
delete this.enabledHandlers[handler.name];
}
}
});
}
/**
* Update the handlers for the current site.
*
* @return {Promise<any>} Resolved when done.
*/
protected updateHandlers() : Promise<any> {
let promises = [],
now = Date.now();
this.logger.debug('Updating handlers for current site.');
this.lastUpdateHandlersStart = now;
// Loop over all the handlers.
for (let name in this.handlers) {
promises.push(this.updateHandler(this.handlers[name], now));
}
return Promise.all(promises).catch(() => {
// Never reject.
});
}
}

View File

@ -0,0 +1,118 @@
// (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 { Platform } from 'ionic-angular';
import { CoreAppProvider } from '../../../providers/app';
import { CoreDomUtilsProvider } from '../../../providers/utils/dom';
import { CoreTimeUtilsProvider } from '../../../providers/utils/time';
import { CoreFileUploaderHandler, CoreFileUploaderHandlerData } from './delegate';
import { CoreFileUploaderHelperProvider } from './helper';
import { CoreFileUploaderProvider } from './fileuploader';
/**
* Handler to upload any type of file.
*/
@Injectable()
export class CoreFileUploaderFileHandler implements CoreFileUploaderHandler {
name = 'CoreFileUploaderFile';
priority = 1200;
constructor(private appProvider: CoreAppProvider, private platform: Platform, private timeUtils: CoreTimeUtilsProvider,
private uploaderHelper: CoreFileUploaderHelperProvider, private uploaderProvider: CoreFileUploaderProvider,
private domUtils: CoreDomUtilsProvider) {}
/**
* Whether or not the handler is enabled on a site level.
*
* @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
*/
isEnabled(): boolean|Promise<boolean> {
return this.platform.is('android') || !this.appProvider.isMobile() ||
(this.platform.is('ios') && this.platform.version().major >= 9);
}
/**
* Given a list of mimetypes, return the ones that are supported by the handler.
*
* @param {string[]} [mimetypes] List of mimetypes.
* @return {string[]} Supported mimetypes.
*/
getSupportedMimetypes(mimetypes: string[]) : string[] {
return mimetypes;
}
/**
* Get the data to display the handler.
*
* @return {CoreFileUploaderHandlerData} Data.
*/
getData() : CoreFileUploaderHandlerData {
const isIOS = this.platform.is('ios');
return {
title: isIOS ? 'core.fileuploader.more' : 'core.fileuploader.file',
class: 'core-fileuploader-file-handler',
icon: isIOS ? 'more' : 'folder',
afterRender: (maxSize: number, upload: boolean, allowOffline: boolean, mimetypes: string[]) => {
// Add an invisible file input in the file handler.
// It needs to be done like this because the action sheet items don't accept inputs.
const element = document.querySelector('.core-fileuploader-file-handler');
if (element) {
const input = document.createElement('input');
input.setAttribute('type', 'file');
if (mimetypes && mimetypes.length && (!this.platform.is('android') || mimetypes.length == 1)) {
// Don't use accept attribute in Android with several mimetypes, it's not supported.
input.setAttribute('accept', mimetypes.join(', '));
}
input.addEventListener('change', (evt: Event) => {
let file = input.files[0],
fileName;
input.value = ''; // Unset input.
if (!file) {
return;
}
// Verify that the mimetype of the file is supported, in case the accept attribute isn't supported.
const error = this.uploaderProvider.isInvalidMimetype(mimetypes, file.name, file.type);
if (error) {
this.domUtils.showErrorModal(error);
return;
}
fileName = file.name;
if (isIOS) {
// Check the name of the file and add a timestamp if needed (take picture).
const matches = fileName.match(/image\.(jpe?g|png)/);
if (matches) {
fileName = 'image_' + this.timeUtils.readableTimestamp() + '.' + matches[1];
}
}
// Upload the picked file.
this.uploaderHelper.uploadFileObject(file, maxSize, upload, allowOffline, fileName).then((result) => {
this.uploaderHelper.fileUploaded(result);
}).catch((error) => {
if (error) {
this.domUtils.showErrorModal(error);
}
});
});
element.appendChild(input);
}
}
};
}
}

View File

@ -0,0 +1,498 @@
// (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 { Platform } from 'ionic-angular';
import { MediaFile } from '@ionic-native/media-capture';
import { TranslateService } from '@ngx-translate/core';
import { CoreFileProvider } from '../../../providers/file';
import { CoreFilepoolProvider } from '../../../providers/filepool';
import { CoreLoggerProvider } from '../../../providers/logger';
import { CoreSitesProvider } from '../../../providers/sites';
import { CoreMimetypeUtilsProvider } from '../../../providers/utils/mimetype';
import { CoreTextUtilsProvider } from '../../../providers/utils/text';
import { CoreTimeUtilsProvider } from '../../../providers/utils/time';
import { CoreUtilsProvider } from '../../../providers/utils/utils';
import { CoreWSFileUploadOptions } from '../../../providers/ws';
/**
* Interface for file upload options.
*/
export interface CoreFileUploaderOptions extends CoreWSFileUploadOptions {
deleteAfterUpload?: boolean; // Whether the file should be deleted after the upload (if success).
};
/**
* Service to upload files.
*/
@Injectable()
export class CoreFileUploaderProvider {
public static LIMITED_SIZE_WARNING = 1048576; // 1 MB.
public static WIFI_SIZE_WARNING = 10485760; // 10 MB.
protected logger;
constructor(logger: CoreLoggerProvider, private fileProvider: CoreFileProvider, private textUtils: CoreTextUtilsProvider,
private utils: CoreUtilsProvider, private sitesProvider: CoreSitesProvider, private timeUtils: CoreTimeUtilsProvider,
private mimeUtils: CoreMimetypeUtilsProvider, private filepoolProvider: CoreFilepoolProvider,
private platform: Platform, private translate: TranslateService) {
this.logger = logger.getInstance('CoreFileUploaderProvider');
}
/**
* Add a dot to the beginning of an extension.
*
* @param {string} extension Extension.
* @return {string} Treated extension.
*/
protected addDot(extension: string) : string {
return '.' + extension;
}
/**
* Compares two file lists and returns if they are different.
*
* @param {any[]} a First file list.
* @param {any[]} b Second file list.
* @return {boolean} Whether both lists are different.
*/
areFileListDifferent(a: any[], b: any[]) : boolean {
a = a || [];
b = b || [];
if (a.length != b.length) {
return true;
}
// Currently we are going to compare the order of the files as well.
// This function can be improved comparing more fields or not comparing the order.
for (let i = 0; i < a.length; i++) {
if ((a[i].name || a[i].filename) != (b[i].name || b[i].filename)) {
return true;
}
}
return false;
}
/**
* Clear temporary attachments to be uploaded.
* Attachments already saved in an offline store will NOT be deleted.
*
* @param {any[]} files List of files.
*/
clearTmpFiles(files: any[]) : void {
// Delete the local files.
files.forEach((file) => {
if (!file.offline && file.remove) {
// Pass an empty function to prevent missing parameter error.
file.remove(() => {});
}
});
}
/**
* Get the upload options for a file taken with the Camera Cordova plugin.
*
* @param {string} uri File URI.
* @param {boolean} [isFromAlbum] True if the image was taken from album, false if it's a new image taken with camera.
* @return {CoreFileUploaderOptions} Options.
*/
getCameraUploadOptions(uri: string, isFromAlbum?: boolean) : CoreFileUploaderOptions {
let extension = this.mimeUtils.getExtension(uri),
mimetype = this.mimeUtils.getMimeType(extension),
isIOS = this.platform.is('ios'),
options: CoreFileUploaderOptions = {
deleteAfterUpload: !isFromAlbum,
mimeType: mimetype
};
if (isIOS && (mimetype == 'image/jpeg' || mimetype == 'image/png')) {
// In iOS, the pictures can have repeated names, even if they come from the album.
options.fileName = 'image_' + this.timeUtils.readableTimestamp() + '.' + extension;
} else {
// Use the same name that the file already has.
options.fileName = this.fileProvider.getFileAndDirectoryFromPath(uri).name;
}
if (isFromAlbum) {
// If the file was picked from the album, delete it only if it was copied to the app's folder.
options.deleteAfterUpload = this.fileProvider.isFileInAppFolder(uri);
if (this.platform.is('android')) {
// Picking an image from album in Android adds a timestamp at the end of the file. Delete it.
options.fileName = options.fileName.replace(/(\.[^\.]*)\?[^\.]*$/, '$1');
}
}
return options;
}
/**
* Get the upload options for a file of any type.
*
* @param {string} uri File URI.
* @param {string} name File name.
* @param {string} type File type.
* @param {boolean} [deleteAfterUpload] Whether the file should be deleted after upload.
* @param {string} [fileArea] File area to upload the file to. It defaults to 'draft'.
* @param {number} [itemId] Draft ID to upload the file to, 0 to create new.
* @return {CoreFileUploaderOptions} Options.
*/
getFileUploadOptions(uri: string, name: string, type: string, deleteAfterUpload?: boolean, fileArea?: string, itemId?: number)
: CoreFileUploaderOptions {
let options : CoreFileUploaderOptions = {};
options.fileName = name;
options.mimeType = type || this.mimeUtils.getMimeType(this.mimeUtils.getFileExtension(options.fileName));
options.deleteAfterUpload = !!deleteAfterUpload;
options.itemId = itemId || 0;
options.fileArea = fileArea;
return options;
}
/**
* Get the upload options for a file taken with the media capture Cordova plugin.
*
* @param {MediaFile} mediaFile File object to upload.
* @return {CoreFileUploaderOptions} Options.
*/
getMediaUploadOptions(mediaFile: MediaFile) : CoreFileUploaderOptions {
let options : CoreFileUploaderOptions = {},
filename = mediaFile.name,
split;
// Add a timestamp to the filename to make it unique.
split = filename.split('.');
split[0] += '_' + this.timeUtils.readableTimestamp();
filename = split.join('.');
options.fileName = filename;
options.deleteAfterUpload = true;
if (mediaFile.type) {
options.mimeType = mediaFile.type;
} else {
options.mimeType = this.mimeUtils.getMimeType(this.mimeUtils.getFileExtension(options.fileName));
}
return options;
}
/**
* Get the files stored in a folder, marking them as offline.
*
* @param {string} folderPath Folder where to get the files.
* @return {Promise<any[]>} Promise resolved with the list of files.
*/
getStoredFiles(folderPath: string) : Promise<any[]> {
return this.fileProvider.getDirectoryContents(folderPath).then((files) => {
return this.markOfflineFiles(files);
});
}
/**
* Get stored files from combined online and offline file object.
*
* @param {{online: any[], offline: number}} filesObject The combined offline and online files object.
* @param {string} folderPath Folder path to get files from.
* @return {Promise<any[]>} Promise resolved with files.
*/
getStoredFilesFromOfflineFilesObject(filesObject: {online: any[], offline: number}, folderPath: string) : Promise<any[]> {
let files = [];
if (filesObject) {
if (filesObject.online && filesObject.online.length > 0) {
files = this.utils.clone(filesObject.online);
}
if (filesObject.offline > 0) {
return this.getStoredFiles(folderPath).then((offlineFiles) => {
return files.concat(offlineFiles);
}).catch(() => {
// Ignore not found files.
return files;
});
}
}
return Promise.resolve(files);
}
/**
* Check if a file's mimetype is invalid based on the list of accepted mimetypes. This function needs either the file's
* mimetype or the file's path/name.
*
* @param {string[]} [mimetypes] List of supported mimetypes. If undefined, all mimetypes supported.
* @param {string} [path] File's path or name.
* @param {string} [mimetype] File's mimetype.
* @return {string} Undefined if file is valid, error message if file is invalid.
*/
isInvalidMimetype(mimetypes?: string[], path?: string, mimetype?: string) : string {
let extension;
if (mimetypes) {
// Verify that the mimetype of the file is supported.
if (mimetype) {
extension = this.mimeUtils.getExtension(mimetype);
} else {
extension = this.mimeUtils.getFileExtension(path);
mimetype = this.mimeUtils.getMimeType(extension);
}
if (mimetype && mimetypes.indexOf(mimetype) == -1) {
extension = extension || this.translate.instant('core.unknown');
return this.translate.instant('core.fileuploader.invalidfiletype', {$a: extension});
}
}
}
/**
* Mark files as offline.
*
* @param {any[]} files Files to mark as offline.
* @return {any[]} Files marked as offline.
*/
markOfflineFiles(files: any[]) : any[] {
// Mark the files as pending offline.
files.forEach((file) => {
file.offline = true;
file.filename = file.name;
});
return files;
}
/**
* Parse filetypeList to get the list of allowed mimetypes and the data to render information.
*
* @param {string} filetypeList Formatted string list where the mimetypes can be checked.
* @return {{info: any[], mimetypes: string[]}} Mimetypes and the filetypes informations.
*/
prepareFiletypeList(filetypeList: string) : {info: any[], mimetypes: string[]} {
let filetypes = filetypeList.split(/[;, ]+/g),
mimetypes = {}, // Use an object to prevent duplicates.
typesInfo = [];
filetypes.forEach((filetype) => {
filetype = filetype.trim();
if (filetype) {
if (filetype.indexOf('/') != -1) {
// It's a mimetype.
typesInfo.push({
name: this.mimeUtils.getMimetypeDescription(filetype),
extlist: this.mimeUtils.getExtensions(filetype).map(this.addDot).join(' ')
});
mimetypes[filetype] = true;
} else if (filetype.indexOf('.') === 0) {
// It's an extension.
let mimetype = this.mimeUtils.getMimeType(filetype);
typesInfo.push({
name: mimetype ? this.mimeUtils.getMimetypeDescription(mimetype) : false,
extlist: filetype
});
if (mimetype) {
mimetypes[mimetype] = true;
}
} else {
// It's a group.
let groupExtensions = this.mimeUtils.getGroupMimeInfo(filetype, 'extensions'),
groupMimetypes = this.mimeUtils.getGroupMimeInfo(filetype, 'mimetypes');
if (groupExtensions.length > 0) {
typesInfo.push({
name: this.mimeUtils.getTranslatedGroupName(filetype),
extlist: groupExtensions ? groupExtensions.map(this.addDot).join(' ') : ''
});
groupMimetypes.forEach((mimetype) => {
if (mimetype) {
mimetypes[mimetype] = true;
}
});
} else {
// Treat them as extensions.
filetype = this.addDot(filetype);
let mimetype = this.mimeUtils.getMimeType(filetype);
typesInfo.push({
name: mimetype ? this.mimeUtils.getMimetypeDescription(mimetype) : false,
extlist: filetype
});
if (mimetype) {
mimetypes[mimetype] = true;
}
}
}
}
});
return {
info: typesInfo,
mimetypes: Object.keys(mimetypes)
};
}
/**
* Given a list of files (either online files or local files), store the local files in a local folder
* to be uploaded later.
*
* @param {string} folderPath Path of the folder where to store the files.
* @param {any[]} files List of files.
* @return {Promise<{online: any[], offline: number}>} Promise resolved if success.
*/
storeFilesToUpload(folderPath: string, files: any[]) : Promise<{online: any[], offline: number}> {
let result = {
online: [],
offline: 0
};
if (!files || !files.length) {
return Promise.resolve(result);
}
// Remove unused files from previous saves.
return this.fileProvider.removeUnusedFiles(folderPath, files).then(() => {
let promises = [];
files.forEach((file) => {
if (file.filename && !file.name) {
// It's an online file, add it to the result and ignore it.
result.online.push({
filename: file.filename,
fileurl: file.fileurl
});
} else if (!file.name) {
// Error.
promises.push(Promise.reject(null));
} else if (file.fullPath && file.fullPath.indexOf(folderPath) != -1) {
// File already in the submission folder.
result.offline++;
} else {
// Local file, copy it. Use copy instead of move to prevent having a unstable state if
// some copies succeed and others don't.
let destFile = this.textUtils.concatenatePaths(folderPath, file.name);
promises.push(this.fileProvider.copyFile(file.toURL(), destFile));
result.offline++;
}
});
return Promise.all(promises).then(() => {
return result;
});
});
}
/**
* Upload a file.
*
* @param {string} uri File URI.
* @param {CoreFileUploaderOptions} [options] Options for the upload.
* @param {Function} [onProgress] Function to call on progress.
* @param {string} [siteId] Id of the site to upload the file to. If not defined, use current site.
* @return {Promise<any>} Promise resolved when done.
*/
uploadFile(uri: string, options?: CoreFileUploaderOptions, onProgress?: (event: ProgressEvent) => any,
siteId?: string) : Promise<any> {
options = options || {};
const deleteAfterUpload = options.deleteAfterUpload,
ftOptions = this.utils.clone(options);
delete ftOptions.deleteAfterUpload;
return this.sitesProvider.getSite(siteId).then((site) => {
return site.uploadFile(uri, ftOptions, onProgress);
}).then((result) => {
if (deleteAfterUpload) {
setTimeout(() => {
// Use set timeout, otherwise in Electron the upload threw an error sometimes.
this.fileProvider.removeExternalFile(uri);
}, 500);
}
return result;
});
}
/**
* Upload a file to a draft area. If the file is an online file it will be downloaded and then re-uploaded.
*
* @param {any} file Online file or local FileEntry.
* @param {number} [itemId] Draft ID to use. Undefined or 0 to create a new draft ID.
* @param {string} [component] The component to set to the downloaded files.
* @param {string|number} [componentId] An ID to use in conjunction with the component.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<number>} Promise resolved with the itemId.
*/
uploadOrReuploadFile(file: any, itemId?: number, component?: string, componentId?: string|number,
siteId?: string) : Promise<number> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
let promise,
fileName;
if (file.filename && !file.name) {
// It's an online file. We need to download it and re-upload it.
fileName = file.filename;
promise = this.filepoolProvider.downloadUrl(siteId, file.url || file.fileurl, false, component, componentId,
file.timemodified, undefined, undefined, file).then((path) => {
return this.fileProvider.getExternalFile(path);
});
} else {
// Local file, we already have the file entry.
fileName = file.name;
promise = Promise.resolve(file);
}
return promise.then((fileEntry) => {
// Now upload the file.
let options = this.getFileUploadOptions(fileEntry.toURL(), fileName, fileEntry.type, true, 'draft', itemId);
return this.uploadFile(fileEntry.toURL(), options, undefined, siteId).then((result) => {
return result.itemid;
});
});
}
/**
* Given a list of files (either online files or local files), upload them to a draft area and return the draft ID.
* Online files will be downloaded and then re-uploaded.
* If there are no files to upload it will return a fake draft ID (1).
*
* @param {any[]} files List of files.
* @param {string} [component] The component to set to the downloaded files.
* @param {string|number} [componentId] An ID to use in conjunction with the component.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<number>} Promise resolved with the itemId.
*/
uploadOrReuploadFiles(files: any[], component?: string, componentId?: string|number, siteId?: string) : Promise<number> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
if (!files || !files.length) {
// Return fake draft ID.
return Promise.resolve(1);
}
// Upload only the first file first to get a draft id.
return this.uploadOrReuploadFile(files[0], 0, component, componentId, siteId).then((itemId) => {
let promises = [];
for (let i = 1; i < files.length; i++) {
let file = files[i];
promises.push(this.uploadOrReuploadFile(file, itemId, component, componentId, siteId));
}
return Promise.all(promises).then(() => {
return itemId;
});
});
}
}

View File

@ -0,0 +1,687 @@
// (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 { ActionSheetController, ActionSheet, Platform } from 'ionic-angular';
import { MediaCapture, MediaFile } from '@ionic-native/media-capture';
import { Camera, CameraOptions } from '@ionic-native/camera';
import { TranslateService } from '@ngx-translate/core';
import { CoreAppProvider } from '../../../providers/app';
import { CoreFileProvider } from '../../../providers/file';
import { CoreLoggerProvider } from '../../../providers/logger';
import { CoreDomUtilsProvider } from '../../../providers/utils/dom';
import { CoreTextUtilsProvider } from '../../../providers/utils/text';
import { CoreUtilsProvider, PromiseDefer } from '../../../providers/utils/utils';
import { CoreFileUploaderProvider, CoreFileUploaderOptions } from './fileuploader';
import { CoreFileUploaderDelegate } from './delegate';
/**
* Helper service to upload files.
*/
@Injectable()
export class CoreFileUploaderHelperProvider {
protected logger;
protected filePickerDeferred: PromiseDefer;
protected actionSheet: ActionSheet;
constructor(logger: CoreLoggerProvider, private appProvider: CoreAppProvider, private translate: TranslateService,
private fileUploaderProvider: CoreFileUploaderProvider, private domUtils: CoreDomUtilsProvider,
private textUtils: CoreTextUtilsProvider, private fileProvider: CoreFileProvider, private utils: CoreUtilsProvider,
private actionSheetCtrl: ActionSheetController, private uploaderDelegate: CoreFileUploaderDelegate,
private mediaCapture: MediaCapture, private camera: Camera, private platform: Platform) {
this.logger = logger.getInstance('CoreFileUploaderProvider');
}
/**
* Show a confirmation modal to the user if the size of the file is bigger than the allowed threshold.
*
* @param {number} size File size.
* @param {boolean} [alwaysConfirm] True to show a confirm even if the size isn't high.
* @param {boolean} [allowOffline] True to allow uploading in offline.
* @param {number} [wifiThreshold] Threshold for WiFi connection. Default: CoreFileUploaderProvider.WIFI_SIZE_WARNING.
* @param {number} [limitedThreshold] Threshold for limited connection. Default: CoreFileUploaderProvider.LIMITED_SIZE_WARNING.
* @return {Promise<void>} Promise resolved when the user confirms or if there's no need to show a modal.
*/
confirmUploadFile(size: number, alwaysConfirm?: boolean, allowOffline?: boolean, wifiThreshold?: number,
limitedThreshold?: number) : Promise<void> {
if (size == 0) {
return Promise.resolve();
}
if (!allowOffline && !this.appProvider.isOnline()) {
return Promise.reject(this.translate.instant('core.fileuploader.errormustbeonlinetoupload'));
}
wifiThreshold = typeof wifiThreshold == 'undefined' ? CoreFileUploaderProvider.WIFI_SIZE_WARNING : wifiThreshold;
limitedThreshold = typeof limitedThreshold == 'undefined' ? CoreFileUploaderProvider.LIMITED_SIZE_WARNING : limitedThreshold;
if (size < 0) {
return this.domUtils.showConfirm(this.translate.instant('core.fileuploader.confirmuploadunknownsize'));
} else if (size >= wifiThreshold || (this.appProvider.isNetworkAccessLimited() && size >= limitedThreshold)) {
let readableSize = this.textUtils.bytesToSize(size, 2);
return this.domUtils.showConfirm(this.translate.instant('core.fileuploader.confirmuploadfile', {size: readableSize}));
} else if (alwaysConfirm) {
return this.domUtils.showConfirm(this.translate.instant('core.areyousure'));
} else {
return Promise.resolve();
}
}
/**
* Create a temporary copy of a file and upload it.
*
* @param {any} file File to copy and upload.
* @param {boolean} [upload] True if the file should be uploaded, false to return the copy of the file.
* @param {string} [name] Name to use when uploading the file. If not defined, use the file's name.
* @return {Promise<any>} Promise resolved when the file is uploaded.
*/
copyAndUploadFile(file: any, upload?: boolean, name?: string) : Promise<any> {
name = name || file.name;
let modal = this.domUtils.showModalLoading('core.fileuploader.readingfile', true),
fileData;
// We have the data of the file to be uploaded, but not its URL (needed). Create a copy of the file to upload it.
return this.fileProvider.readFileData(file, this.fileProvider.FORMATARRAYBUFFER).then((data) => {
fileData = data;
// Get unique name for the copy.
return this.fileProvider.getUniqueNameInFolder(this.fileProvider.TMPFOLDER, name);
}).then((newName) => {
let filePath = this.textUtils.concatenatePaths(this.fileProvider.TMPFOLDER, newName);
return this.fileProvider.writeFile(filePath, fileData);
}).catch((error) => {
this.logger.error('Error reading file to upload.', error);
modal.dismiss();
return Promise.reject(this.translate.instant('core.fileuploader.errorreadingfile'));
}).then((fileEntry) => {
modal.dismiss();
if (upload) {
// Pass true to delete the copy after the upload.
return this.uploadGenericFile(fileEntry.toURL(), name, file.type, true);
} else {
return fileEntry;
}
});
}
/**
* Copy or move a file to the app temporary folder.
*
* @param {string} path Path of the file.
* @param {boolean} shouldDelete True if original file should be deleted (move), false otherwise (copy).
* @param {number} [maxSize] Max size of the file. If not defined or -1, no max size.
* @param {string} [defaultExt] Defaut extension to use if the file doesn't have any.
* @return {Promise<any>} Promise resolved with the copied file.
*/
protected copyToTmpFolder(path: string, shouldDelete: boolean, maxSize?: number, defaultExt?: string) : Promise<any> {
let fileName = this.fileProvider.getFileAndDirectoryFromPath(path).name,
promise,
fileTooLarge;
// Check that size isn't too large.
if (typeof maxSize != 'undefined' && maxSize != -1) {
promise = this.fileProvider.getExternalFile(path).then((fileEntry) => {
return this.fileProvider.getFileObjectFromFileEntry(fileEntry).then((file) => {
if (file.size > maxSize) {
fileTooLarge = file;
}
});
}).catch(() => {
// Ignore failures.
});
} else {
promise = Promise.resolve();
}
return promise.then(() => {
if (fileTooLarge) {
return this.errorMaxBytes(maxSize, fileTooLarge.name);
}
// File isn't too large.
// Picking an image from album in Android adds a timestamp at the end of the file. Delete it.
fileName = fileName.replace(/(\.[^\.]*)\?[^\.]*$/, '$1');
// Get a unique name in the folder to prevent overriding another file.
return this.fileProvider.getUniqueNameInFolder(this.fileProvider.TMPFOLDER, fileName, defaultExt);
}).then((newName) => {
// Now move or copy the file.
const destPath = this.textUtils.concatenatePaths(this.fileProvider.TMPFOLDER, newName);
if (shouldDelete) {
return this.fileProvider.moveExternalFile(path, destPath);
} else {
return this.fileProvider.copyExternalFile(path, destPath);
}
});
}
/**
* Function called when trying to upload a file bigger than max size. Shows an error.
*
* @param {number} maxSize Max size (bytes).
* @param {string} fileName Name of the file.
* @return {Promise<any>} Rejected promise.
*/
protected errorMaxBytes(maxSize: number, fileName: string) : Promise<any> {
let errorMessage = this.translate.instant('core.fileuploader.maxbytesfile', {$a: {
file: fileName,
size: this.textUtils.bytesToSize(maxSize, 2)
}});
this.domUtils.showErrorModal(errorMessage);
return Promise.reject(null);
}
/**
* Function called when the file picker is closed.
*/
filePickerClosed() : void {
if (this.filePickerDeferred) {
this.filePickerDeferred.reject();
this.filePickerDeferred = undefined;
}
// Close the action sheet if it's opened.
if (this.actionSheet) {
this.actionSheet.dismiss();
}
}
/**
* Function to call once a file is uploaded using the file picker.
*
* @param {any} result Result of the upload process.
*/
fileUploaded(result: any) : void {
if (this.filePickerDeferred) {
this.filePickerDeferred.resolve(result);
this.filePickerDeferred = undefined;
}
// Close the action sheet if it's opened.
if (this.actionSheet) {
this.actionSheet.dismiss();
}
}
/**
* Open the "file picker" to select and upload a file.
*
* @param {number} [maxSize] Max size of the file to upload. If not defined or -1, no max size.
* @param {string} [title] File picker title.
* @param {string[]} [mimetypes] List of supported mimetypes. If undefined, all mimetypes supported.
* @return {Promise<any>} Promise resolved when a file is uploaded, rejected if file picker is closed without a file uploaded.
* The resolve value is the response of the upload request.
*/
selectAndUploadFile(maxSize?: number, title?: string, mimetypes?: string[]) : Promise<any> {
return this.selectFileWithPicker(maxSize, false, title, mimetypes, true);
}
/**
* Open the "file picker" to select a file without uploading it.
*
* @param {number} [maxSize] Max size of the file. If not defined or -1, no max size.
* @param {boolean} [allowOffline] True to allow selecting in offline, false to require connection.
* @param {string} [title] File picker title.
* @param {string[]} [mimetypes] List of supported mimetypes. If undefined, all mimetypes supported.
* @return {Promise<any>} Promise resolved when a file is selected, rejected if file picker is closed without selecting a file.
* The resolve value is the FileEntry of a copy of the picked file, so it can be deleted afterwards.
*/
selectFile(maxSize?: number, allowOffline?: boolean, title?: string, mimetypes?: string[])
: Promise<any> {
return this.selectFileWithPicker(maxSize, allowOffline, title, mimetypes, false);
}
/**
* Open the "file picker" to select a file and maybe uploading it.
*
* @param {number} [maxSize] Max size of the file. If not defined or -1, no max size.
* @param {boolean} [allowOffline] True to allow selecting in offline, false to require connection.
* @param {string} [title] File picker title.
* @param {string[]} [mimetypes] List of supported mimetypes. If undefined, all mimetypes supported.
* @param {boolean} [upload] Whether the file should be uploaded.
* @return {Promise<any>} Promise resolved when a file is selected/uploaded, rejected if file picker is closed.
*/
protected selectFileWithPicker(maxSize?: number, allowOffline?: boolean, title?: string, mimetypes?: string[],
upload?: boolean) : Promise<any> {
// Create the cancel button and get the handlers to upload the file.
let buttons: any[] = [{
text: this.translate.instant('core.cancel'),
role: 'cancel',
handler: () => {
// User cancelled the action sheet.
this.filePickerClosed();
}
}],
handlers = this.uploaderDelegate.getHandlers(mimetypes);
this.filePickerDeferred = this.utils.promiseDefer();
// Sort the handlers by priority.
handlers.sort((a, b) => {
return a.priority <= b.priority ? 1 : -1;
});
// Create a button for each handler.
handlers.forEach((handler) => {
buttons.push({
text: this.translate.instant(handler.title),
icon: handler.icon,
cssClass: handler.class,
handler: () => {
if (!handler.action) {
// Nothing to do.
return false;
}
if (!allowOffline && !this.appProvider.isOnline()) {
// Not allowed, show error.
this.domUtils.showErrorModal('core.fileuploader.errormustbeonlinetoupload', true);
return false;
}
handler.action(maxSize, upload, allowOffline, handler.mimetypes).then((data) => {
if (data.treated) {
// The handler already treated the file. Return the result.
return data.result;
} else {
// The handler didn't treat the file, we need to do it.
if (data.fileEntry) {
// The handler provided us a fileEntry, use it.
return this.uploadFileEntry(data.fileEntry, data.delete, maxSize, upload, allowOffline);
} else if (data.path) {
// The handler provided a path. First treat it like it's a relative path.
return this.fileProvider.getFile(data.path).catch(() => {
// File not found, it's probably an absolute path.
return this.fileProvider.getExternalFile(data.path);
}).then((fileEntry) => {
// File found, treat it.
return this.uploadFileEntry(fileEntry, data.delete, maxSize, upload, allowOffline);
});
}
// Nothing received, fail.
return Promise.reject('No file received');
}
}).then((result) => {
// Success uploading or picking, return the result.
this.fileUploaded(result);
}).catch((error) => {
if (error) {
this.domUtils.showErrorModal(error);
}
});
// Do not close the action sheet, it will be closed if success.
return false;
}
});
});
this.actionSheet = this.actionSheetCtrl.create({
title: title ? title : this.translate.instant('core.fileuploader.' + (upload ? 'uploadafile' : 'selectafile')),
buttons: buttons
});
this.actionSheet.present();
// Call afterRender for each button.
setTimeout(() => {
handlers.forEach((handler) => {
if (handler.afterRender) {
handler.afterRender(maxSize, upload, allowOffline, handler.mimetypes);
}
});
}, 500);
return this.filePickerDeferred.promise;
}
/**
* Convenience function to upload a file on a certain site, showing a confirm if needed.
*
* @param {any} fileEntry FileEntry of the file to upload.
* @param {boolean} [deleteAfterUpload] Whether the file should be deleted after upload.
* @param {string} [siteId] Id of the site to upload the file to. If not defined, use current site.
* @return {Promise<any>} Promise resolved when the file is uploaded.
*/
showConfirmAndUploadInSite(fileEntry: any, deleteAfterUpload?: boolean, siteId?: string) : Promise<void> {
return this.fileProvider.getFileObjectFromFileEntry(fileEntry).then((file) => {
return this.confirmUploadFile(file.size).then(() => {
return this.uploadGenericFile(fileEntry.toURL(), file.name, file.type, deleteAfterUpload, siteId).then(() => {
this.domUtils.showAlertTranslated('core.success', 'core.fileuploader.fileuploaded');
});
}).catch((err) => {
if (err) {
this.domUtils.showErrorModal(err);
}
return Promise.reject(null);
});
}, () => {
this.domUtils.showErrorModal('core.fileuploader.errorreadingfile', true);
return Promise.reject(null);
});
}
/**
* Treat a capture audio/video error.
*
* @param {any} error Error returned by the Cordova plugin. Can be a string or an object.
* @param {string} defaultMessage Key of the default message to show.
* @return {Promise<any>} Rejected promise. If it doesn't have an error message it means it was cancelled.
*/
protected treatCaptureError(error: any, defaultMessage: string) : Promise<any> {
// Cancelled or error. If cancelled, error is an object with code = 3.
if (error) {
if (typeof error === 'string') {
this.logger.error('Error while recording audio/video: ' + error);
if (error.indexOf('No Activity found') > -1) {
// User doesn't have an app to do this.
return Promise.reject(this.translate.instant('core.fileuploader.errornoapp'));
} else {
return Promise.reject(this.translate.instant(defaultMessage));
}
} else {
if (error.code != 3) {
// Error, not cancelled.
this.logger.error('Error while recording audio/video', error);
return Promise.reject(this.translate.instant(defaultMessage));
} else {
this.logger.debug('Cancelled');
}
}
}
return Promise.reject(null);
}
/**
* Treat a capture image or browse album error.
*
* @param {string} error Error returned by the Cordova plugin.
* @param {string} defaultMessage Key of the default message to show.
* @return {Promise<any>} Rejected promise. If it doesn't have an error message it means it was cancelled.
*/
protected treatImageError(error: string, defaultMessage: string) : Promise<any> {
// Cancelled or error.
if (error) {
if (typeof error == 'string') {
if (error.toLowerCase().indexOf('error') > -1 || error.toLowerCase().indexOf('unable') > -1) {
this.logger.error('Error getting image: ' + error);
return Promise.reject(error);
} else {
// User cancelled.
this.logger.debug('Cancelled');
}
} else {
return Promise.reject(this.translate.instant(defaultMessage));
}
}
return Promise.reject(null);
}
/**
* Convenient helper for the user to record and upload a video.
*
* @param {boolean} isAudio True if uploading an audio, false if it's a video.
* @param {number} maxSize Max size of the upload. -1 for no max size.
* @param {boolean} [upload] True if the file should be uploaded, false to return the picked file.
* @param {string[]} [mimetypes] List of supported mimetypes. If undefined, all mimetypes supported.
* @return {Promise<any>} Promise resolved when done.
*/
uploadAudioOrVideo(isAudio: boolean, maxSize: number, upload?: boolean, mimetypes?: string[]) : Promise<any> {
this.logger.debug('Trying to record a video file');
const options = {limit: 1, mimetypes: mimetypes},
promise = isAudio ? this.mediaCapture.captureAudio(options) : this.mediaCapture.captureVideo(options);
// The mimetypes param is only for desktop apps, the Cordova plugin doesn't support it.
return promise.then((medias) => {
// We used limit 1, we only want 1 media.
let media: MediaFile = medias[0],
path = media.fullPath,
error = this.fileUploaderProvider.isInvalidMimetype(mimetypes, path); // Verify that the mimetype is supported.
if (error) {
return Promise.reject(error);
}
if (upload) {
return this.uploadFile(path, maxSize, true, this.fileUploaderProvider.getMediaUploadOptions(media));
} else {
// Copy or move the file to our temporary folder.
return this.copyToTmpFolder(path, true, maxSize);
}
}, (error) => {
const defaultError = isAudio ? 'core.fileuploader.errorcapturingaudio' : 'core.fileuploader.errorcapturingvideo';
return this.treatCaptureError(error, defaultError);
});
}
/**
* Uploads a file of any type.
* This function will not check the size of the file, please check it before calling this function.
*
* @param {string} uri File URI.
* @param {string} name File name.
* @param {string} type File type.
* @param {boolean} [deleteAfterUpload] Whether the file should be deleted after upload.
* @param {string} [siteId] Id of the site to upload the file to. If not defined, use current site.
* @return {Promise<any>} Promise resolved when the file is uploaded.
*/
uploadGenericFile(uri: string, name: string, type: string, deleteAfterUpload?: boolean, siteId?: string) : Promise<any> {
let options = this.fileUploaderProvider.getFileUploadOptions(uri, name, type, deleteAfterUpload);
return this.uploadFile(uri, -1, false, options, siteId);
}
/**
* Convenient helper for the user to upload an image, either from the album or taking it with the camera.
*
* @param {Boolean} fromAlbum True if the image should be selected from album, false if it should be taken with camera.
* @param {Number} maxSize Max size of the upload. -1 for no max size.
* @param {Boolean} upload True if the image should be uploaded, false to return the picked file.
* @param {string[]} [mimetypes] List of supported mimetypes. If undefined, all mimetypes supported.
* @return {Promise} The reject contains the error message, if there is no error message
* then we can consider that this is a silent fail.
*/
uploadImage(fromAlbum, maxSize, upload, mimetypes) {
this.logger.debug('Trying to capture an image with camera');
let options: CameraOptions = {
quality: 50,
destinationType: this.camera.DestinationType.FILE_URI,
correctOrientation: true
};
if (fromAlbum) {
const imageSupported = !mimetypes || this.utils.indexOfRegexp(mimetypes, /^image\//) > -1,
videoSupported = !mimetypes || this.utils.indexOfRegexp(mimetypes, /^video\//) > -1;
options.sourceType = this.camera.PictureSourceType.PHOTOLIBRARY;
options.popoverOptions = {
x: 10,
y: 10,
width: this.platform.width() - 200,
height: this.platform.height() - 200,
arrowDir: this.camera.PopoverArrowDirection.ARROW_ANY
};
// Determine the mediaType based on the mimetypes.
if (imageSupported && !videoSupported) {
options.mediaType = this.camera.MediaType.PICTURE;
} else if (!imageSupported && videoSupported) {
options.mediaType = this.camera.MediaType.VIDEO;
} else if (this.platform.is('ios')) {
// Only get all media in iOS because in Android using this option allows uploading any kind of file.
options.mediaType = this.camera.MediaType.ALLMEDIA;
}
} else if (mimetypes) {
if (mimetypes.indexOf('image/jpeg') > -1) {
options.encodingType = this.camera.EncodingType.JPEG;
} else if (mimetypes.indexOf('image/png') > -1) {
options.encodingType = this.camera.EncodingType.PNG;
}
}
return this.camera.getPicture(options).then((path) => {
let error = this.fileUploaderProvider.isInvalidMimetype(mimetypes, path); // Verify that the mimetype is supported.
if (error) {
return Promise.reject(error);
}
if (upload) {
return this.uploadFile(path, maxSize, true, this.fileUploaderProvider.getCameraUploadOptions(path, fromAlbum));
} else {
// Copy or move the file to our temporary folder.
return this.copyToTmpFolder(path, !fromAlbum, maxSize, 'jpg');
}
}, (error) => {
let defaultError = fromAlbum ? 'core.fileuploader.errorgettingimagealbum' : 'core.fileuploader.errorcapturingimage';
return this.treatImageError(error, defaultError);
});
}
/**
* Upload a file given the file entry.
*
* @param {any} fileEntry The file entry.
* @param {boolean} deleteAfter True if the file should be deleted once treated.
* @param {number} [maxSize] Max size of the file. If not defined or -1, no max size.
* @param {boolean} [upload] True if the file should be uploaded, false to return the picked file.
* @param {boolean} [allowOffline] True to allow selecting in offline, false to require connection.
* @param {string} [name] Name to use when uploading the file. If not defined, use the file's name.
* @return {Promise<any>} Promise resolved when done.
*/
uploadFileEntry(fileEntry: any, deleteAfter: boolean, maxSize?: number, upload?: boolean, allowOffline?: boolean,
name?: string) : Promise<any> {
return this.fileProvider.getFileObjectFromFileEntry(fileEntry).then((file) => {
return this.uploadFileObject(file, maxSize, upload, allowOffline, name).then((result) => {
if (deleteAfter) {
// We have uploaded and deleted a copy of the file. Now delete the original one.
this.fileProvider.removeFileByFileEntry(fileEntry);
}
return result;
});
});
}
/**
* Upload a file given the file object.
*
* @param {any} file The file object.
* @param {number} [maxSize] Max size of the file. If not defined or -1, no max size.
* @param {boolean} [upload] True if the file should be uploaded, false to return the picked file.
* @param {boolean} [allowOffline] True to allow selecting in offline, false to require connection.
* @param {string} [name] Name to use when uploading the file. If not defined, use the file's name.
* @return {Promise<any>} Promise resolved when done.
*/
uploadFileObject(file: any, maxSize?: number, upload?: boolean, allowOffline?: boolean, name?: string) : Promise<any> {
if (maxSize != -1 && file.size > maxSize) {
return this.errorMaxBytes(maxSize, file.name);
}
return this.confirmUploadFile(file.size, false, allowOffline).then(() => {
// We have the data of the file to be uploaded, but not its URL (needed). Create a copy of the file to upload it.
return this.copyAndUploadFile(file, upload, name);
});
}
/**
* Convenience function to upload a file, allowing to retry if it fails.
*
* @param {string} path Absolute path of the file to upload.
* @param {number} maxSize Max size of the upload. -1 for no max size.
* @param {boolean} checkSize True to check size.
* @param {CoreFileUploaderOptions} Options.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved if the file is uploaded, rejected otherwise.
*/
protected uploadFile(path: string, maxSize: number, checkSize: boolean, options: CoreFileUploaderOptions, siteId?: string)
: Promise<any> {
let errorStr = this.translate.instant('core.error'),
retryStr = this.translate.instant('core.retry'),
uploadingStr = this.translate.instant('core.fileuploader.uploading'),
promise,
file,
errorUploading = (error) => {
// Allow the user to retry.
return this.domUtils.showConfirm(error, errorStr, retryStr).then(() => {
// Try again.
return this.uploadFile(path, maxSize, checkSize, options, siteId);
}, () => {
// User cancelled. Delete the file if needed.
if (options.deleteAfterUpload) {
this.fileProvider.removeExternalFile(path);
}
return Promise.reject(null);
});
};
if (!this.appProvider.isOnline()) {
return errorUploading(this.translate.instant('core.fileuploader.errormustbeonlinetoupload'));
}
if (checkSize) {
// Check that file size is the right one.
promise = this.fileProvider.getExternalFile(path).then((fileEntry) => {
return this.fileProvider.getFileObjectFromFileEntry(fileEntry).then((f) => {
file = f;
return file.size;
});
}).catch(() => {
// Ignore failures.
});
} else {
promise = Promise.resolve(0);
}
return promise.then((size) => {
if (maxSize != -1 && size > maxSize) {
return this.errorMaxBytes(maxSize, file.name);
}
if (size > 0) {
return this.confirmUploadFile(size);
}
}).then(() => {
// File isn't too large and user confirmed, let's upload.
let modal = this.domUtils.showModalLoading(uploadingStr);
return this.fileUploaderProvider.uploadFile(path, options, (progress: ProgressEvent) => {
// Progress uploading.
if (progress && progress.lengthComputable) {
let perc = Math.min((progress.loaded / progress.total) * 100, 100);
if (perc >= 0) {
modal.setContent(this.translate.instant('core.fileuploader.uploadingperc', {$a: perc.toFixed(1)}));
if (modal._cmp && modal._cmp.changeDetectorRef) {
// Force a change detection, otherwise the content is not updated.
modal._cmp.changeDetectorRef.detectChanges();
}
}
}
}, siteId).catch((error) => {
this.logger.error('Error uploading file.', error);
modal.dismiss();
if (typeof error != 'string') {
error = this.translate.instant('core.fileuploader.errorwhileuploading');
}
return errorUploading(error);
}).finally(() => {
modal.dismiss();
});
});
}
}

View File

@ -0,0 +1,88 @@
// (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 { Platform } from 'ionic-angular';
import { CoreAppProvider } from '../../../providers/app';
import { CoreUtilsProvider } from '../../../providers/utils/utils';
import { CoreFileUploaderHandler, CoreFileUploaderHandlerData } from './delegate';
import { CoreFileUploaderHelperProvider } from './helper';
/**
* Handler to record a video to upload it.
*/
@Injectable()
export class CoreFileUploaderVideoHandler implements CoreFileUploaderHandler {
name = 'CoreFileUploaderVideo';
priority = 1400;
constructor(private appProvider: CoreAppProvider, private utils: CoreUtilsProvider, private platform: Platform,
private uploaderHelper: CoreFileUploaderHelperProvider) {}
/**
* Whether or not the handler is enabled on a site level.
*
* @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
*/
isEnabled(): boolean|Promise<boolean> {
return this.appProvider.isMobile() || (this.appProvider.canGetUserMedia() && this.appProvider.canRecordMedia());
}
/**
* Given a list of mimetypes, return the ones that are supported by the handler.
*
* @param {string[]} [mimetypes] List of mimetypes.
* @return {string[]} Supported mimetypes.
*/
getSupportedMimetypes(mimetypes: string[]) : string[] {
if (this.platform.is('ios')) {
// iOS records as MOV.
return this.utils.filterByRegexp(mimetypes, /^video\/quicktime$/);
} else if (this.platform.is('android')) {
// In Android we don't know the format the video will be recorded, so accept any video mimetype.
return this.utils.filterByRegexp(mimetypes, /^video\//);
} else {
// In desktop, support video formats that are supported by MediaRecorder.
let mediaRecorder = (<any>window).MediaRecorder;
if (mediaRecorder) {
return mimetypes.filter(function(type) {
let matches = type.match(/^video\//);
return matches && matches.length && mediaRecorder.isTypeSupported(type);
});
}
}
return [];
}
/**
* Get the data to display the handler.
*
* @return {CoreFileUploaderHandlerData} Data.
*/
getData() : CoreFileUploaderHandlerData {
return {
title: 'core.fileuploader.video',
class: 'core-fileuploader-video-handler',
icon: 'videocam',
action: (maxSize?: number, upload?: boolean, allowOffline?: boolean, mimetypes?: string[]) => {
return this.uploaderHelper.uploadAudioOrVideo(false, maxSize, upload, mimetypes).then((result) => {
return {
treated: true,
result: result
};
});
}
};
}
}

View File

@ -236,7 +236,7 @@ export class CoreLoginEmailSignupPage {
if (result.success) {
// Show alert and ho back.
let message = this.translate.instant('core.login.emailconfirmsent', {$a: params.email});
this.domUtils.showAlert('core.success', message);
this.domUtils.showAlert(this.translate.instant('core.success'), message);
this.navCtrl.pop();
} else {
if (result.warnings && result.warnings.length) {

View File

@ -80,7 +80,7 @@ export class CoreMainMenuPage implements OnDestroy {
}
this.subscription = this.menuDelegate.getHandlers().subscribe((handlers) => {
this.tabs = handlers.slice(0, CoreMainMenuProvider.NUM_MAIN_HANDLERS); // Get main handlers.
handlers = handlers.slice(0, CoreMainMenuProvider.NUM_MAIN_HANDLERS); // Get main handlers.
// Check if handlers are already in tabs. Add the ones that aren't.
// @todo: https://github.com/ionic-team/ionic/issues/13633

View File

@ -0,0 +1,11 @@
{
"chooseaccountstorefile": "Choose an account to store the file in.",
"chooseactionrepeatedfile": "A file with this name already exists. Do you want to replace the existing file or rename it to \"{{$a}}\"?",
"errorreceivefilenosites": "There are no sites stored. Please add a site before sharing a file with the app.",
"nosharedfiles": "There are no shared files stored on this site.",
"nosharedfilestoupload": "You have no files to upload here. If you want to upload a file from another app, locate the file and click the 'Open in' button.",
"rename": "Rename",
"replace": "Replace",
"sharedfiles": "Shared files",
"successstorefile": "File successfully stored. Select the file to upload to your private files or use in an activity."
}

View File

@ -0,0 +1,24 @@
<ion-header>
<ion-navbar>
<ion-title>{{ 'core.sharedfiles.sharedfiles' | translate }}</ion-title>
</ion-navbar>
</ion-header>
<ion-content>
<core-loading [hideUntil]="loaded">
<ion-list>
<ion-item text-wrap>
<p class="item-heading">{{ 'core.sharedfiles.chooseaccountstorefile' | translate }}</p>
<p>{{fileName}}</p>
</ion-item>
<a ion-item *ngFor="let site of sites" (click)="storeInSite(site.id)">
<img [src]="site.avatar" item-start>
<h2>{{site.fullName}}</h2>
<p><core-format-text clean="true" [text]="site.siteName"></core-format-text></p>
<p>{{site.siteUrl}}</p>
</a>
</ion-list>
</core-loading>
</ion-content>

View File

@ -0,0 +1,33 @@
// (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 { NgModule } from '@angular/core';
import { IonicPageModule } from 'ionic-angular';
import { CoreSharedFilesChooseSitePage } from './choose-site';
import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '../../../../components/components.module';
import { CoreDirectivesModule } from '../../../../directives/directives.module';
@NgModule({
declarations: [
CoreSharedFilesChooseSitePage
],
imports: [
CoreComponentsModule,
CoreDirectivesModule,
IonicPageModule.forChild(CoreSharedFilesChooseSitePage),
TranslateModule.forChild()
]
})
export class CoreSharedFilesChooseSitePageModule {}

View File

@ -0,0 +1,89 @@
// (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 { Component, OnInit } from '@angular/core';
import { IonicPage, NavController, NavParams } from 'ionic-angular';
import { CoreFileProvider } from '../../../../providers/file';
import { CoreSitesProvider } from '../../../../providers/sites';
import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
import { CoreSharedFilesHelperProvider } from '../../providers/helper';
/**
* Modal to display the list of sites to choose one to store a shared file.
*/
@IonicPage()
@Component({
selector: 'page-core-shared-files-choose-site',
templateUrl: 'choose-site.html',
})
export class CoreSharedFilesChooseSitePage implements OnInit {
fileName: string;
sites: any[];
loaded: boolean;
protected filePath: string;
protected fileEntry: any;
constructor(private navCtrl: NavController, navParams: NavParams, private sharedFilesHelper: CoreSharedFilesHelperProvider,
private sitesProvider: CoreSitesProvider, private domUtils: CoreDomUtilsProvider,
private fileProvider: CoreFileProvider) {
this.filePath = navParams.get('filePath');
}
/**
* Component being initialized.
*/
ngOnInit() {
if (!this.filePath) {
this.domUtils.showErrorModal('Error reading file.');
this.navCtrl.pop();
return;
}
let fileAndDir = this.fileProvider.getFileAndDirectoryFromPath(this.filePath);
this.fileName = fileAndDir.name;
// Get the file.
this.fileProvider.getFile(this.filePath).then((fe) => {
this.fileEntry = fe;
this.fileName = this.fileEntry.name;
}).catch(() => {
this.domUtils.showErrorModal('Error reading file.');
this.navCtrl.pop();
});
// Get the sites.
this.sitesProvider.getSites().then((sites) => {
this.sites = sites;
}).finally(() => {
this.loaded = true;
});
}
/**
* Store the file in a certain site.
*
* @param {string} siteId Site ID.
*/
storeInSite(siteId: string) : void {
this.loaded = false;
this.sharedFilesHelper.storeSharedFileInSite(this.fileEntry, siteId).then(() => {
this.navCtrl.pop();
}).finally(() => {
this.loaded = true;
});
};
}

View File

@ -0,0 +1,32 @@
<ion-header>
<ion-navbar>
<ion-title><core-format-text [text]="title"></core-format-text></ion-title>
<ion-buttons end *ngIf="isModal">
<button ion-button icon-only (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
<ion-icon name="close"></ion-icon>
</button>
</ion-buttons>
</ion-navbar>
</ion-header>
<ion-content>
<ion-refresher [enabled]="filesLoaded" (ionRefresh)="refreshFiles($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<!-- Allow selecting the site to view. -->
<core-site-picker [hidden]="!filesLoaded" [initialSite]="siteId" (siteSelected)="changeSite($event)"></core-site-picker>
<ion-item-divider color="light"></ion-item-divider>
<core-loading [hideUntil]="filesLoaded">
<ion-list *ngIf="files && files.length > 0">
<div *ngFor="let file of files; let idx = index">
<core-local-file *ngIf="file.isFile" [file]="file" [manage]="manage" [overrideClick]="pick" (onClick)="filePicked(file)" (onDelete)="fileDeleted(idx)" (onRename)="fileRenamed(idx, $event)"></core-local-file>
<a ion-item text-wrap class="item-media" *ngIf="!file.isFile" (click)="openFolder(file)">
<img src="assets/img/files/folder-64.png" alt="{{ 'core.folder' | translate }}" role="presentation" item-start>
<p>{{ file.name }}</p>
</a>
</div>
</ion-list>
<core-empty-box *ngIf="files && !files.length && manage" icon="folder" [message]="'core.sharedfiles.nosharedfiles' | translate"></core-empty-box>
<core-empty-box *ngIf="files && !files.length && !manage" icon="folder" [message]="'core.sharedfiles.nosharedfilestoupload' | translate"></core-empty-box>
</core-loading>
</ion-content>

View File

@ -0,0 +1,33 @@
// (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 { NgModule } from '@angular/core';
import { IonicPageModule } from 'ionic-angular';
import { CoreSharedFilesListPage } from './list';
import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '../../../../components/components.module';
import { CoreDirectivesModule } from '../../../../directives/directives.module';
@NgModule({
declarations: [
CoreSharedFilesListPage
],
imports: [
CoreComponentsModule,
CoreDirectivesModule,
IonicPageModule.forChild(CoreSharedFilesListPage),
TranslateModule.forChild()
]
})
export class CoreSharedFilesListPageModule {}

View File

@ -0,0 +1,181 @@
// (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 { Component, OnInit, OnDestroy } from '@angular/core';
import { IonicPage, ViewController, NavParams, NavController } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { CoreEventsProvider } from '../../../../providers/events';
import { CoreFileProvider } from '../../../../providers/file';
import { CoreSitesProvider } from '../../../../providers/sites';
import { CoreTextUtilsProvider } from '../../../../providers/utils/text';
import { CoreSharedFilesProvider } from '../../providers/sharedfiles';
/**
* Modal to display the list of shared files.
*/
@IonicPage()
@Component({
selector: 'page-core-shared-files-list',
templateUrl: 'list.html',
})
export class CoreSharedFilesListPage implements OnInit, OnDestroy {
siteId: string;
isModal: boolean;
manage: boolean;
pick: boolean; // To pick a file you MUST use a modal.
path: string = '';
title: string;
filesLoaded: boolean;
files: any[];
protected mimetypes: string[];
protected shareObserver;
constructor(private viewCtrl: ViewController, navParams: NavParams, private sharedFilesProvider: CoreSharedFilesProvider,
private sitesProvider: CoreSitesProvider, private textUtils: CoreTextUtilsProvider, private translate: TranslateService,
private fileProvider: CoreFileProvider, private eventsProvider: CoreEventsProvider, private navCtrl: NavController) {
this.siteId = navParams.get('siteId') || this.sitesProvider.getCurrentSiteId();
this.mimetypes = navParams.get('mimetypes');
this.isModal = !!navParams.get('isModal');
this.manage = !!navParams.get('manage');
this.pick = !!navParams.get('pick');
this.path = navParams.get('path') || '';
}
/**
* Component being initialized.
*/
ngOnInit() {
this.loadFiles();
// Listen for new files shared with the app.
this.shareObserver = this.eventsProvider.on(CoreEventsProvider.FILE_SHARED, (data) => {
if (data.siteId == this.siteId) {
// File was stored in current site, refresh the list.
this.filesLoaded = false;
this.loadFiles().finally(() => {
this.filesLoaded = true;
});
}
});
}
/**
* Load the files.
*/
protected loadFiles() {
if (this.path) {
this.title = this.fileProvider.getFileAndDirectoryFromPath(this.path).name;
} else {
this.title = this.translate.instant('core.sharedfiles.sharedfiles');
}
return this.sharedFilesProvider.getSiteSharedFiles(this.siteId, this.path, this.mimetypes).then((files) => {
this.files = files;
this.filesLoaded = true;
});
}
/**
* Close modal.
*/
closeModal() : void {
this.viewCtrl.dismiss();
}
/**
* Refresh the list of files.
*
* @param {any} refresher Refresher.
*/
refreshFiles(refresher: any) : void {
this.loadFiles().finally(() => {
refresher.complete();
});
}
/**
* Called when a file is deleted. Remove the file from the list.
*
* @param {number} index Position of the file.
*/
fileDeleted(index: number) : void {
this.files.splice(index, 1);
}
/**
* Called when a file is renamed. Update the list.
*
* @param {number} index Position of the file.
* @param {any} file New FileEntry.
*/
fileRenamed(index: number, file: any) : void {
this.files[index] = file;
}
/**
* Open a subfolder.
*
* @param {any} folder The folder to open.
*/
openFolder(folder: any) : void {
let path = this.textUtils.concatenatePaths(this.path, folder.name);
if (this.isModal) {
// In Modal we don't want to open a new page because we cannot dismiss the modal from the new page.
this.path = path;
this.filesLoaded = false;
this.loadFiles();
} else {
this.navCtrl.push('CoreSharedFilesListPage', {
path: path,
manage: this.manage,
pick: this.pick,
siteId: this.siteId,
mimetypes: this.mimetypes,
isModal: this.isModal
});
}
}
/**
* Change site loaded.
*
* @param {string} id Site to load.
*/
changeSite(id: string) : void {
this.siteId = id;
this.path = '';
this.filesLoaded = false;
this.loadFiles();
}
/**
* A file was picked.
*
* @param {any} file Picked file.
*/
filePicked(file: any) : void {
this.viewCtrl.dismiss(file);
}
/**
* Component destroyed.
*/
ngOnDestroy() {
if (this.shareObserver) {
this.shareObserver.off();
}
}
}

View File

@ -0,0 +1,176 @@
// (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 { AlertController, ModalController } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { CoreAppProvider } from '../../../providers/app';
import { CoreFileProvider } from '../../../providers/file';
import { CoreLoggerProvider } from '../../../providers/logger';
import { CoreInitDelegate } from '../../../providers/init';
import { CoreSitesProvider } from '../../../providers/sites';
import { CoreDomUtilsProvider } from '../../../providers/utils/dom';
import { CoreUtilsProvider } from '../../../providers/utils/utils';
import { CoreSharedFilesProvider } from './sharedfiles';
import { CoreFileUploaderProvider } from '../../fileuploader/providers/fileuploader';
/**
* Helper service to share files with the app.
*/
@Injectable()
export class CoreSharedFilesHelperProvider {
protected logger;
constructor(logger: CoreLoggerProvider, private alertCtrl: AlertController, private translate: TranslateService,
private utils: CoreUtilsProvider, private sitesProvider: CoreSitesProvider, private modalCtrl: ModalController,
private fileUploaderProvider: CoreFileUploaderProvider, private initDelegate: CoreInitDelegate,
private sharedFilesProvider: CoreSharedFilesProvider, private domUtils: CoreDomUtilsProvider,
private fileProvider: CoreFileProvider, private appProvider: CoreAppProvider) {
this.logger = logger.getInstance('CoreSharedFilesHelperProvider');
}
/**
* Ask a user if he wants to replace a file (using originalName) or rename it (using newName).
*
* @param {string} originalName Original name.
* @param {string} newName New name.
* @return {Promise<string>} Promise resolved with the name to use when the user chooses. Rejected if user cancels.
*/
askRenameReplace(originalName: string, newName: string) : Promise<string> {
const deferred = this.utils.promiseDefer(),
alert = this.alertCtrl.create({
title: this.translate.instant('core.sharedfiles.sharedfiles'),
message: this.translate.instant('core.sharedfiles.chooseactionrepeatedfile', {$a: newName}),
buttons: [
{
text: this.translate.instant('core.sharedfiles.rename'),
handler: () => {
deferred.resolve(newName);
}
},
{
text: this.translate.instant('core.sharedfiles.replace'),
handler: () => {
deferred.resolve(originalName);
}
}
]
});
alert.present();
return deferred.promise;
}
/**
* Go to the choose site view.
*
* @param {string} filePath File path to send to the view.
*/
goToChooseSite(filePath: string) : void {
let navCtrl = this.appProvider.getRootNavController();
navCtrl.push('CoreSharedFilesChooseSitePage', {filePath: filePath});
}
/**
* Open the view to select a shared file.
*
* @param {string[]} [mimetypes] List of supported mimetypes. If undefined, all mimetypes supported.
* @return {Promise<any>} Promise resolved when a file is picked, rejected if file picker is closed without selecting a file.
*/
pickSharedFile(mimetypes?: string[]) : Promise<any> {
return new Promise((resolve, reject) => {
let modal = this.modalCtrl.create('CoreSharedFilesListPage', {mimetypes: mimetypes, isModal: true, pick: true});
modal.present();
modal.onDidDismiss((file: any) => {
if (!file) {
// User cancelled.
reject();
return;
}
const error = this.fileUploaderProvider.isInvalidMimetype(mimetypes, file.fullPath);
if (error) {
reject(error);
} else {
resolve({
path: file.fullPath,
treated: false
});
}
})
});
}
/**
* Checks if there is a new file received in iOS and move it to the shared folder of current site.
* If more than one site is found, the user will have to choose the site where to store it in.
* If more than one file is found, treat only the first one.
*
* @return {Promise<any>} Promise resolved when done.
*/
searchIOSNewSharedFiles() : Promise<any> {
return this.initDelegate.ready().then(() => {
let navCtrl = this.appProvider.getRootNavController();
if (navCtrl && navCtrl.getActive().id == 'CoreSharedFilesChooseSite') {
// We're already treating a shared file. Abort.
return Promise.reject(null);
}
return this.sharedFilesProvider.checkIOSNewFiles().then((fileEntry) => {
return this.sitesProvider.getSitesIds().then((siteIds) => {
if (!siteIds.length) {
// No sites stored, show error and delete the file.
this.domUtils.showErrorModal('core.sharedfiles.errorreceivefilenosites', true);
return this.sharedFilesProvider.deleteInboxFile(fileEntry);
} else if (siteIds.length == 1) {
return this.storeSharedFileInSite(fileEntry, siteIds[0]);
} else {
this.goToChooseSite(fileEntry.fullPath);
}
});
});
});
}
/**
* Store a shared file in a site's shared files folder.
*
* @param {any} fileEntry Shared file entry.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when done.
*/
storeSharedFileInSite(fileEntry: any, siteId?: string) : Promise<any> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
// First of all check if there's already a file with the same name in the shared files folder.
const sharedFilesDirPath = this.sharedFilesProvider.getSiteSharedFilesDirPath(siteId);
return this.fileProvider.getUniqueNameInFolder(sharedFilesDirPath, fileEntry.name).then((newName) => {
if (newName == fileEntry.name) {
// No file with the same name. Use the original file name.
return newName;
} else {
// Repeated name. Ask the user what he wants to do.
return this.askRenameReplace(fileEntry.name, newName);
}
}).then((name) => {
return this.sharedFilesProvider.storeFileInSite(fileEntry, name, siteId).catch(function(err) {
this.domUtils.showErrorModal(err || 'Error moving file.');
}).finally(() => {
this.sharedFilesProvider.deleteInboxFile(fileEntry);
this.domUtils.showAlertTranslated('core.success', 'core.sharedfiles.successstorefile');
});
});
}
}

View File

@ -0,0 +1,243 @@
// (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 { CoreAppProvider } from '../../../providers/app';
import { CoreEventsProvider } from '../../../providers/events';
import { CoreFileProvider } from '../../../providers/file';
import { CoreLoggerProvider } from '../../../providers/logger';
import { CoreSitesProvider } from '../../../providers/sites';
import { CoreMimetypeUtilsProvider } from '../../../providers/utils/mimetype';
import { CoreTextUtilsProvider } from '../../../providers/utils/text';
import { Md5 } from 'ts-md5/dist/md5';
import { SQLiteDB } from '../../../classes/sqlitedb';
/**
* Service to share files with the app.
*/
@Injectable()
export class CoreSharedFilesProvider {
public static SHARED_FILES_FOLDER = 'sharedfiles';
// Variables for the database.
protected SHARED_FILES_TABLE = 'wscache';
protected tableSchema = {
name: this.SHARED_FILES_TABLE,
columns: [
{
name: 'id',
type: 'TEXT',
primaryKey: true
}
]
}
protected logger;
protected appDB: SQLiteDB;
constructor(logger: CoreLoggerProvider, private fileProvider: CoreFileProvider, appProvider: CoreAppProvider,
private textUtils: CoreTextUtilsProvider, private mimeUtils: CoreMimetypeUtilsProvider,
private sitesProvider: CoreSitesProvider, private eventsProvider: CoreEventsProvider) {
this.logger = logger.getInstance('CoreSharedFilesProvider');
this.appDB = appProvider.getDB();
this.appDB.createTableFromSchema(this.tableSchema);
}
/**
* Checks if there is a new file received in iOS. If more than one file is found, treat only the first one.
* The file returned is marked as "treated" and will be deleted in the next execution.
*
* @return {Promise<any>} Promise resolved with a new file to be treated. If no new files found, promise is rejected.
*/
checkIOSNewFiles() : Promise<any> {
this.logger.debug('Search for new files on iOS');
return this.fileProvider.getDirectoryContents('Inbox').then((entries) => {
if (entries.length > 0) {
let promises = [],
fileToReturn;
entries.forEach((entry) => {
const fileId = this.getFileId(entry);
// Check if file was already treated.
promises.push(this.isFileTreated(fileId).then(() => {
// File already treated, delete it. Don't return delete promise, we'll ignore errors.
this.deleteInboxFile(entry);
}).catch(() => {
// File not treated before.
this.logger.debug('Found new file ' + entry.name + ' shared with the app.');
if (!fileToReturn) {
fileToReturn = entry;
}
}));
});
return Promise.all(promises).then(() => {
let fileId;
if (fileToReturn) {
// Mark it as "treated".
fileId = this.getFileId(fileToReturn);
return this.markAsTreated(fileId).then(() => {
this.logger.debug('File marked as "treated": ' + fileToReturn.name);
return fileToReturn;
});
} else {
return Promise.reject(null);
}
});
} else {
return Promise.reject(null);
}
});
}
/**
* Deletes a file in the Inbox folder (shared with the app).
*
* @param {any} entry FileEntry.
* @return {Promise<any>} Promise resolved when done, rejected otherwise.
*/
deleteInboxFile(entry: any) : Promise<any> {
this.logger.debug('Delete inbox file: ' + entry.name);
return this.fileProvider.removeFileByFileEntry(entry).catch(() => {
// Ignore errors.
}).then(() => {
return this.unmarkAsTreated(this.getFileId(entry)).then(() => {
this.logger.debug('"Treated" mark removed from file: ' + entry.name);
}).catch((error) => {
this.logger.debug('Error deleting "treated" mark from file: ' + entry.name, error);
return Promise.reject(error);
});
});
}
/**
* Get the ID of a file for managing "treated" files.
*
* @param {any} entry FileEntry.
* @return {string} File ID.
*/
protected getFileId(entry: any) : string {
return <string> Md5.hashAsciiStr(entry.name);
}
/**
* Get the shared files stored in a site.
*
* @param {string} [siteId] Site ID. If not defined, current site.
* @param {string} [path] Path to search inside the site shared folder.
* @param {string[]} [mimetypes] List of supported mimetypes. If undefined, all mimetypes supported.
* @return {Promise<any[]>} Promise resolved with the files.
*/
getSiteSharedFiles(siteId?: string, path?: string, mimetypes?: string[]) : Promise<any[]> {
let pathToGet = this.getSiteSharedFilesDirPath(siteId);
if (path) {
pathToGet = this.textUtils.concatenatePaths(pathToGet, path);
}
return this.fileProvider.getDirectoryContents(pathToGet).then((files) => {
if (mimetypes) {
// Only show files with the right mimetype and the ones we cannot determine the mimetype.
files = files.filter((file) => {
const extension = this.mimeUtils.getFileExtension(file.name),
mimetype = this.mimeUtils.getMimeType(extension);
return !mimetype || mimetypes.indexOf(mimetype) > -1;
});
}
return files;
}).catch(() => {
// Directory not found, return empty list.
return [];
});
}
/**
* Get the path to a site's shared files folder.
*
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {string} Path.
*/
getSiteSharedFilesDirPath(siteId?: string) : string {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
return this.fileProvider.getSiteFolder(siteId) + '/' + CoreSharedFilesProvider.SHARED_FILES_FOLDER;
}
/**
* Check if a file has been treated already.
*
* @param {string} fileId File ID.
* @return {Promise<any>} Resolved if treated, rejected otherwise.
*/
protected isFileTreated(fileId: string) : Promise<any> {
return this.appDB.getRecord(this.SHARED_FILES_TABLE, {id: fileId});
}
/**
* Mark a file as treated.
*
* @param {string} fileId File ID.
* @return {Promise<any>} Promise resolved when marked.
*/
protected markAsTreated(fileId: string) : Promise<any> {
// Check if it's already marked.
return this.isFileTreated(fileId).catch(() => {
// Doesn't exist, insert it.
return this.appDB.insertRecord(this.SHARED_FILES_TABLE, {id: fileId});
});
}
/**
* Store a file in a site's shared folder.
*
* @param {any} entry File entry.
* @param {string} [newName] Name of the new file. If not defined, use original file's name.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>}Promise resolved when done.
*/
storeFileInSite(entry: any, newName?: string, siteId?: string) : Promise<any> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
if (!entry || !siteId) {
return Promise.reject(null);
}
newName = newName || entry.name;
const sharedFilesFolder = this.getSiteSharedFilesDirPath(siteId),
newPath = this.textUtils.concatenatePaths(sharedFilesFolder, newName);
// Create dir if it doesn't exist already.
return this.fileProvider.createDir(sharedFilesFolder).then(() => {
return this.fileProvider.moveFile(entry.fullPath, newPath).then((newFile) => {
this.eventsProvider.trigger(CoreEventsProvider.FILE_SHARED, {siteId: siteId, name: newName});
return newFile;
});
});
}
/**
* Unmark a file as treated.
*
* @param {string} fileId File ID.
* @return {Promise<any>} Resolved when unmarked.
*/
protected unmarkAsTreated(fileId: string) : Promise<any> {
return this.appDB.deleteRecords(this.SHARED_FILES_TABLE, {id: fileId});
}
}

View File

@ -0,0 +1,64 @@
// (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 { Platform } from 'ionic-angular';
import { CoreFileUploaderHandler, CoreFileUploaderHandlerData } from '../../fileuploader/providers/delegate';
import { CoreSharedFilesHelperProvider } from './helper';
/**
* Handler to upload files from the album.
*/
@Injectable()
export class CoreSharedFilesUploadHandler implements CoreFileUploaderHandler {
name = 'CoreSharedFilesUpload';
priority = 1300;
constructor(private sharedFilesHelper: CoreSharedFilesHelperProvider, private platform: Platform) {}
/**
* Whether or not the handler is enabled on a site level.
*
* @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
*/
isEnabled(): boolean|Promise<boolean> {
return this.platform.is('ios');
}
/**
* Given a list of mimetypes, return the ones that are supported by the handler.
*
* @param {string[]} [mimetypes] List of mimetypes.
* @return {string[]} Supported mimetypes.
*/
getSupportedMimetypes(mimetypes: string[]) : string[] {
return mimetypes;
}
/**
* Get the data to display the handler.
*
* @return {CoreFileUploaderHandlerData} Data.
*/
getData() : CoreFileUploaderHandlerData {
return {
title: 'core.sharedfiles.sharedfiles',
class: 'core-sharedfiles-fileuploader-handler',
icon: 'folder',
action: (maxSize?: number, upload?: boolean, allowOffline?: boolean, mimetypes?: string[]) => {
// Don't use the params because the file won't be uploaded, it is returned to the fileuploader.
return this.sharedFilesHelper.pickSharedFile(mimetypes);
}
};
}
}

View File

@ -0,0 +1,47 @@
// (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 { NgModule } from '@angular/core';
import { Platform } from 'ionic-angular';
import { CoreSharedFilesProvider } from './providers/sharedfiles';
import { CoreSharedFilesHelperProvider } from './providers/helper';
import { CoreSharedFilesUploadHandler } from './providers/upload-handler';
import { CoreFileUploaderDelegate } from '../fileuploader/providers/delegate';
@NgModule({
declarations: [
],
imports: [
],
providers: [
CoreSharedFilesProvider,
CoreSharedFilesHelperProvider,
CoreSharedFilesUploadHandler
]
})
export class CoreSharedFilesModule {
constructor(platform: Platform, delegate: CoreFileUploaderDelegate, handler: CoreSharedFilesUploadHandler,
helper: CoreSharedFilesHelperProvider) {
// Register the handler.
delegate.registerHandler(handler);
if (platform.is('ios')) {
// Check if there are new files at app start and when the app is resumed.
helper.searchIOSNewSharedFiles();
platform.resume.subscribe(() => {
helper.searchIOSNewSharedFiles();
});
}
}
}

View File

@ -54,6 +54,9 @@ export class CoreSecondsToHMSPipe implements PipeTransform {
seconds = numberSeconds;
}
// Don't allow decimals.
seconds = Math.floor(seconds);
hours = Math.floor(seconds / CoreConstants.secondsHour);
seconds -= hours * CoreConstants.secondsHour;
minutes = Math.floor(seconds / CoreConstants.secondsMinute);

View File

@ -46,6 +46,7 @@ export class CoreEventsProvider {
public static IAB_LOAD_START = 'inappbrowser_load_start';
public static IAB_EXIT = 'inappbrowser_exit';
public static APP_LAUNCHED_URL = 'app_launched_url'; // App opened with a certain URL (custom URL scheme).
public static FILE_SHARED = 'file_shared';
logger;
observables: {[s: string] : Subject<any>} = {};

View File

@ -457,7 +457,7 @@ export class CoreFileProvider {
// Create file (and parent folders) to prevent errors.
return this.createFile(path).then((fileEntry) => {
if (this.isHTMLAPI && this.appProvider.isDesktop() &&
if (this.isHTMLAPI && !this.appProvider.isDesktop() &&
(typeof data == 'string' || data.toString() == '[object ArrayBuffer]')) {
// We need to write Blobs.
let type = this.mimeUtils.getMimeType(this.mimeUtils.getFileExtension(path));
@ -832,7 +832,7 @@ export class CoreFileProvider {
* @param {string} [defaultExt] Default extension to use if no extension found in the file.
* @return {Promise<string>} Promise resolved with the unique file name.
*/
getUniqueNameInFolder(dirPath: string, fileName: string, defaultExt: string) : Promise<string> {
getUniqueNameInFolder(dirPath: string, fileName: string, defaultExt?: string) : Promise<string> {
// Get existing files in the folder.
return this.getDirectoryContents(dirPath).then((entries) => {
let files = {},
@ -923,4 +923,14 @@ export class CoreFileProvider {
// Ignore errors, maybe it doesn't exist.
});
}
/**
* Check if a file is inside the app's folder.
*
* @param {string} path The absolute path of the file to check.
* @return {boolean} Whether the file is in the app's folder.
*/
isFileInAppFolder(path: string) : boolean {
return path.indexOf(this.basePath) != -1;
}
}

View File

@ -259,6 +259,33 @@ export class CoreUtilsProvider {
}
}
/**
* Clone a variable. It should be an object, array or primitive type.
*
* @param {any} source The variable to clone.
* @return {any} Cloned variable.
*/
clone(source: any) : any {
if (Array.isArray(source)) {
// Clone the array and all the entries.
let newArray = [];
for (let i = 0; i < source.length; i++) {
newArray[i] = this.clone(source[i]);
}
return newArray;
} else if (typeof source == 'object') {
// Clone the object and all the subproperties.
let newObject = {};
for (let name in source) {
newObject[name] = this.clone(source[name]);
}
return newObject;
} else {
// Primitive type or unknown, return it as it is.
return source;
}
}
/**
* Copy properties from one object to another.
*