commit
85507d75fb
|
@ -2115,6 +2115,36 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"@ionic-native/camera": {
|
||||
"version": "5.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@ionic-native/camera/-/camera-5.29.0.tgz",
|
||||
"integrity": "sha512-JOmFb2eWeh8zZWu2JlNVRbhcSvOcwiTSdoabEfGtw0ITXs0FzuRmzAQgF2PQGyPA8844wkr3T5IUhcMpYxW6UQ==",
|
||||
"requires": {
|
||||
"@types/cordova": "^0.0.34"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/cordova": {
|
||||
"version": "0.0.34",
|
||||
"resolved": "https://registry.npmjs.org/@types/cordova/-/cordova-0.0.34.tgz",
|
||||
"integrity": "sha1-6nrd907Ow9dimCegw54smt3HPQQ="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@ionic-native/chooser": {
|
||||
"version": "5.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@ionic-native/chooser/-/chooser-5.29.0.tgz",
|
||||
"integrity": "sha512-1/+zr+SbijWqd0FomOh83aQb8vqH2qO2CAlgX2FyjJuK4fgt3BF9GMXpzTjkd/qrHO9rbxUMFAcrQAv/HAVNiA==",
|
||||
"requires": {
|
||||
"@types/cordova": "^0.0.34"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/cordova": {
|
||||
"version": "0.0.34",
|
||||
"resolved": "https://registry.npmjs.org/@types/cordova/-/cordova-0.0.34.tgz",
|
||||
"integrity": "sha1-6nrd907Ow9dimCegw54smt3HPQQ="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@ionic-native/clipboard": {
|
||||
"version": "5.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@ionic-native/clipboard/-/clipboard-5.28.0.tgz",
|
||||
|
@ -2247,6 +2277,36 @@
|
|||
"@types/cordova": "^0.0.34"
|
||||
}
|
||||
},
|
||||
"@ionic-native/media": {
|
||||
"version": "5.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@ionic-native/media/-/media-5.29.0.tgz",
|
||||
"integrity": "sha512-XC8MtrbeR0X0I6B0FABStc2mSAmgIQidaRjFqP4jBAElAwjZC7PHwaDyyVJUOR1Rx5Nest46hZAU6jpAPZ8+pw==",
|
||||
"requires": {
|
||||
"@types/cordova": "^0.0.34"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/cordova": {
|
||||
"version": "0.0.34",
|
||||
"resolved": "https://registry.npmjs.org/@types/cordova/-/cordova-0.0.34.tgz",
|
||||
"integrity": "sha1-6nrd907Ow9dimCegw54smt3HPQQ="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@ionic-native/media-capture": {
|
||||
"version": "5.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@ionic-native/media-capture/-/media-capture-5.29.0.tgz",
|
||||
"integrity": "sha512-5NdTXQGbrpXLeeLbI+cGQaeNmpmOrPC9vgX4jvUT6whUdDXGZ93wLT1/eeRj208czNiqbdetjG8Dji3OJZ5MKA==",
|
||||
"requires": {
|
||||
"@types/cordova": "^0.0.34"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/cordova": {
|
||||
"version": "0.0.34",
|
||||
"resolved": "https://registry.npmjs.org/@types/cordova/-/cordova-0.0.34.tgz",
|
||||
"integrity": "sha1-6nrd907Ow9dimCegw54smt3HPQQ="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@ionic-native/network": {
|
||||
"version": "5.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@ionic-native/network/-/network-5.28.0.tgz",
|
||||
|
|
|
@ -38,6 +38,8 @@
|
|||
"@angular/platform-browser": "~10.0.0",
|
||||
"@angular/platform-browser-dynamic": "~10.0.0",
|
||||
"@angular/router": "~10.0.0",
|
||||
"@ionic-native/camera": "^5.29.0",
|
||||
"@ionic-native/chooser": "^5.29.0",
|
||||
"@ionic-native/clipboard": "^5.28.0",
|
||||
"@ionic-native/core": "^5.0.0",
|
||||
"@ionic-native/device": "^5.28.0",
|
||||
|
@ -51,6 +53,8 @@
|
|||
"@ionic-native/ionic-webview": "^5.28.0",
|
||||
"@ionic-native/keyboard": "^5.28.0",
|
||||
"@ionic-native/local-notifications": "^5.28.0",
|
||||
"@ionic-native/media": "^5.29.0",
|
||||
"@ionic-native/media-capture": "^5.29.0",
|
||||
"@ionic-native/network": "^5.28.0",
|
||||
"@ionic-native/push": "^5.28.0",
|
||||
"@ionic-native/qr-scanner": "^5.28.0",
|
||||
|
|
|
@ -228,12 +228,6 @@
|
|||
"addon.coursecompletion.requirement": "block_completionstatus",
|
||||
"addon.coursecompletion.status": "moodle",
|
||||
"addon.coursecompletion.viewcoursereport": "completion",
|
||||
"addon.files.couldnotloadfiles": "local_moodlemobileapp",
|
||||
"addon.files.emptyfilelist": "local_moodlemobileapp",
|
||||
"addon.files.erroruploadnotworking": "local_moodlemobileapp",
|
||||
"addon.files.files": "moodle",
|
||||
"addon.files.privatefiles": "moodle",
|
||||
"addon.files.sitefiles": "moodle",
|
||||
"addon.messageoutput_airnotifier.processorsettingsdesc": "local_moodlemobileapp",
|
||||
"addon.messages.acceptandaddcontact": "message",
|
||||
"addon.messages.addcontact": "message",
|
||||
|
@ -1049,6 +1043,12 @@
|
|||
"addon.notifications.notifications": "local_moodlemobileapp",
|
||||
"addon.notifications.playsound": "local_moodlemobileapp",
|
||||
"addon.notifications.therearentnotificationsyet": "local_moodlemobileapp",
|
||||
"addon.privatefiles.couldnotloadfiles": "local_moodlemobileapp",
|
||||
"addon.privatefiles.emptyfilelist": "local_moodlemobileapp",
|
||||
"addon.privatefiles.erroruploadnotworking": "local_moodlemobileapp",
|
||||
"addon.privatefiles.files": "moodle",
|
||||
"addon.privatefiles.privatefiles": "moodle",
|
||||
"addon.privatefiles.sitefiles": "moodle",
|
||||
"addon.storagemanager.deletecourse": "local_moodlemobileapp",
|
||||
"addon.storagemanager.deletecourses": "local_moodlemobileapp",
|
||||
"addon.storagemanager.deletedatafrom": "local_moodlemobileapp",
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"couldnotloadfiles": "The list of files could not be loaded.",
|
||||
"emptyfilelist": "There are no files to show.",
|
||||
"erroruploadnotworking": "Unfortunately it is currently not possible to upload files to your site.",
|
||||
"files": "Files",
|
||||
"privatefiles": "Private files",
|
||||
"sitefiles": "Site files"
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>{{ title }}</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-refresher slot="fixed" [disabled]="!filesLoaded || (!showPrivateFiles && !showSiteFiles)"
|
||||
(ionRefresh)="refreshData($event)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
|
||||
<core-loading [hideUntil]="filesLoaded" *ngIf="showPrivateFiles || showSiteFiles">
|
||||
<!-- Allow selecting the files to see: private or site. -->
|
||||
<div class="ion-padding" *ngIf="showPrivateFiles && showSiteFiles && !path">
|
||||
<ion-select [(ngModel)]="root" (ngModelChange)="rootChanged()" interface="popover" class="core-button-select">
|
||||
<ion-select-option value="my">{{ 'addon.privatefiles.privatefiles' | translate }}</ion-select-option>
|
||||
<ion-select-option value="site">{{ 'addon.privatefiles.sitefiles' | translate }}</ion-select-option>
|
||||
</ion-select>
|
||||
</div>
|
||||
|
||||
<!-- Display info about space used and space left. -->
|
||||
<ion-card class="core-info-card" *ngIf="userQuota && filesInfo && filesInfo.filecount > 0">
|
||||
<ion-item><ion-label>
|
||||
{{ 'core.quotausage' | translate:{$a: {used: spaceUsed, total: userQuotaReadable} } }}
|
||||
</ion-label></ion-item>
|
||||
</ion-card>
|
||||
|
||||
<!-- List of files. -->
|
||||
<ion-list *ngIf="files && files.length > 0">
|
||||
<ng-container *ngFor="let file of files">
|
||||
<ion-item button *ngIf="file.isdir" class="item-file" (click)="openFolder(file)">
|
||||
<ion-thumbnail slot="start">
|
||||
<img [src]="file.imgPath" alt="" role="presentation">
|
||||
</ion-thumbnail>
|
||||
<ion-label>{{file.filename}}</ion-label>
|
||||
</ion-item>
|
||||
<core-file *ngIf="!file.isdir" [file]="file" [component]="component" [componentId]="file.contextid"></core-file>
|
||||
</ng-container>
|
||||
</ion-list>
|
||||
|
||||
<!-- Message telling there are no files. -->
|
||||
<core-empty-box *ngIf="!files || !files.length" icon="folder" [message]="'addon.privatefiles.emptyfilelist' | translate">
|
||||
</core-empty-box>
|
||||
</core-loading>
|
||||
|
||||
<!-- Upload a private file. -->
|
||||
<ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="showUpload && root != 'site' && !path">
|
||||
<ion-fab-button (click)="uploadFile()" [attr.aria-label]="'core.fileuploader.uploadafile' | translate">
|
||||
<ion-icon name="fa-plus"></ion-icon>
|
||||
</ion-fab-button>
|
||||
</ion-fab>
|
||||
</ion-content>
|
|
@ -0,0 +1,49 @@
|
|||
// (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 { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { CoreComponentsModule } from '@components/components.module';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
|
||||
import { AddonPrivateFilesIndexPage } from './index.page';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: AddonPrivateFilesIndexPage,
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild(routes),
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
FormsModule,
|
||||
TranslateModule.forChild(),
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule,
|
||||
],
|
||||
declarations: [
|
||||
AddonPrivateFilesIndexPage,
|
||||
],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class AddonPrivateFilesIndexPageModule {}
|
|
@ -0,0 +1,270 @@
|
|||
// (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 { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { IonRefresher, NavController } from '@ionic/angular';
|
||||
import { Md5 } from 'ts-md5/dist/md5';
|
||||
|
||||
import { CoreApp } from '@services/app';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
import { Translate } from '@singletons/core.singletons';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import {
|
||||
AddonPrivateFiles,
|
||||
AddonPrivateFilesProvider,
|
||||
AddonPrivateFilesFile,
|
||||
AddonPrivateFilesGetUserInfoWSResult,
|
||||
AddonPrivateFilesGetFilesWSParams,
|
||||
} from '@addon/privatefiles/services/privatefiles';
|
||||
import { AddonPrivateFilesHelper } from '@addon/privatefiles/services/privatefiles.helper';
|
||||
import { CoreUtils } from '@/app/services/utils/utils';
|
||||
|
||||
/**
|
||||
* Page that displays the list of files.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'page-addon-privatefiles-index',
|
||||
templateUrl: 'index.html',
|
||||
})
|
||||
export class AddonPrivateFilesIndexPage implements OnInit, OnDestroy {
|
||||
|
||||
title!: string; // Page title.
|
||||
root?: 'my' | 'site'; // The root of the files loaded: 'my' or 'site'.
|
||||
path?: AddonPrivateFilesGetFilesWSParams; // The path of the directory being loaded. If empty path it means load the root.
|
||||
showPrivateFiles!: boolean; // Whether the user can view private files.
|
||||
showSiteFiles!: boolean; // Whether the user can view site files.
|
||||
showUpload!: boolean; // Whether the user can upload files.
|
||||
userQuota?: number; // The user quota (in bytes).
|
||||
filesInfo?: AddonPrivateFilesGetUserInfoWSResult; // Info about private files (size, number of files, etc.).
|
||||
spaceUsed?: string; // Space used in a readable format.
|
||||
userQuotaReadable?: string; // User quota in a readable format.
|
||||
files?: AddonPrivateFilesFile[]; // List of files.
|
||||
component!: string; // Component to link the file downloads to.
|
||||
filesLoaded = false; // Whether the files are loaded.
|
||||
|
||||
protected updateSiteObserver: CoreEventObserver;
|
||||
|
||||
constructor(
|
||||
protected route: ActivatedRoute,
|
||||
protected navCtrl: NavController,
|
||||
) {
|
||||
// Update visibility if current site info is updated.
|
||||
this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
|
||||
this.setVisibility();
|
||||
}, CoreSites.instance.getCurrentSiteId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.root = this.route.snapshot.queryParams['root'];
|
||||
|
||||
if (this.route.snapshot.queryParams['contextid']) {
|
||||
// Loading a certain folder.
|
||||
this.path = {
|
||||
contextid: this.route.snapshot.queryParams['contextid'],
|
||||
component: this.route.snapshot.queryParams['component'],
|
||||
filearea: this.route.snapshot.queryParams['filearea'],
|
||||
itemid: this.route.snapshot.queryParams['itemid'],
|
||||
filepath: this.route.snapshot.queryParams['filepath'],
|
||||
filename: this.route.snapshot.queryParams['filename'],
|
||||
};
|
||||
}
|
||||
|
||||
this.title = this.path?.filename || Translate.instance.instant('addon.privatefiles.files');
|
||||
|
||||
this.setVisibility();
|
||||
this.userQuota = CoreSites.instance.getCurrentSite()?.getInfo()?.userquota;
|
||||
|
||||
if (!this.root) {
|
||||
// Load private files by default.
|
||||
if (this.showPrivateFiles) {
|
||||
this.root = 'my';
|
||||
} else if (this.showSiteFiles) {
|
||||
this.root = 'site';
|
||||
}
|
||||
}
|
||||
|
||||
if (this.root) {
|
||||
this.rootChanged();
|
||||
} else {
|
||||
this.filesLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set visibility of some items based on site data.
|
||||
*/
|
||||
protected setVisibility(): void {
|
||||
this.showPrivateFiles = AddonPrivateFiles.instance.canViewPrivateFiles();
|
||||
this.showSiteFiles = AddonPrivateFiles.instance.canViewSiteFiles();
|
||||
this.showUpload = AddonPrivateFiles.instance.canUploadFiles();
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the data.
|
||||
*
|
||||
* @param refresher Refresher.
|
||||
*/
|
||||
refreshData(event?: CustomEvent<IonRefresher>): void {
|
||||
this.refreshFiles().finally(() => {
|
||||
event?.detail.complete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Function called when the root has changed.
|
||||
*/
|
||||
rootChanged(): void {
|
||||
this.filesLoaded = false;
|
||||
this.component = this.root == 'my' ? AddonPrivateFilesProvider.PRIVATE_FILES_COMPONENT :
|
||||
AddonPrivateFilesProvider.SITE_FILES_COMPONENT;
|
||||
|
||||
this.fetchFiles().finally(() => {
|
||||
this.filesLoaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a new file.
|
||||
*/
|
||||
async uploadFile(): Promise<void> {
|
||||
const canUpload = await AddonPrivateFiles.instance.versionCanUploadFiles();
|
||||
|
||||
if (!canUpload) {
|
||||
CoreDomUtils.instance.showAlertTranslated('core.notice', 'addon.privatefiles.erroruploadnotworking');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!CoreApp.instance.isOnline()) {
|
||||
CoreDomUtils.instance.showErrorModal('core.fileuploader.errormustbeonlinetoupload', true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await AddonPrivateFilesHelper.instance.uploadPrivateFile(this.filesInfo);
|
||||
|
||||
// File uploaded, refresh the list.
|
||||
this.filesLoaded = false;
|
||||
|
||||
await CoreUtils.instance.ignoreErrors(this.refreshFiles());
|
||||
|
||||
this.filesLoaded = true;
|
||||
} catch (error) {
|
||||
CoreDomUtils.instance.showErrorModalDefault(error, 'core.fileuploader.errorwhileuploading', true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the files.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async fetchFiles(): Promise<void> {
|
||||
try {
|
||||
if (this.path) {
|
||||
// Path is set, serve the files the user requested.
|
||||
this.files = await AddonPrivateFiles.instance.getFiles(this.path);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// The path is unknown, the user must be requesting a root.
|
||||
if (this.root == 'site') {
|
||||
this.title = Translate.instance.instant('addon.privatefiles.sitefiles');
|
||||
|
||||
this.files = await AddonPrivateFiles.instance.getSiteFiles();
|
||||
} else if (this.root == 'my') {
|
||||
this.title = Translate.instance.instant('addon.privatefiles.files');
|
||||
|
||||
this.files = await AddonPrivateFiles.instance.getPrivateFiles();
|
||||
|
||||
if (this.showUpload && AddonPrivateFiles.instance.canGetPrivateFilesInfo() && this.userQuota &&
|
||||
this.userQuota > 0) {
|
||||
// Get the info to calculate the available size.
|
||||
this.filesInfo = await AddonPrivateFiles.instance.getPrivateFilesInfo();
|
||||
|
||||
this.spaceUsed = CoreTextUtils.instance.bytesToSize(this.filesInfo.filesizewithoutreferences, 1);
|
||||
this.userQuotaReadable = CoreTextUtils.instance.bytesToSize(this.userQuota, 1);
|
||||
} else {
|
||||
// User quota isn't useful, delete it.
|
||||
delete this.userQuota;
|
||||
}
|
||||
} else {
|
||||
// Unknown root.
|
||||
CoreDomUtils.instance.showErrorModal('addon.privatefiles.couldnotloadfiles', true);
|
||||
}
|
||||
} catch (error) {
|
||||
CoreDomUtils.instance.showErrorModalDefault(error, 'addon.privatefiles.couldnotloadfiles', true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the displayed files.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async refreshFiles(): Promise<void> {
|
||||
try {
|
||||
await Promise.all([
|
||||
AddonPrivateFiles.instance.invalidateDirectory(this.root, this.path),
|
||||
AddonPrivateFiles.instance.invalidatePrivateFilesInfoForUser(),
|
||||
]);
|
||||
} finally {
|
||||
await this.fetchFiles();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a folder.
|
||||
*
|
||||
* @param folder Folder to open.
|
||||
*/
|
||||
openFolder(folder: AddonPrivateFilesFile): void {
|
||||
const params = {
|
||||
contextid: folder.contextid,
|
||||
component: folder.component || '',
|
||||
filearea: folder.filearea || '',
|
||||
itemid: folder.itemid || 0,
|
||||
filepath: folder.filepath || '',
|
||||
filename: folder.filename || '',
|
||||
};
|
||||
|
||||
if (folder.component) {
|
||||
// Delete unused elements that may break the request.
|
||||
params.filename = '';
|
||||
}
|
||||
|
||||
const hash = <string> Md5.hashAsciiStr(JSON.stringify(params));
|
||||
|
||||
this.navCtrl.navigateForward([`../${hash}`], {
|
||||
relativeTo: this.route,
|
||||
queryParams: params,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Page destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.updateSiteObserver?.off();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
// (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 { Routes } from '@angular/router';
|
||||
|
||||
import { CoreMainMenuDelegate } from '@core/mainmenu/services/mainmenu.delegate';
|
||||
import { CoreMainMenuRoutingModule } from '@core/mainmenu/mainmenu-routing.module';
|
||||
import { AddonPrivateFilesMainMenuHandler } from './services/handlers/mainmenu';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: 'addon-privatefiles',
|
||||
loadChildren: () => import('@addon/privatefiles/privatefiles.module').then(m => m.AddonPrivateFilesModule),
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [CoreMainMenuRoutingModule.forChild(routes)],
|
||||
exports: [CoreMainMenuRoutingModule],
|
||||
providers: [
|
||||
AddonPrivateFilesMainMenuHandler,
|
||||
],
|
||||
})
|
||||
export class AddonPrivateFilesInitModule {
|
||||
|
||||
constructor(
|
||||
mainMenuDelegate: CoreMainMenuDelegate,
|
||||
mainMenuHandler: AddonPrivateFilesMainMenuHandler,
|
||||
) {
|
||||
mainMenuDelegate.registerHandler(mainMenuHandler);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
// (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 { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'root', // Fake "hash".
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{
|
||||
path: ':hash',
|
||||
loadChildren: () => import('./pages/index/index.page.module').then( m => m.AddonPrivateFilesIndexPageModule),
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class AddonPrivateFilesRoutingModule {}
|
|
@ -0,0 +1,25 @@
|
|||
// (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 { AddonPrivateFilesRoutingModule } from './privatefiles-routing.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
AddonPrivateFilesRoutingModule,
|
||||
],
|
||||
declarations: [],
|
||||
})
|
||||
export class AddonPrivateFilesModule {}
|
|
@ -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 { Injectable } from '@angular/core';
|
||||
|
||||
import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '@core/mainmenu/services/mainmenu.delegate';
|
||||
import { AddonPrivateFiles } from '@addon/privatefiles/services/privatefiles';
|
||||
|
||||
/**
|
||||
* Handler to inject an option into main menu.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonPrivateFilesMainMenuHandler implements CoreMainMenuHandler {
|
||||
|
||||
name = 'AddonPrivateFiles';
|
||||
priority = 400;
|
||||
|
||||
/**
|
||||
* Check if the handler is enabled on a site level.
|
||||
*
|
||||
* @return Whether or not the handler is enabled on a site level.
|
||||
*/
|
||||
async isEnabled(): Promise<boolean> {
|
||||
return AddonPrivateFiles.instance.isPluginEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data needed to render the handler.
|
||||
*
|
||||
* @return Data needed to render the handler.
|
||||
*/
|
||||
getDisplayData(): CoreMainMenuHandlerData {
|
||||
return {
|
||||
icon: 'fas-folder',
|
||||
title: 'addon.privatefiles.files',
|
||||
page: 'addon-privatefiles',
|
||||
subPage: 'root',
|
||||
class: 'addon-privatefiles-handler',
|
||||
};
|
||||
}
|
||||
|
||||
}
|
|
@ -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 { CoreSites } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreFileUploaderHelper } from '@core/fileuploader/services/fileuploader.helper';
|
||||
import { AddonPrivateFiles, AddonPrivateFilesGetUserInfoWSResult } from './privatefiles';
|
||||
import { CoreError } from '@classes/errors/error';
|
||||
import { makeSingleton, Translate } from '@singletons/core.singletons';
|
||||
|
||||
/**
|
||||
* Service that provides some helper functions regarding private and site files.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AddonPrivateFilesHelperProvider {
|
||||
|
||||
/**
|
||||
* Select a file, upload it and move it to private files.
|
||||
*
|
||||
* @param info Private files info. See AddonPrivateFilesProvider.getPrivateFilesInfo.
|
||||
* @return Promise resolved when a file is uploaded, rejected otherwise.
|
||||
*/
|
||||
async uploadPrivateFile(info?: AddonPrivateFilesGetUserInfoWSResult): Promise<void> {
|
||||
// Calculate the max size.
|
||||
const currentSite = CoreSites.instance.getCurrentSite();
|
||||
let maxSize = currentSite?.getInfo()?.usermaxuploadfilesize || -1;
|
||||
let userQuota = currentSite?.getInfo()?.userquota;
|
||||
|
||||
if (userQuota === 0) {
|
||||
// 0 means ignore user quota. In the app it is -1.
|
||||
userQuota = -1;
|
||||
} else if (userQuota !== undefined && userQuota > 0 && info !== undefined) {
|
||||
userQuota = userQuota - info.filesizewithoutreferences;
|
||||
}
|
||||
|
||||
if (userQuota !== undefined) {
|
||||
// Use the minimum value.
|
||||
maxSize = Math.min(maxSize, userQuota);
|
||||
}
|
||||
|
||||
// Select and upload the file.
|
||||
const result = await CoreFileUploaderHelper.instance.selectAndUploadFile(maxSize);
|
||||
|
||||
if (!result) {
|
||||
throw new CoreError(Translate.instance.instant('core.fileuploader.errorwhileuploading'));
|
||||
}
|
||||
|
||||
// File uploaded. Move it to private files.
|
||||
const modal = await CoreDomUtils.instance.showModalLoading('core.fileuploader.uploading', true);
|
||||
|
||||
try {
|
||||
await AddonPrivateFiles.instance.moveFromDraftToPrivate(result.itemid);
|
||||
|
||||
CoreDomUtils.instance.showToast('core.fileuploader.fileuploaded', true, undefined, 'core-toast-success');
|
||||
} finally {
|
||||
modal.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class AddonPrivateFilesHelper extends makeSingleton(AddonPrivateFilesHelperProvider) {}
|
|
@ -0,0 +1,497 @@
|
|||
// (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 { CoreSites } from '@services/sites';
|
||||
import { CoreMimetypeUtils } from '@services/utils/mimetype';
|
||||
import { CoreWSExternalWarning } from '@services/ws';
|
||||
import { CoreSite } from '@classes/site';
|
||||
import { makeSingleton } from '@singletons/core.singletons';
|
||||
|
||||
/**
|
||||
* Service to handle my files and site files.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AddonPrivateFilesProvider {
|
||||
|
||||
// Keep old names for backwards compatibility.
|
||||
static readonly PRIVATE_FILES_COMPONENT = 'mmaFilesMy';
|
||||
static readonly SITE_FILES_COMPONENT = 'mmaFilesSite';
|
||||
|
||||
protected readonly ROOT_CACHE_KEY = 'mmaFiles:';
|
||||
|
||||
/**
|
||||
* Check if core_user_get_private_files_info WS call is available.
|
||||
*
|
||||
* @return Whether the WS is available, false otherwise.
|
||||
*/
|
||||
canGetPrivateFilesInfo(): boolean {
|
||||
return CoreSites.instance.wsAvailableInCurrentSite('core_user_get_private_files_info');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user can view his private files.
|
||||
*
|
||||
* @return Whether the user can view his private files.
|
||||
*/
|
||||
canViewPrivateFiles(): boolean {
|
||||
const currentSite = CoreSites.instance.getCurrentSite();
|
||||
if (!currentSite) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return currentSite.canAccessMyFiles() && !this.isPrivateFilesDisabledInSite();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user can view site files.
|
||||
*
|
||||
* @return Whether the user can view site files.
|
||||
*/
|
||||
canViewSiteFiles(): boolean {
|
||||
return !this.isSiteFilesDisabledInSite();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user can upload private files.
|
||||
*
|
||||
* @return Whether the user can upload private files.
|
||||
*/
|
||||
canUploadFiles(): boolean {
|
||||
const currentSite = CoreSites.instance.getCurrentSite();
|
||||
if (!currentSite) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return currentSite.canAccessMyFiles() && currentSite.canUploadFiles() && !this.isUploadDisabledInSite();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of files.
|
||||
*
|
||||
* @param params A list of parameters accepted by the Web service.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with the files.
|
||||
*/
|
||||
async getFiles(params: AddonPrivateFilesGetFilesWSParams, siteId?: string): Promise<AddonPrivateFilesFile[]> {
|
||||
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
|
||||
const preSets = {
|
||||
cacheKey: this.getFilesListCacheKey(params),
|
||||
updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
|
||||
};
|
||||
|
||||
const result: AddonPrivateFilesGetFilesWSResult = await site.read('core_files_get_files', params, preSets);
|
||||
|
||||
if (!result.files) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return result.files.map((entry) => {
|
||||
entry.fileurl = entry.url;
|
||||
|
||||
if (entry.isdir) {
|
||||
entry.imgPath = CoreMimetypeUtils.instance.getFolderIcon();
|
||||
} else {
|
||||
entry.imgPath = CoreMimetypeUtils.instance.getFileIcon(entry.filename);
|
||||
}
|
||||
|
||||
return entry;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for file list WS calls.
|
||||
*
|
||||
* @param params Params of the WS.
|
||||
* @return Cache key.
|
||||
*/
|
||||
protected getFilesListCacheKey(params: AddonPrivateFilesGetFilesWSParams): string {
|
||||
const root = !params.component ? 'site' : 'my';
|
||||
|
||||
return this.ROOT_CACHE_KEY + 'list:' + root + ':' + params.contextid + ':' + params.filepath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the private files of the current user.
|
||||
*
|
||||
* @return Promise resolved with the files.
|
||||
*/
|
||||
getPrivateFiles(): Promise<AddonPrivateFilesFile[]> {
|
||||
return this.getFiles(this.getPrivateFilesRootParams());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get params to get root private files directory.
|
||||
*
|
||||
* @return Params.
|
||||
*/
|
||||
protected getPrivateFilesRootParams(): AddonPrivateFilesGetFilesWSParams {
|
||||
return {
|
||||
contextid: -1,
|
||||
component: 'user',
|
||||
filearea: 'private',
|
||||
contextlevel: 'user',
|
||||
instanceid: CoreSites.instance.getCurrentSite()?.getUserId(),
|
||||
itemid: 0,
|
||||
filepath: '',
|
||||
filename: '',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get private files info.
|
||||
*
|
||||
* @param userId User ID. If not defined, current user in the site.
|
||||
* @param siteId Site ID. If not defined, use current site.
|
||||
* @return Promise resolved with the info.
|
||||
*/
|
||||
async getPrivateFilesInfo(userId?: number, siteId?: string): Promise<AddonPrivateFilesGetUserInfoWSResult> {
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
|
||||
userId = userId || site.getUserId();
|
||||
|
||||
const params: AddonPrivateFilesGetUserInfoWSParams = {
|
||||
userid: userId,
|
||||
};
|
||||
const preSets = {
|
||||
cacheKey: this.getPrivateFilesInfoCacheKey(userId),
|
||||
updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
|
||||
};
|
||||
|
||||
return site.read('core_user_get_private_files_info', params, preSets);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cache key for private files info WS calls.
|
||||
*
|
||||
* @param userId User ID.
|
||||
* @return Cache key.
|
||||
*/
|
||||
protected getPrivateFilesInfoCacheKey(userId: number): string {
|
||||
return this.getPrivateFilesInfoCommonCacheKey() + ':' + userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the common part of the cache keys for private files info WS calls.
|
||||
*
|
||||
* @return Cache key.
|
||||
*/
|
||||
protected getPrivateFilesInfoCommonCacheKey(): string {
|
||||
return this.ROOT_CACHE_KEY + 'privateInfo';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the site files.
|
||||
*
|
||||
* @return Promise resolved with the files.
|
||||
*/
|
||||
getSiteFiles(): Promise<AddonPrivateFilesFile[]> {
|
||||
return this.getFiles(this.getSiteFilesRootParams());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get params to get root site files directory.
|
||||
*
|
||||
* @return Params.
|
||||
*/
|
||||
protected getSiteFilesRootParams(): AddonPrivateFilesGetFilesWSParams {
|
||||
return {
|
||||
contextid: 0,
|
||||
component: '',
|
||||
filearea: '',
|
||||
itemid: 0,
|
||||
filepath: '',
|
||||
filename: '',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates list of files in a certain directory.
|
||||
*
|
||||
* @param root Root of the directory ('my' for private files, 'site' for site files).
|
||||
* @param params Params to the directory.
|
||||
* @param siteId Site ID. If not defined, use current site.
|
||||
* @return Promise resolved when the data is invalidated.
|
||||
*/
|
||||
async invalidateDirectory(root?: 'my' | 'site', params?: AddonPrivateFilesGetFilesWSParams, siteId?: string): Promise<void> {
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!params) {
|
||||
if (root === 'site') {
|
||||
params = this.getSiteFilesRootParams();
|
||||
} else {
|
||||
params = this.getPrivateFilesRootParams();
|
||||
}
|
||||
}
|
||||
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
|
||||
await site.invalidateWsCacheForKey(this.getFilesListCacheKey(params));
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates private files info for all users.
|
||||
*
|
||||
* @param siteId Site ID. If not defined, use current site.
|
||||
* @return Promise resolved when the data is invalidated.
|
||||
*/
|
||||
async invalidatePrivateFilesInfo(siteId?: string): Promise<void> {
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
|
||||
await site.invalidateWsCacheForKeyStartingWith(this.getPrivateFilesInfoCommonCacheKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates private files info for a certain user.
|
||||
*
|
||||
* @param userId User ID. If not defined, current user in the site.
|
||||
* @param siteId Site ID. If not defined, use current site.
|
||||
* @return Promise resolved when the data is invalidated.
|
||||
*/
|
||||
async invalidatePrivateFilesInfoForUser(userId?: number, siteId?: string): Promise<void> {
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
|
||||
await site.invalidateWsCacheForKey(this.getPrivateFilesInfoCacheKey(userId || site.getUserId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Files is disabled in a certain site.
|
||||
*
|
||||
* @param siteId Site Id. If not defined, use current site.
|
||||
* @return Promise resolved with true if disabled, rejected or resolved with false otherwise.
|
||||
*/
|
||||
async isDisabled(siteId?: string): Promise<boolean> {
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
|
||||
return this.isDisabledInSite(site);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Files is disabled in a certain site.
|
||||
*
|
||||
* @param site Site. If not defined, use current site.
|
||||
* @return Whether it's disabled.
|
||||
*/
|
||||
isDisabledInSite(site: CoreSite): boolean {
|
||||
site = site || CoreSites.instance.getCurrentSite();
|
||||
|
||||
return site.isFeatureDisabled('CoreMainMenuDelegate_AddonPrivateFiles');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether or not the plugin is enabled.
|
||||
*
|
||||
* @return True if enabled, false otherwise.
|
||||
*/
|
||||
isPluginEnabled(): boolean {
|
||||
return this.canViewPrivateFiles() || this.canViewSiteFiles() || this.canUploadFiles();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if private files is disabled in a certain site.
|
||||
*
|
||||
* @param siteId Site Id. If not defined, use current site.
|
||||
* @return Promise resolved with true if disabled, rejected or resolved with false otherwise.
|
||||
*/
|
||||
async isPrivateFilesDisabled(siteId?: string): Promise<boolean> {
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
|
||||
return this.isPrivateFilesDisabledInSite(site);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if private files is disabled in a certain site.
|
||||
*
|
||||
* @param site Site. If not defined, use current site.
|
||||
* @return Whether it's disabled.
|
||||
*/
|
||||
isPrivateFilesDisabledInSite(site?: CoreSite): boolean {
|
||||
site = site || CoreSites.instance.getCurrentSite();
|
||||
|
||||
return !!site && site.isFeatureDisabled('AddonPrivateFilesPrivateFiles');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if site files is disabled in a certain site.
|
||||
*
|
||||
* @param siteId Site Id. If not defined, use current site.
|
||||
* @return Promise resolved with true if disabled, rejected or resolved with false otherwise.
|
||||
*/
|
||||
async isSiteFilesDisabled(siteId?: string): Promise<boolean> {
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
|
||||
return this.isSiteFilesDisabledInSite(site);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if site files is disabled in a certain site.
|
||||
*
|
||||
* @param site Site. If not defined, use current site.
|
||||
* @return Whether it's disabled.
|
||||
*/
|
||||
isSiteFilesDisabledInSite(site?: CoreSite): boolean {
|
||||
site = site || CoreSites.instance.getCurrentSite();
|
||||
|
||||
return !!site && site.isFeatureDisabled('AddonPrivateFilesSiteFiles');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if upload files is disabled in a certain site.
|
||||
*
|
||||
* @param siteId Site Id. If not defined, use current site.
|
||||
* @return Promise resolved with true if disabled, rejected or resolved with false otherwise.
|
||||
*/
|
||||
async isUploadDisabled(siteId?: string): Promise<boolean> {
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
|
||||
return this.isUploadDisabledInSite(site);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if upload files is disabled in a certain site.
|
||||
*
|
||||
* @param site Site. If not defined, use current site.
|
||||
* @return Whether it's disabled.
|
||||
*/
|
||||
isUploadDisabledInSite(site?: CoreSite): boolean {
|
||||
site = site || CoreSites.instance.getCurrentSite();
|
||||
|
||||
return !!site && site.isFeatureDisabled('AddonPrivateFilesUpload');
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a file from draft area to private files.
|
||||
*
|
||||
* @param draftId The draft area ID of the file.
|
||||
* @param siteid ID of the site. If not defined, use current site.
|
||||
* @return Promise resolved in success, rejected otherwise.
|
||||
*/
|
||||
async moveFromDraftToPrivate(draftId: number, siteId?: string): Promise<null> {
|
||||
const params: AddonPrivateFilesAddUserPrivateFilesWSParams = {
|
||||
draftid: draftId,
|
||||
};
|
||||
const preSets = {
|
||||
responseExpected: false,
|
||||
};
|
||||
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
|
||||
return site.write('core_user_add_user_private_files', params, preSets);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the Moodle version in order to check if upload files is working.
|
||||
*
|
||||
* @param siteId Site ID. If not defined, use current site.
|
||||
* @return Promise resolved with true if WS is working, false otherwise.
|
||||
*/
|
||||
async versionCanUploadFiles(siteId?: string): Promise<boolean> {
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
|
||||
// Upload private files doesn't work for Moodle 3.1.0 due to a bug.
|
||||
return site.isVersionGreaterEqualThan('3.1.1');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class AddonPrivateFiles extends makeSingleton(AddonPrivateFilesProvider) {}
|
||||
|
||||
/**
|
||||
* File data returned by core_files_get_files.
|
||||
*/
|
||||
export type AddonPrivateFilesFile = {
|
||||
contextid: number;
|
||||
component: string;
|
||||
filearea: string;
|
||||
itemid: number;
|
||||
filepath: string;
|
||||
filename: string;
|
||||
isdir: boolean;
|
||||
url: string;
|
||||
timemodified: number;
|
||||
timecreated?: number; // Time created.
|
||||
filesize?: number; // File size.
|
||||
author?: string; // File owner.
|
||||
license?: string; // File license.
|
||||
} & AddonPrivateFilesFileCalculatedData;
|
||||
|
||||
/**
|
||||
* Calculated data for AddonPrivateFilesFile.
|
||||
*/
|
||||
export type AddonPrivateFilesFileCalculatedData = {
|
||||
fileurl: string; // File URL, using same name as CoreWSExternalFile.
|
||||
imgPath?: string; // Path to file icon's image.
|
||||
};
|
||||
/**
|
||||
* Params of WS core_files_get_files.
|
||||
*/
|
||||
export type AddonPrivateFilesGetFilesWSParams = {
|
||||
contextid: number; // Context id Set to -1 to use contextlevel and instanceid.
|
||||
component: string; // Component.
|
||||
filearea: string; // File area.
|
||||
itemid: number; // Associated id.
|
||||
filepath: string; // File path.
|
||||
filename: string; // File name.
|
||||
modified?: number; // Timestamp to return files changed after this time.
|
||||
contextlevel?: string; // The context level for the file location.
|
||||
instanceid?: number; // The instance id for where the file is located.
|
||||
};
|
||||
|
||||
/**
|
||||
* Result of WS core_files_get_files.
|
||||
*/
|
||||
export type AddonPrivateFilesGetFilesWSResult = {
|
||||
parents: {
|
||||
contextid: number;
|
||||
component: string;
|
||||
filearea: string;
|
||||
itemid: number;
|
||||
filepath: string;
|
||||
filename: string;
|
||||
}[];
|
||||
files: AddonPrivateFilesFile[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Params of core_user_get_private_files_info WS.
|
||||
*/
|
||||
export type AddonPrivateFilesGetUserInfoWSParams = {
|
||||
userid?: number; // Id of the user, default to current user.
|
||||
};
|
||||
|
||||
/**
|
||||
* Data returned by core_user_get_private_files_info WS.
|
||||
*/
|
||||
export type AddonPrivateFilesGetUserInfoWSResult = {
|
||||
filecount: number; // Number of files in the area.
|
||||
foldercount: number; // Number of folders in the area.
|
||||
filesize: number; // Total size of the files in the area.
|
||||
filesizewithoutreferences: number; // Total size of the area excluding file references.
|
||||
warnings?: CoreWSExternalWarning[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Params of core_user_add_user_private_files WS.
|
||||
*/
|
||||
export type AddonPrivateFilesAddUserPrivateFilesWSParams = {
|
||||
draftid: number; // Draft area id.
|
||||
};
|
|
@ -16,7 +16,7 @@ import { Component, OnInit } from '@angular/core';
|
|||
import { NavController } from '@ionic/angular';
|
||||
|
||||
import { CoreLangProvider } from '@services/lang';
|
||||
import { CoreLoginHelperProvider } from '@core/login/services/helper';
|
||||
import { CoreLoginHelperProvider } from '@core/login/services/login.helper';
|
||||
import { CoreEvents, CoreEventSessionExpiredData } from '@singletons/events';
|
||||
import { Network, NgZone, Platform } from '@singletons/core.singletons';
|
||||
import { CoreApp } from '@services/app';
|
||||
|
|
|
@ -61,6 +61,10 @@ import { CoreEmulatorModule } from '@core/emulator/emulator.module';
|
|||
import { CoreLoginModule } from '@core/login/login.module';
|
||||
import { CoreCoursesModule } from '@core/courses/courses.module';
|
||||
import { CoreSettingsInitModule } from '@core/settings/settings-init.module';
|
||||
import { CoreFileUploaderInitModule } from '@core/fileuploader/fileuploader-init.module';
|
||||
|
||||
// Import addons init modules.
|
||||
import { AddonPrivateFilesInitModule } from '@addon/privatefiles/privatefiles-init.module';
|
||||
|
||||
import { setSingletonsInjector } from '@singletons/core.singletons';
|
||||
|
||||
|
@ -91,6 +95,8 @@ export function createTranslateLoader(http: HttpClient): TranslateHttpLoader {
|
|||
CoreLoginModule,
|
||||
CoreCoursesModule,
|
||||
CoreSettingsInitModule,
|
||||
CoreFileUploaderInitModule,
|
||||
AddonPrivateFilesInitModule,
|
||||
],
|
||||
providers: [
|
||||
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
// (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 { CoreError } from './error';
|
||||
|
||||
/**
|
||||
* Capture error.
|
||||
*/
|
||||
export class CoreCaptureError extends CoreError {
|
||||
|
||||
code: number;
|
||||
|
||||
constructor(code: number, message?: string) {
|
||||
super(message);
|
||||
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
// (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 {
|
||||
Component,
|
||||
Input,
|
||||
OnInit,
|
||||
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',
|
||||
templateUrl: 'core-chrono.html',
|
||||
})
|
||||
export class CoreChronoComponent implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
@Input() running?: boolean; // Set it to true to start the chrono. Set it to false to stop it.
|
||||
@Input() startTime = 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 = 0;
|
||||
protected interval?: number;
|
||||
|
||||
constructor(protected changeDetectorRef: ChangeDetectorRef) {
|
||||
this.onEnd = new EventEmitter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.time = this.startTime || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being changed.
|
||||
*/
|
||||
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
|
||||
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 = window.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.changeDetectorRef.detectChanges();
|
||||
}, 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the chrono, leaving the same time it has.
|
||||
*/
|
||||
protected stop(): void {
|
||||
clearInterval(this.interval);
|
||||
delete this.interval;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.stop();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
<span role="timer">{{ time / 1000 | coreSecondsToHMS }}</span>
|
|
@ -17,6 +17,7 @@ import { CommonModule } from '@angular/common';
|
|||
import { IonicModule } from '@ionic/angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { CoreChronoComponent } from './chrono/chrono';
|
||||
import { CoreDownloadRefreshComponent } from './download-refresh/download-refresh';
|
||||
import { CoreFileComponent } from './file/file';
|
||||
import { CoreIconComponent } from './icon/icon';
|
||||
|
@ -35,6 +36,7 @@ import { CorePipesModule } from '@app/pipes/pipes.module';
|
|||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
CoreChronoComponent,
|
||||
CoreDownloadRefreshComponent,
|
||||
CoreFileComponent,
|
||||
CoreIconComponent,
|
||||
|
@ -56,6 +58,7 @@ import { CorePipesModule } from '@app/pipes/pipes.module';
|
|||
CorePipesModule,
|
||||
],
|
||||
exports: [
|
||||
CoreChronoComponent,
|
||||
CoreDownloadRefreshComponent,
|
||||
CoreFileComponent,
|
||||
CoreIconComponent,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<ion-item *ngIf="file" button class="ion-text-wrap item-media" (click)="download($event, true)" detail="false">
|
||||
<ion-item *ngIf="file" button class="ion-text-wrap item-file" (click)="download($event, true)" detail="false">
|
||||
<ion-thumbnail slot="start">
|
||||
<img [src]="fileIcon" alt="" role="presentation" />
|
||||
</ion-thumbnail>
|
||||
|
|
|
@ -95,11 +95,10 @@ export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit {
|
|||
setTimeout(() => {
|
||||
// Content is loaded so, center the spinner on the content itself.
|
||||
this.element.classList.add('core-loading-loaded');
|
||||
setTimeout(() => {
|
||||
// Change CSS to force calculate height.
|
||||
this.content?.nativeElement.classList.add('core-loading-content');
|
||||
this.content?.nativeElement.classList.remove('core-loading-content-loading');
|
||||
}, 500);
|
||||
// Change CSS to force calculate height.
|
||||
// Removed 500ms timeout to avoid reallocating html.
|
||||
this.content?.nativeElement.classList.add('core-loading-content');
|
||||
this.content?.nativeElement.classList.remove('core-loading-content-loading');
|
||||
});
|
||||
} else {
|
||||
this.element.classList.remove('core-loading-loaded');
|
||||
|
|
|
@ -17,7 +17,7 @@ import { NavController } from '@ionic/angular';
|
|||
import { CoreSiteBasicInfo, CoreSites } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { Translate } from '@singletons/core.singletons';
|
||||
import { CoreLoginHelper } from '@core/login/services/helper';
|
||||
import { CoreLoginHelper } from '@core/login/services/login.helper';
|
||||
import { CoreContentLinksAction } from '../../services/contentlinks.delegate';
|
||||
import { CoreContentLinksHelper } from '../../services/contentlinks.helper';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
|
|
@ -17,7 +17,7 @@ import { NavController } from '@ionic/angular';
|
|||
import { CoreSites } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreLoginHelper } from '@core/login/services/helper';
|
||||
import { CoreLoginHelper } from '@core/login/services/login.helper';
|
||||
import { CoreContentLinksDelegate, CoreContentLinksAction } from './contentlinks.delegate';
|
||||
import { CoreSite } from '@classes/site';
|
||||
import { CoreMainMenu } from '@core/mainmenu/services/mainmenu';
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title *ngIf="title">{{ title | translate }}</ion-title>
|
||||
|
||||
<ion-buttons slot="end">
|
||||
<ion-button (click)="cancel()">{{ 'core.cancel' | translate }}</ion-button>
|
||||
<ion-button *ngIf="hasCaptured" (click)="done()">{{ 'core.done' | translate }}</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</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>
|
||||
|
||||
<!-- Recording audio. -->
|
||||
<div *ngIf="isAudio" class="core-audio-record-container">
|
||||
<!-- Canvas to show audio waves when recording audio in browser. -->
|
||||
<canvas [hidden]="hasCaptured || isCordovaAudioCapture" class="core-audio-canvas" #streamAudio></canvas>
|
||||
|
||||
<!-- Button to start/stop in mobile devices. -->
|
||||
<ion-button fill="clear" *ngIf="!hasCaptured && isCordovaAudioCapture" (click)="actionClicked()"
|
||||
[attr.aria-label]="title">
|
||||
<ion-icon *ngIf="!isCapturing" name="fa-microphone" slot="icon-only"></ion-icon>
|
||||
<ion-icon *ngIf="isCapturing" name="fa-square" slot="icon-only"></ion-icon>
|
||||
</ion-button>
|
||||
|
||||
<!-- Audio player to listen to the result. -->
|
||||
<audio [hidden]="!hasCaptured" class="core-audio-captured" controls #previewAudio></audio>
|
||||
</div>
|
||||
</div>
|
||||
</core-loading>
|
||||
</ion-content>
|
||||
|
||||
<ion-footer *ngIf="readyToCapture">
|
||||
<ion-row>
|
||||
<ion-col></ion-col>
|
||||
<ion-col class="ion-text-center">
|
||||
<ion-button fill="clear" *ngIf="!hasCaptured && !isCordovaAudioCapture" (click)="actionClicked()"
|
||||
[attr.aria-label]="title">
|
||||
<ion-icon *ngIf="!isCapturing && isAudio" name="fa-microphone" slot="icon-only"></ion-icon>
|
||||
<ion-icon *ngIf="!isCapturing && isVideo" name="fa-video" slot="icon-only"></ion-icon>
|
||||
<ion-icon *ngIf="isImage" name="fa-camera" slot="icon-only"></ion-icon>
|
||||
<ion-icon *ngIf="isCapturing" name="fa-square" slot="icon-only"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button fill="clear" *ngIf="hasCaptured" (click)="discard()" [attr.aria-label]="'core.discard' | translate">
|
||||
<ion-icon color="danger" name="fa-trash" slot="icon-only"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
<ion-col class="ion-padding ion-text-end 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,69 @@
|
|||
:host {
|
||||
.core-av-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.core-webcam-image-canvas {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.core-audio-record-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
.core-audio-canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.core-audio-captured {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
height: 120px;
|
||||
width: 120px;
|
||||
|
||||
.icon {
|
||||
font-size: 120px;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
audio {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ion-footer {
|
||||
background-color: var(--gray);
|
||||
border-top: 1px solid var(--gray-dark);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,588 @@
|
|||
// (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 { Component, OnInit, OnDestroy, ViewChild, ElementRef, ChangeDetectorRef, Input } from '@angular/core';
|
||||
import { MediaObject } from '@ionic-native/media/ngx';
|
||||
import { FileEntry } from '@ionic-native/file/ngx';
|
||||
import { MediaFile } from '@ionic-native/media-capture/ngx';
|
||||
|
||||
import { CoreApp } from '@services/app';
|
||||
import { CoreFile, CoreFileProvider } from '@services/file';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreMimetypeUtils } from '@services/utils/mimetype';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
import { CoreTimeUtils } from '@services/utils/time';
|
||||
import { Platform, ModalController, Media, Translate } from '@singletons/core.singletons';
|
||||
import { CoreError } from '@classes/errors/error';
|
||||
import { CoreCaptureError } from '@classes/errors/captureerror';
|
||||
import { CoreCanceledError } from '@classes/errors/cancelederror';
|
||||
|
||||
/**
|
||||
* Page to capture media in browser, or to capture audio in mobile devices.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'core-emulator-capture-media',
|
||||
templateUrl: 'capture-media.html',
|
||||
styleUrls: ['capture-media.scss'],
|
||||
})
|
||||
export class CoreEmulatorCaptureMediaComponent implements OnInit, OnDestroy {
|
||||
|
||||
@Input() type?: 'audio' | 'video' | 'image' | 'captureimage';
|
||||
@Input() maxTime?: number; // Max time to capture.
|
||||
@Input() facingMode?: string; // Camera facing mode.
|
||||
@Input() mimetype?: string;
|
||||
@Input() extension?: string;
|
||||
@Input() quality?: number; // Only for images.
|
||||
@Input() returnDataUrl?: boolean; // Whether it should return a data img. Only for images.
|
||||
|
||||
@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.
|
||||
resetChrono?: boolean; // Boolean to reset the chrono.
|
||||
isCordovaAudioCapture?: boolean; // Whether it's capturing audio using Cordova plugin.
|
||||
|
||||
protected isCaptureImage?: boolean; // To identify if it's capturing an image using media capture plugin (instead of camera).
|
||||
protected mediaRecorder?: MediaRecorder; // To record video/audio.
|
||||
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;
|
||||
protected audioDrawer?: {start: () => void; stop: () => void }; // To start/stop the display of audio sound.
|
||||
|
||||
// Variables for Cordova Media capture.
|
||||
protected mediaFile?: MediaObject;
|
||||
protected filePath?: string;
|
||||
protected fileEntry?: FileEntry;
|
||||
|
||||
constructor(
|
||||
protected changeDetectorRef: ChangeDetectorRef,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.initVariables();
|
||||
|
||||
if (this.isCordovaAudioCapture) {
|
||||
this.initCordovaMediaPlugin();
|
||||
} else {
|
||||
this.initHtmlCapture();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize some variables based on the params.
|
||||
*/
|
||||
protected initVariables(): void {
|
||||
this.facingMode = this.facingMode || 'environment';
|
||||
this.quality = this.quality || 0.92;
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
this.isCordovaAudioCapture = CoreApp.instance.isMobile() && this.isAudio;
|
||||
|
||||
if (this.isCordovaAudioCapture) {
|
||||
this.extension = Platform.instance.is('ios') ? 'wav' : 'aac';
|
||||
this.returnDataUrl = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Init recording with Cordova media plugin.
|
||||
*
|
||||
* @return Promise resolved when ready.
|
||||
*/
|
||||
protected async initCordovaMediaPlugin(): Promise<void> {
|
||||
this.filePath = this.getFilePath();
|
||||
let absolutePath = CoreTextUtils.instance.concatenatePaths(CoreFile.instance.getBasePathInstant(), this.filePath);
|
||||
|
||||
if (Platform.instance.is('ios')) {
|
||||
// In iOS we need to remove the file:// part.
|
||||
absolutePath = absolutePath.replace(/^file:\/\//, '');
|
||||
}
|
||||
|
||||
try {
|
||||
// First create the file.
|
||||
this.fileEntry = await CoreFile.instance.createFile(this.filePath);
|
||||
|
||||
// Now create the media instance.
|
||||
this.mediaFile = Media.instance.create(absolutePath);
|
||||
this.readyToCapture = true;
|
||||
this.previewMedia = this.previewAudio?.nativeElement;
|
||||
} catch (error) {
|
||||
this.dismissWithError(-1, error.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Init HTML recorder for browser
|
||||
* .
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async initHtmlCapture(): Promise<void> {
|
||||
const constraints = {
|
||||
video: this.isAudio ? false : { facingMode: this.facingMode },
|
||||
audio: !this.isImage,
|
||||
};
|
||||
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
|
||||
let chunks: Blob[] = [];
|
||||
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 MediaRecorder(this.localMediaStream, { mimeType: this.mimetype });
|
||||
|
||||
// When video or audio is recorded, add it to the list of chunks.
|
||||
this.mediaRecorder.ondataavailable = (e): void => {
|
||||
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 = (): void => {
|
||||
this.mediaBlob = new Blob(chunks);
|
||||
chunks = [];
|
||||
|
||||
if (this.previewMedia) {
|
||||
this.previewMedia.src = window.URL.createObjectURL(this.mediaBlob);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (!this.isImage && !this.isVideo) {
|
||||
// It's ready to capture.
|
||||
this.readyToCapture = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.streamVideo) {
|
||||
throw new CoreError('Video element not found.');
|
||||
}
|
||||
|
||||
let hasLoaded = false;
|
||||
|
||||
// If stream isn't ready in a while, show error.
|
||||
const waitTimeout = window.setTimeout(() => {
|
||||
if (!hasLoaded) {
|
||||
// Show error.
|
||||
hasLoaded = true;
|
||||
this.dismissWithError(-1, 'Cannot connect to webcam.');
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
// Listen for stream ready to display the stream.
|
||||
this.streamVideo.nativeElement.onloadedmetadata = (): void => {
|
||||
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.changeDetectorRef.detectChanges();
|
||||
};
|
||||
|
||||
// Set the stream as the source of the video.
|
||||
if ('srcObject' in this.streamVideo.nativeElement) {
|
||||
this.streamVideo.nativeElement.srcObject = this.localMediaStream;
|
||||
} else {
|
||||
// Fallback for old browsers.
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/srcObject#Examples
|
||||
this.streamVideo.nativeElement.src = window.URL.createObjectURL(this.localMediaStream);
|
||||
}
|
||||
} 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 stream Stream returned by getUserMedia.
|
||||
*/
|
||||
protected initAudioDrawer(stream: MediaStream): void {
|
||||
if (!this.streamAudio) {
|
||||
return;
|
||||
}
|
||||
|
||||
let skip = true;
|
||||
let running = false;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const audioCtx = new (window.AudioContext || (<any> window).webkitAudioContext)();
|
||||
const canvasCtx = this.streamAudio.nativeElement.getContext('2d');
|
||||
const source = audioCtx.createMediaStreamSource(stream);
|
||||
const analyser = audioCtx.createAnalyser();
|
||||
const bufferLength = analyser.frequencyBinCount;
|
||||
const dataArray = new Uint8Array(bufferLength);
|
||||
const width = this.streamAudio.nativeElement.width;
|
||||
const height = this.streamAudio.nativeElement.height;
|
||||
const drawAudio = (): void => {
|
||||
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;
|
||||
}
|
||||
|
||||
const sliceWidth = width / bufferLength;
|
||||
let 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++) {
|
||||
const v = dataArray[i] / 128.0;
|
||||
const 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: (): void => {
|
||||
if (running) {
|
||||
return;
|
||||
}
|
||||
|
||||
running = true;
|
||||
drawAudio();
|
||||
},
|
||||
stop: (): void => {
|
||||
running = false;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Main action clicked: record or stop recording.
|
||||
*/
|
||||
async actionClicked(): Promise<void> {
|
||||
if (this.isCapturing) {
|
||||
// It's capturing, stop.
|
||||
this.stopCapturing();
|
||||
this.changeDetectorRef.detectChanges();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isImage) {
|
||||
// Start the capture.
|
||||
this.isCapturing = true;
|
||||
this.resetChrono = false;
|
||||
|
||||
if (this.isCordovaAudioCapture) {
|
||||
this.mediaFile?.startRecord();
|
||||
if (this.previewMedia) {
|
||||
this.previewMedia.src = '';
|
||||
}
|
||||
} else {
|
||||
this.mediaRecorder?.start();
|
||||
}
|
||||
|
||||
this.changeDetectorRef.detectChanges();
|
||||
} else {
|
||||
if (!this.imgCanvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the image from the video and set it to the canvas, using video width/height.
|
||||
const width = this.streamVideo?.nativeElement.videoWidth;
|
||||
const height = this.streamVideo?.nativeElement.videoHeight;
|
||||
const loadingModal = await CoreDomUtils.instance.showModalLoading();
|
||||
|
||||
|
||||
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.
|
||||
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.
|
||||
*/
|
||||
async cancel(): Promise<void> {
|
||||
if (this.hasCaptured) {
|
||||
try {
|
||||
await CoreDomUtils.instance.showConfirm(Translate.instance.instant('core.confirmcanceledit'));
|
||||
} catch {
|
||||
// Canceled.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Send a "cancelled" error like the Cordova plugin does.
|
||||
this.dismissWithCanceledError('Canceled.', 'Camera cancelled');
|
||||
|
||||
if (this.isCordovaAudioCapture && this.filePath) {
|
||||
// Delete the tmp file.
|
||||
CoreFile.instance.removeFile(this.filePath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Discard the captured media.
|
||||
*/
|
||||
discard(): void {
|
||||
this.previewMedia?.pause();
|
||||
this.streamVideo?.nativeElement.play();
|
||||
this.audioDrawer?.start();
|
||||
|
||||
this.hasCaptured = false;
|
||||
this.isCapturing = false;
|
||||
this.resetChrono = true;
|
||||
delete this.mediaBlob;
|
||||
this.changeDetectorRef.detectChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the modal, returning some data (success).
|
||||
*
|
||||
* @param data Data to return.
|
||||
*/
|
||||
dismissWithData(data?: [MediaFile] | string): void {
|
||||
ModalController.instance.dismiss(data, 'success');
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the modal, returning an error.
|
||||
*
|
||||
* @param code Error code. Will not be used if it's a Camera capture.
|
||||
* @param message Error message.
|
||||
* @param cameraMessage A specific message to use if it's a Camera capture. If not set, message will be used.
|
||||
*/
|
||||
dismissWithCanceledError(message: string, cameraMessage?: string): void {
|
||||
const isCamera = this.isImage && !this.isCaptureImage;
|
||||
const error = isCamera ? new CoreCanceledError(cameraMessage || message) : new CoreCaptureError(3, message);
|
||||
|
||||
ModalController.instance.dismiss(error, 'error');
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the modal, returning an error.
|
||||
*
|
||||
* @param code Error code. Will not be used if it's a Camera capture.
|
||||
* @param message Error message.
|
||||
* @param 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 {
|
||||
const isCamera = this.isImage && !this.isCaptureImage;
|
||||
const error = isCamera ? new CoreError(cameraMessage || message) : new CoreCaptureError(code, message);
|
||||
|
||||
ModalController.instance.dismiss(error, 'error');
|
||||
}
|
||||
|
||||
/**
|
||||
* Done capturing, write the file.
|
||||
*/
|
||||
async done(): Promise<void> {
|
||||
if (this.returnDataUrl) {
|
||||
// Return the image as a base64 string.
|
||||
this.dismissWithData((<HTMLCanvasElement> this.imgCanvas?.nativeElement).toDataURL(this.mimetype, this.quality));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.mediaBlob && !this.isCordovaAudioCapture) {
|
||||
// Shouldn't happen.
|
||||
CoreDomUtils.instance.showErrorModal('Please capture the media first.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let fileEntry = this.fileEntry;
|
||||
const loadingModal = await CoreDomUtils.instance.showModalLoading();
|
||||
|
||||
try {
|
||||
if (!this.isCordovaAudioCapture) {
|
||||
// Capturing in browser. Write the blob in a file.
|
||||
if (!this.mediaBlob) {
|
||||
// Shouldn't happen.
|
||||
throw new Error('Please capture the media first.');
|
||||
}
|
||||
|
||||
fileEntry = await CoreFile.instance.writeFile(this.getFilePath(), this.mediaBlob);
|
||||
}
|
||||
|
||||
if (!fileEntry) {
|
||||
throw new CoreError('File not found.');
|
||||
}
|
||||
|
||||
if (this.isImage && !this.isCaptureImage) {
|
||||
this.dismissWithData(fileEntry.toURL());
|
||||
} else {
|
||||
// The capture plugin should return a MediaFile, not a FileEntry. Convert it.
|
||||
const metadata = await CoreFile.instance.getMetadata(fileEntry);
|
||||
|
||||
let mimetype: string | undefined;
|
||||
if (this.extension) {
|
||||
mimetype = CoreMimetypeUtils.instance.getMimeType(this.extension);
|
||||
}
|
||||
|
||||
const mediaFile: MediaFile = {
|
||||
name: fileEntry.name,
|
||||
fullPath: fileEntry.nativeURL || fileEntry.fullPath,
|
||||
type: mimetype || '',
|
||||
lastModifiedDate: metadata.modificationTime,
|
||||
size: metadata.size,
|
||||
getFormatData: (): void => {
|
||||
// Nothing to do.
|
||||
},
|
||||
};
|
||||
|
||||
this.dismissWithData([mediaFile]);
|
||||
}
|
||||
} catch (err) {
|
||||
CoreDomUtils.instance.showErrorModal(err);
|
||||
} finally {
|
||||
loadingModal.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get path to the file where the media will be stored.
|
||||
*
|
||||
* @return Path.
|
||||
*/
|
||||
protected getFilePath(): string {
|
||||
const fileName = this.type + '_' + CoreTimeUtils.instance.readableTimestamp() + '.' + this.extension;
|
||||
|
||||
return CoreTextUtils.instance.concatenatePaths(CoreFileProvider.TMPFOLDER, 'media/' + fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop capturing. Only for video and audio.
|
||||
*/
|
||||
stopCapturing(): void {
|
||||
this.isCapturing = false;
|
||||
this.hasCaptured = true;
|
||||
|
||||
if (this.isCordovaAudioCapture) {
|
||||
this.mediaFile?.stopRecord();
|
||||
if (this.previewMedia && this.fileEntry) {
|
||||
this.previewMedia.src = CoreFile.instance.convertFileSrc(this.fileEntry.toURL());
|
||||
}
|
||||
} else {
|
||||
this.streamVideo && this.streamVideo.nativeElement.pause();
|
||||
this.audioDrawer && this.audioDrawer.stop();
|
||||
this.mediaRecorder && this.mediaRecorder.stop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Page destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.mediaFile?.release();
|
||||
|
||||
if (this.localMediaStream) {
|
||||
const tracks = this.localMediaStream.getTracks();
|
||||
tracks.forEach((track) => {
|
||||
track.stop();
|
||||
});
|
||||
}
|
||||
this.streamVideo?.nativeElement.pause();
|
||||
this.previewMedia?.pause();
|
||||
this.audioDrawer?.stop();
|
||||
delete this.mediaBlob;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export type CaptureMediaComponentInputs = {
|
||||
type: 'audio' | 'video' | 'image' | 'captureimage';
|
||||
maxTime?: number; // Max time to capture.
|
||||
facingMode?: string; // Camera facing mode.
|
||||
mimetype?: string;
|
||||
extension?: string;
|
||||
quality?: number; // Only for images.
|
||||
returnDataUrl?: boolean; // Whether it should return a data img. Only for images.
|
||||
};
|
|
@ -0,0 +1,41 @@
|
|||
// (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 { CommonModule } from '@angular/common';
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { CoreComponentsModule } from '@app/components/components.module';
|
||||
import { CoreDirectivesModule } from '@app/directives/directives.module';
|
||||
import { CorePipesModule } from '@app/pipes/pipes.module';
|
||||
import { CoreEmulatorCaptureMediaComponent } from './capture-media/capture-media';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
CoreEmulatorCaptureMediaComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule.forRoot(),
|
||||
TranslateModule.forChild(),
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule,
|
||||
CorePipesModule,
|
||||
],
|
||||
exports: [
|
||||
CoreEmulatorCaptureMediaComponent,
|
||||
],
|
||||
})
|
||||
export class CoreEmulatorComponentsModule {}
|
|
@ -16,9 +16,12 @@ import { NgModule } from '@angular/core';
|
|||
import { Platform } from '@ionic/angular';
|
||||
|
||||
import { CoreInitDelegate } from '@services/init';
|
||||
import { CoreEmulatorHelperProvider } from './services/helper';
|
||||
import { CoreEmulatorHelperProvider } from './services/emulator.helper';
|
||||
import { CoreEmulatorComponentsModule } from './components/components.module';
|
||||
|
||||
// Ionic Native services.
|
||||
import { Camera } from '@ionic-native/camera/ngx';
|
||||
import { Chooser } from '@ionic-native/chooser/ngx';
|
||||
import { Clipboard } from '@ionic-native/clipboard/ngx';
|
||||
import { Device } from '@ionic-native/device/ngx';
|
||||
import { Diagnostic } from '@ionic-native/diagnostic/ngx';
|
||||
|
@ -31,6 +34,8 @@ import { InAppBrowser } from '@ionic-native/in-app-browser/ngx';
|
|||
import { WebView } from '@ionic-native/ionic-webview/ngx';
|
||||
import { Keyboard } from '@ionic-native/keyboard/ngx';
|
||||
import { LocalNotifications } from '@ionic-native/local-notifications/ngx';
|
||||
import { Media } from '@ionic-native/media/ngx';
|
||||
import { MediaCapture } from '@ionic-native/media-capture/ngx';
|
||||
import { Network } from '@ionic-native/network/ngx';
|
||||
import { Push } from '@ionic-native/push/ngx';
|
||||
import { QRScanner } from '@ionic-native/qr-scanner/ngx';
|
||||
|
@ -41,12 +46,14 @@ import { WebIntent } from '@ionic-native/web-intent/ngx';
|
|||
import { Zip } from '@ionic-native/zip/ngx';
|
||||
|
||||
// Mock services.
|
||||
import { CameraMock } from './services/camera';
|
||||
import { ClipboardMock } from './services/clipboard';
|
||||
import { FileMock } from './services/file';
|
||||
import { FileOpenerMock } from './services/file-opener';
|
||||
import { FileTransferMock } from './services/file-transfer';
|
||||
import { GeolocationMock } from './services/geolocation';
|
||||
import { InAppBrowserMock } from './services/inappbrowser';
|
||||
import { MediaCaptureMock } from './services/media-capture';
|
||||
import { NetworkMock } from './services/network';
|
||||
import { ZipMock } from './services/zip';
|
||||
|
||||
|
@ -63,9 +70,15 @@ import { ZipMock } from './services/zip';
|
|||
declarations: [
|
||||
],
|
||||
imports: [
|
||||
CoreEmulatorComponentsModule,
|
||||
],
|
||||
providers: [
|
||||
CoreEmulatorHelperProvider,
|
||||
{
|
||||
provide: Camera,
|
||||
deps: [Platform],
|
||||
useFactory: (platform: Platform): Camera => platform.is('cordova') ? new Camera() : new CameraMock(),
|
||||
},
|
||||
Chooser,
|
||||
{
|
||||
provide: Clipboard,
|
||||
deps: [Platform], // Use platform instead of AppProvider to prevent errors with singleton injection.
|
||||
|
@ -101,6 +114,16 @@ import { ZipMock } from './services/zip';
|
|||
},
|
||||
Keyboard,
|
||||
LocalNotifications,
|
||||
{
|
||||
provide: Media,
|
||||
deps: [],
|
||||
useFactory: (): Media => new Media(),
|
||||
},
|
||||
{
|
||||
provide: MediaCapture,
|
||||
deps: [Platform],
|
||||
useFactory: (platform: Platform): MediaCapture => platform.is('cordova') ? new MediaCapture() : new MediaCaptureMock(),
|
||||
},
|
||||
{
|
||||
provide: Network,
|
||||
deps: [Platform],
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
// (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 { Camera, CameraOptions } from '@ionic-native/camera/ngx';
|
||||
|
||||
import { CoreEmulatorCaptureHelper } from './capture.helper';
|
||||
|
||||
/**
|
||||
* Emulates the Cordova Camera plugin in browser.
|
||||
*/
|
||||
@Injectable()
|
||||
export class CameraMock extends Camera {
|
||||
|
||||
/**
|
||||
* Remove intermediate image files that are kept in temporary storage after calling camera.getPicture.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
cleanup(): Promise<any> {
|
||||
// This function is iOS only, nothing to do.
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a picture.
|
||||
*
|
||||
* @param options Options that you want to pass to the camera.
|
||||
* @return Promise resolved when captured.
|
||||
*/
|
||||
getPicture(options: CameraOptions): Promise<string> {
|
||||
return CoreEmulatorCaptureHelper.instance.captureMedia('image', options);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,219 @@
|
|||
// (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 { CaptureAudioOptions, CaptureImageOptions, CaptureVideoOptions, MediaFile } from '@ionic-native/media-capture/ngx';
|
||||
|
||||
import { CoreMimetypeUtils } from '@services/utils/mimetype';
|
||||
import { makeSingleton, ModalController } from '@singletons/core.singletons';
|
||||
import { CaptureMediaComponentInputs, CoreEmulatorCaptureMediaComponent } from '../components/capture-media/capture-media';
|
||||
|
||||
/**
|
||||
* Helper service with some features to capture media (image, audio, video).
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
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',
|
||||
};
|
||||
|
||||
videoMimeType?: string;
|
||||
audioMimeType?: string;
|
||||
|
||||
/**
|
||||
* Capture media (image, audio, video).
|
||||
*
|
||||
* @param type Type of media: image, audio, video.
|
||||
* @param options Optional options.
|
||||
* @return Promise resolved when captured, rejected if error.
|
||||
*/
|
||||
captureMedia(type: 'image', options?: MockCameraOptions): Promise<string>;
|
||||
captureMedia(type: 'captureimage', options?: MockCaptureImageOptions): Promise<MediaFile[]>;
|
||||
captureMedia(type: 'audio', options?: MockCaptureAudioOptions): Promise<MediaFile[]>;
|
||||
captureMedia(type: 'video', options?: MockCaptureVideoOptions): Promise<MediaFile[]>;
|
||||
async captureMedia(
|
||||
type: 'image' | 'captureimage' | 'audio' | 'video',
|
||||
options?: MockCameraOptions | MockCaptureImageOptions | MockCaptureAudioOptions | MockCaptureVideoOptions,
|
||||
): Promise<MediaFile[] | string> {
|
||||
options = options || {};
|
||||
|
||||
// Build the params to send to the modal.
|
||||
const params: CaptureMediaComponentInputs = {
|
||||
type: type,
|
||||
};
|
||||
|
||||
// Initialize some data based on the type of media to capture.
|
||||
if (type == 'video') {
|
||||
const mimeAndExt = this.getMimeTypeAndExtension(type, options.mimetypes);
|
||||
params.mimetype = mimeAndExt.mimetype;
|
||||
params.extension = mimeAndExt.extension;
|
||||
} else if (type == 'audio') {
|
||||
const mimeAndExt = this.getMimeTypeAndExtension(type, options.mimetypes);
|
||||
params.mimetype = mimeAndExt.mimetype;
|
||||
params.extension = mimeAndExt.extension;
|
||||
} else if (type == 'image') {
|
||||
if ('sourceType' in options && options.sourceType !== undefined && options.sourceType != 1) {
|
||||
return Promise.reject('This source type is not supported in browser.');
|
||||
}
|
||||
|
||||
if ('cameraDirection' in options && options.cameraDirection == 1) {
|
||||
params.facingMode = 'user';
|
||||
}
|
||||
|
||||
if ('encodingType' in options && options.encodingType == 1) {
|
||||
params.mimetype = 'image/png';
|
||||
params.extension = 'png';
|
||||
} else {
|
||||
params.mimetype = 'image/jpeg';
|
||||
params.extension = 'jpeg';
|
||||
}
|
||||
|
||||
if ('quality' in options && options.quality !== undefined && options.quality >= 0 && options.quality <= 100) {
|
||||
params.quality = options.quality / 100;
|
||||
}
|
||||
|
||||
if ('destinationType' in options && options.destinationType == 0) {
|
||||
params.returnDataUrl = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ('duration' in options && options.duration) {
|
||||
params.maxTime = options.duration * 1000;
|
||||
}
|
||||
|
||||
const modal = await ModalController.instance.create({
|
||||
component: CoreEmulatorCaptureMediaComponent,
|
||||
cssClass: 'core-modal-fullscreen',
|
||||
componentProps: params,
|
||||
});
|
||||
|
||||
modal.present();
|
||||
|
||||
const result = await modal.onDidDismiss();
|
||||
|
||||
if (result.role == 'success') {
|
||||
return result.data;
|
||||
} else {
|
||||
throw result.data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mimetype and extension to capture media.
|
||||
*
|
||||
* @param type Type of media: image, audio, video.
|
||||
* @param mimetypes List of supported mimetypes. If undefined, all mimetypes supported.
|
||||
* @return An object with mimetype and extension to use.
|
||||
*/
|
||||
protected getMimeTypeAndExtension(type: string, mimetypes?: string[]): { extension?: string; mimetype?: string } {
|
||||
const result: { extension?: string; mimetype?: string } = {};
|
||||
|
||||
if (mimetypes?.length) {
|
||||
// Search for a supported mimetype.
|
||||
for (let i = 0; i < mimetypes.length; i++) {
|
||||
const mimetype = mimetypes[i];
|
||||
const matches = mimetype.match(new RegExp('^' + type + '/'));
|
||||
|
||||
if (matches?.length && window.MediaRecorder.isTypeSupported(mimetype)) {
|
||||
result.mimetype = mimetype;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.mimetype) {
|
||||
// Found a supported mimetype in the mimetypes array, get the extension.
|
||||
result.extension = CoreMimetypeUtils.instance.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 Whether the function is supported.
|
||||
*/
|
||||
protected initGetUserMedia(): boolean {
|
||||
return !!navigator.mediaDevices.getUserMedia;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the mimetypes to use when capturing.
|
||||
*/
|
||||
protected initMimeTypes(): void {
|
||||
// Determine video and audio mimetype to use.
|
||||
for (const mimeType in this.possibleVideoMimeTypes) {
|
||||
if (window.MediaRecorder.isTypeSupported(mimeType)) {
|
||||
this.videoMimeType = mimeType;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (const mimeType in this.possibleAudioMimeTypes) {
|
||||
if (window.MediaRecorder.isTypeSupported(mimeType)) {
|
||||
this.audioMimeType = mimeType;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the Mocks that need it.
|
||||
*
|
||||
* @return Promise resolved when loaded.
|
||||
*/
|
||||
load(): Promise<void> {
|
||||
if (typeof window.MediaRecorder != 'undefined' && this.initGetUserMedia()) {
|
||||
this.initMimeTypes();
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class CoreEmulatorCaptureHelper extends makeSingleton(CoreEmulatorCaptureHelperProvider) {}
|
||||
|
||||
export interface MockCameraOptions extends CameraOptions {
|
||||
mimetypes?: string[]; // Allowed mimetypes.
|
||||
}
|
||||
export interface MockCaptureImageOptions extends CaptureImageOptions {
|
||||
mimetypes?: string[]; // Allowed mimetypes.
|
||||
}
|
||||
export interface MockCaptureAudioOptions extends CaptureAudioOptions {
|
||||
mimetypes?: string[]; // Allowed mimetypes.
|
||||
}
|
||||
export interface MockCaptureVideoOptions extends CaptureVideoOptions {
|
||||
mimetypes?: string[]; // Allowed mimetypes.
|
||||
}
|
|
@ -25,7 +25,9 @@ import { FileTransferErrorMock } from './file-transfer';
|
|||
/**
|
||||
* Helper service for the emulator feature. It also acts as an init handler.
|
||||
*/
|
||||
@Injectable()
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class CoreEmulatorHelperProvider implements CoreInitHandler {
|
||||
|
||||
name = 'CoreEmulator';
|
|
@ -0,0 +1,62 @@
|
|||
// (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 {
|
||||
MediaCapture,
|
||||
CaptureAudioOptions,
|
||||
CaptureImageOptions,
|
||||
CaptureVideoOptions,
|
||||
MediaFile,
|
||||
} from '@ionic-native/media-capture/ngx';
|
||||
|
||||
import { CoreEmulatorCaptureHelper } from './capture.helper';
|
||||
|
||||
/**
|
||||
* Emulates the Cordova MediaCapture plugin in browser.
|
||||
*/
|
||||
@Injectable()
|
||||
export class MediaCaptureMock extends MediaCapture {
|
||||
|
||||
/**
|
||||
* Start the audio recorder application and return information about captured audio clip files.
|
||||
*
|
||||
* @param options Options.
|
||||
* @return Promise resolved when captured.
|
||||
*/
|
||||
captureAudio(options: CaptureAudioOptions): Promise<MediaFile[]> {
|
||||
return CoreEmulatorCaptureHelper.instance.captureMedia('audio', options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the camera application and return information about captured image files.
|
||||
*
|
||||
* @param options Options.
|
||||
* @return Promise resolved when captured.
|
||||
*/
|
||||
captureImage(options: CaptureImageOptions): Promise<MediaFile[]> {
|
||||
return CoreEmulatorCaptureHelper.instance.captureMedia('captureimage', options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the video recorder application and return information about captured video clip files.
|
||||
*
|
||||
* @param options Options.
|
||||
* @return Promise resolved when captured.
|
||||
*/
|
||||
captureVideo(options: CaptureVideoOptions): Promise<MediaFile[]> {
|
||||
return CoreEmulatorCaptureHelper.instance.captureMedia('video', options);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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) {}
|
|
@ -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;
|
||||
};
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
}
|
|
@ -16,7 +16,7 @@ import { Component } from '@angular/core';
|
|||
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { ModalController, Translate } from '@singletons/core.singletons';
|
||||
import { CoreLoginHelperProvider } from '@core/login/services/helper';
|
||||
import { CoreLoginHelperProvider } from '@core/login/services/login.helper';
|
||||
|
||||
/**
|
||||
* Component that displays help to connect to a site.
|
||||
|
|
|
@ -16,7 +16,7 @@ import { Component } from '@angular/core';
|
|||
|
||||
import { CoreConfig } from '@services/config';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreLoginHelperProvider } from '@core/login/services/helper';
|
||||
import { CoreLoginHelperProvider } from '@core/login/services/login.helper';
|
||||
import { ModalController } from '@singletons/core.singletons';
|
||||
|
||||
/**
|
||||
|
|
|
@ -16,7 +16,7 @@ import { Component } from '@angular/core';
|
|||
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreLoginHelper } from '@core/login/services/helper';
|
||||
import { CoreLoginHelper } from '@core/login/services/login.helper';
|
||||
import { Translate } from '@singletons/core.singletons';
|
||||
|
||||
/**
|
||||
|
|
|
@ -21,7 +21,7 @@ import { CoreApp } from '@services/app';
|
|||
import { CoreSites } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreLoginHelper, CoreLoginHelperProvider } from '@core/login/services/helper';
|
||||
import { CoreLoginHelper, CoreLoginHelperProvider } from '@core/login/services/login.helper';
|
||||
import { CoreConstants } from '@core/constants';
|
||||
import { Translate } from '@singletons/core.singletons';
|
||||
import { CoreSiteIdentityProvider, CoreSitePublicConfigResponse } from '@classes/site';
|
||||
|
|
|
@ -22,7 +22,7 @@ import { CoreDomUtils } from '@services/utils/dom';
|
|||
import { CoreTextUtils } from '@services/utils/text';
|
||||
import { CoreCountry, CoreUtils } from '@services/utils/utils';
|
||||
import { CoreWS, CoreWSExternalWarning } from '@services/ws';
|
||||
import { AuthEmailSignupProfileFieldsCategory, AuthEmailSignupSettings, CoreLoginHelper } from '@core/login/services/helper';
|
||||
import { AuthEmailSignupProfileFieldsCategory, AuthEmailSignupSettings, CoreLoginHelper } from '@core/login/services/login.helper';
|
||||
import { CoreConstants } from '@core/constants';
|
||||
import { Translate } from '@singletons/core.singletons';
|
||||
import { CoreSitePublicConfigResponse } from '@classes/site';
|
||||
|
|
|
@ -18,7 +18,7 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
|||
import { NavController } from '@ionic/angular';
|
||||
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreLoginHelper } from '@core/login/services/helper';
|
||||
import { CoreLoginHelper } from '@core/login/services/login.helper';
|
||||
import { Translate, Platform } from '@singletons/core.singletons';
|
||||
import { CoreWSExternalWarning } from '@services/ws';
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ import { CoreInit } from '@services/init';
|
|||
import { SplashScreen } from '@singletons/core.singletons';
|
||||
import { CoreConstants } from '@core/constants';
|
||||
import { CoreSites } from '@/app/services/sites';
|
||||
import { CoreLoginHelper } from '@/app/core/login/services/helper';
|
||||
import { CoreLoginHelper } from '@/app/core/login/services/login.helper';
|
||||
|
||||
/**
|
||||
* Page that displays a "splash screen" while the app is being initialized.
|
||||
|
|
|
@ -49,10 +49,14 @@
|
|||
<ion-grid class="ion-padding">
|
||||
<ion-row>
|
||||
<ion-col>
|
||||
<ion-button expand="block" color="light" (click)="cancel($event)">{{ 'core.login.cancel' | translate }}</ion-button>
|
||||
<ion-button expand="block" color="light" (click)="cancel($event)">
|
||||
{{ 'core.login.cancel' | translate }}
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
<ion-col>
|
||||
<ion-button type="submit" expand="block" [disabled]="!credForm.valid">{{ 'core.login.loginbutton' | translate }}</ion-button>
|
||||
<ion-button type="submit" expand="block" [disabled]="!credForm.valid">
|
||||
{{ 'core.login.loginbutton' | translate }}
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
|
|
|
@ -21,7 +21,7 @@ import { CoreApp } from '@services/app';
|
|||
import { CoreSites } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreLoginHelper } from '@core/login/services/helper';
|
||||
import { CoreLoginHelper } from '@core/login/services/login.helper';
|
||||
import { CoreSiteIdentityProvider, CoreSitePublicConfigResponse } from '@classes/site';
|
||||
import { CoreEvents } from '@singletons/events';
|
||||
import { CoreError } from '@classes/errors/error';
|
||||
|
|
|
@ -20,7 +20,7 @@ import { CoreSites } from '@services/sites';
|
|||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreMimetypeUtils } from '@services/utils/mimetype';
|
||||
import { CoreLoginHelper } from '@core/login/services/helper';
|
||||
import { CoreLoginHelper } from '@core/login/services/login.helper';
|
||||
import { CoreSite } from '@classes/site';
|
||||
|
||||
/**
|
||||
|
|
|
@ -22,7 +22,7 @@ import { CoreConfig } from '@services/config';
|
|||
import { CoreSites, CoreSiteCheckResponse, CoreLoginSiteInfo, CoreSitesDemoSiteData } from '@services/sites';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreLoginHelper, CoreLoginHelperProvider } from '@core/login/services/helper';
|
||||
import { CoreLoginHelper, CoreLoginHelperProvider } from '@core/login/services/login.helper';
|
||||
import { CoreSite } from '@classes/site';
|
||||
import { CoreError } from '@classes/errors/error';
|
||||
import { CoreConstants } from '@core/constants';
|
||||
|
|
|
@ -33,12 +33,12 @@
|
|||
<ion-button *ngIf="showDelete" slot="end" fill="clear" color="danger" (click)="deleteSite($event, site)"
|
||||
[attr.aria-label]="'core.delete' | translate">
|
||||
<ion-icon name="fa-trash" slot="icon-only"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
<ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end">
|
||||
<ion-fab-button (click)="add()" [attr.aria-label]="'core.add' | translate">
|
||||
<ion-icon name="fa-plus"></ion-icon>
|
||||
</ion-fab-button>
|
||||
</ion-fab>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
<ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end">
|
||||
<ion-fab-button (click)="add()" [attr.aria-label]="'core.add' | translate">
|
||||
<ion-icon name="fa-plus"></ion-icon>
|
||||
</ion-fab-button>
|
||||
</ion-fab>
|
||||
</ion-content>
|
||||
|
|
|
@ -18,7 +18,7 @@ import { Component, OnInit } from '@angular/core';
|
|||
|
||||
import { CoreSiteBasicInfo, CoreSites } from '@services/sites';
|
||||
import { CoreLogger } from '@singletons/logger';
|
||||
import { CoreLoginHelper } from '../../services/helper';
|
||||
import { CoreLoginHelper } from '@core/login/services/login.helper';
|
||||
|
||||
/**
|
||||
* Page that displays a "splash screen" while the app is being initialized.
|
||||
|
|
|
@ -2,9 +2,11 @@
|
|||
<ion-tab-bar slot="bottom" [hidden]="hidden">
|
||||
<ion-spinner *ngIf="!loaded"></ion-spinner>
|
||||
|
||||
<ion-tab-button tab="redirect" [disabled]="true" [hidden]="true"></ion-tab-button> <!-- [root]="redirectPage" [rootParams]="redirectParams" -->
|
||||
<ion-tab-button tab="redirect" [disabled]="true" [hidden]="true"></ion-tab-button>
|
||||
<!-- @todo: [root]="redirectPage" [rootParams]="redirectParams" -->
|
||||
|
||||
<ion-tab-button (ionTabButtonClick)="tabClicked($event, tab.page)" [hidden]="!loaded && tab.hide" *ngFor="let tab of tabs" [tab]="tab.page" [disabled]="tab.hide" layout="label-hide" class="{{tab.class}}">
|
||||
<ion-tab-button (ionTabButtonClick)="tabClicked($event, tab.page)" [hidden]="!loaded && tab.hide" *ngFor="let tab of tabs"
|
||||
[tab]="tab.page" [disabled]="tab.hide" layout="label-hide" class="{{tab.class}}">
|
||||
<ion-icon [name]="tab.icon"></ion-icon>
|
||||
<ion-label>{{ tab.title | translate }}</ion-label>
|
||||
<ion-badge *ngIf="tab.badge">{{ tab.badge }}</ion-badge>
|
||||
|
|
|
@ -19,6 +19,7 @@ import { Subscription } from 'rxjs';
|
|||
|
||||
import { CoreApp } from '@services/app';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
import { CoreEvents, CoreEventObserver, CoreEventLoadPageMainMenuData } from '@singletons/events';
|
||||
import { CoreMainMenu } from '../../services/mainmenu';
|
||||
import { CoreMainMenuDelegate, CoreMainMenuHandlerToDisplay } from '../../services/mainmenu.delegate';
|
||||
|
@ -242,8 +243,10 @@ export class CoreMainMenuPage implements OnInit, OnDestroy {
|
|||
return;
|
||||
}
|
||||
|
||||
const trimmedUrl = CoreTextUtils.instance.trimCharacter(this.router.url, '/');
|
||||
|
||||
// Current tab was clicked. Check if user is already at root level.
|
||||
if (this.router.url == '/mainmenu/' + page) {
|
||||
if (trimmedUrl == CoreTextUtils.instance.trimCharacter(page, '/')) {
|
||||
// Already at root level, nothing to do.
|
||||
return;
|
||||
}
|
||||
|
@ -255,8 +258,17 @@ export class CoreMainMenuPage implements OnInit, OnDestroy {
|
|||
try {
|
||||
const tab = this.tabs.find((tab) => tab.page == page);
|
||||
|
||||
// Use tab's subPage to check if user is already at root level.
|
||||
if (tab?.subPage && trimmedUrl ==
|
||||
CoreTextUtils.instance.trimCharacter(CoreTextUtils.instance.concatenatePaths(tab.page, tab.subPage), '/')) {
|
||||
// Already at root level, nothing to do.
|
||||
return;
|
||||
}
|
||||
|
||||
if (tab?.title) {
|
||||
await CoreDomUtils.instance.showConfirm(Translate.instance.instant('core.confirmgotabroot', { name: tab.title }));
|
||||
await CoreDomUtils.instance.showConfirm(Translate.instance.instant('core.confirmgotabroot', {
|
||||
name: Translate.instance.instant(tab.title),
|
||||
}));
|
||||
} else {
|
||||
await CoreDomUtils.instance.showConfirm(Translate.instance.instant('core.confirmgotabrootdefault'));
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ import { Subscription } from 'rxjs';
|
|||
import { CoreSites } from '@services/sites';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreSiteInfo } from '@classes/site';
|
||||
import { CoreLoginHelper } from '@core/login/services/helper';
|
||||
import { CoreLoginHelper } from '@core/login/services/login.helper';
|
||||
import { CoreMainMenuDelegate, CoreMainMenuHandlerData } from '../../services/mainmenu.delegate';
|
||||
import { CoreMainMenu, CoreMainMenuCustomItem } from '../../services/mainmenu';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
|
|
|
@ -55,6 +55,7 @@ export class CoreHomeMainMenuHandler implements CoreMainMenuHandler {
|
|||
icon: 'fa-home',
|
||||
title: 'core.mainmenu.home',
|
||||
page: 'home',
|
||||
// @todo: subPage? The page can change due to core-tabs.
|
||||
class: 'core-home-handler',
|
||||
};
|
||||
}
|
||||
|
|
|
@ -32,6 +32,13 @@ export interface CoreMainMenuHandlerData {
|
|||
*/
|
||||
page: string;
|
||||
|
||||
/**
|
||||
* Sub page loaded when the handler page is loaded.
|
||||
* If your module performs a redirect when it's opened you need to specify the sub page in here.
|
||||
* E.g. if page is 'foo' but it redirects to 'foo/bar' when opened, this value must be 'bar'.
|
||||
*/
|
||||
subPage?: string;
|
||||
|
||||
/**
|
||||
* Title to display for the handler.
|
||||
*/
|
||||
|
|
|
@ -16,7 +16,6 @@ import { NgModule } from '@angular/core';
|
|||
import { Routes } from '@angular/router';
|
||||
|
||||
import { CoreMainMenuRoutingModule } from '@core/mainmenu/mainmenu-routing.module';
|
||||
import { CoreSettingsHelperProvider } from './services/settings.helper';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
|
@ -37,12 +36,11 @@ const routes: Routes = [
|
|||
CoreMainMenuRoutingModule,
|
||||
],
|
||||
providers: [
|
||||
CoreSettingsHelperProvider,
|
||||
],
|
||||
})
|
||||
export class CoreSettingsInitModule {
|
||||
|
||||
constructor(settingsHelper: CoreSettingsHelperProvider) {
|
||||
constructor() {
|
||||
// @todo
|
||||
// settingsHelper.initDomSettings();
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import { NgModule } from '@angular/core';
|
|||
import { CoreCreateLinksPipe } from './create-links.pipe';
|
||||
import { CoreFormatDatePipe } from './format-date.pipe';
|
||||
import { CoreNoTagsPipe } from './no-tags.pipe';
|
||||
import { CoreSecondsToHMSPipe } from './seconds-to-hms.pipe';
|
||||
import { CoreTimeAgoPipe } from './time-ago.pipe';
|
||||
import { CoreBytesToSizePipe } from './bytes-to-size.pipe';
|
||||
|
||||
|
@ -26,6 +27,7 @@ import { CoreBytesToSizePipe } from './bytes-to-size.pipe';
|
|||
CoreTimeAgoPipe,
|
||||
CoreFormatDatePipe,
|
||||
CoreBytesToSizePipe,
|
||||
CoreSecondsToHMSPipe,
|
||||
],
|
||||
imports: [],
|
||||
exports: [
|
||||
|
@ -34,6 +36,7 @@ import { CoreBytesToSizePipe } from './bytes-to-size.pipe';
|
|||
CoreTimeAgoPipe,
|
||||
CoreFormatDatePipe,
|
||||
CoreBytesToSizePipe,
|
||||
CoreSecondsToHMSPipe,
|
||||
],
|
||||
})
|
||||
export class CorePipesModule {}
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
// (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 { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
import { CoreLogger } from '@singletons/logger';
|
||||
import { CoreConstants } from '@core/constants';
|
||||
|
||||
/**
|
||||
* Pipe to convert a number of seconds to Hours:Minutes:Seconds.
|
||||
*
|
||||
* This converts a number of seconds to Hours:Minutes:Seconds. If the number of seconds is negative, returns 00:00:00.
|
||||
*/
|
||||
@Pipe({
|
||||
name: 'coreSecondsToHMS',
|
||||
})
|
||||
export class CoreSecondsToHMSPipe implements PipeTransform {
|
||||
|
||||
protected logger: CoreLogger;
|
||||
|
||||
constructor() {
|
||||
this.logger = CoreLogger.getInstance('CoreSecondsToHMSPipe');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a number of seconds to Hours:Minutes:Seconds.
|
||||
*
|
||||
* @param seconds Number of seconds.
|
||||
* @return Formatted seconds.
|
||||
*/
|
||||
transform(seconds: string | number): string {
|
||||
if (!seconds || seconds < 0) {
|
||||
seconds = 0;
|
||||
} else if (typeof seconds == 'string') {
|
||||
// Convert the value to a number.
|
||||
const numberSeconds = parseInt(seconds, 10);
|
||||
if (isNaN(numberSeconds)) {
|
||||
this.logger.error('Invalid value received', seconds);
|
||||
|
||||
return seconds;
|
||||
}
|
||||
seconds = numberSeconds;
|
||||
}
|
||||
|
||||
// Don't allow decimals.
|
||||
seconds = Math.floor(seconds);
|
||||
|
||||
const hours = Math.floor(seconds / CoreConstants.SECONDS_HOUR);
|
||||
seconds -= hours * CoreConstants.SECONDS_HOUR;
|
||||
const minutes = Math.floor(seconds / CoreConstants.SECONDS_MINUTE);
|
||||
seconds -= minutes * CoreConstants.SECONDS_MINUTE;
|
||||
|
||||
return CoreTextUtils.instance.twoDigits(hours) + ':' + CoreTextUtils.instance.twoDigits(minutes) + ':' +
|
||||
CoreTextUtils.instance.twoDigits(seconds);
|
||||
}
|
||||
|
||||
}
|
|
@ -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).
|
||||
*
|
||||
|
|
|
@ -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) {}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1537,7 +1537,7 @@ export class CoreDomUtilsProvider {
|
|||
* @return Promise resolved when modal presented.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
showTextareaPrompt(title: string, message: string, buttons: (string | any)[], placeholder?: string): Promise<any> {
|
||||
showTextareaPrompt(title: string, message: string, buttons: (string | unknown)[], placeholder?: string): Promise<unknown> {
|
||||
// @todo
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -54,14 +54,14 @@ export class CoreTextUtilsProvider {
|
|||
{ old: /_mmaNotifications/g, new: '_AddonNotifications' },
|
||||
{ old: /_mmaMessages/g, new: '_AddonMessages' },
|
||||
{ old: /_mmaCalendar/g, new: '_AddonCalendar' },
|
||||
{ old: /_mmaFiles/g, new: '_AddonFiles' },
|
||||
{ old: /_mmaFiles/g, new: '_AddonPrivateFiles' },
|
||||
{ old: /_mmaParticipants/g, new: '_CoreUserParticipants' },
|
||||
{ old: /_mmaCourseCompletion/g, new: '_AddonCourseCompletion' },
|
||||
{ old: /_mmaNotes/g, new: '_AddonNotes' },
|
||||
{ old: /_mmaBadges/g, new: '_AddonBadges' },
|
||||
{ old: /files_privatefiles/g, new: 'AddonFilesPrivateFiles' },
|
||||
{ old: /files_sitefiles/g, new: 'AddonFilesSiteFiles' },
|
||||
{ old: /files_upload/g, new: 'AddonFilesUpload' },
|
||||
{ old: /files_privatefiles/g, new: 'AddonPrivateFilesPrivateFiles' },
|
||||
{ old: /files_sitefiles/g, new: 'AddonPrivateFilesSiteFiles' },
|
||||
{ old: /files_upload/g, new: 'AddonPrivateFilesUpload' },
|
||||
{ old: /_mmaModAssign/g, new: '_AddonModAssign' },
|
||||
{ old: /_mmaModBook/g, new: '_AddonModBook' },
|
||||
{ old: /_mmaModChat/g, new: '_AddonModChat' },
|
||||
|
@ -891,6 +891,20 @@ export class CoreTextUtilsProvider {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all ocurrences of a certain character from the start and end of a string.
|
||||
*
|
||||
* @param text Text to treat.
|
||||
* @param character Character to remove.
|
||||
* @return Treated text.
|
||||
*/
|
||||
trimCharacter(text: string, character: string): string {
|
||||
const escaped = this.escapeForRegex(character);
|
||||
const regExp = new RegExp(`^${escaped}+|${escaped}+$`, 'g');
|
||||
|
||||
return text.replace(regExp, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* If a number has only 1 digit, add a leading zero to it.
|
||||
*
|
||||
|
|
|
@ -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.
|
||||
};
|
||||
|
|
|
@ -22,8 +22,11 @@ import {
|
|||
ModalController as ModalControllerService,
|
||||
ToastController as ToastControllerService,
|
||||
GestureController as GestureControllerService,
|
||||
ActionSheetController as ActionSheetControllerService,
|
||||
} from '@ionic/angular';
|
||||
|
||||
import { Camera as CameraService } from '@ionic-native/camera/ngx';
|
||||
import { Chooser as ChooserService } from '@ionic-native/chooser/ngx';
|
||||
import { Clipboard as ClipboardService } from '@ionic-native/clipboard/ngx';
|
||||
import { Diagnostic as DiagnosticService } from '@ionic-native/diagnostic/ngx';
|
||||
import { Device as DeviceService } from '@ionic-native/device/ngx';
|
||||
|
@ -36,6 +39,8 @@ import { InAppBrowser as InAppBrowserService } from '@ionic-native/in-app-browse
|
|||
import { WebView as WebViewService } from '@ionic-native/ionic-webview/ngx';
|
||||
import { Keyboard as KeyboardService } from '@ionic-native/keyboard/ngx';
|
||||
import { LocalNotifications as LocalNotificationsService } from '@ionic-native/local-notifications/ngx';
|
||||
import { Media as MediaService } from '@ionic-native/media/ngx';
|
||||
import { MediaCapture as MediaCaptureService } from '@ionic-native/media-capture/ngx';
|
||||
import { Network as NetworkService } from '@ionic-native/network/ngx';
|
||||
import { Push as PushService } from '@ionic-native/push/ngx';
|
||||
import { QRScanner as QRScannerService } from '@ionic-native/qr-scanner/ngx';
|
||||
|
@ -71,6 +76,8 @@ export function makeSingleton<Service>(injectionToken: CoreInjectionToken<Servic
|
|||
}
|
||||
|
||||
// Convert ionic-native services to singleton.
|
||||
export class Camera extends makeSingleton(CameraService) {}
|
||||
export class Chooser extends makeSingleton(ChooserService) {}
|
||||
export class Clipboard extends makeSingleton(ClipboardService) {}
|
||||
export class Device extends makeSingleton(DeviceService) {}
|
||||
export class Diagnostic extends makeSingleton(DiagnosticService) {}
|
||||
|
@ -81,6 +88,8 @@ export class Geolocation extends makeSingleton(GeolocationService) {}
|
|||
export class InAppBrowser extends makeSingleton(InAppBrowserService) {}
|
||||
export class Keyboard extends makeSingleton(KeyboardService) {}
|
||||
export class LocalNotifications extends makeSingleton(LocalNotificationsService) {}
|
||||
export class Media extends makeSingleton(MediaService) {}
|
||||
export class MediaCapture extends makeSingleton(MediaCaptureService) {}
|
||||
export class NativeHttp extends makeSingleton(HTTP) {}
|
||||
export class Network extends makeSingleton(NetworkService) {}
|
||||
export class Push extends makeSingleton(PushService) {}
|
||||
|
@ -96,6 +105,7 @@ export class Zip extends makeSingleton(ZipService) {}
|
|||
export class NgZone extends makeSingleton(NgZoneService) {}
|
||||
export class Http extends makeSingleton(HttpClient) {}
|
||||
export class Platform extends makeSingleton(PlatformService) {}
|
||||
export class ActionSheetController extends makeSingleton(ActionSheetControllerService) {}
|
||||
export class AlertController extends makeSingleton(AlertControllerService) {}
|
||||
export class LoadingController extends makeSingleton(LoadingControllerService) {}
|
||||
export class ModalController extends makeSingleton(ModalControllerService) {}
|
||||
|
|
|
@ -112,6 +112,75 @@ ion-list.list-md {
|
|||
--ion-safe-area-left: calc(-1 * var(--selected-item-border-width));
|
||||
}
|
||||
|
||||
.item.item-file {
|
||||
ion-thumbnail {
|
||||
--size: 32px;
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
}
|
||||
}
|
||||
|
||||
.item.core-primary-item,
|
||||
.item.core-info-item,
|
||||
.item.core-secondary-item,
|
||||
.item.core-tertiary-item,
|
||||
.item.core-success-item,
|
||||
.item.core-warning-item,
|
||||
.item.core-danger-item {
|
||||
--inner-border-width: 0 0 3px 0;
|
||||
}
|
||||
.item.core-primary-item {
|
||||
--border-color: var(--ion-color-primary);
|
||||
}
|
||||
.item.core-info-item,
|
||||
.item.core-secondary-item {
|
||||
--border-color: var(--ion-color-secondary);
|
||||
}
|
||||
.item.core-tertiary-item {
|
||||
--border-color: var(--ion-color-tertiary);
|
||||
}
|
||||
.item.core-success-item {
|
||||
--border-color: var(--ion-color-success);
|
||||
}
|
||||
.item.core-warning-item {
|
||||
--border-color: var(--ion-color-warning);
|
||||
}
|
||||
.item.core-danger-item {
|
||||
--border-color: var(--ion-color-danger);
|
||||
}
|
||||
|
||||
// Card styles
|
||||
|
||||
// Message cards.
|
||||
ion-card.core-primary-card,
|
||||
ion-card.core-info-card,
|
||||
ion-card.core-secondary-card,
|
||||
ion-card.core-tertiary-card,
|
||||
ion-card.core-success-card,
|
||||
ion-card.core-warning-card,
|
||||
ion-card.core-danger-card {
|
||||
border-bottom: 3px solid transparent;
|
||||
}
|
||||
ion-card.core-primary-card {
|
||||
border-bottom-color: var(--ion-color-primary);
|
||||
}
|
||||
ion-card.core-info-card,
|
||||
ion-card.core-secondary-card {
|
||||
border-bottom-color: var(--ion-color-secondary);
|
||||
}
|
||||
ion-card.core-tertiary-card {
|
||||
border-bottom-color: var(--ion-color-tertiary);
|
||||
}
|
||||
ion-card.core-success-card {
|
||||
border-bottom-color: var(--ion-color-success);
|
||||
}
|
||||
ion-card.core-warning-card {
|
||||
border-bottom-color: var(--ion-color-warning);
|
||||
}
|
||||
ion-card.core-danger-card {
|
||||
border-bottom-color: var(--ion-color-danger);
|
||||
}
|
||||
|
||||
// Avatar
|
||||
// -------------------------
|
||||
// Large centered avatar
|
||||
|
@ -131,7 +200,7 @@ img.large-avatar {
|
|||
|
||||
ion-avatar ion-img, ion-avatar img {
|
||||
text-indent: -99999px;
|
||||
background-color: --var(--gray-light);
|
||||
background-color: var(--gray-light);
|
||||
}
|
||||
|
||||
// Action sheet.
|
||||
|
@ -152,3 +221,24 @@ ion-avatar ion-img, ion-avatar img {
|
|||
}
|
||||
}
|
||||
|
||||
// Select.
|
||||
ion-select.core-button-select,
|
||||
.core-button-select {
|
||||
background-color: var(--ion-color-primary-contrast);
|
||||
color: var(--ion-color-primary);
|
||||
white-space: normal;
|
||||
min-height: 45px;
|
||||
}
|
||||
|
||||
// File uploader.
|
||||
.action-sheet-button input.core-fileuploader-file-handler-input {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
min-width: 100%;
|
||||
opacity: 0;
|
||||
z-index: 100;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue