MOBILE-3897 external-content: Update URL when state changes
parent
9f99b78ccf
commit
43ce384da8
|
@ -12,8 +12,17 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// 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 { CoreApp } from '@services/app';
|
||||||
import { CoreFile } from '@services/file';
|
import { CoreFile } from '@services/file';
|
||||||
import { CoreFilepool } from '@services/filepool';
|
import { CoreFilepool } from '@services/filepool';
|
||||||
|
@ -24,6 +33,9 @@ import { CoreUtils } from '@services/utils/utils';
|
||||||
import { Platform } from '@singletons';
|
import { Platform } from '@singletons';
|
||||||
import { CoreLogger } from '@singletons/logger';
|
import { CoreLogger } from '@singletons/logger';
|
||||||
import { CoreError } from '@classes/errors/error';
|
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.
|
* Directive to handle external content.
|
||||||
|
@ -38,7 +50,7 @@ import { CoreError } from '@classes/errors/error';
|
||||||
@Directive({
|
@Directive({
|
||||||
selector: '[core-external-content]',
|
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() siteId?: string; // Site ID to use.
|
||||||
@Input() component?: string; // Component to link the file to.
|
@Input() component?: string; // Component to link the file to.
|
||||||
|
@ -54,6 +66,7 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges {
|
||||||
protected element: Element;
|
protected element: Element;
|
||||||
protected logger: CoreLogger;
|
protected logger: CoreLogger;
|
||||||
protected initialized = false;
|
protected initialized = false;
|
||||||
|
protected fileEventObserver?: CoreEventObserver;
|
||||||
|
|
||||||
constructor(element: ElementRef) {
|
constructor(element: ElementRef) {
|
||||||
|
|
||||||
|
@ -183,34 +196,8 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges {
|
||||||
protected async handleExternalContent(targetAttr: string, url: string, siteId?: string): Promise<void> {
|
protected async handleExternalContent(targetAttr: string, url: string, siteId?: string): Promise<void> {
|
||||||
|
|
||||||
const tagName = this.element.tagName;
|
const tagName = this.element.tagName;
|
||||||
|
|
||||||
if (tagName == 'VIDEO' && targetAttr != 'poster') {
|
if (tagName == 'VIDEO' && targetAttr != 'poster') {
|
||||||
const video = <HTMLVideoElement> this.element;
|
this.handleVideoSubtitles(<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;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const site = await CoreSites.getSite(siteId);
|
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.');
|
throw new CoreError('Site doesn\'t allow downloading files.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download images, tracks and posters if size is unknown.
|
const finalUrl = await this.getUrlToUse(targetAttr, url, site);
|
||||||
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';
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.debug('Using URL ' + finalUrl + ' for ' + url);
|
this.logger.debug('Using URL ' + finalUrl + ' for ' + url);
|
||||||
if (tagName === 'SOURCE') {
|
if (tagName === 'SOURCE') {
|
||||||
|
@ -295,28 +243,7 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges {
|
||||||
this.element.setAttribute('data-original-' + targetAttr, url);
|
this.element.setAttribute('data-original-' + targetAttr, url);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set events to download big files (not downloaded automatically).
|
this.setListeners(targetAttr, url, site);
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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.
|
* 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);
|
this.element.addEventListener('error', listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.fileEventObserver?.off();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -231,7 +231,7 @@ export class CoreSettingsHelperProvider {
|
||||||
* @return Sync promise or null if site is not being syncrhonized.
|
* @return Sync promise or null if site is not being syncrhonized.
|
||||||
*/
|
*/
|
||||||
getSiteSyncPromise(siteId: string): Promise<void> | void {
|
getSiteSyncPromise(siteId: string): Promise<void> | void {
|
||||||
if (this.syncPromises[siteId]) {
|
if (this.syncPromises[siteId] !== undefined) {
|
||||||
return this.syncPromises[siteId];
|
return this.syncPromises[siteId];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -244,7 +244,7 @@ export class CoreSettingsHelperProvider {
|
||||||
* @return Promise resolved when synchronized, rejected if failure.
|
* @return Promise resolved when synchronized, rejected if failure.
|
||||||
*/
|
*/
|
||||||
async synchronizeSite(syncOnlyOnWifi: boolean, siteId: string): Promise<void> {
|
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.
|
// There's already a sync ongoing for this site, return the promise.
|
||||||
return this.syncPromises[siteId];
|
return this.syncPromises[siteId];
|
||||||
}
|
}
|
||||||
|
|
|
@ -559,10 +559,19 @@ export class CoreFilepoolProvider {
|
||||||
async clearFilepool(siteId: string): Promise<void> {
|
async clearFilepool(siteId: string): Promise<void> {
|
||||||
const db = await CoreSites.getSiteDb(siteId);
|
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([
|
await Promise.all([
|
||||||
db.deleteRecords(FILES_TABLE_NAME),
|
db.deleteRecords(FILES_TABLE_NAME),
|
||||||
db.deleteRecords(LINKS_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] || []));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue