Merge pull request #2794 from dpalou/MOBILE-3761

Mobile 3761
main
Pau Ferrer Ocaña 2021-05-28 12:11:19 +02:00 committed by GitHub
commit 8728954fba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 149 additions and 40 deletions

View File

@ -1984,6 +1984,7 @@
"core.openmodinbrowser": "local_moodlemobileapp", "core.openmodinbrowser": "local_moodlemobileapp",
"core.opensecurityquestion": "local_moodlemobileapp", "core.opensecurityquestion": "local_moodlemobileapp",
"core.opensettings": "local_moodlemobileapp", "core.opensettings": "local_moodlemobileapp",
"core.openwith": "local_moodlemobileapp",
"core.othergroups": "group", "core.othergroups": "group",
"core.pagea": "moodle", "core.pagea": "moodle",
"core.parentlanguage": "langconfig", "core.parentlanguage": "langconfig",

View File

@ -45,11 +45,16 @@
<core-format-text [text]="contentText" [filter]="false"></core-format-text> <core-format-text [text]="contentText" [filter]="false"></core-format-text>
</div> </div>
<div class="ion-padding" *ngIf="mode == 'external'"> <ng-container *ngIf="mode == 'external'">
<ion-button expand="block" (click)="open()"> <ion-button expand="block" class="ion-margin" (click)="open(openFileAction.OPEN)">
<ion-icon name="far-file" slot="start" aria-hidden="true"></ion-icon> <ion-icon name="far-file" slot="start" aria-hidden="true"></ion-icon>
{{ 'addon.mod_resource.openthefile' | translate }} {{ 'addon.mod_resource.openthefile' | translate }}
</ion-button> </ion-button>
</div>
<ion-button *ngIf="isIOS" expand="block" class="ion-margin" (click)="open(openFileAction.OPEN_WITH)">
<ion-icon name="far-share-square" slot="start" aria-hidden="true"></ion-icon>
{{ 'core.openwith' | translate }}
</ion-button>
</ng-container>
</core-loading> </core-loading>

View File

@ -20,9 +20,10 @@ import {
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents'; import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
import { CoreCourse, CoreCourseWSModule } from '@features/course/services/course'; import { CoreCourse, CoreCourseWSModule } from '@features/course/services/course';
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate'; import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
import { CoreApp } from '@services/app';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
import { CoreTextUtils } from '@services/utils/text'; import { CoreTextUtils } from '@services/utils/text';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils, OpenFileAction } from '@services/utils/utils';
import { Translate } from '@singletons'; import { Translate } from '@singletons';
import { import {
AddonModResource, AddonModResource,
@ -49,6 +50,8 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource
contentText = ''; contentText = '';
displayDescription = true; displayDescription = true;
warning = ''; warning = '';
isIOS = false;
openFileAction = OpenFileAction;
constructor(@Optional() courseContentsPage?: CoreCourseContentsPage) { constructor(@Optional() courseContentsPage?: CoreCourseContentsPage) {
super('AddonModResourceIndexComponent', courseContentsPage); super('AddonModResourceIndexComponent', courseContentsPage);
@ -61,6 +64,7 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource
super.ngOnInit(); super.ngOnInit();
this.canGetResource = AddonModResource.isGetResourceWSAvailable(); this.canGetResource = AddonModResource.isGetResourceWSAvailable();
this.isIOS = CoreApp.isIOS();
await this.loadContent(); await this.loadContent();
try { try {
@ -155,9 +159,10 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource
/** /**
* Opens a file. * Opens a file.
* *
* @param iOSOpenFileAction Action to do in iOS.
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
async open(): Promise<void> { async open(iOSOpenFileAction?: OpenFileAction): Promise<void> {
let downloadable = await CoreCourseModulePrefetchDelegate.isModuleDownloadable(this.module, this.courseId); let downloadable = await CoreCourseModulePrefetchDelegate.isModuleDownloadable(this.module, this.courseId);
if (downloadable) { if (downloadable) {
@ -166,7 +171,7 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource
downloadable = await AddonModResourceHelper.isMainFileDownloadable(this.module); downloadable = await AddonModResourceHelper.isMainFileDownloadable(this.module);
if (downloadable) { if (downloadable) {
return AddonModResourceHelper.openModuleFile(this.module, this.courseId); return AddonModResourceHelper.openModuleFile(this.module, this.courseId, { iOSOpenFileAction });
} }
} }

View File

@ -2,6 +2,6 @@
"errorwhileloadingthecontent": "Error while loading the content.", "errorwhileloadingthecontent": "Error while loading the content.",
"modifieddate": "Modified {{$a}}", "modifieddate": "Modified {{$a}}",
"modulenameplural": "Files", "modulenameplural": "Files",
"openthefile": "Open the file", "openthefile": "Open",
"uploadeddate": "Uploaded {{$a}}" "uploadeddate": "Uploaded {{$a}}"
} }

View File

@ -18,6 +18,7 @@ import { CoreCourse, CoreCourseAnyModuleData, CoreCourseModuleContentFile } from
import { CoreCourseModule } from '@features/course/services/course-helper'; import { CoreCourseModule } from '@features/course/services/course-helper';
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate'; import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate';
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate'; import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
import { CoreFileHelper } from '@services/file-helper';
import { CoreNavigationOptions, CoreNavigator } from '@services/navigator'; import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreMimetypeUtils } from '@services/utils/mimetype';
import { CoreTextUtils } from '@services/utils/text'; import { CoreTextUtils } from '@services/utils/text';
@ -71,6 +72,7 @@ export class AddonModResourceModuleHandlerService implements CoreCourseModuleHan
handlerData.buttons![0].hidden = status !== CoreConstants.DOWNLOADED || handlerData.buttons![0].hidden = status !== CoreConstants.DOWNLOADED ||
AddonModResourceHelper.isDisplayedInIframe(module); AddonModResourceHelper.isDisplayedInIframe(module);
}; };
const openWithPicker = CoreFileHelper.defaultIsOpenWithPicker();
const handlerData: CoreCourseModuleHandlerData = { const handlerData: CoreCourseModuleHandlerData = {
icon: CoreCourse.getModuleIconSrc(this.modName, 'modicon' in module ? module.modicon : undefined), icon: CoreCourse.getModuleIconSrc(this.modName, 'modicon' in module ? module.modicon : undefined),
@ -88,8 +90,8 @@ export class AddonModResourceModuleHandlerService implements CoreCourseModuleHan
updateStatus: updateStatus.bind(this), updateStatus: updateStatus.bind(this),
buttons: [{ buttons: [{
hidden: true, hidden: true,
icon: 'document', icon: openWithPicker ? 'fas-share-square' : 'fas-file',
label: 'addon.mod_resource.openthefile', label: module.name + ': ' + Translate.instant(openWithPicker ? 'core.openwith' : 'addon.mod_resource.openthefile'),
action: async (event: Event, module: CoreCourseModule, courseId: number): Promise<void> => { action: async (event: Event, module: CoreCourseModule, courseId: number): Promise<void> => {
const hide = await this.hideOpenButton(module, courseId); const hide = await this.hideOpenButton(module, courseId);
if (!hide) { if (!hide) {

View File

@ -25,6 +25,7 @@ import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreMimetypeUtils } from '@services/utils/mimetype';
import { CoreTextUtils } from '@services/utils/text'; import { CoreTextUtils } from '@services/utils/text';
import { CoreUtilsOpenFileOptions } from '@services/utils/utils';
import { makeSingleton } from '@singletons'; import { makeSingleton } from '@singletons';
import { AddonModResource, AddonModResourceProvider } from './resource'; import { AddonModResource, AddonModResourceProvider } from './resource';
@ -171,9 +172,10 @@ export class AddonModResourceHelperProvider {
* *
* @param module Module where to get the contents. * @param module Module where to get the contents.
* @param courseId Course Id, used for completion purposes. * @param courseId Course Id, used for completion purposes.
* @param options Options to open the file.
* @return Resolved when done. * @return Resolved when done.
*/ */
async openModuleFile(module: CoreCourseWSModule, courseId: number): Promise<void> { async openModuleFile(module: CoreCourseWSModule, courseId: number, options: CoreUtilsOpenFileOptions = {}): Promise<void> {
const modal = await CoreDomUtils.showModalLoading(); const modal = await CoreDomUtils.showModalLoading();
try { try {
@ -184,6 +186,8 @@ export class AddonModResourceHelperProvider {
AddonModResourceProvider.COMPONENT, AddonModResourceProvider.COMPONENT,
module.id, module.id,
module.contents, module.contents,
undefined,
options,
); );
try { try {

View File

@ -7,11 +7,16 @@
<p *ngIf="fileSizeReadable">{{ fileSizeReadable }}</p> <p *ngIf="fileSizeReadable">{{ fileSizeReadable }}</p>
<p *ngIf="showTime">{{ timemodified * 1000 | coreFormatDate }}</p> <p *ngIf="showTime">{{ timemodified * 1000 | coreFormatDate }}</p>
</ion-label> </ion-label>
<div slot="end"> <div slot="end" class="flex-row">
<core-download-refresh [status]="state" [enabled]="canDownload" [loading]="isDownloading" <core-download-refresh [status]="state" [enabled]="canDownload" [loading]="isDownloading"
[canTrustDownload]="!alwaysDownload" (action)="download()" size="small"> [canTrustDownload]="!alwaysDownload" (action)="download()" size="small">
</core-download-refresh> </core-download-refresh>
<ion-button fill="clear" *ngIf="isDownloaded && isIOS" (click)="openFile(true)" color="dark"
[title]="openButtonLabel | translate">
<ion-icon slot="icon-only" [name]="openButtonIcon" aria-hidden="true"></ion-icon>
</ion-button>
<ion-button fill="clear" *ngIf="!isDownloading && canDelete" (click)="delete($event)" <ion-button fill="clear" *ngIf="!isDownloading && canDelete" (click)="delete($event)"
[attr.aria-label]="'core.delete' | translate" color="danger" size="small"> [attr.aria-label]="'core.delete' | translate" color="danger" size="small">
<ion-icon slot="icon-only" name="fas-trash" aria-hidden="true"></ion-icon> <ion-icon slot="icon-only" name="fas-trash" aria-hidden="true"></ion-icon>

View File

@ -21,7 +21,7 @@ import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreMimetypeUtils } from '@services/utils/mimetype';
import { CoreUrlUtils } from '@services/utils/url'; import { CoreUrlUtils } from '@services/utils/url';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils, CoreUtilsOpenFileOptions, OpenFileAction } from '@services/utils/utils';
import { CoreTextUtils } from '@services/utils/text'; import { CoreTextUtils } from '@services/utils/text';
import { CoreConstants } from '@/core/constants'; import { CoreConstants } from '@/core/constants';
import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreEventObserver, CoreEvents } from '@singletons/events';
@ -48,16 +48,21 @@ export class CoreFileComponent implements OnInit, OnDestroy {
@Output() onDelete: EventEmitter<void>; // Will notify when the delete button is clicked. @Output() onDelete: EventEmitter<void>; // Will notify when the delete button is clicked.
isDownloading?: boolean; isDownloading?: boolean;
isDownloaded?: boolean;
fileIcon?: string; fileIcon?: string;
fileName!: string; fileName!: string;
fileSizeReadable?: string; fileSizeReadable?: string;
state?: string; state?: string;
timemodified!: number; timemodified!: number;
isIOS = false;
openButtonIcon = '';
openButtonLabel = '';
protected fileUrl!: string; protected fileUrl!: string;
protected siteId?: string; protected siteId?: string;
protected fileSize?: number; protected fileSize?: number;
protected observer?: CoreEventObserver; protected observer?: CoreEventObserver;
protected defaultIsOpenWithPicker = false;
constructor() { constructor() {
this.onDelete = new EventEmitter<void>(); this.onDelete = new EventEmitter<void>();
@ -81,6 +86,11 @@ export class CoreFileComponent implements OnInit, OnDestroy {
this.fileSize = this.file.filesize; this.fileSize = this.file.filesize;
this.fileName = this.file.filename || ''; 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) { if (CoreUtils.isTrueOrOne(this.showSize) && this.fileSize && this.fileSize >= 0) {
this.fileSizeReadable = CoreTextUtils.bytesToSize(this.fileSize, 2); this.fileSizeReadable = CoreTextUtils.bytesToSize(this.fileSize, 2);
} }
@ -128,20 +138,28 @@ export class CoreFileComponent implements OnInit, OnDestroy {
this.state = state; this.state = state;
this.isDownloading = this.canDownload && state === CoreConstants.DOWNLOADING; this.isDownloading = this.canDownload && state === CoreConstants.DOWNLOADING;
this.isDownloaded = this.canDownload && CoreFileHelper.isStateDownloaded(state);
} }
/** /**
* Convenience function to open a file, downloading it if needed. * 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. * @return Promise resolved when file is opened.
*/ */
protected openFile(): Promise<void> { openFile(isOpenButton = false): Promise<void> {
const options: CoreUtilsOpenFileOptions = {};
if (isOpenButton) {
// Use the non-default method.
options.iOSOpenFileAction = this.defaultIsOpenWithPicker ? OpenFileAction.OPEN : OpenFileAction.OPEN_WITH;
}
return CoreFileHelper.downloadAndOpenFile(this.file!, this.component, this.componentId, this.state, (event) => { return CoreFileHelper.downloadAndOpenFile(this.file!, this.component, this.componentId, this.state, (event) => {
if (event && 'calculating' in event && event.calculating) { if (event && 'calculating' in event && event.calculating) {
// The process is calculating some data required for the download, show the spinner. // The process is calculating some data required for the download, show the spinner.
this.isDownloading = true; this.isDownloading = true;
} }
}).catch((error) => { }, undefined, options).catch((error) => {
CoreDomUtils.showErrorModalDefault(error, 'core.errordownloading', true); CoreDomUtils.showErrorModalDefault(error, 'core.errordownloading', true);
}); });
} }
@ -152,7 +170,7 @@ export class CoreFileComponent implements OnInit, OnDestroy {
* @param e Click event. * @param e Click event.
* @param openAfterDownload Whether the file should be opened after download. * @param openAfterDownload Whether the file should be opened after download.
*/ */
async download(e?: Event, openAfterDownload: boolean = false): Promise<void> { async download(e?: Event, openAfterDownload = false): Promise<void> {
e && e.preventDefault(); e && e.preventDefault();
e && e.stopPropagation(); e && e.stopPropagation();

View File

@ -1,5 +1,5 @@
<form (ngSubmit)="changeName(newFileName, $event)" #nameForm> <form (ngSubmit)="changeName(newFileName, $event)" #nameForm>
<ion-item class="ion-text-wrap item-file" (click)="fileClicked($event)" button> <!-- [class.item-input]="editMode" --> <ion-item class="ion-text-wrap item-file" (click)="openFile($event)" button detail="false">
<ion-thumbnail slot="start"> <ion-thumbnail slot="start">
<img [src]="fileIcon" [alt]="fileExtension" role="presentation" /> <img [src]="fileIcon" [alt]="fileExtension" role="presentation" />
</ion-thumbnail> </ion-thumbnail>
@ -17,19 +17,26 @@
(click)="$event.stopPropagation()" core-auto-focus [(ngModel)]="newFileName" *ngIf="editMode"> (click)="$event.stopPropagation()" core-auto-focus [(ngModel)]="newFileName" *ngIf="editMode">
</ion-input> </ion-input>
<div class="buttons" slot="end" *ngIf="manage"> <div class="buttons" slot="end">
<ion-button *ngIf="!editMode" fill="clear" [core-suppress-events] (onClick)="activateEdit($event)" <ion-button fill="clear" *ngIf="isIOS" (click)="openFile($event, true)"
[attr.aria-label]="'core.edit' | translate" color="dark"> [attr.aria-label]="openButtonLabel | translate">
<ion-icon name="fas-pen" slot="icon-only" aria-hidden="true"></ion-icon> <ion-icon slot="icon-only" [name]="openButtonIcon" aria-hidden="true"></ion-icon>
</ion-button> </ion-button>
<ion-button *ngIf="editMode" fill="clear" [attr.aria-label]="'core.save' | translate" color="success" type="submit"> <ng-container *ngIf="manage">
<ion-icon name="fas-check" slot="icon-only" aria-hidden="true"></ion-icon> <ion-button *ngIf="!editMode" fill="clear" [core-suppress-events] (onClick)="activateEdit($event)"
</ion-button> [attr.aria-label]="'core.edit' | translate" color="dark">
<ion-icon name="fas-pen" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
<ion-button fill="clear" (click)="deleteFile($event)" [attr.aria-label]="'core.delete' | translate" color="danger"> <ion-button *ngIf="editMode" fill="clear" [attr.aria-label]="'core.save' | translate" color="success" type="submit">
<ion-icon name="fas-trash" slot="icon-only" aria-hidden="true"></ion-icon> <ion-icon name="fas-check" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button> </ion-button>
<ion-button fill="clear" (click)="deleteFile($event)" [attr.aria-label]="'core.delete' | translate" color="danger">
<ion-icon name="fas-trash" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
</ng-container>
</div> </div>
</ion-item> </ion-item>
</form> </form>

View File

@ -23,8 +23,9 @@ import { CoreDomUtils } from '@services/utils/dom';
import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreMimetypeUtils } from '@services/utils/mimetype';
import { CoreTextUtils } from '@services/utils/text'; import { CoreTextUtils } from '@services/utils/text';
import { CoreTimeUtils } from '@services/utils/time'; import { CoreTimeUtils } from '@services/utils/time';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils, CoreUtilsOpenFileOptions, OpenFileAction } from '@services/utils/utils';
import { CoreForms } from '@singletons/form'; 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. * 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 = ''; newFileName = '';
editMode = false; editMode = false;
relativePath?: string; relativePath?: string;
isIOS = false;
openButtonIcon = '';
openButtonLabel = '';
protected defaultIsOpenWithPicker = false;
/** /**
* Component being initialized. * Component being initialized.
@ -76,6 +82,10 @@ export class CoreLocalFileComponent implements OnInit {
this.timemodified = CoreTimeUtils.userDate(metadata.modificationTime.getTime(), 'core.strftimedatetimeshort'); 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 e Click event.
* @param isOpenButton Whether the open button was clicked.
*/ */
async fileClicked(e: Event): Promise<void> { async openFile(e: Event, isOpenButton = false): Promise<void> {
if (this.editMode) { if (this.editMode) {
return; return;
} }
@ -107,7 +118,7 @@ export class CoreLocalFileComponent implements OnInit {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
if (CoreUtils.isTrueOrOne(this.overrideClick) && this.onClick.observers.length) { if (!isOpenButton && CoreUtils.isTrueOrOne(this.overrideClick) && this.onClick.observers.length) {
this.onClick.emit(); this.onClick.emit();
return; 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.iOSOpenFileAction = this.defaultIsOpenWithPicker ? OpenFileAction.OPEN : OpenFileAction.OPEN_WITH;
}
CoreUtils.openFile(this.file!.toURL(), options);
} }
/** /**

View File

@ -17,6 +17,7 @@
import { CoreColorScheme, CoreZoomLevel } from '@features/settings/services/settings-helper'; import { CoreColorScheme, CoreZoomLevel } from '@features/settings/services/settings-helper';
import { CoreSitesDemoSiteData } from '@services/sites'; import { CoreSitesDemoSiteData } from '@services/sites';
import envJson from '@/assets/env.json'; import envJson from '@/assets/env.json';
import { OpenFileAction } from '@services/utils/utils';
/** /**
* Context levels enumeration. * Context levels enumeration.
@ -173,6 +174,7 @@ export interface EnvironmentConfig {
displayqroncredentialscreen?: boolean; displayqroncredentialscreen?: boolean;
displayqronsitescreen?: boolean; displayqronsitescreen?: boolean;
forceOpenLinksIn: 'app' | 'browser'; forceOpenLinksIn: 'app' | 'browser';
iOSDefaultOpenFileAction?: OpenFileAction;
}; };
export interface EnvironmentBuild { export interface EnvironmentBuild {

View File

@ -99,7 +99,10 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy {
this.hasInfo = !!( this.hasInfo = !!(
this.module.description || this.module.description ||
(this.showActivityDates && this.module.dates && this.module.dates.length) || (this.showActivityDates && this.module.dates && this.module.dates.length) ||
this.module.completiondata (this.module.completiondata &&
((this.showManualCompletion && !this.module.completiondata.isautomatic) ||
(this.showCompletionConditions && this.module.completiondata.isautomatic))
)
); );
if (this.module.handlerData.showDownloadButton) { if (this.module.handlerData.showDownloadButton) {

View File

@ -17,6 +17,7 @@ import { Directive, Input, OnInit, ElementRef } from '@angular/core';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { CoreCourse, CoreCourseModuleContentFile, CoreCourseWSModule } from '@features/course/services/course'; import { CoreCourse, CoreCourseModuleContentFile, CoreCourseWSModule } from '@features/course/services/course';
import { CoreCourseHelper } from '@features/course/services/course-helper'; 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. * 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() 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() 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() files?: CoreCourseModuleContentFile[]; // List of files of the module. If not provided, use module.contents.
@Input() options?: CoreUtilsOpenFileOptions = {};
protected element: HTMLElement; protected element: HTMLElement;
@ -73,6 +75,8 @@ export class CoreCourseDownloadModuleMainFileDirective implements OnInit {
this.component, this.component,
componentId, componentId,
this.files, this.files,
undefined,
this.options,
); );
} catch (error) { } catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'core.errordownloading', true); CoreDomUtils.showErrorModalDefault(error, 'core.errordownloading', true);

View File

@ -31,7 +31,7 @@ import { CoreLogger } from '@singletons/logger';
import { makeSingleton, Translate } from '@singletons'; import { makeSingleton, Translate } from '@singletons';
import { CoreFilepool } from '@services/filepool'; import { CoreFilepool } from '@services/filepool';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils, CoreUtilsOpenFileOptions } from '@services/utils/utils';
import { import {
CoreCourseAnyCourseData, CoreCourseAnyCourseData,
CoreCourseBasicData, CoreCourseBasicData,
@ -656,6 +656,7 @@ export class CoreCourseHelperProvider {
* @param componentId An ID to use in conjunction with the component. * @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 files List of files of the module. If not provided, use module.contents.
* @param siteId The site ID. If not defined, current site. * @param siteId The site ID. If not defined, current site.
* @param options Options to open the file.
* @return Resolved on success. * @return Resolved on success.
*/ */
async downloadModuleAndOpenFile( async downloadModuleAndOpenFile(
@ -665,6 +666,7 @@ export class CoreCourseHelperProvider {
componentId?: string | number, componentId?: string | number,
files?: CoreCourseModuleContentFile[], files?: CoreCourseModuleContentFile[],
siteId?: string, siteId?: string,
options: CoreUtilsOpenFileOptions = {},
): Promise<void> { ): Promise<void> {
siteId = siteId || CoreSites.getCurrentSiteId(); siteId = siteId || CoreSites.getCurrentSiteId();
@ -696,7 +698,7 @@ export class CoreCourseHelperProvider {
const result = await this.downloadModuleWithMainFileIfNeeded(module, courseId, component || '', componentId, files, siteId); const result = await this.downloadModuleWithMainFileIfNeeded(module, courseId, component || '', componentId, files, siteId);
if (CoreUrlUtils.isLocalFileUrl(result.path)) { 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 /* 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); path = await CoreFilepool.getInternalUrlByUrl(siteId, mainFile.fileurl);
} }
await CoreUtils.openFile(path); await CoreUtils.openFile(path, options);
} }
} }

View File

@ -217,6 +217,7 @@
"openmodinbrowser": "Open {{$a}} in browser", "openmodinbrowser": "Open {{$a}} in browser",
"opensecurityquestion": "Open security question", "opensecurityquestion": "Open security question",
"opensettings": "Open settings", "opensettings": "Open settings",
"openwith": "Open with...",
"othergroups": "Other groups", "othergroups": "Other groups",
"pagea": "Page {{$a}}", "pagea": "Page {{$a}}",
"parentlanguage": "", "parentlanguage": "",

View File

@ -22,7 +22,7 @@ import { CoreSites } from '@services/sites';
import { CoreWS, CoreWSFile } from '@services/ws'; import { CoreWS, CoreWSFile } from '@services/ws';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { CoreUrlUtils } from '@services/utils/url'; import { CoreUrlUtils } from '@services/utils/url';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils, CoreUtilsOpenFileOptions, OpenFileAction } from '@services/utils/utils';
import { CoreConstants } from '@/core/constants'; import { CoreConstants } from '@/core/constants';
import { CoreError } from '@classes/errors/error'; import { CoreError } from '@classes/errors/error';
import { makeSingleton, Translate } from '@singletons'; import { makeSingleton, Translate } from '@singletons';
@ -34,6 +34,15 @@ import { CoreNetworkError } from '@classes/errors/network-error';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class CoreFileHelperProvider { 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.iOSDefaultOpenFileAction === OpenFileAction.OPEN_WITH;
}
/** /**
* Convenience function to open a file, downloading it if needed. * 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 state The file's state. If not provided, it will be calculated.
* @param onProgress Function to call on progress. * @param onProgress Function to call on progress.
* @param siteId The site ID. If not defined, current site. * @param siteId The site ID. If not defined, current site.
* @param options Options to open the file.
* @return Resolved on success. * @return Resolved on success.
*/ */
async downloadAndOpenFile( async downloadAndOpenFile(
@ -52,6 +62,7 @@ export class CoreFileHelperProvider {
state?: string, state?: string,
onProgress?: CoreFileHelperOnProgress, onProgress?: CoreFileHelperOnProgress,
siteId?: string, siteId?: string,
options: CoreUtilsOpenFileOptions = {},
): Promise<void> { ): Promise<void> {
siteId = siteId || CoreSites.getCurrentSiteId(); siteId = siteId || CoreSites.getCurrentSiteId();
@ -102,7 +113,7 @@ export class CoreFileHelperProvider {
} }
} }
return CoreUtils.openFile(url); return CoreUtils.openFile(url, options);
} }
/** /**

View File

@ -32,6 +32,7 @@ import { CoreFileSizeSum } from '@services/plugin-file-delegate';
import { CoreViewerQRScannerComponent } from '@features/viewer/components/qr-scanner/qr-scanner'; import { CoreViewerQRScannerComponent } from '@features/viewer/components/qr-scanner/qr-scanner';
import { CoreCanceledError } from '@classes/errors/cancelederror'; import { CoreCanceledError } from '@classes/errors/cancelederror';
import { CoreFileEntry } from '@services/file-helper'; import { CoreFileEntry } from '@services/file-helper';
import { CoreConstants } from '@/core/constants';
type TreeNode<T> = T & { children: TreeNode<T>[] }; type TreeNode<T> = T & { children: TreeNode<T>[] };
@ -907,9 +908,10 @@ export class CoreUtilsProvider {
* Open a file using platform specific method. * Open a file using platform specific method.
* *
* @param path The local path of the file to be open. * @param path The local path of the file to be open.
* @param options Options.
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
async openFile(path: string): Promise<void> { async openFile(path: string, options: CoreUtilsOpenFileOptions = {}): Promise<void> {
// Convert the path to a native path if needed. // Convert the path to a native path if needed.
path = CoreFile.unconvertFileSrc(path); path = CoreFile.unconvertFileSrc(path);
@ -931,7 +933,12 @@ export class CoreUtilsProvider {
} }
try { try {
await FileOpener.open(path, mimetype || ''); const openFileAction = options.iOSOpenFileAction ?? CoreConstants.CONFIG.iOSDefaultOpenFileAction;
if (CoreApp.isIOS() && openFileAction == OpenFileAction.OPEN_WITH) {
await FileOpener.showOpenWithDialog(path, mimetype || '');
} else {
await FileOpener.open(path, mimetype || '');
}
} catch (error) { } catch (error) {
this.logger.error('Error opening file ' + path + ' with mimetype ' + mimetype); this.logger.error('Error opening file ' + path + ' with mimetype ' + mimetype);
this.logger.error('Error: ', JSON.stringify(error)); this.logger.error('Error: ', JSON.stringify(error));
@ -1703,3 +1710,18 @@ export type CoreMenuItem<T = number> = {
label: string; label: string;
value: T | number; value: T | number;
}; };
/**
* Options for opening a file.
*/
export type CoreUtilsOpenFileOptions = {
iOSOpenFileAction?: OpenFileAction; // Action to do when opening a file.
};
/**
* Possible default picker actions.
*/
export enum OpenFileAction {
OPEN = 'open',
OPEN_WITH = 'open-with',
}