MOBILE-3897 external-content: Update URL when state changes

main
Dani Palou 2021-12-23 10:30:34 +01:00
parent 9f99b78ccf
commit 43ce384da8
3 changed files with 184 additions and 94 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('#')) {
/* 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

@ -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] || []));
}
/**