diff --git a/src/core/directives/external-content.ts b/src/core/directives/external-content.ts index 4d84f6845..d3e157dcc 100644 --- a/src/core/directives/external-content.ts +++ b/src/core/directives/external-content.ts @@ -266,9 +266,11 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges { finalUrl = CoreFile.convertFileSrc(finalUrl); } - if (!CoreUrlUtils.isLocalFileUrl(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 a hash to the URL so both URLs are different. */ + 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'; } diff --git a/src/core/directives/format-text.ts b/src/core/directives/format-text.ts index 22b5d3e4a..4869c50b8 100644 --- a/src/core/directives/format-text.ts +++ b/src/core/directives/format-text.ts @@ -555,7 +555,7 @@ export class CoreFormatTextDirective implements OnChanges { }); videos.forEach((video) => { - this.treatMedia(video); + this.treatMedia(video, true); }); iframes.forEach((iframe) => { @@ -673,8 +673,9 @@ export class CoreFormatTextDirective implements OnChanges { * Add media adapt class and apply CoreExternalContentDirective to the media element and its sources and tracks. * * @param element Video or audio to treat. + * @param isVideo Whether it's a video. */ - protected treatMedia(element: HTMLElement): void { + protected treatMedia(element: HTMLElement, isVideo: boolean = false): void { this.addMediaAdaptClass(element); this.addExternalContent(element); @@ -692,8 +693,16 @@ export class CoreFormatTextDirective implements OnChanges { const sources = Array.from(element.querySelectorAll('source')); const tracks = Array.from(element.querySelectorAll('track')); + const hasPoster = isVideo && !!element.getAttribute('poster'); + + if (isVideo && !hasPoster) { + this.fixVideoSrcPlaceholder(element); + } sources.forEach((source) => { + if (isVideo && !hasPoster) { + this.fixVideoSrcPlaceholder(source); + } source.setAttribute('target-src', source.getAttribute('src') || ''); source.removeAttribute('src'); this.addExternalContent(source); @@ -709,6 +718,24 @@ export class CoreFormatTextDirective implements OnChanges { }); } + /** + * Try to fix the placeholder displayed when a video doesn't have a poster. + * + * @param element Element to fix. + */ + protected fixVideoSrcPlaceholder(element: HTMLElement): void { + const src = element.getAttribute('src'); + if (!src) { + return; + } + + if (src.match(/#t=\d/)) { + return; + } + + element.setAttribute('src', src + '#t=0.001'); + } + /** * Add media adapt class and treat the iframe source. * diff --git a/src/core/services/filepool.ts b/src/core/services/filepool.ts index 042558991..98efbef5f 100644 --- a/src/core/services/filepool.ts +++ b/src/core/services/filepool.ts @@ -47,6 +47,7 @@ import { CoreFilepoolQueueDBEntry, } from '@services/database/filepool'; import { CoreFileHelper } from './file-helper'; +import { CoreUrl } from '@singletons/url'; /* * Factory for handling downloading files and retrieve downloaded files. @@ -540,8 +541,12 @@ export class CoreFilepoolProvider { await site.getDb().deleteRecords(PACKAGES_TABLE_NAME); entries.forEach((entry) => { + if (!entry.component) { + return; + } + // Trigger module status changed, setting it as not downloaded. - this.triggerPackageStatusChanged(siteId, CoreConstants.NOT_DOWNLOADED, entry.component!, entry.componentId); + this.triggerPackageStatusChanged(siteId, CoreConstants.NOT_DOWNLOADED, entry.component, entry.componentId); }); } @@ -668,6 +673,13 @@ export class CoreFilepoolProvider { poolFileObject?: CoreFilepoolFileEntry, ): Promise { const fileId = this.getFileIdByUrl(fileUrl); + + // Extract the anchor from the URL (if any). + const anchor = CoreUrl.getUrlAnchor(fileUrl); + if (anchor) { + fileUrl = fileUrl.replace(anchor, ''); + } + const extension = CoreMimetypeUtils.guessExtensionFromUrl(fileUrl); const addExtension = typeof filePath == 'undefined'; const path = filePath || (await this.getFilePath(siteId, fileId, extension)); @@ -680,7 +692,7 @@ export class CoreFilepoolProvider { const downloadId = this.getFileDownloadId(fileUrl, path); - if (this.filePromises[siteId] && this.filePromises[siteId][downloadId]) { + if (this.filePromises[siteId] && this.filePromises[siteId][downloadId] !== undefined) { // There's already a download ongoing for this file in this location, return the promise. return this.filePromises[siteId][downloadId]; } else if (!this.filePromises[siteId]) { @@ -708,7 +720,8 @@ export class CoreFilepoolProvider { extension: fileEntry.extension, }); - return fileEntry.toURL(); + // Add the anchor again to the local URL. + return fileEntry.toURL() + (anchor || ''); }).finally(() => { // Download finished, delete the promise. delete this.filePromises[siteId][downloadId]; @@ -753,11 +766,11 @@ export class CoreFilepoolProvider { if (dirPath) { // Calculate the path to the file. - path = file.filename; + path = file.filename || ''; if (file.filepath && file.filepath !== '/') { path = file.filepath.substr(1) + path; } - path = CoreTextUtils.concatenatePaths(dirPath, path!); + path = CoreTextUtils.concatenatePaths(dirPath, path); } if (prefetch) { @@ -806,7 +819,7 @@ export class CoreFilepoolProvider { ): Promise { const packageId = this.getPackageId(component, componentId); - if (this.packagesPromises[siteId] && this.packagesPromises[siteId][packageId]) { + if (this.packagesPromises[siteId] && this.packagesPromises[siteId][packageId] !== undefined) { // There's already a download ongoing for this package, return the promise. return this.packagesPromises[siteId][packageId]; } else if (!this.packagesPromises[siteId]) { @@ -847,11 +860,11 @@ export class CoreFilepoolProvider { if (dirPath) { // Calculate the path to the file. - path = file.filename; + path = file.filename || ''; if (file.filepath && file.filepath !== '/') { path = file.filepath.substr(1) + path; } - path = CoreTextUtils.concatenatePaths(dirPath, path!); + path = CoreTextUtils.concatenatePaths(dirPath, path); } if (prefetch) { @@ -1011,7 +1024,10 @@ export class CoreFilepoolProvider { url = await this.getInternalUrlById(siteId, fileId); } - return finishSuccessfulDownload(url); + // Add the anchor to the local URL if any. + const anchor = CoreUrl.getUrlAnchor(fileUrl); + + return finishSuccessfulDownload(url + (anchor || '')); } catch (error) { // The file is not downloaded or it's outdated. this.notifyFileDownloading(siteId, fileId, links); @@ -1280,6 +1296,9 @@ export class CoreFilepoolProvider { }); } + // Remove the anchor. + url = CoreUrl.removeUrlAnchor(url); + // Try to guess the filename the target file should have. // We want to keep the original file name so people can easily identify the files after the download. const filename = this.guessFilenameFromUrl(url); @@ -1442,7 +1461,7 @@ export class CoreFilepoolProvider { return CoreConstants.NOT_DOWNLOADABLE; } - fileUrl = CoreFileHelper.getFileUrl(file); + fileUrl = CoreUrl.removeUrlAnchor(CoreFileHelper.getFileUrl(file)); timemodified = file.timemodified || timemodified; revision = revision || this.getRevisionFromUrl(fileUrl); const fileId = this.getFileIdByUrl(fileUrl); @@ -1459,7 +1478,7 @@ export class CoreFilepoolProvider { const downloadId = this.getFileDownloadId(fileUrl, filePath); - if (this.filePromises[siteId] && this.filePromises[siteId][downloadId]) { + if (this.filePromises[siteId] && this.filePromises[siteId][downloadId] !== undefined) { return CoreConstants.DOWNLOADING; } @@ -1552,11 +1571,14 @@ export class CoreFilepoolProvider { try { // We found the file entry, now look for the file on disk. - if (mode === 'src') { - return await this.getInternalSrcById(siteId, fileId); - } else { - return await this.getInternalUrlById(siteId, fileId); - } + const path = mode === 'src' ? + await this.getInternalSrcById(siteId, fileId) : + await this.getInternalUrlById(siteId, fileId); + + // Add the anchor to the local URL if any. + const anchor = CoreUrl.getUrlAnchor(fileUrl); + + return path + (anchor || ''); } catch (error) { // The file is not on disk. // We could not retrieve the file, delete the entries associated with that ID. @@ -1730,7 +1752,7 @@ export class CoreFilepoolProvider { */ getPackageDownloadPromise(siteId: string, component: string, componentId?: string | number): Promise | undefined { const packageId = this.getPackageId(component, componentId); - if (this.packagesPromises[siteId] && this.packagesPromises[siteId][packageId]) { + if (this.packagesPromises[siteId] && this.packagesPromises[siteId][packageId] !== undefined) { return this.packagesPromises[siteId][packageId]; } } @@ -2745,7 +2767,7 @@ export class CoreFilepoolProvider { await site.getDb().updateRecords(PACKAGES_TABLE_NAME, newData, { id: packageId }); // Success updating, trigger event. - this.triggerPackageStatusChanged(site.id!, newData.status, component, componentId); + this.triggerPackageStatusChanged(site.getId(), newData.status, component, componentId); return newData.status; } diff --git a/src/core/services/utils/mimetype.ts b/src/core/services/utils/mimetype.ts index 78ea7322f..ddf65901d 100644 --- a/src/core/services/utils/mimetype.ts +++ b/src/core/services/utils/mimetype.ts @@ -313,6 +313,11 @@ export class CoreMimetypeUtilsProvider { if (position > -1) { candidate = candidate.substr(0, position); } + // Remove anchor if any. + position = candidate.indexOf('#'); + if (position > -1) { + candidate = candidate.substr(0, position); + } if (EXTENSION_REGEX.test(candidate)) { extension = candidate; diff --git a/src/core/services/utils/url.ts b/src/core/services/utils/url.ts index 2f7cd0736..79aafb51c 100644 --- a/src/core/services/utils/url.ts +++ b/src/core/services/utils/url.ts @@ -58,6 +58,10 @@ export class CoreUrlUtilsProvider { * @return URL with params. */ addParamsToUrl(url: string, params?: Record, anchor?: string, boolToNumber?: boolean): string { + // Remove any existing anchor to add the params before it. + const urlAndAnchor = url.split('#'); + url = urlAndAnchor[0]; + let separator = url.indexOf('?') != -1 ? '&' : '?'; for (const key in params) { @@ -75,6 +79,15 @@ export class CoreUrlUtilsProvider { } } + // Re-add the anchor if any. + if (urlAndAnchor.length > 1) { + // Remove the URL from the array. + urlAndAnchor.shift(); + + // Use a join in case there is more than one #. + url += '#' + urlAndAnchor.join('#'); + } + if (anchor) { url += '#' + anchor; } diff --git a/src/core/singletons/url.ts b/src/core/singletons/url.ts index 5c8b1f10f..097f05155 100644 --- a/src/core/singletons/url.ts +++ b/src/core/singletons/url.ts @@ -203,4 +203,32 @@ export class CoreUrl { && CoreText.removeEndingSlash(partsA?.path) === CoreText.removeEndingSlash(partsB?.path); } + /** + * Get the anchor of a URL. If there's more than one they'll all be returned, separated by #. + * E.g. myurl.com#foo=1#bar=2 will return #foo=1#bar=2. + * + * @param url URL. + * @return Anchor, undefined if no anchor. + */ + static getUrlAnchor(url: string): string | undefined { + const firstAnchorIndex = url.indexOf('#'); + if (firstAnchorIndex === -1) { + return; + } + + return url.substr(firstAnchorIndex); + } + + /** + * Remove the anchor from a URL. + * + * @param url URL. + * @return URL without anchor if any. + */ + static removeUrlAnchor(url: string): string { + const urlAndAnchor = url.split('#'); + + return urlAndAnchor[0]; + } + }