diff --git a/package.json b/package.json index c5cbebe2e..31f1ad798 100644 --- a/package.json +++ b/package.json @@ -33,141 +33,141 @@ "ionic:build:before": "gulp" }, "dependencies": { - "@angular/animations": "^11.0.1", - "@angular/common": "~10.0.0", - "@angular/core": "~10.0.0", - "@angular/forms": "~10.0.0", - "@angular/platform-browser": "~10.0.0", - "@angular/platform-browser-dynamic": "~10.0.0", - "@angular/router": "~10.0.0", - "@ionic-native/badge": "^5.33.0", - "@ionic-native/camera": "^5.33.0", - "@ionic-native/chooser": "^5.33.0", - "@ionic-native/clipboard": "^5.33.0", - "@ionic-native/core": "^5.33.0", - "@ionic-native/device": "^5.33.0", - "@ionic-native/diagnostic": "^5.33.0", - "@ionic-native/file": "^5.33.0", - "@ionic-native/file-opener": "^5.33.0", - "@ionic-native/file-transfer": "^5.33.0", - "@ionic-native/geolocation": "^5.33.0", - "@ionic-native/http": "^5.33.0", - "@ionic-native/in-app-browser": "^5.33.0", - "@ionic-native/ionic-webview": "^5.33.0", - "@ionic-native/keyboard": "^5.33.0", - "@ionic-native/local-notifications": "^5.33.0", - "@ionic-native/media": "^5.33.0", - "@ionic-native/media-capture": "^5.33.0", - "@ionic-native/network": "^5.33.0", - "@ionic-native/push": "^5.33.0", - "@ionic-native/qr-scanner": "^5.33.0", - "@ionic-native/splash-screen": "^5.33.0", - "@ionic-native/sqlite": "^5.33.0", - "@ionic-native/status-bar": "^5.33.0", - "@ionic-native/web-intent": "^5.33.0", - "@ionic-native/zip": "^5.33.0", - "@ionic/angular": "^5.6.6", - "@ngx-translate/core": "^13.0.0", - "@ngx-translate/http-loader": "^6.0.0", - "@types/chart.js": "^2.9.31", + "@angular/animations": "11.0.1", + "@angular/common": "10.0.14", + "@angular/core": "10.0.14", + "@angular/forms": "10.0.14", + "@angular/platform-browser": "10.0.14", + "@angular/platform-browser-dynamic": "10.0.14", + "@angular/router": "10.0.14", + "@ionic-native/badge": "5.33.0", + "@ionic-native/camera": "5.33.0", + "@ionic-native/chooser": "5.33.0", + "@ionic-native/clipboard": "5.33.0", + "@ionic-native/core": "5.33.0", + "@ionic-native/device": "5.33.0", + "@ionic-native/diagnostic": "5.33.0", + "@ionic-native/file": "5.33.0", + "@ionic-native/file-opener": "5.33.0", + "@ionic-native/file-transfer": "5.33.0", + "@ionic-native/geolocation": "5.33.0", + "@ionic-native/http": "5.33.0", + "@ionic-native/in-app-browser": "5.33.0", + "@ionic-native/ionic-webview": "5.33.0", + "@ionic-native/keyboard": "5.33.0", + "@ionic-native/local-notifications": "5.33.0", + "@ionic-native/media": "5.33.0", + "@ionic-native/media-capture": "5.33.0", + "@ionic-native/network": "5.33.0", + "@ionic-native/push": "5.33.0", + "@ionic-native/qr-scanner": "5.33.0", + "@ionic-native/splash-screen": "5.33.0", + "@ionic-native/sqlite": "5.33.0", + "@ionic-native/status-bar": "5.33.0", + "@ionic-native/web-intent": "5.33.0", + "@ionic-native/zip": "5.33.0", + "@ionic/angular": "5.6.6", + "@ngx-translate/core": "13.0.0", + "@ngx-translate/http-loader": "6.0.0", + "@types/chart.js": "2.9.31", "@types/cordova": "0.0.34", - "@types/cordova-plugin-file-transfer": "^1.6.2", - "@types/dom-mediacapture-record": "^1.0.7", - "chart.js": "^2.9.4", - "com-darryncampbell-cordova-plugin-intent": "^1.3.0", - "cordova": "^10.0.0", - "cordova-android": "^9.1.0", - "cordova-android-support-gradle-release": "^3.0.1", - "cordova-clipboard": "^1.3.0", - "cordova-ios": "^6.2.0", - "cordova-plugin-add-swift-support": "^2.0.2", - "cordova-plugin-advanced-http": "^3.1.0", - "cordova-plugin-badge": "^0.8.8", - "cordova-plugin-camera": "^5.0.1", - "cordova-plugin-chooser": "^1.3.2", - "cordova-plugin-customurlscheme": "^5.0.2", - "cordova-plugin-device": "^2.0.3", - "cordova-plugin-file": "^6.0.2", - "cordova-plugin-file-opener2": "^3.0.5", + "@types/cordova-plugin-file-transfer": "1.6.2", + "@types/dom-mediacapture-record": "1.0.7", + "chart.js": "2.9.4", + "com-darryncampbell-cordova-plugin-intent": "1.3.0", + "cordova": "10.0.0", + "cordova-android": "9.1.0", + "cordova-android-support-gradle-release": "3.0.1", + "cordova-clipboard": "1.3.0", + "cordova-ios": "6.2.0", + "cordova-plugin-add-swift-support": "2.0.2", + "cordova-plugin-advanced-http": "3.1.0", + "cordova-plugin-badge": "0.8.8", + "cordova-plugin-camera": "5.0.1", + "cordova-plugin-chooser": "1.3.2", + "cordova-plugin-customurlscheme": "5.0.2", + "cordova-plugin-device": "2.0.3", + "cordova-plugin-file": "6.0.2", + "cordova-plugin-file-opener2": "3.0.5", "cordova-plugin-file-transfer": "git+https://github.com/moodlemobile/cordova-plugin-file-transfer.git", - "cordova-plugin-geolocation": "^4.1.0", - "cordova-plugin-globalization": "^1.11.0", - "cordova-plugin-inappbrowser": "^5.0.0", - "cordova-plugin-ionic-keyboard": "^2.2.0", - "cordova-plugin-ionic-webview": "^5.0.0", + "cordova-plugin-geolocation": "4.1.0", + "cordova-plugin-globalization": "1.11.0", + "cordova-plugin-inappbrowser": "5.0.0", + "cordova-plugin-ionic-keyboard": "2.2.0", + "cordova-plugin-ionic-webview": "5.0.0", "cordova-plugin-local-notification": "git+https://github.com/moodlemobile/cordova-plugin-local-notification.git#moodle", - "cordova-plugin-media": "^5.0.3", - "cordova-plugin-media-capture": "^3.0.3", - "cordova-plugin-network-information": "^2.0.2", + "cordova-plugin-media": "5.0.3", + "cordova-plugin-media-capture": "3.0.3", + "cordova-plugin-network-information": "2.0.2", "cordova-plugin-qrscanner": "git+https://github.com/moodlemobile/cordova-plugin-qrscanner.git#dist", - "cordova-plugin-screen-orientation": "^3.0.2", - "cordova-plugin-splashscreen": "^6.0.0", - "cordova-plugin-statusbar": "^2.4.3", - "cordova-plugin-whitelist": "^1.3.4", + "cordova-plugin-screen-orientation": "3.0.2", + "cordova-plugin-splashscreen": "6.0.0", + "cordova-plugin-statusbar": "2.4.3", + "cordova-plugin-whitelist": "1.3.4", "cordova-plugin-wkuserscript": "git+https://github.com/moodlemobile/cordova-plugin-wkuserscript.git", "cordova-plugin-wkwebview-cookies": "git+https://github.com/moodlemobile/cordova-plugin-wkwebview-cookies.git", - "cordova-plugin-zip": "^3.1.0", - "cordova-sqlite-storage": "^6.0.0", - "cordova-support-google-services": "^1.2.1", - "cordova.plugins.diagnostic": "^5.0.2", - "core-js": "^3.9.1", - "es6-promise-plugin": "^4.2.2", - "jszip": "^3.5.0", + "cordova-plugin-zip": "3.1.0", + "cordova-sqlite-storage": "6.0.0", + "cordova-support-google-services": "1.3.2", + "cordova.plugins.diagnostic": "5.0.2", + "core-js": "3.9.1", + "es6-promise-plugin": "4.2.2", + "jszip": "3.5.0", "mathjax": "2.7.7", - "moment": "^2.29.0", - "nl.kingsquare.cordova.background-audio": "^1.0.1", - "phonegap-plugin-multidex": "^1.0.0", + "moment": "2.29.0", + "nl.kingsquare.cordova.background-audio": "1.0.1", + "phonegap-plugin-multidex": "1.0.0", "phonegap-plugin-push": "git+https://github.com/moodlemobile/phonegap-plugin-push.git#moodle-v3", - "rxjs": "~6.5.5", - "ts-md5": "^1.2.7", - "tslib": "^2.0.0", - "zone.js": "~0.10.3" + "rxjs": "6.5.5", + "ts-md5": "1.2.7", + "tslib": "2.0.1", + "zone.js": "0.10.3" }, "devDependencies": { - "@angular-devkit/architect": "^0.1101.2", - "@angular-devkit/build-angular": "~0.1000.0", - "@angular-eslint/builder": "^4.2.0", - "@angular-eslint/eslint-plugin": "^4.2.0", - "@angular-eslint/eslint-plugin-template": "^4.2.0", - "@angular-eslint/schematics": "^4.2.0", - "@angular-eslint/template-parser": "^4.2.0", - "@angular/cli": "~10.0.5", - "@angular/compiler": "~10.0.0", - "@angular/compiler-cli": "~10.0.0", - "@angular/language-service": "~10.0.0", - "@ionic/angular-toolkit": "^2.3.0", - "@ionic/cli": "^6.14.1", - "@types/faker": "^5.1.3", - "@types/node": "^12.12.64", - "@types/resize-observer-browser": "^0.1.5", - "@types/webpack-env": "^1.16.0", - "@typescript-eslint/eslint-plugin": "^4.22.0", - "@typescript-eslint/parser": "^4.22.0", - "check-es-compat": "^1.1.1", + "@angular-devkit/architect": "0.1101.2", + "@angular-devkit/build-angular": "0.1000.8", + "@angular-eslint/builder": "4.2.0", + "@angular-eslint/eslint-plugin": "4.2.0", + "@angular-eslint/eslint-plugin-template": "4.2.0", + "@angular-eslint/schematics": "4.2.0", + "@angular-eslint/template-parser": "4.2.0", + "@angular/cli": "10.0.8", + "@angular/compiler": "10.0.14", + "@angular/compiler-cli": "10.0.14", + "@angular/language-service": "10.0.14", + "@ionic/angular-toolkit": "2.3.3", + "@ionic/cli": "6.14.1", + "@types/faker": "5.1.3", + "@types/node": "12.12.64", + "@types/resize-observer-browser": "0.1.5", + "@types/webpack-env": "1.16.0", + "@typescript-eslint/eslint-plugin": "4.22.0", + "@typescript-eslint/parser": "4.22.0", + "check-es-compat": "1.1.1", "cordova-plugin-prevent-override": "git+https://github.com/moodlemobile/cordova-plugin-prevent-override.git", - "eslint": "^7.25.0", - "eslint-config-prettier": "^8.3.0", - "eslint-plugin-header": "^3.1.1", - "eslint-plugin-import": "^2.22.1", - "eslint-plugin-jest": "^24.3.6", - "eslint-plugin-jsdoc": "^32.3.3", - "eslint-plugin-prefer-arrow": "^1.2.3", - "eslint-plugin-promise": "^5.1.0", - "faker": "^5.1.0", - "fs-extra": "^9.1.0", + "eslint": "7.25.0", + "eslint-config-prettier": "8.3.0", + "eslint-plugin-header": "3.1.1", + "eslint-plugin-import": "2.22.1", + "eslint-plugin-jest": "24.3.6", + "eslint-plugin-jsdoc": "32.3.3", + "eslint-plugin-prefer-arrow": "1.2.3", + "eslint-plugin-promise": "5.1.0", + "faker": "5.1.0", + "fs-extra": "9.1.0", "gulp": "4.0.2", - "gulp-clip-empty-files": "^0.1.2", - "gulp-concat": "^2.6.1", - "gulp-flatten": "^0.4.0", - "gulp-htmlmin": "^5.0.1", - "gulp-rename": "^2.0.0", - "gulp-slash": "^1.1.3", - "jest": "^26.5.0", - "jest-preset-angular": "^8.3.1", - "jsonc-parser": "^2.3.1", - "ts-jest": "^26.4.1", - "ts-node": "~8.3.0", - "typescript": "^3.9.9" + "gulp-clip-empty-files": "0.1.2", + "gulp-concat": "2.6.1", + "gulp-flatten": "0.4.0", + "gulp-htmlmin": "5.0.1", + "gulp-rename": "2.0.0", + "gulp-slash": "1.1.3", + "jest": "26.5.2", + "jest-preset-angular": "8.3.1", + "jsonc-parser": "2.3.1", + "ts-jest": "26.4.1", + "ts-node": "8.3.0", + "typescript": "3.9.9" }, "engines": { "node": ">=12.x" @@ -236,6 +236,6 @@ } }, "optionalDependencies": { - "keytar": "^7.2.0" + "keytar": "7.2.0" } } diff --git a/scripts/langindex.json b/scripts/langindex.json index 79e24efe1..0816e51a6 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -1993,6 +1993,7 @@ "core.percentagenumber": "local_moodlemobileapp", "core.phone": "moodle", "core.pictureof": "moodle", + "core.play": "local_moodlemobileapp", "core.previous": "moodle", "core.proceed": "moodle", "core.pulltorefresh": "local_moodlemobileapp", diff --git a/src/addons/mod/folder/pages/index/index.page.ts b/src/addons/mod/folder/pages/index/index.page.ts index 7b7f3fce7..8bb594508 100644 --- a/src/addons/mod/folder/pages/index/index.page.ts +++ b/src/addons/mod/folder/pages/index/index.page.ts @@ -40,6 +40,8 @@ export class AddonModFolderIndexPage extends CoreCourseModuleMainActivityPage('folderInstance'); this.subfolder = CoreNavigator.getRouteParam('subfolder'); + + this.title = this.subfolder?.filename || this.module.name; } } diff --git a/src/addons/mod/forum/components/post/post.ts b/src/addons/mod/forum/components/post/post.ts index a9f5cc381..a7021eee4 100644 --- a/src/addons/mod/forum/components/post/post.ts +++ b/src/addons/mod/forum/components/post/post.ts @@ -259,6 +259,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges forum: this.forum, }, backdropDismiss: false, + cssClass: 'core-modal-fullscreen', }); if (!modalData) { diff --git a/src/addons/mod/resource/components/index/addon-mod-resource-index.html b/src/addons/mod/resource/components/index/addon-mod-resource-index.html index 1d66affb9..68d143323 100644 --- a/src/addons/mod/resource/components/index/addon-mod-resource-index.html +++ b/src/addons/mod/resource/components/index/addon-mod-resource-index.html @@ -47,11 +47,18 @@ - - {{ 'addon.mod_resource.openthefile' | translate }} + + + {{ 'core.play' | translate }} + + + + {{ 'addon.mod_resource.openthefile' | translate }} + - + {{ 'core.openwith' | translate }} diff --git a/src/addons/mod/resource/components/index/index.ts b/src/addons/mod/resource/components/index/index.ts index 1245be191..aca96abae 100644 --- a/src/addons/mod/resource/components/index/index.ts +++ b/src/addons/mod/resource/components/index/index.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit, Optional } from '@angular/core'; +import { Component, OnDestroy, OnInit, Optional } from '@angular/core'; import { CoreError } from '@classes/errors/error'; import { CoreCourseModuleMainResourceComponent, @@ -21,10 +21,13 @@ import { CoreCourseContentsPage } from '@features/course/pages/contents/contents import { CoreCourse, CoreCourseWSModule } from '@features/course/services/course'; import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate'; import { CoreApp } from '@services/app'; +import { CoreFileHelper } from '@services/file-helper'; import { CoreSites } from '@services/sites'; +import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreTextUtils } from '@services/utils/text'; import { CoreUtils, OpenFileAction } from '@services/utils/utils'; -import { Translate } from '@singletons'; +import { Network, NgZone, Translate } from '@singletons'; +import { Subscription } from 'rxjs'; import { AddonModResource, AddonModResourceCustomData, @@ -40,7 +43,7 @@ import { AddonModResourceHelper } from '../../services/resource-helper'; selector: 'addon-mod-resource-index', templateUrl: 'addon-mod-resource-index.html', }) -export class AddonModResourceIndexComponent extends CoreCourseModuleMainResourceComponent implements OnInit { +export class AddonModResourceIndexComponent extends CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy { component = AddonModResourceProvider.COMPONENT; @@ -52,19 +55,35 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource warning = ''; isIOS = false; openFileAction = OpenFileAction; + isOnline = false; + isStreamedFile = false; + shouldOpenInBrowser = false; + + protected onlineObserver?: Subscription; constructor(@Optional() courseContentsPage?: CoreCourseContentsPage) { super('AddonModResourceIndexComponent', courseContentsPage); } /** - * Component being initialized. + * @inheritdoc */ async ngOnInit(): Promise { super.ngOnInit(); this.canGetResource = AddonModResource.isGetResourceWSAvailable(); this.isIOS = CoreApp.isIOS(); + this.isOnline = CoreApp.isOnline(); + + if (this.isIOS) { + // Refresh online status when changes. + this.onlineObserver = Network.onChange().subscribe(() => { + // Execute the callback in the Angular zone, so change detection doesn't stop working. + NgZone.run(() => { + this.isOnline = CoreApp.isOnline(); + }); + }); + } await this.loadContent(); try { @@ -76,19 +95,14 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource } /** - * Perform the invalidate content function. - * - * @return Resolved when done. + * @inheritdoc */ protected async invalidateContent(): Promise { return AddonModResource.invalidateContent(this.module.id, this.courseId); } /** - * Download resource contents. - * - * @param refresh Whether we're refreshing data. - * @return Promise resolved when done. + * @inheritdoc */ protected async fetchContent(refresh?: boolean): Promise { // Load module contents if needed. Passing refresh is needed to force reloading contents. @@ -150,6 +164,14 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource } else { this.mode = 'external'; this.warning = ''; + + if (this.isIOS) { + this.shouldOpenInBrowser = CoreFileHelper.shouldOpenInBrowser(this.module.contents[0]); + } + + const mimetype = await CoreUtils.getMimeTypeFromUrl(CoreFileHelper.getFileUrl(this.module.contents[0])); + + this.isStreamedFile = CoreMimetypeUtils.isStreamedMimetype(mimetype); } } finally { this.fillContextMenu(refresh); @@ -179,4 +201,12 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource await CoreSites.getCurrentSite()?.openInBrowserWithAutoLoginIfSameSite(this.module.url!); } + /** + * @inheritdoc + */ + ngOnDestroy(): void { + super.ngOnDestroy(); + this.onlineObserver?.unsubscribe(); + } + } diff --git a/src/core/components/file/core-file.html b/src/core/components/file/core-file.html index e58497f65..7081fa1e1 100644 --- a/src/core/components/file/core-file.html +++ b/src/core/components/file/core-file.html @@ -12,7 +12,7 @@ [canTrustDownload]="!alwaysDownload" (action)="download()" size="small"> - diff --git a/src/core/components/file/file.ts b/src/core/components/file/file.ts index f33a1ee4c..a2b6b183d 100644 --- a/src/core/components/file/file.ts +++ b/src/core/components/file/file.ts @@ -144,10 +144,14 @@ export class CoreFileComponent implements OnInit, OnDestroy { /** * Convenience function to open a file, downloading it if needed. * + * @param ev Click event (if any). * @param isOpenButton Whether the open button was clicked. * @return Promise resolved when file is opened. */ - openFile(isOpenButton = false): Promise { + openFile(ev?: Event, isOpenButton = false): Promise { + ev?.preventDefault(); + ev?.stopPropagation(); + const options: CoreUtilsOpenFileOptions = {}; if (isOpenButton) { // Use the non-default method. diff --git a/src/core/features/compile/components/compile-html/compile-html.ts b/src/core/features/compile/components/compile-html/compile-html.ts index e3c72975e..f548647b1 100644 --- a/src/core/features/compile/components/compile-html/compile-html.ts +++ b/src/core/features/compile/components/compile-html/compile-html.ts @@ -117,7 +117,7 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck { // eslint-disable-next-line @angular-eslint/no-conflicting-lifecycle async ngOnChanges(changes: Record): Promise { // Only compile if text/javascript has changed or the forceCompile flag has been set to true. - if (this.text && (changes.text || changes.javascript || + if (this.text !== undefined && (changes.text || changes.javascript || (changes.forceCompile && CoreUtils.isTrueOrOne(this.forceCompile)))) { // Create a new component and a new module. diff --git a/src/core/features/course/services/course-helper.ts b/src/core/features/course/services/course-helper.ts index b72b4a201..64d1d832d 100644 --- a/src/core/features/course/services/course-helper.ts +++ b/src/core/features/course/services/course-helper.ts @@ -681,21 +681,29 @@ export class CoreCourseHelperProvider { throw new CoreError(Translate.instant('core.filenotfound')); } - if (!CoreFileHelper.isOpenableInApp(module.contents[0])) { + const mainFile = files[0]; + + if (!CoreFileHelper.isOpenableInApp(mainFile)) { await CoreFileHelper.showConfirmOpenUnsupportedFile(); } const site = await CoreSites.getSite(siteId); - const mainFile = files[0]; - // Check if the file should be opened in browser. if (CoreFileHelper.shouldOpenInBrowser(mainFile)) { - return this.openModuleFileInBrowser(mainFile.fileurl, site, module, courseId, component, componentId, files); + return this.openModuleFileInBrowser(mainFile.fileurl, site, module, courseId, component, componentId, files, options); } // File shouldn't be opened in browser. Download the module if it needs to be downloaded. - const result = await this.downloadModuleWithMainFileIfNeeded(module, courseId, component || '', componentId, files, siteId); + const result = await this.downloadModuleWithMainFileIfNeeded( + module, + courseId, + component || '', + componentId, + files, + siteId, + options, + ); if (CoreUrlUtils.isLocalFileUrl(result.path)) { return CoreUtils.openFile(result.path, options); @@ -740,6 +748,7 @@ export class CoreCourseHelperProvider { * @param component The component to link the files to. * @param componentId An ID to use in conjunction with the component. * @param files List of files of the module. If not provided, use module.contents. + * @param options Options to open the file. Only used if not opened in browser. * @return Resolved on success. */ protected async openModuleFileInBrowser( @@ -750,6 +759,7 @@ export class CoreCourseHelperProvider { component?: string, componentId?: string | number, files?: CoreCourseModuleContentFile[], + options: CoreUtilsOpenFileOptions = {}, ): Promise { if (!CoreApp.isOnline()) { // Not online, get the offline file. It will fail if not found. @@ -760,7 +770,7 @@ export class CoreCourseHelperProvider { throw new CoreNetworkError(); } - return CoreUtils.openFile(path); + return CoreUtils.openFile(path, options); } // Open in browser. @@ -791,6 +801,7 @@ export class CoreCourseHelperProvider { * @param componentId An ID to use in conjunction with the component. * @param files List of files of the module. If not provided, use module.contents. * @param siteId The site ID. If not defined, current site. + * @param options Options to open the file. * @return Promise resolved when done. */ async downloadModuleWithMainFileIfNeeded( @@ -800,6 +811,7 @@ export class CoreCourseHelperProvider { componentId?: string | number, files?: CoreCourseModuleContentFile[], siteId?: string, + options: CoreUtilsOpenFileOptions = {}, ): Promise<{ fixedUrl: string; path: string; status?: string }> { siteId = siteId || CoreSites.getCurrentSiteId(); @@ -840,7 +852,17 @@ export class CoreCourseHelperProvider { } if (!path) { - path = await this.downloadModuleWithMainFile(module, courseId, fixedUrl, files, status, component, componentId, siteId); + path = await this.downloadModuleWithMainFile( + module, + courseId, + fixedUrl, + files, + status, + component, + componentId, + siteId, + options, + ); } return { @@ -862,6 +884,7 @@ export class CoreCourseHelperProvider { * @param component The component to link the files to. * @param componentId An ID to use in conjunction with the component. * @param siteId The site ID. If not defined, current site. + * @param options Options to open the file. * @return Promise resolved when done. */ protected async downloadModuleWithMainFile( @@ -873,6 +896,7 @@ export class CoreCourseHelperProvider { component?: string, componentId?: string | number, siteId?: string, + options: CoreUtilsOpenFileOptions = {}, ): Promise { siteId = siteId || CoreSites.getCurrentSiteId(); @@ -885,7 +909,7 @@ export class CoreCourseHelperProvider { throw new CoreNetworkError(); } - const shouldDownloadFirst = await CoreFilepool.shouldDownloadFileBeforeOpen(fixedUrl, mainFile.filesize); + const shouldDownloadFirst = await CoreFilepool.shouldDownloadFileBeforeOpen(fixedUrl, mainFile.filesize, options); if (shouldDownloadFirst) { // Download and then return the local URL. diff --git a/src/core/features/siteplugins/classes/handlers/main-menu-home-handler.ts b/src/core/features/siteplugins/classes/handlers/main-menu-home-handler.ts index 1e2904b9d..7c0a6b4a9 100644 --- a/src/core/features/siteplugins/classes/handlers/main-menu-home-handler.ts +++ b/src/core/features/siteplugins/classes/handlers/main-menu-home-handler.ts @@ -46,7 +46,7 @@ export class CoreSitePluginsMainMenuHomeHandler extends CoreSitePluginsBaseHandl return { title: this.title, class: this.handlerSchema.displaydata?.class, - page: `siteplugins/${this.plugin.component}/${this.handlerSchema.method}/0`, + page: `siteplugins/homecontent/${this.plugin.component}/${this.handlerSchema.method}`, pageParams: { title: this.title, initResult: this.initResult, diff --git a/src/core/features/siteplugins/siteplugins.module.ts b/src/core/features/siteplugins/siteplugins.module.ts index 7f15ec693..aaf1c573d 100644 --- a/src/core/features/siteplugins/siteplugins.module.ts +++ b/src/core/features/siteplugins/siteplugins.module.ts @@ -28,6 +28,13 @@ const routes: Routes = [ }, ]; +const homeRoutes: Routes = [ + { + path: 'siteplugins/homecontent/:component/:method', + loadChildren: () => import('./pages/plugin-page/plugin-page.module').then( m => m.CoreSitePluginsPluginPageModule), + }, +]; + const courseIndexRoutes: Routes = [ { path: 'siteplugins/:handlerUniqueName', @@ -47,7 +54,7 @@ const moduleRoutes: Routes = [ imports: [ CoreMainMenuTabRoutingModule.forChild(moduleRoutes.concat(routes)), CoreCourseIndexRoutingModule.forChild({ children: courseIndexRoutes }), - CoreMainMenuHomeRoutingModule.forChild({ children: routes }), + CoreMainMenuHomeRoutingModule.forChild({ children: homeRoutes }), CoreSitePluginsComponentsModule, ], providers: [ diff --git a/src/core/lang.json b/src/core/lang.json index 057501d38..b4d4d57f0 100644 --- a/src/core/lang.json +++ b/src/core/lang.json @@ -225,6 +225,7 @@ "percentagenumber": "{{$a}}%", "phone": "Phone", "pictureof": "Picture of {{$a}}", + "play": "Play", "previous": "Previous", "proceed": "Proceed", "pulltorefresh": "Pull to refresh", diff --git a/src/core/services/file-helper.ts b/src/core/services/file-helper.ts index 4df8e5e71..17b5f0d21 100644 --- a/src/core/services/file-helper.ts +++ b/src/core/services/file-helper.ts @@ -73,7 +73,17 @@ export class CoreFileHelperProvider { await this.showConfirmOpenUnsupportedFile(); } - let url = await this.downloadFileIfNeeded(file, fileUrl, component, componentId, timemodified, state, onProgress, siteId); + let url = await this.downloadFileIfNeeded( + file, + fileUrl, + component, + componentId, + timemodified, + state, + onProgress, + siteId, + options, + ); if (!url) { return; @@ -127,6 +137,7 @@ export class CoreFileHelperProvider { * @param state The file's state. If not provided, it will be calculated. * @param onProgress Function to call on progress. * @param siteId The site ID. If not defined, current site. + * @param options Options to open the file. * @return Resolved with the URL to use on success. */ protected async downloadFileIfNeeded( @@ -138,6 +149,7 @@ export class CoreFileHelperProvider { state?: string, onProgress?: CoreFileHelperOnProgress, siteId?: string, + options: CoreUtilsOpenFileOptions = {}, ): Promise { siteId = siteId || CoreSites.getCurrentSiteId(); @@ -172,7 +184,7 @@ export class CoreFileHelperProvider { onProgress({ calculating: true }); } - const shouldDownloadFirst = await CoreFilepool.shouldDownloadFileBeforeOpen(fixedUrl, file.filesize || 0); + const shouldDownloadFirst = await CoreFilepool.shouldDownloadFileBeforeOpen(fixedUrl, file.filesize || 0, options); if (shouldDownloadFirst) { // Download the file first. if (state == CoreConstants.DOWNLOADING) { diff --git a/src/core/services/filepool.ts b/src/core/services/filepool.ts index a819c6963..3cc4c2854 100644 --- a/src/core/services/filepool.ts +++ b/src/core/services/filepool.ts @@ -26,7 +26,7 @@ import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreTextUtils } from '@services/utils/text'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreUrlUtils } from '@services/utils/url'; -import { CoreUtils, PromiseDefer } from '@services/utils/utils'; +import { CoreUtils, CoreUtilsOpenFileOptions, PromiseDefer } from '@services/utils/utils'; import { SQLiteDB } from '@classes/sqlitedb'; import { CoreError } from '@classes/errors/error'; import { CoreConstants } from '@/core/constants'; @@ -2781,7 +2781,7 @@ export class CoreFilepoolProvider { const mimetype = await CoreUtils.getMimeTypeFromUrl(url); // If the file is streaming (audio or video) we reject. - if (mimetype.indexOf('video') != -1 || mimetype.indexOf('audio') != -1) { + if (CoreMimetypeUtils.isStreamedMimetype(mimetype)) { throw new CoreError('File is audio or video.'); } } @@ -2791,6 +2791,7 @@ export class CoreFilepoolProvider { * * @param url File online URL. * @param size File size. + * @param options Options. * @return Promise resolved with boolean: whether file should be downloaded before opening it. * @description * Convenience function to check if a file should be downloaded before opening it. @@ -2800,16 +2801,21 @@ export class CoreFilepoolProvider { * - The file cannot be streamed. * If the file is big and can be streamed, the promise returned by this function will be rejected. */ - async shouldDownloadFileBeforeOpen(url: string, size: number): Promise { + async shouldDownloadFileBeforeOpen(url: string, size: number, options: CoreUtilsOpenFileOptions = {}): Promise { if (size >= 0 && size <= CoreFilepoolProvider.DOWNLOAD_THRESHOLD) { // The file is small, download it. return true; } + if (CoreUtils.shouldOpenWithDialog(options)) { + // Open with dialog needs a local file. + return true; + } + const mimetype = await CoreUtils.getMimeTypeFromUrl(url); // If the file is streaming (audio or video), return false. - return mimetype.indexOf('video') == -1 && mimetype.indexOf('audio') == -1; + return !CoreMimetypeUtils.isStreamedMimetype(mimetype); } /** diff --git a/src/core/services/utils/mimetype.ts b/src/core/services/utils/mimetype.ts index 92ec8ab48..9f5526ef3 100644 --- a/src/core/services/utils/mimetype.ts +++ b/src/core/services/utils/mimetype.ts @@ -562,6 +562,16 @@ export class CoreMimetypeUtilsProvider { return false; } + /** + * Check if a mimetype belongs to a file that can be streamed (audio, video). + * + * @param mimetype Mimetype. + * @return Boolean. + */ + isStreamedMimetype(mimetype: string): boolean { + return mimetype.indexOf('video') != -1 || mimetype.indexOf('audio') != -1; + } + /** * Remove the extension from a path (if any). * diff --git a/src/core/services/utils/utils.ts b/src/core/services/utils/utils.ts index 9849c55ce..7777cd262 100644 --- a/src/core/services/utils/utils.ts +++ b/src/core/services/utils/utils.ts @@ -933,8 +933,7 @@ export class CoreUtilsProvider { } try { - const openFileAction = options.iOSOpenFileAction ?? CoreConstants.CONFIG.iOSDefaultOpenFileAction; - if (CoreApp.isIOS() && openFileAction == OpenFileAction.OPEN_WITH) { + if (this.shouldOpenWithDialog(options)) { await FileOpener.showOpenWithDialog(path, mimetype || ''); } else { await FileOpener.open(path, mimetype || ''); @@ -1652,6 +1651,18 @@ export class CoreUtilsProvider { return this.wait(0); } + /** + * Given some options, check if a file should be opened with showOpenWithDialog. + * + * @param options Options. + * @return Boolean. + */ + shouldOpenWithDialog(options: CoreUtilsOpenFileOptions = {}): boolean { + const openFileAction = options.iOSOpenFileAction ?? CoreConstants.CONFIG.iOSDefaultOpenFileAction; + + return CoreApp.isIOS() && openFileAction == OpenFileAction.OPEN_WITH; + } + } export const CoreUtils = makeSingleton(CoreUtilsProvider);