commit
						af24a778ab
					
				| @ -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(); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -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); | ||||
|  | ||||
| @ -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
 | ||||
|                         } | ||||
|                     } | ||||
|                 })); | ||||
|  | ||||
| @ -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]; | ||||
|         } | ||||
|  | ||||
| @ -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; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -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 === | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user