MOBILE-3761 ios: Allow open files with external app in iOS
parent
f3890cd916
commit
778ad6d803
|
@ -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",
|
||||||
|
|
|
@ -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(false)">
|
||||||
<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(true)">
|
||||||
|
<ion-icon name="far-share-square" slot="start" aria-hidden="true"></ion-icon>
|
||||||
|
{{ 'core.openwith' | translate }}
|
||||||
|
</ion-button>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
</core-loading>
|
</core-loading>
|
||||||
|
|
|
@ -20,6 +20,7 @@ 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 } from '@services/utils/utils';
|
||||||
|
@ -49,6 +50,7 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource
|
||||||
contentText = '';
|
contentText = '';
|
||||||
displayDescription = true;
|
displayDescription = true;
|
||||||
warning = '';
|
warning = '';
|
||||||
|
isIOS = false;
|
||||||
|
|
||||||
constructor(@Optional() courseContentsPage?: CoreCourseContentsPage) {
|
constructor(@Optional() courseContentsPage?: CoreCourseContentsPage) {
|
||||||
super('AddonModResourceIndexComponent', courseContentsPage);
|
super('AddonModResourceIndexComponent', courseContentsPage);
|
||||||
|
@ -61,6 +63,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 +158,10 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource
|
||||||
/**
|
/**
|
||||||
* Opens a file.
|
* Opens a file.
|
||||||
*
|
*
|
||||||
|
* @param useIOSPicker Whether to use the picker in iOS.
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
async open(): Promise<void> {
|
async open(useIOSPicker?: boolean): 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 +170,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, { useIOSPicker });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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}}"
|
||||||
}
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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()">
|
[canTrustDownload]="!alwaysDownload" (action)="download()">
|
||||||
</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">
|
[attr.aria-label]="'core.delete' | translate" color="danger">
|
||||||
<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>
|
||||||
|
|
|
@ -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 } 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.useIOSPicker = !this.defaultIsOpenWithPicker;
|
||||||
|
}
|
||||||
|
|
||||||
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();
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
@ -18,6 +18,12 @@
|
||||||
</ion-input>
|
</ion-input>
|
||||||
|
|
||||||
<div class="buttons" slot="end" *ngIf="manage">
|
<div class="buttons" slot="end" *ngIf="manage">
|
||||||
|
<ion-button fill="clear" *ngIf="isIOS" (click)="openFile($event, true)"
|
||||||
|
[attr.aria-label]="openButtonLabel | translate">
|
||||||
|
<ion-icon slot="icon-only" [name]="openButtonIcon" aria-hidden="true"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
|
|
||||||
|
<ng-container *ngIf="manage">
|
||||||
<ion-button *ngIf="!editMode" fill="clear" [core-suppress-events] (onClick)="activateEdit($event)"
|
<ion-button *ngIf="!editMode" fill="clear" [core-suppress-events] (onClick)="activateEdit($event)"
|
||||||
[attr.aria-label]="'core.edit' | translate" color="dark">
|
[attr.aria-label]="'core.edit' | translate" color="dark">
|
||||||
<ion-icon name="fas-pen" slot="icon-only" aria-hidden="true"></ion-icon>
|
<ion-icon name="fas-pen" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||||
|
@ -30,6 +36,7 @@
|
||||||
<ion-button fill="clear" (click)="deleteFile($event)" [attr.aria-label]="'core.delete' | translate" color="danger">
|
<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-icon name="fas-trash" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -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 } 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.useIOSPicker = !this.defaultIsOpenWithPicker;
|
||||||
|
}
|
||||||
|
|
||||||
|
CoreUtils.openFile(this.file!.toURL(), options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -173,6 +173,7 @@ export interface EnvironmentConfig {
|
||||||
displayqroncredentialscreen?: boolean;
|
displayqroncredentialscreen?: boolean;
|
||||||
displayqronsitescreen?: boolean;
|
displayqronsitescreen?: boolean;
|
||||||
forceOpenLinksIn: 'app' | 'browser';
|
forceOpenLinksIn: 'app' | 'browser';
|
||||||
|
iosopenfilepicker?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface EnvironmentBuild {
|
export interface EnvironmentBuild {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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": "",
|
||||||
|
|
|
@ -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 } 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.iosopenfilepicker;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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 {
|
||||||
|
const useIOSPicker = options.useIOSPicker ?? CoreConstants.CONFIG.iosopenfilepicker;
|
||||||
|
if (useIOSPicker && CoreApp.isIOS()) {
|
||||||
|
await FileOpener.showOpenWithDialog(path, mimetype || '');
|
||||||
|
} else {
|
||||||
await FileOpener.open(path, mimetype || '');
|
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,10 @@ export type CoreMenuItem<T = number> = {
|
||||||
label: string;
|
label: string;
|
||||||
value: T | number;
|
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).
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in New Issue