Merge pull request #3039 from dpalou/MOBILE-3897

Mobile 3897
main
Pau Ferrer Ocaña 2021-12-23 16:34:12 +01:00 committed by GitHub
commit af24a778ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 217 additions and 102 deletions

View File

@ -12,8 +12,17 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Directive, Input, AfterViewInit, ElementRef, OnChanges, SimpleChange, Output, EventEmitter } from '@angular/core';
import {
Directive,
Input,
AfterViewInit,
ElementRef,
OnChanges,
SimpleChange,
Output,
EventEmitter,
OnDestroy,
} from '@angular/core';
import { CoreApp } from '@services/app';
import { CoreFile } from '@services/file';
import { CoreFilepool } from '@services/filepool';
@ -24,6 +33,9 @@ import { CoreUtils } from '@services/utils/utils';
import { Platform } from '@singletons';
import { CoreLogger } from '@singletons/logger';
import { CoreError } from '@classes/errors/error';
import { CoreSite } from '@classes/site';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreConstants } from '../constants';
/**
* Directive to handle external content.
@ -38,7 +50,7 @@ import { CoreError } from '@classes/errors/error';
@Directive({
selector: '[core-external-content]',
})
export class CoreExternalContentDirective implements AfterViewInit, OnChanges {
export class CoreExternalContentDirective implements AfterViewInit, OnChanges, OnDestroy {
@Input() siteId?: string; // Site ID to use.
@Input() component?: string; // Component to link the file to.
@ -54,6 +66,7 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges {
protected element: Element;
protected logger: CoreLogger;
protected initialized = false;
protected fileEventObserver?: CoreEventObserver;
constructor(element: ElementRef) {
@ -183,34 +196,8 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges {
protected async handleExternalContent(targetAttr: string, url: string, siteId?: string): Promise<void> {
const tagName = this.element.tagName;
if (tagName == 'VIDEO' && targetAttr != 'poster') {
const video = <HTMLVideoElement> this.element;
if (video.textTracks) {
// It's a video with subtitles. Fix some issues with subtitles.
video.textTracks.onaddtrack = (event): void => {
const track = <TextTrack> event.track;
if (track) {
track.oncuechange = (): void => {
if (!track.cues) {
return;
}
const line = Platform.is('tablet') || CoreApp.isAndroid() ? 90 : 80;
// Position all subtitles to a percentage of video height.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Array.from(track.cues).forEach((cue: any) => {
cue.snapToLines = false;
cue.line = line;
cue.size = 100; // This solves some Android issue.
});
// Delete listener.
track.oncuechange = null;
};
}
};
}
this.handleVideoSubtitles(<HTMLVideoElement> this.element);
}
const site = await CoreSites.getSite(siteId);
@ -234,46 +221,7 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges {
throw new CoreError('Site doesn\'t allow downloading files.');
}
// Download images, tracks and posters if size is unknown.
const downloadUnknown = tagName == 'IMG' || tagName == 'TRACK' || targetAttr == 'poster';
let finalUrl: string;
if (targetAttr === 'src' && tagName !== 'SOURCE' && tagName !== 'TRACK' && tagName !== 'VIDEO' && tagName !== 'AUDIO') {
finalUrl = await CoreFilepool.getSrcByUrl(
site.getId(),
url,
this.component,
this.componentId,
0,
true,
downloadUnknown,
);
} else if (tagName === 'TRACK') {
// Download tracks right away. Using an online URL for tracks can give a CORS error in Android.
finalUrl = await CoreFilepool.downloadUrl(site.getId(), url, false, this.component, this.componentId);
finalUrl = CoreFile.convertFileSrc(finalUrl);
} else {
finalUrl = await CoreFilepool.getUrlByUrl(
site.getId(),
url,
this.component,
this.componentId,
0,
true,
downloadUnknown,
);
finalUrl = CoreFile.convertFileSrc(finalUrl);
}
if (!CoreUrlUtils.isLocalFileUrl(finalUrl) && !finalUrl.includes('#')) {
/* In iOS, if we use the same URL in embedded file and background download then the download only
downloads a few bytes (cached ones). Add an anchor to the URL so both URLs are different.
Don't add this anchor if the URL already has an anchor, otherwise other anchors might not work.
The downloaded URL won't have anchors so the URLs will already be different. */
finalUrl = finalUrl + '#moodlemobile-embedded';
}
const finalUrl = await this.getUrlToUse(targetAttr, url, site);
this.logger.debug('Using URL ' + finalUrl + ' for ' + url);
if (tagName === 'SOURCE') {
@ -295,28 +243,7 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges {
this.element.setAttribute('data-original-' + targetAttr, url);
}
// Set events to download big files (not downloaded automatically).
if (!CoreUrlUtils.isLocalFileUrl(finalUrl) && targetAttr != 'poster' &&
(tagName == 'VIDEO' || tagName == 'AUDIO' || tagName == 'A' || tagName == 'SOURCE')) {
const eventName = tagName == 'A' ? 'click' : 'play';
let clickableEl = this.element;
if (tagName == 'SOURCE') {
clickableEl = <HTMLElement> CoreDomUtils.closest(this.element, 'video,audio');
if (!clickableEl) {
return;
}
}
clickableEl.addEventListener(eventName, () => {
// User played media or opened a downloadable link.
// Download the file if in wifi and it hasn't been downloaded already (for big files).
if (CoreApp.isWifi()) {
// We aren't using the result, so it doesn't matter which of the 2 functions we call.
CoreFilepool.getUrlByUrl(site.getId(), url, this.component, this.componentId, 0, false);
}
});
}
this.setListeners(targetAttr, url, site);
}
/**
@ -359,6 +286,153 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges {
}
}
/**
* Handle video subtitles if any.
*
* @param video Video element.
*/
protected handleVideoSubtitles(video: HTMLVideoElement): void {
if (!video.textTracks) {
return;
}
// It's a video with subtitles. Fix some issues with subtitles.
video.textTracks.onaddtrack = (event): void => {
const track = <TextTrack> event.track;
if (track) {
track.oncuechange = (): void => {
if (!track.cues) {
return;
}
const line = Platform.is('tablet') || CoreApp.isAndroid() ? 90 : 80;
// Position all subtitles to a percentage of video height.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Array.from(track.cues).forEach((cue: any) => {
cue.snapToLines = false;
cue.line = line;
cue.size = 100; // This solves some Android issue.
});
// Delete listener.
track.oncuechange = null;
};
}
};
}
/**
* Get the URL to use in the element. E.g. if the file is already downloaded it will return the local URL.
*
* @param targetAttr Attribute to modify.
* @param url Original URL to treat.
* @param site Site.
* @return Promise resolved with the URL.
*/
protected async getUrlToUse(targetAttr: string, url: string, site: CoreSite): Promise<string> {
const tagName = this.element.tagName;
let finalUrl: string;
// Download images, tracks and posters if size is unknown.
const downloadUnknown = tagName == 'IMG' || tagName == 'TRACK' || targetAttr == 'poster';
if (targetAttr === 'src' && tagName !== 'SOURCE' && tagName !== 'TRACK' && tagName !== 'VIDEO' && tagName !== 'AUDIO') {
finalUrl = await CoreFilepool.getSrcByUrl(
site.getId(),
url,
this.component,
this.componentId,
0,
true,
downloadUnknown,
);
} else if (tagName === 'TRACK') {
// Download tracks right away. Using an online URL for tracks can give a CORS error in Android.
finalUrl = await CoreFilepool.downloadUrl(site.getId(), url, false, this.component, this.componentId);
finalUrl = CoreFile.convertFileSrc(finalUrl);
} else {
finalUrl = await CoreFilepool.getUrlByUrl(
site.getId(),
url,
this.component,
this.componentId,
0,
true,
downloadUnknown,
);
finalUrl = CoreFile.convertFileSrc(finalUrl);
}
if (!CoreUrlUtils.isLocalFileUrl(finalUrl) && !finalUrl.includes('#') && tagName !== 'A') {
/* In iOS, if we use the same URL in embedded file and background download then the download only
downloads a few bytes (cached ones). Add an anchor to the URL so both URLs are different.
Don't add this anchor if the URL already has an anchor, otherwise other anchors might not work.
The downloaded URL won't have anchors so the URLs will already be different. */
finalUrl = finalUrl + '#moodlemobile-embedded';
}
return finalUrl;
}
/**
* Set listeners if needed.
*
* @param targetAttr Attribute to modify.
* @param url Original URL to treat.
* @param site Site.
* @return Promise resolved when done.
*/
protected async setListeners(targetAttr: string, url: string, site: CoreSite): Promise<void> {
if (this.fileEventObserver) {
// Already listening to events.
return;
}
const tagName = this.element.tagName;
let state = await CoreFilepool.getFileStateByUrl(site.getId(), url);
// Listen for download changes in the file.
const eventName = await CoreFilepool.getFileEventNameByUrl(site.getId(), url);
this.fileEventObserver = CoreEvents.on(eventName, async () => {
const newState = await CoreFilepool.getFileStateByUrl(site.getId(), url);
if (newState === state) {
return;
}
state = newState;
if (state === CoreConstants.DOWNLOADING) {
return;
}
// The file state has changed. Handle the file again, maybe it's downloaded now or the file has been deleted.
this.checkAndHandleExternalContent();
});
// Set events to download big files (not downloaded automatically).
if (targetAttr !== 'poster' && (tagName === 'VIDEO' || tagName === 'AUDIO' || tagName === 'A' || tagName === 'SOURCE')) {
const eventName = tagName == 'A' ? 'click' : 'play';
let clickableEl = this.element;
if (tagName == 'SOURCE') {
clickableEl = <HTMLElement> CoreDomUtils.closest(this.element, 'video,audio');
if (!clickableEl) {
return;
}
}
clickableEl.addEventListener(eventName, () => {
// User played media or opened a downloadable link.
// Download the file if in wifi and it hasn't been downloaded already (for big files).
if (state !== CoreConstants.DOWNLOADED && state !== CoreConstants.DOWNLOADING && CoreApp.isWifi()) {
// We aren't using the result, so it doesn't matter which of the 2 functions we call.
CoreFilepool.getUrlByUrl(site.getId(), url, this.component, this.componentId, 0, false);
}
});
}
}
/**
* Wait for the image to be loaded or error, and emit an event when it happens.
*/
@ -374,4 +448,11 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges {
this.element.addEventListener('error', listener);
}
/**
* @inheritdoc
*/
ngOnDestroy(): void {
this.fileEventObserver?.off();
}
}

View File

@ -26,6 +26,7 @@ import { CoreConstants } from '@/core/constants';
import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper';
import { CoreCustomURLSchemes } from '@services/urlschemes';
import { DomSanitizer } from '@singletons';
import { CoreFilepool } from '@services/filepool';
/**
* Directive to open a link in external browser or in the app.
@ -218,6 +219,26 @@ export class CoreLinkDirective implements OnInit {
}
}
if (currentSite.isSitePluginFileUrl(href)) {
// It's a site file. Check if it's being downloaded right now.
const isDownloading = await CoreFilepool.isFileDownloadingByUrl(currentSite.getId(), href);
if (isDownloading) {
// Wait for the download to finish before opening the file to prevent downloading it twice.
const modal = await CoreDomUtils.showModalLoading();
try {
const path = await CoreFilepool.downloadUrl(currentSite.getId(), href);
return this.openLocalFile(path);
} catch {
// Error downloading, just open the original URL.
} finally {
modal.dismiss();
}
}
}
if (this.autoLogin == 'yes') {
if (this.inApp) {
await currentSite.openInAppWithAutoLogin(href);

View File

@ -457,13 +457,10 @@ export class CoreCourseModulePrefetchDelegateService extends CoreDelegate<CoreCo
size += fileSize;
} catch {
// Error getting size. Check if the file is being downloaded.
try {
await CoreFilepool.isFileDownloadingByUrl(siteId, CoreFileHelper.getFileUrl(file));
const isDownloading = await CoreFilepool.isFileDownloadingByUrl(siteId, CoreFileHelper.getFileUrl(file));
if (isDownloading) {
// If downloading, count as downloaded.
size += file.filesize || 0;
} catch {
// Not downloading and not found in disk, don't add any size
}
}
}));

View File

@ -231,7 +231,7 @@ export class CoreSettingsHelperProvider {
* @return Sync promise or null if site is not being syncrhonized.
*/
getSiteSyncPromise(siteId: string): Promise<void> | void {
if (this.syncPromises[siteId]) {
if (this.syncPromises[siteId] !== undefined) {
return this.syncPromises[siteId];
}
}
@ -244,7 +244,7 @@ export class CoreSettingsHelperProvider {
* @return Promise resolved when synchronized, rejected if failure.
*/
async synchronizeSite(syncOnlyOnWifi: boolean, siteId: string): Promise<void> {
if (this.syncPromises[siteId]) {
if (this.syncPromises[siteId] !== undefined) {
// There's already a sync ongoing for this site, return the promise.
return this.syncPromises[siteId];
}

View File

@ -559,10 +559,19 @@ export class CoreFilepoolProvider {
async clearFilepool(siteId: string): Promise<void> {
const db = await CoreSites.getSiteDb(siteId);
// Read the data first to be able to notify the deletions.
const filesEntries = await db.getAllRecords<CoreFilepoolFileEntry>(FILES_TABLE_NAME);
const filesLinks = await db.getAllRecords<CoreFilepoolLinksRecord>(LINKS_TABLE_NAME);
await Promise.all([
db.deleteRecords(FILES_TABLE_NAME),
db.deleteRecords(LINKS_TABLE_NAME),
]);
// Notify now.
const filesLinksMap = CoreUtils.arrayToObjectMultiple(filesLinks, 'fileId');
filesEntries.forEach(entry => this.notifyFileDeleted(siteId, entry.fileId, filesLinksMap[entry.fileId] || []));
}
/**
@ -2271,13 +2280,19 @@ export class CoreFilepoolProvider {
*
* @param siteId The site ID.
* @param fileUrl File URL.
* @param Promise resolved if file is downloading, rejected otherwise.
* @param Promise resolved with boolean: whether the file is downloading.
*/
async isFileDownloadingByUrl(siteId: string, fileUrl: string): Promise<void> {
async isFileDownloadingByUrl(siteId: string, fileUrl: string): Promise<boolean> {
const file = await this.fixPluginfileURL(siteId, fileUrl);
const fileId = this.getFileIdByUrl(CoreFileHelper.getFileUrl(file));
await this.hasFileInQueue(siteId, fileId);
try {
await this.hasFileInQueue(siteId, fileId);
return true;
} catch {
return false;
}
}
/**

View File

@ -8,6 +8,7 @@ information provided here is intended especially for developers.
Now you have to pass all items and 3 optional params have been added.
- CoreCourseModulePrefetchDelegate.getPrefetchHandlerFor now admits module name instead of full module object.
- CoreCourse.getModuleBasicInfoByInstance and CoreCourse.getModuleBasicInfo have been modified to accept an "options" parameter instead of only siteId.
- The function CoreFilepool.isFileDownloadingByUrl now returns Promise<boolean> instead of relying on resolve/reject.
=== 3.9.5 ===