MOBILE-3565 components: Create core-file component

main
Dani Palou 2020-11-03 09:37:08 +01:00
parent 1ed95a4ade
commit d8de93f794
53 changed files with 407 additions and 17 deletions

View File

@ -17,6 +17,8 @@ import { CommonModule } from '@angular/common';
import { IonicModule } from '@ionic/angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreDownloadRefreshComponent } from './download-refresh/download-refresh';
import { CoreFileComponent } from './file/file';
import { CoreIconComponent } from './icon/icon';
import { CoreIframeComponent } from './iframe/iframe';
import { CoreInputErrorsComponent } from './input-errors/input-errors';
@ -31,6 +33,8 @@ import { CorePipesModule } from '@app/pipes/pipes.module';
@NgModule({
declarations: [
CoreDownloadRefreshComponent,
CoreFileComponent,
CoreIconComponent,
CoreIframeComponent,
CoreInputErrorsComponent,
@ -49,6 +53,8 @@ import { CorePipesModule } from '@app/pipes/pipes.module';
CorePipesModule,
],
exports: [
CoreDownloadRefreshComponent,
CoreFileComponent,
CoreIconComponent,
CoreIframeComponent,
CoreInputErrorsComponent,

View File

@ -0,0 +1,20 @@
<ng-container *ngIf="enabled && !(loading || status === statusDownloading)">
<!-- Download button. -->
<ion-button *ngIf="status == statusNotDownloaded" fill="clear" (click)="download($event, false)" color="dark"
class="core-animate-show-hide" [attr.aria-label]="'core.download' | translate">
<ion-icon slot="icon-only" name="cloud-download"></ion-icon>
</ion-button>
<!-- Refresh button. -->
<ion-button *ngIf="status == statusOutdated || (status == statusDownloaded && !canTrustDownload)" fill="clear"
(click)="download($event, true)" color="dark" class="core-animate-show-hide" [attr.aria-label]="'core.refresh' | translate">
<ion-icon slot="icon-only" name="fas-sync"></ion-icon>
</ion-button>
<!-- Downloaded status icon. -->
<ion-icon *ngIf="status == statusDownloaded && canTrustDownload" class="core-icon-downloaded ion-padding-horizontal" color="success"
name="cloud-done" [attr.aria-label]="'core.downloaded' | translate" role="status"></ion-icon>
</ng-container>
<!-- Spinner. -->
<ion-spinner *ngIf="loading || status === statusDownloading" class="core-animate-show-hide"></ion-spinner>

View File

@ -0,0 +1,8 @@
:host {
font-size: 1.4rem;
display: flex;
flex-flow: row;
align-items: center;
justify-content: space-around;
align-content: center;
}

View File

@ -0,0 +1,59 @@
// (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, Output, EventEmitter } from '@angular/core';
import { CoreConstants } from '@core/constants';
/**
* Component to show a download button with refresh option, the spinner and the status of it.
*
* Usage:
* <core-download-refresh [status]="status" enabled="true" canCheckUpdates="true" action="download()"></core-download-refresh>
*/
@Component({
selector: 'core-download-refresh',
templateUrl: 'core-download-refresh.html',
styleUrls: ['download-refresh.scss'],
})
export class CoreDownloadRefreshComponent {
@Input() status?: string; // Download status.
@Input() enabled = false; // Whether the download is enabled.
@Input() loading = true; // Force loading status when is not downloading.
@Input() canTrustDownload = false; // If false, refresh will be shown if downloaded.
@Output() action: EventEmitter<boolean>; // Will emit an event when the item clicked.
statusDownloaded = CoreConstants.DOWNLOADED;
statusNotDownloaded = CoreConstants.NOT_DOWNLOADED;
statusOutdated = CoreConstants.OUTDATED;
statusDownloading = CoreConstants.DOWNLOADING;
constructor() {
this.action = new EventEmitter();
}
/**
* Download clicked.
*
* @param e Click event.
* @param refresh Whether it's refreshing.
*/
download(e: Event, refresh: boolean): void {
e.preventDefault();
e.stopPropagation();
this.action.emit(refresh);
}
}

View File

@ -0,0 +1,20 @@
<ion-item *ngIf="file" button class="ion-text-wrap item-media" (click)="download($event, true)" detail="false">
<ion-thumbnail slot="start">
<img [src]="fileIcon" alt="" role="presentation" />
</ion-thumbnail>
<ion-label>
<h2>{{fileName}}</h2>
<p *ngIf="fileSizeReadable">{{ fileSizeReadable }}</p>
<p *ngIf="showTime">{{ timemodified * 1000 | coreFormatDate }}</p>
</ion-label>
<div slot="end">
<core-download-refresh [status]="state" [enabled]="canDownload" [loading]="isDownloading"
[canTrustDownload]="!alwaysDownload" (action)="download()">
</core-download-refresh>
<ion-button fill="clear" *ngIf="!isDownloading && canDelete" (click)="delete($event)"
[attr.aria-label]="'core.delete' | translate" color="danger">
<ion-icon slot="icon-only" name="fas-trash"></ion-icon>
</ion-button>
</div>
</ion-item>

View File

@ -0,0 +1,31 @@
:host {
// @todo
// .card-md core-file + core-file > .item-md.item-block > .item-inner,
// core-file + core-file > .item-md.item-block > .item-inner {
// border-top: 1px solid $list-md-border-color;
// }
// .card-ios core-file + core-file > .item-ios.item-block > .item-inner,
// core-file + core-file > .item-ios.item-block > .item-inner {
// border-top: $hairlines-width solid $list-ios-border-color;
// .buttons {
// min-height: 53px;
// min-width: 58px;
// }
// }
// core-file > .item.item-block > .item-inner {
// border-bottom: 0;
// @include safe-area-padding(null, 0px, null, null);
// .buttons {
// display: flex;
// flex-flow: row;
// align-items: center;
// z-index: 1;
// justify-content: space-around;
// align-content: center;
// min-height: 52px;
// min-width: 53px;
// }
// }
}

View File

@ -0,0 +1,253 @@
// (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, Output, OnInit, OnDestroy, EventEmitter } from '@angular/core';
import { CoreApp } from '@services/app';
import { CoreFilepool } from '@services/filepool';
import { CoreFileHelper } from '@services/file-helper';
import { CorePluginFileDelegate } from '@services/plugin-file-delegate';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreMimetypeUtils } from '@services/utils/mimetype';
import { CoreUrlUtils } from '@services/utils/url';
import { CoreUtils } from '@services/utils/utils';
import { CoreTextUtils } from '@services/utils/text';
import { CoreConstants } from '@core/constants';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreWSExternalFile } from '@/app/services/ws';
/**
* Component to handle a remote file. Shows the file name, icon (depending on mimetype) and a button
* to download/refresh it.
*/
@Component({
selector: 'core-file',
templateUrl: 'core-file.html',
styleUrls: ['file.scss'],
})
export class CoreFileComponent implements OnInit, OnDestroy {
@Input() file?: CoreWSExternalFile; // The file.
@Input() component?: string; // Component the file belongs to.
@Input() componentId?: string | number; // Component ID.
@Input() canDelete?: boolean | string; // Whether file can be deleted.
@Input() alwaysDownload?: boolean | string; // Whether it should always display the refresh button when the file is downloaded.
@Input() canDownload?: boolean | string = true; // Whether file can be downloaded.
@Input() showSize?: boolean | string = true; // Whether show filesize.
@Input() showTime?: boolean | string = true; // Whether show file time modified.
@Output() onDelete: EventEmitter<void>; // Will notify when the delete button is clicked.
isDownloading?: boolean;
fileIcon?: string;
fileName!: string;
fileSizeReadable?: string;
state?: string;
timemodified!: number;
protected fileUrl!: string;
protected siteId?: string;
protected fileSize?: number;
protected observer?: CoreEventObserver;
constructor(
protected pluginFileDelegate: CorePluginFileDelegate,
) {
this.onDelete = new EventEmitter<void>();
}
/**
* Component being initialized.
*/
async ngOnInit(): Promise<void> {
if (!this.file) {
return;
}
this.canDelete = CoreUtils.instance.isTrueOrOne(this.canDelete);
this.alwaysDownload = CoreUtils.instance.isTrueOrOne(this.alwaysDownload);
this.canDownload = CoreUtils.instance.isTrueOrOne(this.canDownload);
this.fileUrl = this.file.fileurl;
this.timemodified = this.file.timemodified || 0;
this.siteId = CoreSites.instance.getCurrentSiteId();
this.fileSize = this.file.filesize;
this.fileName = this.file.filename || '';
if (CoreUtils.instance.isTrueOrOne(this.showSize) && this.fileSize && this.fileSize >= 0) {
this.fileSizeReadable = CoreTextUtils.instance.bytesToSize(this.fileSize, 2);
}
this.showTime = CoreUtils.instance.isTrueOrOne(this.showTime) && this.timemodified > 0;
if (this.file.isexternalfile) {
this.alwaysDownload = true; // Always show the download button in external files.
}
this.fileIcon = this.file.mimetype ? CoreMimetypeUtils.instance.getMimetypeIcon(this.file.mimetype) :
CoreMimetypeUtils.instance.getFileIcon(this.fileName);
if (this.canDownload) {
this.calculateState();
try {
// Update state when receiving events about this file.
const eventName = await CoreFilepool.instance.getFileEventNameByUrl(this.siteId, this.fileUrl);
this.observer = CoreEvents.on(eventName, () => {
this.calculateState();
});
} catch (error) {
// File not downloadable.
}
}
}
/**
* Convenience function to get the file state and set variables based on it.
*
* @return Promise resolved when state has been calculated.
*/
protected async calculateState(): Promise<void> {
if (!this.siteId) {
return;
}
const state = await CoreFilepool.instance.getFileStateByUrl(this.siteId, this.fileUrl, this.timemodified);
const site = await CoreSites.instance.getSite(this.siteId);
this.canDownload = site.canDownloadFiles();
this.state = state;
this.isDownloading = this.canDownload && state === CoreConstants.DOWNLOADING;
}
/**
* Convenience function to open a file, downloading it if needed.
*
* @return Promise resolved when file is opened.
*/
protected openFile(): Promise<void> {
return CoreFileHelper.instance.downloadAndOpenFile(this.file!, this.component, this.componentId, this.state, (event) => {
if (event && 'calculating' in event && event.calculating) {
// The process is calculating some data required for the download, show the spinner.
this.isDownloading = true;
}
}).catch((error) => {
CoreDomUtils.instance.showErrorModalDefault(error, 'core.errordownloading', true);
});
}
/**
* Download a file and, optionally, open it afterwards.
*
* @param e Click event.
* @param openAfterDownload Whether the file should be opened after download.
*/
async download(e?: Event, openAfterDownload: boolean = false): Promise<void> {
e && e.preventDefault();
e && e.stopPropagation();
if (!this.file || !this.siteId) {
return;
}
if (this.isDownloading && !openAfterDownload) {
return;
}
if (!this.canDownload || !this.state || this.state == CoreConstants.NOT_DOWNLOADABLE) {
// File cannot be downloaded, just open it.
if (CoreUrlUtils.instance.isLocalFileUrl(this.fileUrl)) {
CoreUtils.instance.openFile(this.fileUrl);
} else {
CoreUtils.instance.openOnlineFile(CoreUrlUtils.instance.unfixPluginfileURL(this.fileUrl));
}
return;
}
if (!CoreApp.instance.isOnline() && (!openAfterDownload || (openAfterDownload &&
!CoreFileHelper.instance.isStateDownloaded(this.state)))) {
CoreDomUtils.instance.showErrorModal('core.networkerrormsg', true);
return;
}
if (openAfterDownload) {
// File needs to be opened now.
try {
await this.openFile();
} catch (error) {
CoreDomUtils.instance.showErrorModalDefault(error, 'core.errordownloading', true);
}
} else {
try {
// File doesn't need to be opened (it's a prefetch). Show confirm modal if file size is defined and it's big.
const size = await this.pluginFileDelegate.getFileSize(this.file, this.siteId);
if (size) {
await CoreDomUtils.instance.confirmDownloadSize({ size: size, total: true });
}
// User confirmed, add the file to queue.
// @todo: Is the invalidate really needed?
await CoreUtils.instance.ignoreErrors(CoreFilepool.instance.invalidateFileByUrl(this.siteId, this.fileUrl));
this.isDownloading = true;
try {
await CoreFilepool.instance.addToQueueByUrl(
this.siteId,
this.fileUrl,
this.component,
this.componentId,
this.timemodified,
undefined,
undefined,
0,
this.file,
);
} catch (error) {
CoreDomUtils.instance.showErrorModalDefault(error, 'core.errordownloading', true);
this.calculateState();
}
} catch (error) {
CoreDomUtils.instance.showErrorModalDefault(error, 'core.errordownloading', true);
}
}
}
/**
* Delete the file.
*
* @param e Click event.
*/
delete(e: Event): void {
e.preventDefault();
e.stopPropagation();
if (this.canDelete) {
this.onDelete.emit();
}
}
/**
* Component destroyed.
*/
ngOnDestroy(): void {
this.observer?.off();
}
}

View File

@ -46,8 +46,8 @@ export class CoreFileHelperProvider {
*/
async downloadAndOpenFile(
file: CoreWSExternalFile,
component: string,
componentId: string | number,
component?: string,
componentId?: string | number,
state?: string,
onProgress?: CoreFileHelperOnProgress,
siteId?: string,

View File

@ -251,7 +251,7 @@ export class CoreFileProvider {
* @return Promise to be resolved when the file is created.
*/
async createFile(path: string, failIfExists?: boolean): Promise<FileEntry> {
const entry = <FileEntry> await this.create(true, path, failIfExists);
const entry = <FileEntry> await this.create(false, path, failIfExists);
return entry;
}
@ -568,8 +568,7 @@ export class CoreFileProvider {
// Create file (and parent folders) to prevent errors.
const fileEntry = await this.createFile(path);
if (this.isHTMLAPI &&
(typeof data == 'string' || data.toString() == '[object ArrayBuffer]')) {
if (this.isHTMLAPI && (typeof data == 'string' || data.toString() == '[object ArrayBuffer]')) {
// We need to write Blobs.
const extension = CoreMimetypeUtils.instance.getFileExtension(path);
const type = extension ? CoreMimetypeUtils.instance.getMimeType(extension) : '';

View File

@ -17,11 +17,14 @@ import { FileEntry } from '@ionic-native/file';
import { CoreFile } from '@services/file';
import { CoreTextUtils } from '@services/utils/text';
import { makeSingleton, Translate, Http } from '@singletons/core.singletons';
import { makeSingleton, Translate } from '@singletons/core.singletons';
import { CoreLogger } from '@singletons/logger';
import { CoreWSExternalFile } from '@services/ws';
import { CoreUtils } from '@services/utils/utils';
import extToMime from '@/assets/exttomime.json';
import mimeToExt from '@/assets/mimetoext.json';
interface MimeTypeInfo {
type: string;
icon?: string;
@ -52,17 +55,8 @@ export class CoreMimetypeUtilsProvider {
constructor() {
this.logger = CoreLogger.getInstance('CoreMimetypeUtilsProvider');
Http.instance.get('assets/exttomime.json').subscribe((result: Record<string, MimeTypeInfo>) => {
this.extToMime = result;
}, () => {
// Error, shouldn't happen.
});
Http.instance.get('assets/mimetoext.json').subscribe((result: Record<string, string[]>) => {
this.mimeToExt = result;
}, () => {
// Error, shouldn't happen
});
this.extToMime = extToMime;
this.mimeToExt = mimeToExt;
}
/**

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB