MOBILE-2302 core: Implement core-file component
|
@ -216,3 +216,12 @@ core-format-text, *[core-format-text] {
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Media item, ideal for icons.
|
||||||
|
.item-media {
|
||||||
|
min-height: $item-media-height + ($content-padding * 2);
|
||||||
|
> img:first-child {
|
||||||
|
max-width: $item-media-width;
|
||||||
|
max-height: $item-media-height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
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.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 |
|
@ -329,6 +329,21 @@ export class SQLiteDB {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format the data to insert in the database. Removes undefined entries so they are stored as null instead of 'undefined'.
|
||||||
|
*
|
||||||
|
* @param {object} data Data to insert.
|
||||||
|
*/
|
||||||
|
protected formatDataToInsert(data: object) : void {
|
||||||
|
// Remove undefined entries and convert null to "NULL".
|
||||||
|
for (let name in data) {
|
||||||
|
let value = data[name];
|
||||||
|
if (typeof value == 'undefined') {
|
||||||
|
delete data[name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all the records from a table.
|
* Get all the records from a table.
|
||||||
*
|
*
|
||||||
|
@ -585,6 +600,8 @@ export class SQLiteDB {
|
||||||
* @return {any[]} Array with the SQL query and the params.
|
* @return {any[]} Array with the SQL query and the params.
|
||||||
*/
|
*/
|
||||||
protected getSqlInsertQuery(table: string, data: object) : any[] {
|
protected getSqlInsertQuery(table: string, data: object) : any[] {
|
||||||
|
this.formatDataToInsert(data);
|
||||||
|
|
||||||
let keys = Object.keys(data),
|
let keys = Object.keys(data),
|
||||||
fields = keys.join(','),
|
fields = keys.join(','),
|
||||||
questionMarks = ',?'.repeat(keys.length).substr(1);
|
questionMarks = ',?'.repeat(keys.length).substr(1);
|
||||||
|
@ -773,6 +790,8 @@ export class SQLiteDB {
|
||||||
sql,
|
sql,
|
||||||
params;
|
params;
|
||||||
|
|
||||||
|
this.formatDataToInsert(data);
|
||||||
|
|
||||||
for (let key in data) {
|
for (let key in data) {
|
||||||
sets.push(`${key} = ?`);
|
sets.push(`${key} = ?`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ import { CoreIframeComponent } from './iframe/iframe';
|
||||||
import { CoreProgressBarComponent } from './progress-bar/progress-bar';
|
import { CoreProgressBarComponent } from './progress-bar/progress-bar';
|
||||||
import { CoreEmptyBoxComponent } from './empty-box/empty-box';
|
import { CoreEmptyBoxComponent } from './empty-box/empty-box';
|
||||||
import { CoreSearchBoxComponent } from './search-box/search-box';
|
import { CoreSearchBoxComponent } from './search-box/search-box';
|
||||||
|
import { CoreFileComponent } from './file/file';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
@ -34,7 +35,8 @@ import { CoreSearchBoxComponent } from './search-box/search-box';
|
||||||
CoreIframeComponent,
|
CoreIframeComponent,
|
||||||
CoreProgressBarComponent,
|
CoreProgressBarComponent,
|
||||||
CoreEmptyBoxComponent,
|
CoreEmptyBoxComponent,
|
||||||
CoreSearchBoxComponent
|
CoreSearchBoxComponent,
|
||||||
|
CoreFileComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
IonicModule,
|
IonicModule,
|
||||||
|
@ -49,7 +51,8 @@ import { CoreSearchBoxComponent } from './search-box/search-box';
|
||||||
CoreIframeComponent,
|
CoreIframeComponent,
|
||||||
CoreProgressBarComponent,
|
CoreProgressBarComponent,
|
||||||
CoreEmptyBoxComponent,
|
CoreEmptyBoxComponent,
|
||||||
CoreSearchBoxComponent
|
CoreSearchBoxComponent,
|
||||||
|
CoreFileComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class CoreComponentsModule {}
|
export class CoreComponentsModule {}
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
<a ion-item text-wrap class="item-media" (click)="download($event, true)" [class.item-2-button-right]="canDelete">
|
||||||
|
<img [src]="fileIcon" alt="" role="presentation" item-start />
|
||||||
|
<p>{{fileName}}</p>
|
||||||
|
<div class="buttons" item-end>
|
||||||
|
<button ion-button clear icon-only (click)="download($event)" *ngIf="!isDownloading && showDownload" [attr.aria-label]="'core.download' | translate">
|
||||||
|
<ion-icon [name]="isDownloaded ? 'refresh' : 'cloud-download'"></ion-icon>
|
||||||
|
</button>
|
||||||
|
<button ion-button clear icon-only (click)="delete($event)" *ngIf="!isDownloading && canDelete" [attr.aria-label]="'core.delete' | translate" color="danger">
|
||||||
|
<ion-icon name="trash"></ion-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<ion-spinner *ngIf="isDownloading" item-end></ion-spinner>
|
||||||
|
</a>
|
|
@ -0,0 +1,28 @@
|
||||||
|
core-loading {
|
||||||
|
.mm-loading-container {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 10px;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mm-loading-content {
|
||||||
|
padding-bottom: 1px; /* This makes height be real */
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mm-loading-noheight .mm-loading-content {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-content > .padding > core-loading > .mm-loading-container,
|
||||||
|
ion-content[padding] > .scroll-content > core-loading > .mm-loading-container,
|
||||||
|
.mm-loading-center .mm-loading-container {
|
||||||
|
display: table;
|
||||||
|
|
||||||
|
.mm-loading-spinner {
|
||||||
|
display: table-cell;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,291 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { Component, Input, Output, OnInit, OnDestroy, EventEmitter } from '@angular/core';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { CoreAppProvider } from '../../providers/app';
|
||||||
|
import { CoreEventsProvider } from '../../providers/events';
|
||||||
|
import { CoreFileProvider } from '../../providers/file';
|
||||||
|
import { CoreFilepoolProvider } from '../../providers/filepool';
|
||||||
|
import { CoreSitesProvider } from '../../providers/sites';
|
||||||
|
import { CoreDomUtilsProvider } from '../../providers/utils/dom';
|
||||||
|
import { CoreMimetypeUtilsProvider } from '../../providers/utils/mimetype';
|
||||||
|
import { CoreUtilsProvider } from '../../providers/utils/utils';
|
||||||
|
import { CoreConstants } from '../../core/constants';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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: 'file.html'
|
||||||
|
})
|
||||||
|
export class CoreFileComponent implements OnInit, OnDestroy {
|
||||||
|
@Input() file: any; // The file. Must have a property 'filename' and a 'fileurl' or 'url'
|
||||||
|
@Input() component?: string; // Component the file belongs to.
|
||||||
|
@Input() componentId?: string|number; // Component ID.
|
||||||
|
@Input() timemodified?: number; // If set, the value will be used to check if the file is outdated.
|
||||||
|
@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.
|
||||||
|
// Use it for files that you cannot determine if they're outdated or not.
|
||||||
|
@Input() canDownload?: boolean|string = true; // Whether file can be downloaded.
|
||||||
|
@Output() onDelete?: EventEmitter<string>; // Will notify when the delete button is clicked.
|
||||||
|
|
||||||
|
isDownloaded: boolean;
|
||||||
|
isDownloading: boolean;
|
||||||
|
showDownload: boolean;
|
||||||
|
fileIcon: string;
|
||||||
|
fileName: string;
|
||||||
|
|
||||||
|
protected fileUrl: string;
|
||||||
|
protected siteId: string;
|
||||||
|
protected fileSize: number;
|
||||||
|
protected observer;
|
||||||
|
|
||||||
|
constructor(private translate: TranslateService, private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider,
|
||||||
|
private domUtils: CoreDomUtilsProvider, private filepoolProvider: CoreFilepoolProvider,
|
||||||
|
private fileProvider: CoreFileProvider, private appProvider: CoreAppProvider,
|
||||||
|
private mimeUtils: CoreMimetypeUtilsProvider, private eventsProvider: CoreEventsProvider) {
|
||||||
|
this.onDelete = new EventEmitter();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being initialized.
|
||||||
|
*/
|
||||||
|
ngOnInit() {
|
||||||
|
this.canDelete = this.utils.isTrueOrOne(this.canDelete);
|
||||||
|
this.alwaysDownload = this.utils.isTrueOrOne(this.alwaysDownload);
|
||||||
|
this.canDownload = this.utils.isTrueOrOne(this.canDownload);
|
||||||
|
this.timemodified = this.timemodified || 0;
|
||||||
|
|
||||||
|
this.fileUrl = this.file.fileurl || this.file.url;
|
||||||
|
this.siteId = this.sitesProvider.getCurrentSiteId();
|
||||||
|
this.fileSize = this.file.filesize;
|
||||||
|
this.fileName = this.file.filename;
|
||||||
|
|
||||||
|
if (this.file.isexternalfile) {
|
||||||
|
this.alwaysDownload = true; // Always show the download button in external files.
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fileIcon = this.mimeUtils.getFileIcon(this.file.filename);
|
||||||
|
|
||||||
|
if (this.canDownload) {
|
||||||
|
this.calculateState();
|
||||||
|
|
||||||
|
// Update state when receiving events about this file.
|
||||||
|
this.filepoolProvider.getFileEventNameByUrl(this.siteId, this.fileUrl).then((eventName) => {
|
||||||
|
this.observer = this.eventsProvider.on(eventName, () => {
|
||||||
|
this.calculateState();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience function to get the file state and set variables based on it.
|
||||||
|
*
|
||||||
|
* @return {Promise<void>} Promise resolved when state has been calculated.
|
||||||
|
*/
|
||||||
|
protected calculateState() : Promise<void> {
|
||||||
|
return this.filepoolProvider.getFileStateByUrl(this.siteId, this.fileUrl, this.timemodified).then((state) => {
|
||||||
|
let canDownload = this.sitesProvider.getCurrentSite().canDownloadFiles();
|
||||||
|
|
||||||
|
this.isDownloaded = state === CoreConstants.downloaded || state === CoreConstants.outdated;
|
||||||
|
this.isDownloading = canDownload && state === CoreConstants.downloading;
|
||||||
|
this.showDownload = canDownload && (state === CoreConstants.notDownloaded || state === CoreConstants.outdated ||
|
||||||
|
(this.alwaysDownload && state === CoreConstants.downloaded));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download the file.
|
||||||
|
*
|
||||||
|
* @return {Promise<string>} Promise resolved when file is downloaded.
|
||||||
|
*/
|
||||||
|
protected downloadFile() : Promise<string> {
|
||||||
|
if (!this.sitesProvider.getCurrentSite().canDownloadFiles()) {
|
||||||
|
this.domUtils.showErrorModal('core.cannotdownloadfiles', true);
|
||||||
|
return Promise.reject(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isDownloading = true;
|
||||||
|
return this.filepoolProvider.downloadUrl(this.siteId, this.fileUrl, false, this.component, this.componentId,
|
||||||
|
this.timemodified, undefined, undefined, this.file).catch(() => {
|
||||||
|
|
||||||
|
// Call calculateState to make sure we have the right state.
|
||||||
|
return this.calculateState().then(() => {
|
||||||
|
if (this.isDownloaded) {
|
||||||
|
return this.filepoolProvider.getInternalUrlByUrl(this.siteId, this.fileUrl);
|
||||||
|
} else {
|
||||||
|
return Promise.reject(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience function to open a file, downloading it if needed.
|
||||||
|
*
|
||||||
|
* @return {Promise<string>} Promise resolved when file is opened.
|
||||||
|
*/
|
||||||
|
protected openFile() : Promise<any> {
|
||||||
|
let fixedUrl = this.sitesProvider.getCurrentSite().fixPluginfileURL(this.fileUrl),
|
||||||
|
promise;
|
||||||
|
|
||||||
|
if (this.fileProvider.isAvailable()) {
|
||||||
|
promise = Promise.resolve().then(() => {
|
||||||
|
// The file system is available.
|
||||||
|
let isWifi = !this.appProvider.isNetworkAccessLimited(),
|
||||||
|
isOnline = this.appProvider.isOnline();
|
||||||
|
|
||||||
|
if (this.isDownloaded && !this.showDownload) {
|
||||||
|
// File is downloaded, get the local file URL.
|
||||||
|
return this.filepoolProvider.getUrlByUrl(this.siteId, this.fileUrl,
|
||||||
|
this.component, this.componentId, this.timemodified, false, false, this.file);
|
||||||
|
} else {
|
||||||
|
if (!isOnline && !this.isDownloaded) {
|
||||||
|
// Not downloaded and user is offline, reject.
|
||||||
|
return Promise.reject(this.translate.instant('core.networkerrormsg'));
|
||||||
|
}
|
||||||
|
|
||||||
|
let isDownloading = this.isDownloading;
|
||||||
|
this.isDownloading = true; // This check could take a while, show spinner.
|
||||||
|
return this.filepoolProvider.shouldDownloadBeforeOpen(fixedUrl, this.fileSize).then(() => {
|
||||||
|
if (isDownloading) {
|
||||||
|
// It's already downloading, stop.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Download and then return the local URL.
|
||||||
|
return this.downloadFile();
|
||||||
|
}, () => {
|
||||||
|
// Start the download if in wifi, but return the URL right away so the file is opened.
|
||||||
|
if (isWifi && isOnline) {
|
||||||
|
this.downloadFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDownloading || !this.isDownloaded || isOnline) {
|
||||||
|
// Not downloaded or outdated and online, return the online URL.
|
||||||
|
return fixedUrl;
|
||||||
|
} else {
|
||||||
|
// Outdated but offline, so we return the local URL.
|
||||||
|
return this.filepoolProvider.getUrlByUrl(this.siteId, this.fileUrl,
|
||||||
|
this.component, this.componentId, this.timemodified, false, false, this.file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Use the online URL.
|
||||||
|
promise = Promise.resolve(fixedUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise.then((url) => {
|
||||||
|
if (!url) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.indexOf('http') === 0) {
|
||||||
|
return this.utils.openOnlineFile(url).catch((error) => {
|
||||||
|
// Error opening the file, some apps don't allow opening online files.
|
||||||
|
if (!this.fileProvider.isAvailable()) {
|
||||||
|
return Promise.reject(error);
|
||||||
|
} else if (this.isDownloading) {
|
||||||
|
return Promise.reject(this.translate.instant('core.erroropenfiledownloading'));
|
||||||
|
}
|
||||||
|
|
||||||
|
let subPromise;
|
||||||
|
|
||||||
|
if (status === CoreConstants.notDownloaded) {
|
||||||
|
// File is not downloaded, download and then return the local URL.
|
||||||
|
subPromise = this.downloadFile();
|
||||||
|
} else {
|
||||||
|
// File is outdated and can't be opened in online, return the local URL.
|
||||||
|
subPromise = this.filepoolProvider.getInternalUrlByUrl(this.siteId, this.fileUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
return subPromise.then((url) => {
|
||||||
|
return this.utils.openFile(url);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return this.utils.openFile(url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download a file and, optionally, open it afterwards.
|
||||||
|
*
|
||||||
|
* @param {Event} e Click event.
|
||||||
|
* @param {boolean} openAfterDownload Whether the file should be opened after download.
|
||||||
|
*/
|
||||||
|
download(e: Event, openAfterDownload: boolean) : void {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
let promise;
|
||||||
|
|
||||||
|
if (this.isDownloading && !openAfterDownload) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.appProvider.isOnline() && (!openAfterDownload || (openAfterDownload && !this.isDownloaded))) {
|
||||||
|
this.domUtils.showErrorModal('core.networkerrormsg', true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (openAfterDownload) {
|
||||||
|
// File needs to be opened now.
|
||||||
|
this.openFile().catch((error) => {
|
||||||
|
this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// File doesn't need to be opened (it's a prefetch). Show confirm modal if file size is defined and it's big.
|
||||||
|
promise = this.fileSize ? this.domUtils.confirmDownloadSize({size: this.fileSize, total: true}) : Promise.resolve();
|
||||||
|
promise.then(() => {
|
||||||
|
// User confirmed, add the file to queue.
|
||||||
|
this.filepoolProvider.invalidateFileByUrl(this.siteId, this.fileUrl).finally(() => {
|
||||||
|
this.isDownloading = true;
|
||||||
|
this.filepoolProvider.addToQueueByUrl(this.siteId, this.fileUrl, this.component,
|
||||||
|
this.componentId, this.timemodified, undefined, undefined, 0, this.file).catch((error) => {
|
||||||
|
this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true);
|
||||||
|
this.calculateState();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the file.
|
||||||
|
*
|
||||||
|
* @param {Event} e Click event.
|
||||||
|
*/
|
||||||
|
deleteFile(e: Event) : void {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (this.canDelete) {
|
||||||
|
this.onDelete.emit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component destroyed.
|
||||||
|
*/
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.observer && this.observer.off();
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,7 +25,7 @@
|
||||||
<p class="item-heading">{{ 'core.teachers' | translate }}</p>
|
<p class="item-heading">{{ 'core.teachers' | translate }}</p>
|
||||||
<p *ngFor="let contact of course.contacts">{{contact.fullname}}</p>
|
<p *ngFor="let contact of course.contacts">{{contact.fullname}}</p>
|
||||||
</a>
|
</a>
|
||||||
<!-- <mm-file ng-repeat="file in course.overviewfiles" file="file" component="{{component}}" component-id="{{course.id}}"></mm-file> -->
|
<core-file *ngFor="let file of course.overviewfiles" [file]="file" [component]="component" [componentId]="course.id"></core-file>
|
||||||
<div *ngIf="!isEnrolled">
|
<div *ngIf="!isEnrolled">
|
||||||
<ion-item text-wrap *ngFor="let instance of selfEnrolInstances">
|
<ion-item text-wrap *ngFor="let instance of selfEnrolInstances">
|
||||||
<p class="item-heading">{{ instance.name }}</p>
|
<p class="item-heading">{{ instance.name }}</p>
|
||||||
|
|
|
@ -350,7 +350,6 @@ export class FileMock extends File {
|
||||||
(<any>navigator).webkitPersistentStorage.requestQuota(500 * 1024 * 1024, (granted) => {
|
(<any>navigator).webkitPersistentStorage.requestQuota(500 * 1024 * 1024, (granted) => {
|
||||||
window.requestFileSystem(LocalFileSystem.PERSISTENT, granted, (entry) => {
|
window.requestFileSystem(LocalFileSystem.PERSISTENT, granted, (entry) => {
|
||||||
basePath = entry.root.toURL();
|
basePath = entry.root.toURL();
|
||||||
// this.fileProvider.setHTMLBasePath(basePath);
|
|
||||||
resolve(basePath);
|
resolve(basePath);
|
||||||
}, reject);
|
}, reject);
|
||||||
}, reject);
|
}, reject);
|
||||||
|
|
|
@ -98,7 +98,7 @@ export class CoreFileProvider {
|
||||||
* @return {boolean} Whether the plugin is available.
|
* @return {boolean} Whether the plugin is available.
|
||||||
*/
|
*/
|
||||||
isAvailable() : boolean {
|
isAvailable() : boolean {
|
||||||
return typeof window.resolveLocalFileSystemURL !== 'undefined' && typeof FileTransfer !== 'undefined';
|
return typeof window.resolveLocalFileSystemURL !== 'undefined';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -327,7 +327,7 @@ export class CoreFilepoolProvider {
|
||||||
component: component,
|
component: component,
|
||||||
componentId: componentId || ''
|
componentId: componentId || ''
|
||||||
};
|
};
|
||||||
return db.insertOrUpdateRecord(this.LINKS_TABLE, newEntry, null);
|
return db.insertOrUpdateRecord(this.LINKS_TABLE, newEntry, undefined);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -410,7 +410,7 @@ export class CoreFilepoolProvider {
|
||||||
* @return {Promise<any>} Promise resolved when the file is downloaded.
|
* @return {Promise<any>} Promise resolved when the file is downloaded.
|
||||||
*/
|
*/
|
||||||
protected addToQueue(siteId: string, fileId: string, url: string, priority: number, revision: number, timemodified: number,
|
protected addToQueue(siteId: string, fileId: string, url: string, priority: number, revision: number, timemodified: number,
|
||||||
filePath: string, options: any = {}, link?: any) : Promise<any> {
|
filePath: string, onProgress?: (event: any) => any, options: any = {}, link?: any) : Promise<any> {
|
||||||
this.logger.debug(`Adding ${fileId} to the queue`);
|
this.logger.debug(`Adding ${fileId} to the queue`);
|
||||||
|
|
||||||
return this.appDB.insertRecord(this.QUEUE_TABLE, {
|
return this.appDB.insertRecord(this.QUEUE_TABLE, {
|
||||||
|
@ -429,7 +429,7 @@ export class CoreFilepoolProvider {
|
||||||
// Check if the queue is running.
|
// Check if the queue is running.
|
||||||
this.checkQueueProcessing();
|
this.checkQueueProcessing();
|
||||||
this.notifyFileDownloading(siteId, fileId);
|
this.notifyFileDownloading(siteId, fileId);
|
||||||
return this.getQueuePromise(siteId, fileId);
|
return this.getQueuePromise(siteId, fileId, true, onProgress);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -479,8 +479,7 @@ export class CoreFilepoolProvider {
|
||||||
|
|
||||||
// Retrieve the queue deferred now if it exists to prevent errors if file is removed from queue
|
// Retrieve the queue deferred now if it exists to prevent errors if file is removed from queue
|
||||||
// while we're checking if the file is in queue.
|
// while we're checking if the file is in queue.
|
||||||
queueDeferred = this.getQueueDeferred(siteId, fileId, false);
|
queueDeferred = this.getQueueDeferred(siteId, fileId, false, onProgress);
|
||||||
queueDeferred.onProgress = onProgress;
|
|
||||||
|
|
||||||
return this.hasFileInQueue(siteId, fileId).then((entry: CoreFilepoolQueueEntry) => {
|
return this.hasFileInQueue(siteId, fileId).then((entry: CoreFilepoolQueueEntry) => {
|
||||||
let foundLink = false,
|
let foundLink = false,
|
||||||
|
@ -530,7 +529,7 @@ export class CoreFilepoolProvider {
|
||||||
// Update only when required.
|
// Update only when required.
|
||||||
this.logger.debug(`Updating file ${fileId} which is already in queue`);
|
this.logger.debug(`Updating file ${fileId} which is already in queue`);
|
||||||
return this.appDB.updateRecords(this.QUEUE_TABLE, newData, primaryKey).then(() => {
|
return this.appDB.updateRecords(this.QUEUE_TABLE, newData, primaryKey).then(() => {
|
||||||
return this.getQueuePromise(siteId, fileId);
|
return this.getQueuePromise(siteId, fileId, true, onProgress);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -540,14 +539,17 @@ export class CoreFilepoolProvider {
|
||||||
// might have finished now and the deferred wouldn't be in the array anymore.
|
// might have finished now and the deferred wouldn't be in the array anymore.
|
||||||
return queueDeferred.promise;
|
return queueDeferred.promise;
|
||||||
} else {
|
} else {
|
||||||
return this.getQueuePromise(siteId, fileId);
|
// Create a new deferred and return its promise.
|
||||||
|
return this.getQueuePromise(siteId, fileId, true, onProgress);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return this.addToQueue(siteId, fileId, fileUrl, priority, revision, timemodified, filePath, options, link);
|
return this.addToQueue(
|
||||||
|
siteId, fileId, fileUrl, priority, revision, timemodified, filePath, onProgress, options, link);
|
||||||
}
|
}
|
||||||
}, () => {
|
}, () => {
|
||||||
// Unsure why we could not get the record, let's add to the queue anyway.
|
// Unsure why we could not get the record, let's add to the queue anyway.
|
||||||
return this.addToQueue(siteId, fileId, fileUrl, priority, revision, timemodified, filePath, options, link);
|
return this.addToQueue(
|
||||||
|
siteId, fileId, fileUrl, priority, revision, timemodified, filePath, onProgress, options, link);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -596,15 +598,17 @@ export class CoreFilepoolProvider {
|
||||||
// Check if the file should be downloaded.
|
// Check if the file should be downloaded.
|
||||||
if (sizeUnknown) {
|
if (sizeUnknown) {
|
||||||
if (downloadUnknown && isWifi) {
|
if (downloadUnknown && isWifi) {
|
||||||
return this.addToQueueByUrl(siteId, fileUrl, component, componentId, timemodified, null, null, 0, options);
|
return this.addToQueueByUrl(
|
||||||
|
siteId, fileUrl, component, componentId, timemodified, undefined, undefined, 0, options);
|
||||||
}
|
}
|
||||||
} else if (size <= this.DOWNLOAD_THRESHOLD || (isWifi && size <= this.WIFI_DOWNLOAD_THRESHOLD)) {
|
} else if (size <= this.DOWNLOAD_THRESHOLD || (isWifi && size <= this.WIFI_DOWNLOAD_THRESHOLD)) {
|
||||||
return this.addToQueueByUrl(siteId, fileUrl, component, componentId, timemodified, null, null, 0, options);
|
return this.addToQueueByUrl(
|
||||||
|
siteId, fileUrl, component, componentId, timemodified, undefined, undefined, 0, options);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// No need to check size, just add it to the queue.
|
// No need to check size, just add it to the queue.
|
||||||
return this.addToQueueByUrl(siteId, fileUrl, component, componentId, timemodified, null, null, 0, options);
|
return this.addToQueueByUrl(siteId, fileUrl, component, componentId, timemodified, undefined, undefined, 0, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -817,9 +821,11 @@ export class CoreFilepoolProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prefetch) {
|
if (prefetch) {
|
||||||
promises.push(this.addToQueueByUrl(siteId, url, component, componentId, timemodified, null, null, 0, options));
|
promises.push(this.addToQueueByUrl(
|
||||||
|
siteId, url, component, componentId, timemodified, undefined, undefined, 0, options));
|
||||||
} else {
|
} else {
|
||||||
promises.push(this.downloadUrl(siteId, url, ignoreStale, component, componentId, timemodified, null, null, options));
|
promises.push(this.downloadUrl(
|
||||||
|
siteId, url, ignoreStale, component, componentId, timemodified, undefined, undefined, options));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -901,7 +907,7 @@ export class CoreFilepoolProvider {
|
||||||
|
|
||||||
if (prefetch) {
|
if (prefetch) {
|
||||||
promise = this.addToQueueByUrl(
|
promise = this.addToQueueByUrl(
|
||||||
siteId, fileUrl, component, componentId, file.timemodified, path, null, 0, options);
|
siteId, fileUrl, component, componentId, file.timemodified, path, undefined, 0, options);
|
||||||
} else {
|
} else {
|
||||||
promise = this.downloadUrl(
|
promise = this.downloadUrl(
|
||||||
siteId, fileUrl, false, component, componentId, file.timemodified, onFileProgress, path, options);
|
siteId, fileUrl, false, component, componentId, file.timemodified, onFileProgress, path, options);
|
||||||
|
@ -1816,9 +1822,10 @@ export class CoreFilepoolProvider {
|
||||||
* @param {string} siteId The site ID.
|
* @param {string} siteId The site ID.
|
||||||
* @param {string} fileId The file ID.
|
* @param {string} fileId The file ID.
|
||||||
* @param {boolean} [create=true] True if it should create a new deferred if it doesn't exist.
|
* @param {boolean} [create=true] True if it should create a new deferred if it doesn't exist.
|
||||||
|
* @param {Function} [onProgress] Function to call on progress.
|
||||||
* @return {any} Deferred.
|
* @return {any} Deferred.
|
||||||
*/
|
*/
|
||||||
protected getQueueDeferred(siteId: string, fileId: string, create = true): any {
|
protected getQueueDeferred(siteId: string, fileId: string, create = true, onProgress?: (event: any) => any): any {
|
||||||
if (!this.queueDeferreds[siteId]) {
|
if (!this.queueDeferreds[siteId]) {
|
||||||
if (!create) {
|
if (!create) {
|
||||||
return;
|
return;
|
||||||
|
@ -1831,6 +1838,11 @@ export class CoreFilepoolProvider {
|
||||||
}
|
}
|
||||||
this.queueDeferreds[siteId][fileId] = this.utils.promiseDefer();
|
this.queueDeferreds[siteId][fileId] = this.utils.promiseDefer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (onProgress) {
|
||||||
|
this.queueDeferreds[siteId][fileId].onProgress = onProgress;
|
||||||
|
}
|
||||||
|
|
||||||
return this.queueDeferreds[siteId][fileId];
|
return this.queueDeferreds[siteId][fileId];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1854,10 +1866,11 @@ export class CoreFilepoolProvider {
|
||||||
* @param {string} siteId The site ID.
|
* @param {string} siteId The site ID.
|
||||||
* @param {string} fileId The file ID.
|
* @param {string} fileId The file ID.
|
||||||
* @param {boolean} [create=true] True if it should create a new promise if it doesn't exist.
|
* @param {boolean} [create=true] True if it should create a new promise if it doesn't exist.
|
||||||
|
* @param {Function} [onProgress] Function to call on progress.
|
||||||
* @return {Promise<any>} Promise.
|
* @return {Promise<any>} Promise.
|
||||||
*/
|
*/
|
||||||
protected getQueuePromise(siteId: string, fileId: string, create = true) : Promise<any> {
|
protected getQueuePromise(siteId: string, fileId: string, create = true, onProgress?: (event: any) => any) : Promise<any> {
|
||||||
return this.getQueueDeferred(siteId, fileId, create).promise;
|
return this.getQueueDeferred(siteId, fileId, create, onProgress).promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -2248,7 +2261,9 @@ export class CoreFilepoolProvider {
|
||||||
|
|
||||||
promise.then(() => {
|
promise.then(() => {
|
||||||
// All good, we schedule next execution.
|
// All good, we schedule next execution.
|
||||||
setTimeout(this.processQueue, this.QUEUE_PROCESS_INTERVAL);
|
setTimeout(() => {
|
||||||
|
this.processQueue();
|
||||||
|
}, this.QUEUE_PROCESS_INTERVAL);
|
||||||
|
|
||||||
}, (error) => {
|
}, (error) => {
|
||||||
|
|
||||||
|
@ -2270,7 +2285,7 @@ export class CoreFilepoolProvider {
|
||||||
* @return {Promise} Resolved on success. Rejected on failure.
|
* @return {Promise} Resolved on success. Rejected on failure.
|
||||||
*/
|
*/
|
||||||
protected processImportantQueueItem() : Promise<any> {
|
protected processImportantQueueItem() : Promise<any> {
|
||||||
return this.appDB.getRecords(this.QUEUE_TABLE, null, 'priority DESC, added ASC', null, 0, 1).then((items) => {
|
return this.appDB.getRecords(this.QUEUE_TABLE, undefined, 'priority DESC, added ASC', undefined, 0, 1).then((items) => {
|
||||||
let item = items.pop();
|
let item = items.pop();
|
||||||
if (!item) {
|
if (!item) {
|
||||||
return Promise.reject(this.ERR_QUEUE_IS_EMPTY);
|
return Promise.reject(this.ERR_QUEUE_IS_EMPTY);
|
||||||
|
@ -2290,16 +2305,17 @@ export class CoreFilepoolProvider {
|
||||||
* @return {Promise<any>} Resolved on success. Rejected on failure.
|
* @return {Promise<any>} Resolved on success. Rejected on failure.
|
||||||
*/
|
*/
|
||||||
protected processQueueItem(item: CoreFilepoolQueueEntry) : Promise<any> {
|
protected processQueueItem(item: CoreFilepoolQueueEntry) : Promise<any> {
|
||||||
|
// Cast optional fields to undefined instead of null.
|
||||||
let siteId = item.siteId,
|
let siteId = item.siteId,
|
||||||
fileId = item.fileId,
|
fileId = item.fileId,
|
||||||
fileUrl = item.url,
|
fileUrl = item.url,
|
||||||
options = {
|
options = {
|
||||||
revision: item.revision,
|
revision: item.revision || undefined,
|
||||||
timemodified: item.timemodified,
|
timemodified: item.timemodified || undefined,
|
||||||
isexternalfile: item.isexternalfile,
|
isexternalfile: item.isexternalfile || undefined,
|
||||||
repositorytype: item.repositorytype
|
repositorytype: item.repositorytype || undefined
|
||||||
},
|
},
|
||||||
filePath = item.path,
|
filePath = item.path || undefined,
|
||||||
links = item.links || [];
|
links = item.links || [];
|
||||||
|
|
||||||
this.logger.debug('Processing queue item: ' + siteId + ', ' + fileId);
|
this.logger.debug('Processing queue item: ' + siteId + ', ' + fileId);
|
||||||
|
@ -2335,7 +2351,7 @@ export class CoreFilepoolProvider {
|
||||||
// Whoops, we have an error...
|
// Whoops, we have an error...
|
||||||
let dropFromQueue = false;
|
let dropFromQueue = false;
|
||||||
|
|
||||||
if (typeof errorObject != 'undefined' && errorObject.source === fileUrl) {
|
if (errorObject && errorObject.source === fileUrl) {
|
||||||
// This is most likely a FileTransfer error.
|
// This is most likely a FileTransfer error.
|
||||||
if (errorObject.code === 1) { // FILE_NOT_FOUND_ERR.
|
if (errorObject.code === 1) { // FILE_NOT_FOUND_ERR.
|
||||||
// The file was not found, most likely a 404, we remove from queue.
|
// The file was not found, most likely a 404, we remove from queue.
|
||||||
|
|
|
@ -185,7 +185,7 @@ export class CoreMimetypeUtilsProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'img/files/' + icon + '-64.png';
|
return 'assets/img/files/' + icon + '-64.png';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -194,7 +194,7 @@ export class CoreMimetypeUtilsProvider {
|
||||||
* @return {string} The path to a folder icon.
|
* @return {string} The path to a folder icon.
|
||||||
*/
|
*/
|
||||||
getFolderIcon() : string {
|
getFolderIcon() : string {
|
||||||
return 'img/files/folder-64.png';
|
return 'assets/img/files/folder-64.png';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -171,3 +171,10 @@ $item-wp-avatar-size: 54px;
|
||||||
|
|
||||||
@import "roboto";
|
@import "roboto";
|
||||||
@import "noto-sans";
|
@import "noto-sans";
|
||||||
|
|
||||||
|
// Moodle Mobile variables
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
// Small avatar ideal for icons.
|
||||||
|
$item-media-width: 32px !default;
|
||||||
|
$item-media-height: 32px !default;
|
||||||
|
|