diff --git a/scripts/langindex.json b/scripts/langindex.json index af613b569..c2cbfdb35 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -1984,6 +1984,7 @@ "core.openmodinbrowser": "local_moodlemobileapp", "core.opensecurityquestion": "local_moodlemobileapp", "core.opensettings": "local_moodlemobileapp", + "core.openwith": "local_moodlemobileapp", "core.othergroups": "group", "core.pagea": "moodle", "core.parentlanguage": "langconfig", 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 efddbf59d..769e29baa 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 @@ -45,11 +45,16 @@ -
- + + {{ '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 c6afd4d61..be18c8f05 100644 --- a/src/addons/mod/resource/components/index/index.ts +++ b/src/addons/mod/resource/components/index/index.ts @@ -20,6 +20,7 @@ import { 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 { CoreSites } from '@services/sites'; import { CoreTextUtils } from '@services/utils/text'; import { CoreUtils } from '@services/utils/utils'; @@ -49,6 +50,7 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource contentText = ''; displayDescription = true; warning = ''; + isIOS = false; constructor(@Optional() courseContentsPage?: CoreCourseContentsPage) { super('AddonModResourceIndexComponent', courseContentsPage); @@ -61,6 +63,7 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource super.ngOnInit(); this.canGetResource = AddonModResource.isGetResourceWSAvailable(); + this.isIOS = CoreApp.isIOS(); await this.loadContent(); try { @@ -155,9 +158,10 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource /** * Opens a file. * + * @param useIOSPicker Whether to use the picker in iOS. * @return Promise resolved when done. */ - async open(): Promise { + async open(useIOSPicker?: boolean): Promise { let downloadable = await CoreCourseModulePrefetchDelegate.isModuleDownloadable(this.module, this.courseId); if (downloadable) { @@ -166,7 +170,7 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource downloadable = await AddonModResourceHelper.isMainFileDownloadable(this.module); if (downloadable) { - return AddonModResourceHelper.openModuleFile(this.module, this.courseId); + return AddonModResourceHelper.openModuleFile(this.module, this.courseId, { useIOSPicker }); } } diff --git a/src/addons/mod/resource/lang.json b/src/addons/mod/resource/lang.json index bd7e9cefb..2449d53e3 100644 --- a/src/addons/mod/resource/lang.json +++ b/src/addons/mod/resource/lang.json @@ -2,6 +2,6 @@ "errorwhileloadingthecontent": "Error while loading the content.", "modifieddate": "Modified {{$a}}", "modulenameplural": "Files", - "openthefile": "Open the file", + "openthefile": "Open", "uploadeddate": "Uploaded {{$a}}" -} \ No newline at end of file +} diff --git a/src/addons/mod/resource/services/handlers/module.ts b/src/addons/mod/resource/services/handlers/module.ts index 24590f58a..672482df8 100644 --- a/src/addons/mod/resource/services/handlers/module.ts +++ b/src/addons/mod/resource/services/handlers/module.ts @@ -18,6 +18,7 @@ import { CoreCourse, CoreCourseAnyModuleData, CoreCourseModuleContentFile } from import { CoreCourseModule } from '@features/course/services/course-helper'; import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate'; import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate'; +import { CoreFileHelper } from '@services/file-helper'; import { CoreNavigationOptions, CoreNavigator } from '@services/navigator'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreTextUtils } from '@services/utils/text'; @@ -71,6 +72,7 @@ export class AddonModResourceModuleHandlerService implements CoreCourseModuleHan handlerData.buttons![0].hidden = status !== CoreConstants.DOWNLOADED || AddonModResourceHelper.isDisplayedInIframe(module); }; + const openWithPicker = CoreFileHelper.defaultIsOpenWithPicker(); const handlerData: CoreCourseModuleHandlerData = { icon: CoreCourse.getModuleIconSrc(this.modName, 'modicon' in module ? module.modicon : undefined), @@ -88,8 +90,8 @@ export class AddonModResourceModuleHandlerService implements CoreCourseModuleHan updateStatus: updateStatus.bind(this), buttons: [{ hidden: true, - icon: 'document', - label: 'addon.mod_resource.openthefile', + icon: openWithPicker ? 'fas-share-square' : 'fas-file', + label: module.name + ': ' + Translate.instant(openWithPicker ? 'core.openwith' : 'addon.mod_resource.openthefile'), action: async (event: Event, module: CoreCourseModule, courseId: number): Promise => { const hide = await this.hideOpenButton(module, courseId); if (!hide) { diff --git a/src/addons/mod/resource/services/resource-helper.ts b/src/addons/mod/resource/services/resource-helper.ts index 48d170947..f8ae97537 100644 --- a/src/addons/mod/resource/services/resource-helper.ts +++ b/src/addons/mod/resource/services/resource-helper.ts @@ -25,6 +25,7 @@ import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreTextUtils } from '@services/utils/text'; +import { CoreUtilsOpenFileOptions } from '@services/utils/utils'; import { makeSingleton } from '@singletons'; import { AddonModResource, AddonModResourceProvider } from './resource'; @@ -171,9 +172,10 @@ export class AddonModResourceHelperProvider { * * @param module Module where to get the contents. * @param courseId Course Id, used for completion purposes. + * @param options Options to open the file. * @return Resolved when done. */ - async openModuleFile(module: CoreCourseWSModule, courseId: number): Promise { + async openModuleFile(module: CoreCourseWSModule, courseId: number, options: CoreUtilsOpenFileOptions = {}): Promise { const modal = await CoreDomUtils.showModalLoading(); try { @@ -184,6 +186,8 @@ export class AddonModResourceHelperProvider { AddonModResourceProvider.COMPONENT, module.id, module.contents, + undefined, + options, ); try { diff --git a/src/core/components/file/core-file.html b/src/core/components/file/core-file.html index 4b0dd5522..4ce67bb44 100644 --- a/src/core/components/file/core-file.html +++ b/src/core/components/file/core-file.html @@ -7,11 +7,16 @@

{{ fileSizeReadable }}

{{ timemodified * 1000 | coreFormatDate }}

-
+
+ + + + diff --git a/src/core/components/file/file.ts b/src/core/components/file/file.ts index 85874ef89..380c99897 100644 --- a/src/core/components/file/file.ts +++ b/src/core/components/file/file.ts @@ -21,7 +21,7 @@ import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreUrlUtils } from '@services/utils/url'; -import { CoreUtils } from '@services/utils/utils'; +import { CoreUtils, CoreUtilsOpenFileOptions } from '@services/utils/utils'; import { CoreTextUtils } from '@services/utils/text'; import { CoreConstants } from '@/core/constants'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; @@ -48,16 +48,21 @@ export class CoreFileComponent implements OnInit, OnDestroy { @Output() onDelete: EventEmitter; // Will notify when the delete button is clicked. isDownloading?: boolean; + isDownloaded?: boolean; fileIcon?: string; fileName!: string; fileSizeReadable?: string; state?: string; timemodified!: number; + isIOS = false; + openButtonIcon = ''; + openButtonLabel = ''; protected fileUrl!: string; protected siteId?: string; protected fileSize?: number; protected observer?: CoreEventObserver; + protected defaultIsOpenWithPicker = false; constructor() { this.onDelete = new EventEmitter(); @@ -81,6 +86,11 @@ export class CoreFileComponent implements OnInit, OnDestroy { this.fileSize = this.file.filesize; this.fileName = this.file.filename || ''; + this.isIOS = CoreApp.isIOS(); + this.defaultIsOpenWithPicker = CoreFileHelper.defaultIsOpenWithPicker(); + this.openButtonIcon = this.defaultIsOpenWithPicker ? 'fas-file' : 'fas-share-square'; + this.openButtonLabel = this.defaultIsOpenWithPicker ? 'core.openfile' : 'core.openwith'; + if (CoreUtils.isTrueOrOne(this.showSize) && this.fileSize && this.fileSize >= 0) { this.fileSizeReadable = CoreTextUtils.bytesToSize(this.fileSize, 2); } @@ -128,20 +138,28 @@ export class CoreFileComponent implements OnInit, OnDestroy { this.state = state; this.isDownloading = this.canDownload && state === CoreConstants.DOWNLOADING; + this.isDownloaded = this.canDownload && CoreFileHelper.isStateDownloaded(state); } /** * Convenience function to open a file, downloading it if needed. * + * @param isOpenButton Whether the open button was clicked. * @return Promise resolved when file is opened. */ - protected openFile(): Promise { + openFile(isOpenButton = false): Promise { + const options: CoreUtilsOpenFileOptions = {}; + if (isOpenButton) { + // Use the non-default method. + options.useIOSPicker = !this.defaultIsOpenWithPicker; + } + return CoreFileHelper.downloadAndOpenFile(this.file!, this.component, this.componentId, this.state, (event) => { if (event && 'calculating' in event && event.calculating) { // The process is calculating some data required for the download, show the spinner. this.isDownloading = true; } - }).catch((error) => { + }, undefined, options).catch((error) => { CoreDomUtils.showErrorModalDefault(error, 'core.errordownloading', true); }); } @@ -152,7 +170,7 @@ export class CoreFileComponent implements OnInit, OnDestroy { * @param e Click event. * @param openAfterDownload Whether the file should be opened after download. */ - async download(e?: Event, openAfterDownload: boolean = false): Promise { + async download(e?: Event, openAfterDownload = false): Promise { e && e.preventDefault(); e && e.stopPropagation(); diff --git a/src/core/components/local-file/core-local-file.html b/src/core/components/local-file/core-local-file.html index 1fb756998..0e8552f0a 100644 --- a/src/core/components/local-file/core-local-file.html +++ b/src/core/components/local-file/core-local-file.html @@ -1,5 +1,5 @@
- + @@ -18,18 +18,25 @@
- - + + - - - + + + + - - - + + + + + + + +
diff --git a/src/core/components/local-file/local-file.ts b/src/core/components/local-file/local-file.ts index 9cab8887d..204eca8e8 100644 --- a/src/core/components/local-file/local-file.ts +++ b/src/core/components/local-file/local-file.ts @@ -23,8 +23,9 @@ import { CoreDomUtils } from '@services/utils/dom'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreTextUtils } from '@services/utils/text'; import { CoreTimeUtils } from '@services/utils/time'; -import { CoreUtils } from '@services/utils/utils'; +import { CoreUtils, CoreUtilsOpenFileOptions } from '@services/utils/utils'; import { CoreForms } from '@singletons/form'; +import { CoreApp } from '@services/app'; /** * Component to handle a local file. Only files inside the app folder can be managed. @@ -55,6 +56,11 @@ export class CoreLocalFileComponent implements OnInit { newFileName = ''; editMode = false; relativePath?: string; + isIOS = false; + openButtonIcon = ''; + openButtonLabel = ''; + + protected defaultIsOpenWithPicker = false; /** * Component being initialized. @@ -76,6 +82,10 @@ export class CoreLocalFileComponent implements OnInit { this.timemodified = CoreTimeUtils.userDate(metadata.modificationTime.getTime(), 'core.strftimedatetimeshort'); + this.isIOS = CoreApp.isIOS(); + this.defaultIsOpenWithPicker = CoreFileHelper.defaultIsOpenWithPicker(); + this.openButtonIcon = this.defaultIsOpenWithPicker ? 'fas-file' : 'fas-share-square'; + this.openButtonLabel = this.defaultIsOpenWithPicker ? 'core.openfile' : 'core.openwith'; } /** @@ -95,11 +105,12 @@ export class CoreLocalFileComponent implements OnInit { } /** - * File clicked. + * Open file. * * @param e Click event. + * @param isOpenButton Whether the open button was clicked. */ - async fileClicked(e: Event): Promise { + async openFile(e: Event, isOpenButton = false): Promise { if (this.editMode) { return; } @@ -107,7 +118,7 @@ export class CoreLocalFileComponent implements OnInit { e.preventDefault(); e.stopPropagation(); - if (CoreUtils.isTrueOrOne(this.overrideClick) && this.onClick.observers.length) { + if (!isOpenButton && CoreUtils.isTrueOrOne(this.overrideClick) && this.onClick.observers.length) { this.onClick.emit(); return; @@ -121,7 +132,13 @@ export class CoreLocalFileComponent implements OnInit { } } - CoreUtils.openFile(this.file!.toURL()); + const options: CoreUtilsOpenFileOptions = {}; + if (isOpenButton) { + // Use the non-default method. + options.useIOSPicker = !this.defaultIsOpenWithPicker; + } + + CoreUtils.openFile(this.file!.toURL(), options); } /** diff --git a/src/core/constants.ts b/src/core/constants.ts index 8330e31d9..abf5e5e11 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -173,6 +173,7 @@ export interface EnvironmentConfig { displayqroncredentialscreen?: boolean; displayqronsitescreen?: boolean; forceOpenLinksIn: 'app' | 'browser'; + iosopenfilepicker?: boolean; }; export interface EnvironmentBuild { diff --git a/src/core/features/course/directives/download-module-main-file.ts b/src/core/features/course/directives/download-module-main-file.ts index bf62ab094..6f63da894 100644 --- a/src/core/features/course/directives/download-module-main-file.ts +++ b/src/core/features/course/directives/download-module-main-file.ts @@ -17,6 +17,7 @@ import { Directive, Input, OnInit, ElementRef } from '@angular/core'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreCourse, CoreCourseModuleContentFile, CoreCourseWSModule } from '@features/course/services/course'; import { CoreCourseHelper } from '@features/course/services/course-helper'; +import { CoreUtilsOpenFileOptions } from '@services/utils/utils'; /** * Directive to allow downloading and open the main file of a module. @@ -36,6 +37,7 @@ export class CoreCourseDownloadModuleMainFileDirective implements OnInit { @Input() component?: string; // Component to link the file to. @Input() componentId?: string | number; // Component ID to use in conjunction with the component. If not defined, use moduleId. @Input() files?: CoreCourseModuleContentFile[]; // List of files of the module. If not provided, use module.contents. + @Input() options?: CoreUtilsOpenFileOptions = {}; protected element: HTMLElement; @@ -73,6 +75,8 @@ export class CoreCourseDownloadModuleMainFileDirective implements OnInit { this.component, componentId, this.files, + undefined, + this.options, ); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'core.errordownloading', true); diff --git a/src/core/features/course/services/course-helper.ts b/src/core/features/course/services/course-helper.ts index b0f1e8f69..b72b4a201 100644 --- a/src/core/features/course/services/course-helper.ts +++ b/src/core/features/course/services/course-helper.ts @@ -31,7 +31,7 @@ import { CoreLogger } from '@singletons/logger'; import { makeSingleton, Translate } from '@singletons'; import { CoreFilepool } from '@services/filepool'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreUtils } from '@services/utils/utils'; +import { CoreUtils, CoreUtilsOpenFileOptions } from '@services/utils/utils'; import { CoreCourseAnyCourseData, CoreCourseBasicData, @@ -656,6 +656,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 Resolved on success. */ async downloadModuleAndOpenFile( @@ -665,6 +666,7 @@ export class CoreCourseHelperProvider { componentId?: string | number, files?: CoreCourseModuleContentFile[], siteId?: string, + options: CoreUtilsOpenFileOptions = {}, ): Promise { siteId = siteId || CoreSites.getCurrentSiteId(); @@ -696,7 +698,7 @@ export class CoreCourseHelperProvider { const result = await this.downloadModuleWithMainFileIfNeeded(module, courseId, component || '', componentId, files, siteId); if (CoreUrlUtils.isLocalFileUrl(result.path)) { - return CoreUtils.openFile(result.path); + return CoreUtils.openFile(result.path, options); } /* In iOS, if we use the same URL in embedded browser and background download then the download only @@ -724,7 +726,7 @@ export class CoreCourseHelperProvider { path = await CoreFilepool.getInternalUrlByUrl(siteId, mainFile.fileurl); } - await CoreUtils.openFile(path); + await CoreUtils.openFile(path, options); } } diff --git a/src/core/lang.json b/src/core/lang.json index 1d13f4d39..057501d38 100644 --- a/src/core/lang.json +++ b/src/core/lang.json @@ -217,6 +217,7 @@ "openmodinbrowser": "Open {{$a}} in browser", "opensecurityquestion": "Open security question", "opensettings": "Open settings", + "openwith": "Open with...", "othergroups": "Other groups", "pagea": "Page {{$a}}", "parentlanguage": "", diff --git a/src/core/services/file-helper.ts b/src/core/services/file-helper.ts index 566155087..c90f46183 100644 --- a/src/core/services/file-helper.ts +++ b/src/core/services/file-helper.ts @@ -22,7 +22,7 @@ import { CoreSites } from '@services/sites'; import { CoreWS, CoreWSFile } from '@services/ws'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreUrlUtils } from '@services/utils/url'; -import { CoreUtils } from '@services/utils/utils'; +import { CoreUtils, CoreUtilsOpenFileOptions } from '@services/utils/utils'; import { CoreConstants } from '@/core/constants'; import { CoreError } from '@classes/errors/error'; import { makeSingleton, Translate } from '@singletons'; @@ -34,6 +34,15 @@ import { CoreNetworkError } from '@classes/errors/network-error'; @Injectable({ providedIn: 'root' }) export class CoreFileHelperProvider { + /** + * Check if the default behaviour of the app is open file with picker. + * + * @return Boolean. + */ + defaultIsOpenWithPicker(): boolean { + return CoreApp.isIOS() && !!CoreConstants.CONFIG.iosopenfilepicker; + } + /** * Convenience function to open a file, downloading it if needed. * @@ -43,6 +52,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 on success. */ async downloadAndOpenFile( @@ -52,6 +62,7 @@ export class CoreFileHelperProvider { state?: string, onProgress?: CoreFileHelperOnProgress, siteId?: string, + options: CoreUtilsOpenFileOptions = {}, ): Promise { siteId = siteId || CoreSites.getCurrentSiteId(); @@ -102,7 +113,7 @@ export class CoreFileHelperProvider { } } - return CoreUtils.openFile(url); + return CoreUtils.openFile(url, options); } /** diff --git a/src/core/services/utils/utils.ts b/src/core/services/utils/utils.ts index da8c27fd7..2493c1592 100644 --- a/src/core/services/utils/utils.ts +++ b/src/core/services/utils/utils.ts @@ -32,6 +32,7 @@ import { CoreFileSizeSum } from '@services/plugin-file-delegate'; import { CoreViewerQRScannerComponent } from '@features/viewer/components/qr-scanner/qr-scanner'; import { CoreCanceledError } from '@classes/errors/cancelederror'; import { CoreFileEntry } from '@services/file-helper'; +import { CoreConstants } from '@/core/constants'; type TreeNode = T & { children: TreeNode[] }; @@ -907,9 +908,10 @@ export class CoreUtilsProvider { * Open a file using platform specific method. * * @param path The local path of the file to be open. + * @param options Options. * @return Promise resolved when done. */ - async openFile(path: string): Promise { + async openFile(path: string, options: CoreUtilsOpenFileOptions = {}): Promise { // Convert the path to a native path if needed. path = CoreFile.unconvertFileSrc(path); @@ -931,7 +933,12 @@ export class CoreUtilsProvider { } try { - await FileOpener.open(path, mimetype || ''); + const useIOSPicker = options.useIOSPicker ?? CoreConstants.CONFIG.iosopenfilepicker; + if (useIOSPicker && CoreApp.isIOS()) { + await FileOpener.showOpenWithDialog(path, mimetype || ''); + } else { + await FileOpener.open(path, mimetype || ''); + } } catch (error) { this.logger.error('Error opening file ' + path + ' with mimetype ' + mimetype); this.logger.error('Error: ', JSON.stringify(error)); @@ -1703,3 +1710,10 @@ export type CoreMenuItem = { label: string; value: T | number; }; + +/** + * Options for opening a file. + */ +export type CoreUtilsOpenFileOptions = { + useIOSPicker?: boolean; // Whether to let user choose app to open the file in iOS. Defaults to false (preview). +};