MOBILE-3585 core: Implement fileuploader

main
Dani Palou 2020-11-06 15:32:00 +01:00
parent 105430c877
commit f6a64ae122
16 changed files with 2373 additions and 11 deletions

View File

@ -19,7 +19,14 @@ import { CoreApp } from '@services/app';
import { CoreDB } from '@services/db';
import { CoreEvents } from '@singletons/events';
import { CoreFile } from '@services/file';
import { CoreWS, CoreWSPreSets, CoreWSFileUploadOptions, CoreWSAjaxPreSets, CoreWSExternalWarning } from '@services/ws';
import {
CoreWS,
CoreWSPreSets,
CoreWSFileUploadOptions,
CoreWSAjaxPreSets,
CoreWSExternalWarning,
CoreWSUploadFileResult,
} from '@services/ws';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreTextUtils } from '@services/utils/text';
import { CoreTimeUtils } from '@services/utils/time';
@ -1070,16 +1077,16 @@ export class CoreSite {
* @param onProgress Function to call on progress.
* @return Promise resolved when uploaded.
*/
uploadFile<T = unknown>(
uploadFile(
filePath: string,
options: CoreWSFileUploadOptions,
onProgress?: (event: ProgressEvent) => void,
): Promise<T> {
): Promise<CoreWSUploadFileResult> {
if (!options.fileArea) {
options.fileArea = 'draft';
}
return CoreWS.instance.uploadFile<T>(filePath, options, {
return CoreWS.instance.uploadFile(filePath, options, {
siteUrl: this.siteUrl,
wsToken: this.token || '',
}, onProgress);

View File

@ -0,0 +1,53 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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 { CoreFileUploaderDelegate } from './services/fileuploader.delegate';
import { CoreFileUploaderAlbumHandler } from './services/handlers/album';
import { CoreFileUploaderAudioHandler } from './services/handlers/audio';
import { CoreFileUploaderCameraHandler } from './services/handlers/camera';
import { CoreFileUploaderFileHandler } from './services/handlers/file';
import { CoreFileUploaderVideoHandler } from './services/handlers/video';
@NgModule({
imports: [],
declarations: [],
providers: [
CoreFileUploaderAlbumHandler,
CoreFileUploaderAudioHandler,
CoreFileUploaderCameraHandler,
CoreFileUploaderFileHandler,
CoreFileUploaderVideoHandler,
],
})
export class CoreFileUploaderInitModule {
constructor(
delegate: CoreFileUploaderDelegate,
albumHandler: CoreFileUploaderAlbumHandler,
audioHandler: CoreFileUploaderAudioHandler,
cameraHandler: CoreFileUploaderCameraHandler,
videoHandler: CoreFileUploaderVideoHandler,
fileHandler: CoreFileUploaderFileHandler,
) {
delegate.registerHandler(albumHandler);
delegate.registerHandler(audioHandler);
delegate.registerHandler(cameraHandler);
delegate.registerHandler(videoHandler);
delegate.registerHandler(fileHandler);
}
}

View File

@ -0,0 +1,29 @@
{
"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",
"readingfileperc": "Reading file: {{$a}}%",
"selectafile": "Select a file",
"uploadafile": "Upload a file",
"uploading": "Uploading",
"uploadingperc": "Uploading: {{$a}}%",
"video": "Video"
}

View File

@ -0,0 +1,198 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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 { FileEntry } from '@ionic-native/file';
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
import { CoreEvents } from '@singletons/events';
import { CoreWSUploadFileResult } from '@services/ws';
/**
* Interface that all handlers must implement.
*/
export interface CoreFileUploaderHandler extends CoreDelegateHandler {
/**
* Handler's priority. The highest priority, the highest position.
*/
priority?: number;
/**
* Given a list of mimetypes, return the ones that are supported by the handler.
*
* @param mimetypes List of mimetypes.
* @return Supported mimetypes.
*/
getSupportedMimetypes(mimetypes: string[]): string[];
/**
* Get the data to display the handler.
*
* @return 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.
*/
title: string;
/**
* The icon to display in the handler.
*/
icon?: string;
/**
* The class to assign to the handler item.
*/
class?: string;
/**
* Action to perform when the handler is clicked.
*
* @param maxSize Max size of the file. If not defined or -1, no max size.
* @param upload Whether the file should be uploaded.
* @param allowOffline True to allow selecting in offline, false to require connection.
* @param mimetypes List of supported mimetypes. If undefined, all mimetypes supported.
* @return 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 maxSize Max size of the file. If not defined or -1, no max size.
* @param upload Whether the file should be uploaded.
* @param allowOffline True to allow selecting in offline, false to require connection.
* @param 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).
*/
treated: boolean;
/**
* The path of the file picked. Required if treated=false and fileEntry is not set.
*/
path?: string;
/**
* The fileEntry of the file picked. Required if treated=false and path is not set.
*/
fileEntry?: FileEntry;
/**
* Whether the file should be deleted after the upload. Ignored if treated=true.
*/
delete?: boolean;
/**
* The result of picking/uploading the file. Ignored if treated=false.
*/
result?: CoreWSUploadFileResult | FileEntry;
}
/**
* Data returned by the delegate for each handler.
*/
export interface CoreFileUploaderHandlerDataToReturn extends CoreFileUploaderHandlerData {
/**
* Handler's priority.
*/
priority?: number;
/**
* Supported mimetypes.
*/
mimetypes?: string[];
}
/**
* Delegate to register handlers to be shown in the file picker.
*/
@Injectable({
providedIn: 'root',
})
export class CoreFileUploaderDelegate extends CoreDelegate<CoreFileUploaderHandler> {
constructor() {
super('CoreFileUploaderDelegate', true);
CoreEvents.on(CoreEvents.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 mimetypes List of supported mimetypes. If undefined, all mimetypes supported.
* @return List of handlers data.
*/
getHandlers(mimetypes?: string[]): CoreFileUploaderHandlerDataToReturn[] {
const handlers: CoreFileUploaderHandlerDataToReturn[] = [];
for (const name in this.enabledHandlers) {
const handler = this.enabledHandlers[name];
let supportedMimetypes: string[] | undefined;
if (mimetypes) {
if (!handler.getSupportedMimetypes) {
// Handler doesn't implement a required function, don't add it.
continue;
}
supportedMimetypes = handler.getSupportedMimetypes(mimetypes);
if (!supportedMimetypes.length) {
// Handler doesn't support any mimetype, don't add it.
continue;
}
}
const data: CoreFileUploaderHandlerDataToReturn = handler.getData();
data.priority = handler.priority;
data.mimetypes = supportedMimetypes;
handlers.push(data);
}
// Sort them by priority.
handlers.sort((a, b) => (a.priority || 0) <= (b.priority || 0) ? 1 : -1);
return handlers;
}
}

View File

@ -0,0 +1,875 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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 { ActionSheetButton } from '@ionic/core';
import { CameraOptions } from '@ionic-native/camera/ngx';
import { ChooserResult } from '@ionic-native/chooser/ngx';
import { FileEntry, IFile } from '@ionic-native/file/ngx';
import { MediaFile } from '@ionic-native/media-capture/ngx';
import { CoreApp } from '@services/app';
import { CoreFile, CoreFileProvider, CoreFileProgressEvent } from '@services/file';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreMimetypeUtils } from '@services/utils/mimetype';
import { CoreTextUtils } from '@services/utils/text';
import { CoreUtils, PromiseDefer } from '@services/utils/utils';
import { makeSingleton, Translate, Camera, Chooser, Platform, ActionSheetController } from '@singletons/core.singletons';
import { CoreLogger } from '@singletons/logger';
import { CoreCanceledError } from '@classes/errors/cancelederror';
import { CoreError } from '@classes/errors/error';
import { CoreFileUploader, CoreFileUploaderProvider, CoreFileUploaderOptions } from './fileuploader';
import { CoreFileUploaderDelegate } from './fileuploader.delegate';
import { CoreCaptureError } from '@/app/classes/errors/captureerror';
import { CoreIonLoadingElement } from '@/app/classes/ion-loading';
import { CoreWSUploadFileResult } from '@/app/services/ws';
/**
* Helper service to upload files.
*/
@Injectable({
providedIn: 'root',
})
export class CoreFileUploaderHelperProvider {
protected logger: CoreLogger;
protected filePickerDeferred?: PromiseDefer<CoreWSUploadFileResult | FileEntry>;
protected actionSheet?: HTMLIonActionSheetElement;
constructor(protected uploaderDelegate: CoreFileUploaderDelegate) {
this.logger = CoreLogger.getInstance('CoreFileUploaderHelperProvider');
}
/**
* Choose any type of file and upload it.
*
* @param maxSize Max size of the upload. -1 for no max size.
* @param upload True if the file should be uploaded, false to return the picked file.
* @param mimetypes List of supported mimetypes. If undefined, all mimetypes supported.
* @param allowOffline True to allow uploading in offline.
* @return Promise resolved when done.
*/
async chooseAndUploadFile(
maxSize?: number,
upload?: boolean,
allowOffline?: boolean,
mimetypes?: string[],
): Promise<CoreWSUploadFileResult | FileEntry> {
const modal = await CoreDomUtils.instance.showModalLoading();
const result = await Chooser.instance.getFile(mimetypes ? mimetypes.join(',') : undefined);
modal.dismiss();
if (!result) {
// User canceled.
throw new CoreCanceledError();
}
if (result.name == 'File') {
// In some Android 4.4 devices the file name cannot be retrieved. Try to use the one from the URI.
result.name = this.getChosenFileNameFromPath(result) || result.name;
}
// Verify that the mimetype is supported.
const error = CoreFileUploader.instance.isInvalidMimetype(mimetypes, result.name, result.mediaType);
if (error) {
throw new CoreError(error);
}
const options = CoreFileUploader.instance.getFileUploadOptions(result.uri, result.name, result.mediaType, true);
if (upload) {
return this.uploadFile(result.uri, maxSize || -1, true, options);
} else {
return this.copyToTmpFolder(result.uri, false, maxSize, undefined, options);
}
}
/**
* Show a confirmation modal to the user if the size of the file is bigger than the allowed threshold.
*
* @param size File size.
* @param alwaysConfirm True to show a confirm even if the size isn't high.
* @param allowOffline True to allow uploading in offline.
* @param wifiThreshold Threshold for WiFi connection. Default: CoreFileUploaderProvider.WIFI_SIZE_WARNING.
* @param limitedThreshold Threshold for limited connection. Default: CoreFileUploaderProvider.LIMITED_SIZE_WARNING.
* @return Promise resolved when the user confirms or if there's no need to show a modal.
*/
async confirmUploadFile(
size: number,
alwaysConfirm?: boolean,
allowOffline?: boolean,
wifiThreshold?: number,
limitedThreshold?: number,
): Promise<void> {
if (size == 0) {
return;
}
if (!allowOffline && !CoreApp.instance.isOnline()) {
throw new CoreError(Translate.instance.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 CoreDomUtils.instance.showConfirm(Translate.instance.instant('core.fileuploader.confirmuploadunknownsize'));
} else if (size >= wifiThreshold || (CoreApp.instance.isNetworkAccessLimited() && size >= limitedThreshold)) {
const readableSize = CoreTextUtils.instance.bytesToSize(size, 2);
return CoreDomUtils.instance.showConfirm(
Translate.instance.instant('core.fileuploader.confirmuploadfile', { size: readableSize }),
);
} else if (alwaysConfirm) {
return CoreDomUtils.instance.showConfirm(Translate.instance.instant('core.areyousure'));
}
}
/**
* Create a temporary copy of a file and upload it.
*
* @param file File to copy and upload.
* @param upload True if the file should be uploaded, false to return the copy of the file.
* @param name Name to use when uploading the file. If not defined, use the file's name.
* @return Promise resolved when the file is uploaded.
*/
async copyAndUploadFile(file: IFile | File, upload?: boolean, name?: string): Promise<CoreWSUploadFileResult | FileEntry> {
name = name || file.name;
const modal = await CoreDomUtils.instance.showModalLoading('core.fileuploader.readingfile', true);
let fileEntry: FileEntry | undefined;
try {
// Get unique name for the copy.
const newName = await CoreFile.instance.getUniqueNameInFolder(CoreFileProvider.TMPFOLDER, name);
const filePath = CoreTextUtils.instance.concatenatePaths(CoreFileProvider.TMPFOLDER, newName);
// Write the data into the file.
fileEntry = await CoreFile.instance.writeFileDataInFile(
file,
filePath,
(progress: CoreFileProgressEvent) => this.showProgressModal(modal, 'core.fileuploader.readingfileperc', progress),
);
} catch (error) {
this.logger.error('Error reading file to upload.', error);
modal.dismiss();
throw error;
}
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 path Path of the file.
* @param shouldDelete True if original file should be deleted (move), false otherwise (copy).
* @param maxSize Max size of the file. If not defined or -1, no max size.
* @param defaultExt Defaut extension to use if the file doesn't have any.
* @return Promise resolved with the copied file.
*/
protected async copyToTmpFolder(
path: string,
shouldDelete: boolean,
maxSize?: number,
defaultExt?: string,
options?: CoreFileUploaderOptions,
): Promise<FileEntry> {
const fileName = options?.fileName || CoreFile.instance.getFileAndDirectoryFromPath(path).name;
// Check that size isn't too large.
if (typeof maxSize != 'undefined' && maxSize != -1) {
try {
const fileEntry = await CoreFile.instance.getExternalFile(path);
const fileData = await CoreFile.instance.getFileObjectFromFileEntry(fileEntry);
if (fileData.size > maxSize) {
throw this.createMaxBytesError(maxSize, fileEntry.name);
}
} catch (error) {
// Ignore failures.
}
}
// File isn't too large.
// Get a unique name in the folder to prevent overriding another file.
const newName = await CoreFile.instance.getUniqueNameInFolder(CoreFileProvider.TMPFOLDER, fileName, defaultExt);
// Now move or copy the file.
const destPath = CoreTextUtils.instance.concatenatePaths(CoreFileProvider.TMPFOLDER, newName);
if (shouldDelete) {
return CoreFile.instance.moveExternalFile(path, destPath);
} else {
return CoreFile.instance.copyExternalFile(path, destPath);
}
}
/**
* Function called when trying to upload a file bigger than max size. Creates an error instance.
*
* @param maxSize Max size (bytes).
* @param fileName Name of the file.
* @return Message.
*/
protected createMaxBytesError(maxSize: number, fileName: string): CoreError {
return new CoreError(Translate.instance.instant('core.fileuploader.maxbytesfile', {
$a: {
file: fileName,
size: CoreTextUtils.instance.bytesToSize(maxSize, 2),
},
}));
}
/**
* Function called when the file picker is closed.
*/
filePickerClosed(): void {
if (this.filePickerDeferred) {
this.filePickerDeferred.reject(new CoreCanceledError());
this.filePickerDeferred = undefined;
}
}
/**
* Function to call once a file is uploaded using the file picker.
*
* @param result Result of the upload process.
*/
fileUploaded(result: CoreWSUploadFileResult | FileEntry): void {
if (this.filePickerDeferred) {
this.filePickerDeferred.resolve(result);
this.filePickerDeferred = undefined;
}
// Close the action sheet if it's opened.
this.actionSheet?.dismiss();
}
/**
* Given the result of choosing a file, try to get its file name from the path.
*
* @param result Chosen file data.
* @return File name, undefined if cannot get it.
*/
protected getChosenFileNameFromPath(result: ChooserResult): string | undefined {
const nameAndDir = CoreFile.instance.getFileAndDirectoryFromPath(result.uri);
if (!nameAndDir.name) {
return;
}
let extension = CoreMimetypeUtils.instance.getFileExtension(nameAndDir.name);
if (!extension) {
// The URI doesn't have an extension, add it now.
extension = CoreMimetypeUtils.instance.getExtension(result.mediaType);
if (extension) {
nameAndDir.name += '.' + extension;
}
}
return decodeURIComponent(nameAndDir.name);
}
/**
* Open the "file picker" to select and upload a file.
*
* @param maxSize Max size of the file to upload. If not defined or -1, no max size.
* @param title File picker title.
* @param mimetypes List of supported mimetypes. If undefined, all mimetypes supported.
* @return 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.
*/
async selectAndUploadFile(maxSize?: number, title?: string, mimetypes?: string[]): Promise<CoreWSUploadFileResult> {
return <CoreWSUploadFileResult> await this.selectFileWithPicker(maxSize, false, title, mimetypes, true);
}
/**
* Open the "file picker" to select a file without uploading it.
*
* @param maxSize Max size of the file. If not defined or -1, no max size.
* @param allowOffline True to allow selecting in offline, false to require connection.
* @param title File picker title.
* @param mimetypes List of supported mimetypes. If undefined, all mimetypes supported.
* @return 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.
*/
async selectFile(maxSize?: number, allowOffline?: boolean, title?: string, mimetypes?: string[]): Promise<FileEntry> {
return <FileEntry> await this.selectFileWithPicker(maxSize, allowOffline, title, mimetypes, false);
}
/**
* Open the "file picker" to select a file and maybe uploading it.
*
* @param maxSize Max size of the file. If not defined or -1, no max size.
* @param allowOffline True to allow selecting in offline, false to require connection.
* @param title File picker title.
* @param mimetypes List of supported mimetypes. If undefined, all mimetypes supported.
* @param upload Whether the file should be uploaded.
* @return Promise resolved when a file is selected/uploaded, rejected if file picker is closed.
*/
protected async selectFileWithPicker(
maxSize?: number,
allowOffline?: boolean,
title?: string,
mimetypes?: string[],
upload?: boolean,
): Promise<CoreWSUploadFileResult | FileEntry> {
// Create the cancel button and get the handlers to upload the file.
const buttons: ActionSheetButton[] = [{
text: Translate.instance.instant('core.cancel'),
role: 'cancel',
handler: (): void => {
// User cancelled the action sheet.
this.filePickerClosed();
},
}];
const handlers = this.uploaderDelegate.getHandlers(mimetypes);
this.filePickerDeferred = CoreUtils.instance.promiseDefer();
// Create a button for each handler.
handlers.forEach((handler) => {
buttons.push({
text: Translate.instance.instant(handler.title),
icon: handler.icon,
cssClass: handler.class,
handler: async (): Promise<boolean> => {
if (!handler.action) {
// Nothing to do.
return false;
}
if (!allowOffline && !CoreApp.instance.isOnline()) {
// Not allowed, show error.
CoreDomUtils.instance.showErrorModal('core.fileuploader.errormustbeonlinetoupload', true);
return false;
}
try {
const data = await handler.action(maxSize, upload, allowOffline, handler.mimetypes);
if (data.treated) {
// The handler already treated the file. Return the result.
this.fileUploaded(data.result!);
return true;
} else if (data.fileEntry) {
// The handler provided us a fileEntry, use it.
await this.uploadFileEntry(data.fileEntry, !!data.delete, maxSize, upload, allowOffline);
return true;
} else if (data.path) {
let fileEntry: FileEntry;
try {
// The handler provided a path. First treat it like it's a relative path.
fileEntry = await CoreFile.instance.getFile(data.path);
} catch (error) {
// File not found, it's probably an absolute path.
fileEntry = await CoreFile.instance.getExternalFile(data.path);
}
// File found, treat it.
await this.uploadFileEntry(fileEntry, !!data.delete, maxSize, upload, allowOffline);
return true;
}
// Nothing received, fail.
throw new CoreError('No file received');
} catch (error) {
CoreDomUtils.instance.showErrorModalDefault(
error,
Translate.instance.instant('core.fileuploader.errorreadingfile'),
);
return false;
}
},
});
});
this.actionSheet = await ActionSheetController.instance.create({
header: title ? title : Translate.instance.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 fileEntry FileEntry of the file to upload.
* @param deleteAfterUpload Whether the file should be deleted after upload.
* @param siteId Id of the site to upload the file to. If not defined, use current site.
* @return Promise resolved when the file is uploaded.
*/
async showConfirmAndUploadInSite(fileEntry: FileEntry, deleteAfterUpload?: boolean, siteId?: string): Promise<void> {
try {
const file = await CoreFile.instance.getFileObjectFromFileEntry(fileEntry);
await this.confirmUploadFile(file.size);
await this.uploadGenericFile(fileEntry.toURL(), file.name, file.type, deleteAfterUpload, siteId);
CoreDomUtils.instance.showToast('core.fileuploader.fileuploaded', true, undefined, 'core-toast-success');
} catch (error) {
CoreDomUtils.instance.showErrorModalDefault(error, 'core.fileuploader.errorreadingfile', true);
throw error;
}
}
/**
* Treat a capture audio/video error.
*
* @param error Error returned by the Cordova plugin.
* @param defaultMessage Key of the default message to show.
* @return Rejected promise.
*/
protected treatCaptureError(error: CoreCaptureError, defaultMessage: string): CoreError {
// Cancelled or error. If cancelled, error is an object with code = 3.
if (error) {
if (error.code != 3) {
// Error, not cancelled.
this.logger.error('Error while recording audio/video', error);
const message = this.isNoAppError(error) ? Translate.instance.instant('core.fileuploader.errornoapp') :
(error.message || Translate.instance.instant(defaultMessage));
throw new CoreError(message);
} else {
throw new CoreCanceledError();
}
}
throw new CoreError('Error capturing media');
}
/**
* Check if a capture error is because there is no app to capture.
*
* @param error Error.
* @return Whether it's because there is no app.
*/
protected isNoAppError(error: CoreCaptureError): boolean {
return error && error.code == 20;
}
/**
* Treat a capture image or browse album error.
*
* @param error Error returned by the Cordova plugin.
* @param defaultMessage Key of the default message to show.
* @return Rejected promise. If it doesn't have an error message it means it was cancelled.
*/
protected treatImageError(error: string | CoreError | CoreCaptureError, defaultMessage: string): CoreError {
// Cancelled or error.
if (!error) {
return new CoreError(defaultMessage);
}
if (typeof error == 'string') {
if (error.toLowerCase().indexOf('no image selected') > -1) {
// User cancelled.
return new CoreCanceledError();
}
return new CoreError(error);
} else if ('code' in error && error.code == 3) {
throw new CoreCanceledError();
} else {
throw error;
}
}
/**
* Convenient helper for the user to record and upload a video.
*
* @param isAudio True if uploading an audio, false if it's a video.
* @param maxSize Max size of the upload. -1 for no max size.
* @param upload True if the file should be uploaded, false to return the picked file.
* @param mimetypes List of supported mimetypes. If undefined, all mimetypes supported.
* @return Promise resolved when done.
*/
async uploadAudioOrVideo(
isAudio: boolean,
maxSize?: number,
upload?: boolean,
mimetypes?: string[],
): Promise<CoreWSUploadFileResult | FileEntry> {
this.logger.debug('Trying to record a ' + (isAudio ? 'audio' : 'video') + ' file');
// The mimetypes param is only for browser, the Cordova plugin doesn't support it.
const captureOptions = { limit: 1, mimetypes: mimetypes };
let media: MediaFile;
try {
const medias = isAudio ? await CoreFileUploader.instance.captureAudio(captureOptions) :
await CoreFileUploader.instance.captureVideo(captureOptions);
media = medias[0]; // We used limit 1, we only want 1 media.
} catch (error) {
if (isAudio && this.isNoAppError(error) && CoreApp.instance.isMobile() &&
(!Platform.instance.is('android') || CoreApp.instance.getPlatformMajorVersion() < 10)) {
// No app to record audio, fallback to capture it ourselves.
// In Android it will only be done in Android 9 or lower because there's a bug in the plugin.
try {
media = await CoreFileUploader.instance.captureAudioInApp();
} catch (error) {
throw this.treatCaptureError(error, 'core.fileuploader.errorcapturingaudio'); // Throw the right error.
}
} else {
const defaultError = isAudio ? 'core.fileuploader.errorcapturingaudio' : 'core.fileuploader.errorcapturingvideo';
throw this.treatCaptureError(error, defaultError); // Throw the right error.
}
}
let path = media.fullPath;
const error = CoreFileUploader.instance.isInvalidMimetype(mimetypes, path); // Verify that the mimetype is supported.
if (error) {
throw new Error(error);
}
// Make sure the path has the protocol. In iOS it doesn't.
if (CoreApp.instance.isMobile() && path.indexOf('file://') == -1) {
path = 'file://' + path;
}
const options = CoreFileUploader.instance.getMediaUploadOptions(media);
if (upload) {
return this.uploadFile(path, maxSize || -1, true, options);
} else {
// Copy or move the file to our temporary folder.
return this.copyToTmpFolder(path, true, maxSize, undefined, options);
}
}
/**
* Uploads a file of any type.
* This function will not check the size of the file, please check it before calling this function.
*
* @param uri File URI.
* @param name File name.
* @param type File type.
* @param deleteAfterUpload Whether the file should be deleted after upload.
* @param siteId Id of the site to upload the file to. If not defined, use current site.
* @return Promise resolved when the file is uploaded.
*/
uploadGenericFile(
uri: string,
name: string,
type: string,
deleteAfterUpload?: boolean,
siteId?: string,
): Promise<CoreWSUploadFileResult> {
const options = CoreFileUploader.instance.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 fromAlbum True if the image should be selected from album, false if it should be taken with camera.
* @param maxSize Max size of the upload. -1 for no max size.
* @param upload True if the file should be uploaded, false to return the picked file.
* @param mimetypes List of supported mimetypes. If undefined, all mimetypes supported.
* @return Promise resolved when done.
*/
async uploadImage(
fromAlbum: boolean,
maxSize?: number,
upload?: boolean,
mimetypes?: string[],
): Promise<CoreWSUploadFileResult | FileEntry> {
this.logger.debug('Trying to capture an image with camera');
const options: CameraOptions = {
quality: 50,
destinationType: Camera.instance.DestinationType.FILE_URI,
correctOrientation: true,
};
if (fromAlbum) {
const imageSupported = !mimetypes || CoreUtils.instance.indexOfRegexp(mimetypes, /^image\//) > -1;
const videoSupported = !mimetypes || CoreUtils.instance.indexOfRegexp(mimetypes, /^video\//) > -1;
options.sourceType = Camera.instance.PictureSourceType.PHOTOLIBRARY;
options.popoverOptions = {
x: 10,
y: 10,
width: Platform.instance.width() - 200,
height: Platform.instance.height() - 200,
arrowDir: Camera.instance.PopoverArrowDirection.ARROW_ANY,
};
// Determine the mediaType based on the mimetypes.
if (imageSupported && !videoSupported) {
options.mediaType = Camera.instance.MediaType.PICTURE;
} else if (!imageSupported && videoSupported) {
options.mediaType = Camera.instance.MediaType.VIDEO;
} else if (CoreApp.instance.isIOS()) {
// Only get all media in iOS because in Android using this option allows uploading any kind of file.
options.mediaType = Camera.instance.MediaType.ALLMEDIA;
}
} else if (mimetypes) {
if (mimetypes.indexOf('image/jpeg') > -1) {
options.encodingType = Camera.instance.EncodingType.JPEG;
} else if (mimetypes.indexOf('image/png') > -1) {
options.encodingType = Camera.instance.EncodingType.PNG;
}
}
let path: string | undefined;
try {
path = await CoreFileUploader.instance.getPicture(options);
} catch (error) {
const defaultError = fromAlbum ? 'core.fileuploader.errorgettingimagealbum' : 'core.fileuploader.errorcapturingimage';
throw this.treatImageError(error, defaultError);
}
const error = CoreFileUploader.instance.isInvalidMimetype(mimetypes, path); // Verify that the mimetype is supported.
if (error) {
throw new CoreError(error);
}
const uploadOptions = CoreFileUploader.instance.getCameraUploadOptions(path, fromAlbum);
if (upload) {
return this.uploadFile(path, maxSize || -1, true, uploadOptions);
} else {
// Copy or move the file to our temporary folder.
return this.copyToTmpFolder(path, !fromAlbum, maxSize, 'jpg', uploadOptions);
}
}
/**
* Upload a file given the file entry.
*
* @param fileEntry The file entry.
* @param deleteAfter True if the file should be deleted once treated.
* @param maxSize Max size of the file. If not defined or -1, no max size.
* @param upload True if the file should be uploaded, false to return the picked file.
* @param allowOffline True to allow selecting in offline, false to require connection.
* @param name Name to use when uploading the file. If not defined, use the file's name.
* @return Promise resolved when done.
*/
async uploadFileEntry(
fileEntry: FileEntry,
deleteAfter: boolean,
maxSize?: number,
upload?: boolean,
allowOffline?: boolean,
name?: string,
): Promise<CoreWSUploadFileResult | FileEntry> {
const file = await CoreFile.instance.getFileObjectFromFileEntry(fileEntry);
const result = await this.uploadFileObject(file, maxSize, upload, allowOffline, name);
if (deleteAfter) {
// We have uploaded and deleted a copy of the file. Now delete the original one.
CoreFile.instance.removeFileByFileEntry(fileEntry);
}
return result;
}
/**
* Upload a file given the file object.
*
* @param file The file object.
* @param maxSize Max size of the file. If not defined or -1, no max size.
* @param upload True if the file should be uploaded, false to return the picked file.
* @param allowOffline True to allow selecting in offline, false to require connection.
* @param name Name to use when uploading the file. If not defined, use the file's name.
* @return Promise resolved when done.
*/
async uploadFileObject(
file: IFile | File,
maxSize?: number,
upload?: boolean,
allowOffline?: boolean,
name?: string,
): Promise<CoreWSUploadFileResult | FileEntry> {
if (maxSize !== undefined && maxSize != -1 && file.size > maxSize) {
throw this.createMaxBytesError(maxSize, file.name);
}
if (upload) {
await this.confirmUploadFile(file.size, false, allowOffline);
}
// 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 path Absolute path of the file to upload.
* @param maxSize Max size of the upload. -1 for no max size.
* @param checkSize True to check size.
* @param Options.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved if the file is uploaded, rejected otherwise.
*/
async uploadFile(
path: string,
maxSize: number,
checkSize: boolean,
options: CoreFileUploaderOptions,
siteId?: string,
): Promise<CoreWSUploadFileResult> {
const errorStr = Translate.instance.instant('core.error');
const retryStr = Translate.instance.instant('core.retry');
const uploadingStr = Translate.instance.instant('core.fileuploader.uploading');
const errorUploading = async (error): Promise<CoreWSUploadFileResult> => {
// Allow the user to retry.
try {
await CoreDomUtils.instance.showConfirm(error, errorStr, retryStr);
} catch (error) {
// User cancelled. Delete the file if needed.
if (options.deleteAfterUpload) {
CoreFile.instance.removeExternalFile(path);
}
throw new CoreCanceledError();
}
// Try again.
return this.uploadFile(path, maxSize, checkSize, options, siteId);
};
if (!CoreApp.instance.isOnline()) {
return errorUploading(Translate.instance.instant('core.fileuploader.errormustbeonlinetoupload'));
}
let file: IFile | undefined;
let size = 0;
if (checkSize) {
try {
// Check that file size is the right one.
const fileEntry = await CoreFile.instance.getExternalFile(path);
file = await CoreFile.instance.getFileObjectFromFileEntry(fileEntry);
size = file.size;
} catch (error) {
// Ignore failures.
}
}
if (maxSize != -1 && size > maxSize) {
throw this.createMaxBytesError(maxSize, file!.name);
}
if (size > 0) {
await this.confirmUploadFile(size);
}
// File isn't too large and user confirmed, let's upload.
const modal = await CoreDomUtils.instance.showModalLoading(uploadingStr);
try {
return await CoreFileUploader.instance.uploadFile(
path,
options,
(progress: ProgressEvent) => {
this.showProgressModal(modal, 'core.fileuploader.uploadingperc', progress);
},
siteId,
);
} catch (error) {
this.logger.error('Error uploading file.', error);
modal.dismiss();
return errorUploading(error);
} finally {
modal.dismiss();
}
}
/**
* Show a progress modal.
*
* @param modal The modal where to show the progress.
* @param stringKey The key of the string to display.
* @param progress The progress event.
*/
protected showProgressModal(
modal: CoreIonLoadingElement,
stringKey: string,
progress: ProgressEvent | CoreFileProgressEvent,
): void {
if (!progress || !progress.lengthComputable) {
return;
}
// Calculate the progress percentage.
const perc = Math.min((progress.loaded! / progress.total!) * 100, 100);
if (isNaN(perc) || perc < 0) {
return;
}
const contentElement = modal.loading?.querySelector('.loading-content');
if (contentElement) {
contentElement.innerHTML = Translate.instance.instant(stringKey, { $a: perc.toFixed(1) });
}
}
}
export class CoreFileUploaderHelper extends makeSingleton(CoreFileUploaderHelperProvider) {}

View File

@ -0,0 +1,664 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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 { CameraOptions } from '@ionic-native/camera/ngx';
import { FileEntry } from '@ionic-native/file/ngx';
import { MediaFile, CaptureError, CaptureAudioOptions, CaptureVideoOptions } from '@ionic-native/media-capture/ngx';
import { Subject } from 'rxjs';
import { CoreApp } from '@services/app';
import { CoreFile } from '@services/file';
import { CoreFilepool } from '@services/filepool';
import { CoreSites } from '@services/sites';
import { CoreMimetypeUtils } from '@services/utils/mimetype';
import { CoreTextUtils } from '@services/utils/text';
import { CoreTimeUtils } from '@services/utils/time';
import { CoreUtils } from '@services/utils/utils';
import { CoreWSExternalFile, CoreWSFileUploadOptions, CoreWSUploadFileResult } from '@services/ws';
import { makeSingleton, Translate, MediaCapture, ModalController, Camera } from '@singletons/core.singletons';
import { CoreLogger } from '@singletons/logger';
import { CoreEmulatorCaptureMediaComponent } from '@core/emulator/components/capture-media/capture-media';
import { CoreError } from '@/app/classes/errors/error';
/**
* File upload options.
*/
export interface CoreFileUploaderOptions extends CoreWSFileUploadOptions {
/**
* Whether the file should be deleted after the upload (if success).
*/
deleteAfterUpload?: boolean;
}
/**
* Service to upload files.
*/
@Injectable({
providedIn: 'root',
})
export class CoreFileUploaderProvider {
static readonly LIMITED_SIZE_WARNING = 1048576; // 1 MB.
static readonly WIFI_SIZE_WARNING = 10485760; // 10 MB.
protected logger: CoreLogger;
// Observers to notify when a media file starts/stops being recorded/selected.
onGetPicture: Subject<boolean> = new Subject<boolean>();
onAudioCapture: Subject<boolean> = new Subject<boolean>();
onVideoCapture: Subject<boolean> = new Subject<boolean>();
constructor() {
this.logger = CoreLogger.getInstance('CoreFileUploaderProvider');
}
/**
* Add a dot to the beginning of an extension.
*
* @param extension Extension.
* @return Treated extension.
*/
protected addDot(extension: string): string {
return '.' + extension;
}
/**
* Compares two file lists and returns if they are different.
*
* @param a First file list.
* @param b Second file list.
* @return Whether both lists are different.
*/
areFileListDifferent(a: (CoreWSExternalFile | FileEntry)[], b: (CoreWSExternalFile | FileEntry)[]): 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 (CoreFile.instance.getFileName(a[i]) != CoreFile.instance.getFileName(b[i])) {
return true;
}
}
return false;
}
/**
* Start the audio recorder application and return information about captured audio clip files.
*
* @param options Options.
* @return Promise resolved with the result.
*/
async captureAudio(options: CaptureAudioOptions): Promise<MediaFile[] | CaptureError> {
this.onAudioCapture.next(true);
try {
return await MediaCapture.instance.captureAudio(options);
} finally {
this.onAudioCapture.next(false);
}
}
/**
* Record an audio file without using an external app.
*
* @return Promise resolved with the file.
*/
async captureAudioInApp(): Promise<MediaFile> {
const params = {
type: 'audio',
};
const modal = await ModalController.instance.create({
component: CoreEmulatorCaptureMediaComponent,
cssClass: 'core-modal-fullscreen',
componentProps: params,
backdropDismiss: false,
});
modal.present();
const result = await modal.onWillDismiss();
if (result.role == 'success') {
return result.data[0];
} else {
throw result.data;
}
}
/**
* Start the video recorder application and return information about captured video clip files.
*
* @param options Options.
* @return Promise resolved with the result.
*/
async captureVideo(options: CaptureVideoOptions): Promise<MediaFile[] | CaptureError> {
this.onVideoCapture.next(true);
try {
return await MediaCapture.instance.captureVideo(options);
} finally {
this.onVideoCapture.next(false);
}
}
/**
* Clear temporary attachments to be uploaded.
* Attachments already saved in an offline store will NOT be deleted.
*
* @param files List of files.
*/
clearTmpFiles(files: (CoreWSExternalFile | FileEntry)[]): void {
// Delete the local files.
files.forEach((file) => {
if ('remove' in file) {
// Pass an empty function to prevent missing parameter error.
file.remove(() => {
// Nothing to do.
});
}
});
}
/**
* Get the upload options for a file taken with the Camera Cordova plugin.
*
* @param uri File URI.
* @param isFromAlbum True if the image was taken from album, false if it's a new image taken with camera.
* @return Options.
*/
getCameraUploadOptions(uri: string, isFromAlbum?: boolean): CoreFileUploaderOptions {
const extension = CoreMimetypeUtils.instance.guessExtensionFromUrl(uri);
const mimetype = CoreMimetypeUtils.instance.getMimeType(extension);
const isIOS = CoreApp.instance.isIOS();
const options: CoreFileUploaderOptions = {
deleteAfterUpload: !isFromAlbum,
mimeType: mimetype,
};
const fileName = CoreFile.instance.getFileAndDirectoryFromPath(uri).name;
if (isIOS && (mimetype == 'image/jpeg' || mimetype == 'image/png')) {
// In iOS, the pictures can have repeated names, even if they come from the album.
// Add a timestamp to the filename to make it unique.
const split = fileName.split('.');
split[0] += '_' + CoreTimeUtils.instance.readableTimestamp();
options.fileName = split.join('.');
} else {
// Use the same name that the file already has.
options.fileName = fileName;
}
if (isFromAlbum) {
// If the file was picked from the album, delete it only if it was copied to the app's folder.
options.deleteAfterUpload = CoreFile.instance.isFileInAppFolder(uri);
if (CoreApp.instance.isAndroid()) {
// 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 uri File URI.
* @param name File name.
* @param mimetype File mimetype.
* @param deleteAfterUpload Whether the file should be deleted after upload.
* @param fileArea File area to upload the file to. It defaults to 'draft'.
* @param itemId Draft ID to upload the file to, 0 to create new.
* @return Options.
*/
getFileUploadOptions(
uri: string,
name: string,
mimetype?: string,
deleteAfterUpload?: boolean,
fileArea?: string,
itemId?: number,
): CoreFileUploaderOptions {
const options: CoreFileUploaderOptions = {};
options.fileName = name;
options.mimeType = mimetype || CoreMimetypeUtils.instance.getMimeType(
CoreMimetypeUtils.instance.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 File object to upload.
* @return Options.
*/
getMediaUploadOptions(mediaFile: MediaFile): CoreFileUploaderOptions {
const options: CoreFileUploaderOptions = {};
let filename = mediaFile.name;
if (!filename.match(/_\d{14}(\..*)?$/)) {
// Add a timestamp to the filename to make it unique.
const split = filename.split('.');
split[0] += '_' + CoreTimeUtils.instance.readableTimestamp();
filename = split.join('.');
}
options.fileName = filename;
options.deleteAfterUpload = true;
if (mediaFile.type) {
options.mimeType = mediaFile.type;
} else {
options.mimeType = CoreMimetypeUtils.instance.getMimeType(
CoreMimetypeUtils.instance.getFileExtension(options.fileName),
);
}
return options;
}
/**
* Take a picture or video, or load one from the library.
*
* @param options Options.
* @return Promise resolved with the result.
*/
getPicture(options: CameraOptions): Promise<string> {
this.onGetPicture.next(true);
return Camera.instance.getPicture(options).finally(() => {
this.onGetPicture.next(false);
});
}
/**
* Get the files stored in a folder, marking them as offline.
*
* @param folderPath Folder where to get the files.
* @return Promise resolved with the list of files.
*/
async getStoredFiles(folderPath: string): Promise<FileEntry[]> {
return <FileEntry[]> await CoreFile.instance.getDirectoryContents(folderPath);
}
/**
* Get stored files from combined online and offline file object.
*
* @param filesObject The combined offline and online files object.
* @param folderPath Folder path to get files from.
* @return Promise resolved with files.
*/
async getStoredFilesFromOfflineFilesObject(
filesObject: CoreFileUploaderStoreFilesResult,
folderPath: string,
): Promise<(CoreWSExternalFile | FileEntry)[]> {
let files: (CoreWSExternalFile | FileEntry)[] = [];
if (filesObject.online.length > 0) {
files = CoreUtils.instance.clone(filesObject.online);
}
if (filesObject.offline > 0) {
const offlineFiles = await CoreUtils.instance.ignoreErrors(this.getStoredFiles(folderPath));
if (offlineFiles) {
files = files.concat(offlineFiles);
}
}
return 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 mimetypes List of supported mimetypes. If undefined, all mimetypes supported.
* @param path File's path or name.
* @param mimetype File's mimetype.
* @return Undefined if file is valid, error message if file is invalid.
*/
isInvalidMimetype(mimetypes?: string[], path?: string, mimetype?: string): string | undefined {
let extension: string | undefined;
if (mimetypes) {
// Verify that the mimetype of the file is supported.
if (mimetype) {
extension = CoreMimetypeUtils.instance.getExtension(mimetype);
if (mimetypes.indexOf(mimetype) == -1) {
// Get the "main" mimetype of the extension.
// It's possible that the list of accepted mimetypes only includes the "main" mimetypes.
mimetype = CoreMimetypeUtils.instance.getMimeType(extension);
}
} else if (path) {
extension = CoreMimetypeUtils.instance.getFileExtension(path);
mimetype = CoreMimetypeUtils.instance.getMimeType(extension);
} else {
throw new CoreError('No mimetype or path supplied.');
}
if (mimetype && mimetypes.indexOf(mimetype) == -1) {
extension = extension || Translate.instance.instant('core.unknown');
return Translate.instance.instant('core.fileuploader.invalidfiletype', { $a: extension });
}
}
}
/**
* Mark files as offline.
*
* @param files Files to mark as offline.
* @return Files marked as offline.
* @deprecated since 3.9.5. Now stored files no longer have an offline property.
*/
markOfflineFiles(files: FileEntry[]): FileEntry[] {
return files;
}
/**
* Parse filetypeList to get the list of allowed mimetypes and the data to render information.
*
* @param filetypeList Formatted string list where the mimetypes can be checked.
* @return Mimetypes and the filetypes informations. Undefined if all types supported.
*/
prepareFiletypeList(filetypeList: string): CoreFileUploaderTypeList | undefined {
filetypeList = filetypeList?.trim();
if (!filetypeList || filetypeList == '*') {
// All types supported, return undefined.
return;
}
const filetypes = filetypeList.split(/[;, ]+/g);
const mimetypes: Record<string, boolean> = {}; // Use an object to prevent duplicates.
const typesInfo: CoreFileUploaderTypeListInfoEntry[] = [];
filetypes.forEach((filetype) => {
filetype = filetype.trim();
if (!filetype) {
return;
}
if (filetype.indexOf('/') != -1) {
// It's a mimetype.
typesInfo.push({
name: CoreMimetypeUtils.instance.getMimetypeDescription(filetype),
extlist: CoreMimetypeUtils.instance.getExtensions(filetype).map(this.addDot).join(' '),
});
mimetypes[filetype] = true;
} else if (filetype.indexOf('.') === 0) {
// It's an extension.
const mimetype = CoreMimetypeUtils.instance.getMimeType(filetype);
typesInfo.push({
name: mimetype && CoreMimetypeUtils.instance.getMimetypeDescription(mimetype),
extlist: filetype,
});
if (mimetype) {
mimetypes[mimetype] = true;
}
} else {
// It's a group.
const groupExtensions = CoreMimetypeUtils.instance.getGroupMimeInfo(filetype, 'extensions');
const groupMimetypes = CoreMimetypeUtils.instance.getGroupMimeInfo(filetype, 'mimetypes');
if (groupExtensions && groupExtensions.length > 0) {
typesInfo.push({
name: CoreMimetypeUtils.instance.getTranslatedGroupName(filetype),
extlist: groupExtensions.map(this.addDot).join(' '),
});
groupMimetypes?.forEach((mimetype) => {
if (mimetype) {
mimetypes[mimetype] = true;
}
});
} else {
// Treat them as extensions.
filetype = this.addDot(filetype);
const mimetype = CoreMimetypeUtils.instance.getMimeType(filetype);
typesInfo.push({
name: mimetype && CoreMimetypeUtils.instance.getMimetypeDescription(mimetype),
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 folderPath Path of the folder where to store the files.
* @param files List of files.
* @return Promise resolved if success.
*/
async storeFilesToUpload(
folderPath: string,
files: (CoreWSExternalFile | FileEntry)[],
): Promise<CoreFileUploaderStoreFilesResult> {
const result: CoreFileUploaderStoreFilesResult = {
online: [],
offline: 0,
};
if (!files || !files.length) {
return result;
}
// Remove unused files from previous saves.
await CoreFile.instance.removeUnusedFiles(folderPath, files);
await Promise.all(files.map(async (file) => {
if (!CoreUtils.instance.isFileEntry(file)) {
// 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.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.
const destFile = CoreTextUtils.instance.concatenatePaths(folderPath, file.name);
result.offline++;
await CoreFile.instance.copyFile(file.toURL(), destFile);
}
}));
return result;
}
/**
* Upload a file.
*
* @param uri File URI.
* @param options Options for the upload.
* @param onProgress Function to call on progress.
* @param siteId Id of the site to upload the file to. If not defined, use current site.
* @return Promise resolved when done.
*/
async uploadFile(
uri: string,
options?: CoreFileUploaderOptions,
onProgress?: (event: ProgressEvent) => void,
siteId?: string,
): Promise<CoreWSUploadFileResult> {
options = options || {};
const deleteAfterUpload = options.deleteAfterUpload;
const ftOptions = CoreUtils.instance.clone(options);
delete ftOptions.deleteAfterUpload;
const site = await CoreSites.instance.getSite(siteId);
const result = await site.uploadFile(uri, ftOptions, onProgress);
if (deleteAfterUpload) {
CoreFile.instance.removeExternalFile(uri);
}
return result;
}
/**
* Upload a file to a draft area and return the draft ID.
*
* If the file is an online file it will be downloaded and then re-uploaded.
* If the file is a local file it will not be deleted from the device after upload.
*
* @param file Online file or local FileEntry.
* @param itemId Draft ID to use. Undefined or 0 to create a new draft ID.
* @param component The component to set to the downloaded files.
* @param componentId An ID to use in conjunction with the component.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the itemId.
*/
async uploadOrReuploadFile(
file: CoreWSExternalFile | FileEntry,
itemId?: number,
component?: string,
componentId?: string | number,
siteId?: string,
): Promise<number> {
siteId = siteId || CoreSites.instance.getCurrentSiteId();
let fileName: string | undefined;
let fileEntry: FileEntry | undefined;
const isOnline = !CoreUtils.instance.isFileEntry(file);
if (CoreUtils.instance.isFileEntry(file)) {
// Local file, we already have the file entry.
fileName = file.name;
fileEntry = file;
} else {
// It's an online file. We need to download it and re-upload it.
fileName = file.filename;
const path = await CoreFilepool.instance.downloadUrl(
siteId,
file.fileurl,
false,
component,
componentId,
file.timemodified,
undefined,
undefined,
file,
);
fileEntry = await CoreFile.instance.getExternalFile(path);
}
// Now upload the file.
const extension = CoreMimetypeUtils.instance.getFileExtension(fileName!);
const mimetype = extension ? CoreMimetypeUtils.instance.getMimeType(extension) : undefined;
const options = this.getFileUploadOptions(fileEntry.toURL(), fileName!, mimetype, isOnline, 'draft', itemId);
const result = await this.uploadFile(fileEntry.toURL(), options, undefined, siteId);
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.
* Local files are not deleted from the device after upload.
* If there are no files to upload it will return a fake draft ID (1).
*
* @param files List of files.
* @param component The component to set to the downloaded files.
* @param componentId An ID to use in conjunction with the component.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the itemId.
*/
async uploadOrReuploadFiles(
files: (CoreWSExternalFile | FileEntry)[],
component?: string,
componentId?: string | number,
siteId?: string,
): Promise<number> {
siteId = siteId || CoreSites.instance.getCurrentSiteId();
if (!files || !files.length) {
// Return fake draft ID.
return 1;
}
// Upload only the first file first to get a draft id.
const itemId = await this.uploadOrReuploadFile(files[0], 0, component, componentId, siteId);
const promises: Promise<number>[] = [];
for (let i = 1; i < files.length; i++) {
const file = files[i];
promises.push(this.uploadOrReuploadFile(file, itemId, component, componentId, siteId));
}
await Promise.all(promises);
return itemId;
}
}
export class CoreFileUploader extends makeSingleton(CoreFileUploaderProvider) {}
export type CoreFileUploaderStoreFilesResult = {
online: CoreWSExternalFile[]; // List of online files.
offline: number; // Number of offline files.
};
export type CoreFileUploaderTypeList = {
info: CoreFileUploaderTypeListInfoEntry[];
mimetypes: string[];
};
export type CoreFileUploaderTypeListInfoEntry = {
name?: string;
extlist: string;
};

View File

@ -0,0 +1,77 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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 { CoreApp } from '@services/app';
import { CoreUtils } from '@services/utils/utils';
import { CoreFileUploaderHandler, CoreFileUploaderHandlerData, CoreFileUploaderHandlerResult } from '../fileuploader.delegate';
import { CoreFileUploaderHelper } from '../fileuploader.helper';
/**
* Handler to upload files from the album.
*/
@Injectable()
export class CoreFileUploaderAlbumHandler implements CoreFileUploaderHandler {
name = 'CoreFileUploaderAlbum';
priority = 2000;
/**
* Whether or not the handler is enabled on a site level.
*
* @return Promise resolved with true if enabled.
*/
async isEnabled(): Promise<boolean> {
return CoreApp.instance.isMobile();
}
/**
* Given a list of mimetypes, return the ones that are supported by the handler.
*
* @param mimetypes List of mimetypes.
* @return Supported mimetypes.
*/
getSupportedMimetypes(mimetypes: string[]): string[] {
// Album allows picking images and videos.
return CoreUtils.instance.filterByRegexp(mimetypes, /^(image|video)\//);
}
/**
* Get the data to display the handler.
*
* @return Data.
*/
getData(): CoreFileUploaderHandlerData {
return {
title: 'core.fileuploader.photoalbums',
class: 'core-fileuploader-album-handler',
icon: 'images',
action: async (
maxSize?: number,
upload?: boolean,
allowOffline?: boolean,
mimetypes?: string[],
): Promise<CoreFileUploaderHandlerResult> => {
const result = await CoreFileUploaderHelper.instance.uploadImage(true, maxSize, upload, mimetypes);
return {
treated: true,
result: result,
};
},
};
}
}

View File

@ -0,0 +1,92 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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 { CoreApp } from '@services/app';
import { CoreUtils } from '@services/utils/utils';
import { CoreFileUploaderHandler, CoreFileUploaderHandlerData, CoreFileUploaderHandlerResult } from '../fileuploader.delegate';
import { CoreFileUploaderHelper } from '../fileuploader.helper';
/**
* Handler to record an audio to upload it.
*/
@Injectable()
export class CoreFileUploaderAudioHandler implements CoreFileUploaderHandler {
name = 'CoreFileUploaderAudio';
priority = 1600;
/**
* Whether or not the handler is enabled on a site level.
*
* @return Promise resolved with true if enabled.
*/
async isEnabled(): Promise<boolean> {
return CoreApp.instance.isMobile() || (CoreApp.instance.canGetUserMedia() && CoreApp.instance.canRecordMedia());
}
/**
* Given a list of mimetypes, return the ones that are supported by the handler.
*
* @param mimetypes List of mimetypes.
* @return Supported mimetypes.
*/
getSupportedMimetypes(mimetypes: string[]): string[] {
if (CoreApp.instance.isIOS()) {
// In iOS it's recorded as WAV.
return CoreUtils.instance.filterByRegexp(mimetypes, /^audio\/wav$/);
} else if (CoreApp.instance.isAndroid()) {
// In Android we don't know the format the audio will be recorded, so accept any audio mimetype.
return CoreUtils.instance.filterByRegexp(mimetypes, /^audio\//);
} else {
// In browser, support audio formats that are supported by MediaRecorder.
if (MediaRecorder) {
return mimetypes.filter((type) => {
const matches = type.match(/^audio\//);
return matches && matches.length && MediaRecorder.isTypeSupported(type);
});
}
}
return [];
}
/**
* Get the data to display the handler.
*
* @return Data.
*/
getData(): CoreFileUploaderHandlerData {
return {
title: 'core.fileuploader.audio',
class: 'core-fileuploader-audio-handler',
icon: 'mic',
action: async (
maxSize?: number,
upload?: boolean,
allowOffline?: boolean,
mimetypes?: string[],
): Promise<CoreFileUploaderHandlerResult> => {
const result = await CoreFileUploaderHelper.instance.uploadAudioOrVideo(true, maxSize, upload, mimetypes);
return {
treated: true,
result: result,
};
},
};
}
}

View File

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

View File

@ -0,0 +1,158 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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 { CoreApp } from '@services/app';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreFileUploaderHandler, CoreFileUploaderHandlerData, CoreFileUploaderHandlerResult } from '../fileuploader.delegate';
import { CoreFileUploaderHelper } from '../fileuploader.helper';
import { CoreFileUploader } from '../fileuploader';
import { Translate } from '@singletons/core.singletons';
/**
* Handler to upload any type of file.
*/
@Injectable()
export class CoreFileUploaderFileHandler implements CoreFileUploaderHandler {
name = 'CoreFileUploaderFile';
priority = 1200;
/**
* Whether or not the handler is enabled on a site level.
*
* @return Promise resolved with true if enabled.
*/
async isEnabled(): Promise<boolean> {
return true;
}
/**
* Given a list of mimetypes, return the ones that are supported by the handler.
*
* @param mimetypes List of mimetypes.
* @return Supported mimetypes.
*/
getSupportedMimetypes(mimetypes: string[]): string[] {
return mimetypes;
}
/**
* Get the data to display the handler.
*
* @return Data.
*/
getData(): CoreFileUploaderHandlerData {
const handler: CoreFileUploaderHandlerData = {
title: 'core.fileuploader.file',
class: 'core-fileuploader-file-handler',
icon: 'folder',
};
if (CoreApp.instance.isMobile()) {
handler.action = async (
maxSize?: number,
upload?: boolean,
allowOffline?: boolean,
mimetypes?: string[],
): Promise<CoreFileUploaderHandlerResult> => {
const result = await CoreFileUploaderHelper.instance.chooseAndUploadFile(maxSize, upload, allowOffline, mimetypes);
return {
treated: true,
result: result,
};
};
} else {
handler.afterRender = (
maxSize?: number,
upload?: boolean,
allowOffline?: boolean,
mimetypes?: string[],
): void => {
// 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) {
return;
}
const input = document.createElement('input');
input.setAttribute('type', 'file');
input.classList.add('core-fileuploader-file-handler-input');
if (mimetypes && mimetypes.length && (!CoreApp.instance.isAndroid() || 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', async () => {
const file = input.files?.[0];
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 = CoreFileUploader.instance.isInvalidMimetype(mimetypes, file.name, file.type);
if (error) {
CoreDomUtils.instance.showErrorModal(error);
return;
}
try {
// Upload the picked file.
const result = await CoreFileUploaderHelper.instance.uploadFileObject(
file,
maxSize,
upload,
allowOffline,
file.name,
);
CoreFileUploaderHelper.instance.fileUploaded(result);
} catch (error) {
CoreDomUtils.instance.showErrorModalDefault(
error,
Translate.instance.instant('core.fileuploader.errorreadingfile'),
);
}
});
if (CoreApp.instance.isIOS()) {
// In iOS, the click on the input stopped working for some reason. We need to put it 1 level higher.
element.parentElement?.appendChild(input);
// Animate the button when the input is clicked.
input.addEventListener('mousedown', () => {
element.classList.add('activated');
});
input.addEventListener('mouseup', () => {
setTimeout(() => {
element.classList.remove('activated');
}, 80);
});
} else {
element.appendChild(input);
}
};
}
return handler;
}
}

View File

@ -0,0 +1,92 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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 { CoreApp } from '@services/app';
import { CoreUtils } from '@services/utils/utils';
import { CoreFileUploaderHandler, CoreFileUploaderHandlerData, CoreFileUploaderHandlerResult } from '../fileuploader.delegate';
import { CoreFileUploaderHelper } from '../fileuploader.helper';
/**
* Handler to record a video to upload it.
*/
@Injectable()
export class CoreFileUploaderVideoHandler implements CoreFileUploaderHandler {
name = 'CoreFileUploaderVideo';
priority = 1400;
/**
* Whether or not the handler is enabled on a site level.
*
* @return Promise resolved with true if enabled.
*/
async isEnabled(): Promise<boolean> {
return CoreApp.instance.isMobile() || (CoreApp.instance.canGetUserMedia() && CoreApp.instance.canRecordMedia());
}
/**
* Given a list of mimetypes, return the ones that are supported by the handler.
*
* @param mimetypes List of mimetypes.
* @return Supported mimetypes.
*/
getSupportedMimetypes(mimetypes: string[]): string[] {
if (CoreApp.instance.isIOS()) {
// In iOS it's recorded as MOV.
return CoreUtils.instance.filterByRegexp(mimetypes, /^video\/quicktime$/);
} else if (CoreApp.instance.isAndroid()) {
// In Android we don't know the format the video will be recorded, so accept any video mimetype.
return CoreUtils.instance.filterByRegexp(mimetypes, /^video\//);
} else {
// In browser, support video formats that are supported by MediaRecorder.
if (MediaRecorder) {
return mimetypes.filter((type) => {
const matches = type.match(/^video\//);
return matches?.length && MediaRecorder.isTypeSupported(type);
});
}
}
return [];
}
/**
* Get the data to display the handler.
*
* @return Data.
*/
getData(): CoreFileUploaderHandlerData {
return {
title: 'core.fileuploader.video',
class: 'core-fileuploader-video-handler',
icon: 'videocam',
action: async (
maxSize?: number,
upload?: boolean,
allowOffline?: boolean,
mimetypes?: string[],
): Promise<CoreFileUploaderHandlerResult> => {
const result = await CoreFileUploaderHelper.instance.uploadAudioOrVideo(false, maxSize, upload, mimetypes);
return {
treated: true,
result: result,
};
},
};
}
}

View File

@ -23,7 +23,7 @@ import { CoreUrlUtils } from '@services/utils/url';
import { SQLiteDB, SQLiteDBTableSchema } from '@classes/sqlitedb';
import { CoreConstants } from '@core/constants';
import { makeSingleton, Keyboard, Network, StatusBar, Platform } from '@singletons/core.singletons';
import { makeSingleton, Keyboard, Network, StatusBar, Platform, Device } from '@singletons/core.singletons';
import { CoreLogger } from '@singletons/logger';
import { DBNAME, SCHEMA_VERSIONS_TABLE_NAME, SCHEMA_VERSIONS_TABLE_SCHEMA, SchemaVersionsDBEntry } from '@services/app.db';
@ -240,6 +240,17 @@ export class CoreAppProvider {
return storesConfig.default;
}
/**
* Get platform major version number.
*/
getPlatformMajorVersion(): number {
if (!this.isMobile()) {
return 0;
}
return Number(Device.instance.version?.split('.')[0]);
}
/**
* Checks if the app is running in a 64 bits desktop environment (not browser).
*

View File

@ -1239,6 +1239,16 @@ export class CoreFileProvider {
return !path || !path.match(/^[a-z0-9]+:\/\//i) || path.indexOf(this.basePath) != -1;
}
/**
* Get the file's name.
*
* @param file The file.
* @return The file name.
*/
getFileName(file: CoreWSExternalFile | FileEntry): string | undefined {
return CoreUtils.instance.isFileEntry(file) ? file.name : file.filename;
}
}
export class CoreFile extends makeSingleton(CoreFileProvider) {}

View File

@ -26,7 +26,7 @@ import { CoreSite } from '@classes/site';
import { CoreQueueRunner } from '@classes/queue-runner';
import { CoreError } from '@classes/errors/error';
import { CoreConstants } from '@core/constants';
import { makeSingleton, NgZone, Platform, Translate, LocalNotifications, Push, Device } from '@singletons/core.singletons';
import { makeSingleton, NgZone, Platform, Translate, LocalNotifications, Push } from '@singletons/core.singletons';
import { CoreLogger } from '@singletons/logger';
import {
APP_SCHEMA,
@ -173,7 +173,7 @@ export class CoreLocalNotificationsProvider {
*/
canDisableSound(): boolean {
// Only allow disabling sound in Android 7 or lower. In iOS and Android 8+ it can easily be done with system settings.
return this.isAvailable() && CoreApp.instance.isAndroid() && Number(Device.instance.version?.split('.')[0]) < 8;
return this.isAvailable() && CoreApp.instance.isAndroid() && CoreApp.instance.getPlatformMajorVersion() < 8;
}
/**

View File

@ -361,7 +361,9 @@ export class CoreMimetypeUtilsProvider {
* @param field The field to get. If not supplied, all the info will be returned.
* @return Info for the group.
*/
getGroupMimeInfo(group: string, field?: string): MimeTypeGroupInfo {
getGroupMimeInfo(group: string): MimeTypeGroupInfo;
getGroupMimeInfo(group: string, field: string): string[] | undefined;
getGroupMimeInfo(group: string, field?: string): MimeTypeGroupInfo | string[] | undefined {
if (typeof this.groupsMimeInfo[group] == 'undefined') {
this.fillGroupMimeInfo(group);
}
@ -379,7 +381,11 @@ export class CoreMimetypeUtilsProvider {
* @param extension Extension.
* @return Mimetype.
*/
getMimeType(extension: string): string | undefined {
getMimeType(extension?: string): string | undefined {
if (!extension) {
return;
}
extension = this.cleanExtension(extension);
if (this.extToMime[extension] && this.extToMime[extension].type) {

View File

@ -737,12 +737,12 @@ export class CoreWSProvider {
* @param onProgress Function to call on progress.
* @return Promise resolved when uploaded.
*/
async uploadFile<T = unknown>(
async uploadFile(
filePath: string,
options: CoreWSFileUploadOptions,
preSets: CoreWSPreSets,
onProgress?: (event: ProgressEvent) => void,
): Promise<T> {
): Promise<CoreWSUploadFileResult> {
this.logger.debug(`Trying to upload file: ${filePath}`);
if (!filePath || !options || !preSets) {
@ -1193,3 +1193,16 @@ export type CoreWSDownloadedFileEntry = FileEntry & {
extension: string; // File extension.
path: string; // File path.
};
export type CoreWSUploadFileResult = {
component: string; // Component the file was uploaded to.
context: string; // Context the file was uploaded to.
userid: number; // User that uploaded the file.
filearea: string; // File area the file was uploaded to.
filename: string; // File name.
filepath: string; // File path.
itemid: number; // Item ID the file was uploaded to.
license: string; // File license.
author: string; // Author name.
source: string; // File source.
};