MOBILE-3565 components: Create core-file component
|
@ -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,
|
||||
|
|
|
@ -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>
|
|
@ -0,0 +1,8 @@
|
|||
:host {
|
||||
font-size: 1.4rem;
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
align-content: center;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
// }
|
||||
// }
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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) : '';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 4.5 KiB |
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 4.0 KiB |
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 4.8 KiB |
After Width: | Height: | Size: 4.8 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 5.7 KiB |
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 3.1 KiB |
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 4.5 KiB |
After Width: | Height: | Size: 3.9 KiB |
After Width: | Height: | Size: 4.9 KiB |
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 5.3 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 2.8 KiB |