commit
6e899ad5a8
File diff suppressed because it is too large
Load Diff
|
@ -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",
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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 {}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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.
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -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],
|
||||
|
|
|
@ -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>
|
||||
|
|
@ -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 {}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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."
|
||||
}
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
@ -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 {}
|
|
@ -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;
|
||||
});
|
||||
};
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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 {}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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});
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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>} = {};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
Loading…
Reference in New Issue