+
+
+
+
+
+
+
+
+
+
+ {{ 'core.play' | translate }}
+
+
+
+ {{ 'addon.mod_resource.openthefile' | translate }}
+
+
+
+
+
+ {{ 'core.openwith' | translate }}
+
+
diff --git a/src/addons/mod/resource/components/index/index.scss b/src/addons/mod/resource/components/index/index.scss
new file mode 100644
index 000000000..b14ad8392
--- /dev/null
+++ b/src/addons/mod/resource/components/index/index.scss
@@ -0,0 +1,11 @@
+@import "~theme/globals";
+
+:host {
+ .addon-mod_resource-outdated {
+ @include padding(4px, 0px, 0px, 0px);
+
+ ion-icon {
+ font-size: 24px;
+ }
+ }
+}
diff --git a/src/addons/mod/resource/components/index/index.ts b/src/addons/mod/resource/components/index/index.ts
index 3987378fc..a5d38511a 100644
--- a/src/addons/mod/resource/components/index/index.ts
+++ b/src/addons/mod/resource/components/index/index.ts
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import { CoreConstants } from '@/core/constants';
import { Component, OnDestroy, OnInit, Optional } from '@angular/core';
import { CoreError } from '@classes/errors/error';
import { CoreCourseModuleMainResourceComponent } from '@features/course/classes/main-resource-component';
@@ -21,6 +22,7 @@ import { CoreCourseModulePrefetchDelegate } from '@features/course/services/modu
import { CoreApp } from '@services/app';
import { CoreFileHelper } from '@services/file-helper';
import { CoreSites } from '@services/sites';
+import { CoreDomUtils } from '@services/utils/dom';
import { CoreMimetypeUtils } from '@services/utils/mimetype';
import { CoreTextUtils } from '@services/utils/text';
import { CoreUtils, OpenFileAction } from '@services/utils/utils';
@@ -39,6 +41,7 @@ import { AddonModResourceHelper } from '../../services/resource-helper';
@Component({
selector: 'addon-mod-resource-index',
templateUrl: 'addon-mod-resource-index.html',
+ styleUrls: ['index.scss'],
})
export class AddonModResourceIndexComponent extends CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy {
@@ -55,6 +58,14 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource
isStreamedFile = false;
shouldOpenInBrowser = false;
+ // Variables for 'external' mode.
+ type = '';
+ readableSize = '';
+ timecreated = -1;
+ timemodified = -1;
+ isExternalFile = false;
+ outdatedStatus = CoreConstants.OUTDATED;
+
protected onlineObserver?: Subscription;
constructor(@Optional() courseContentsPage?: CoreCourseContentsPage) {
@@ -70,15 +81,13 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource
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();
- });
+ // 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 {
@@ -153,13 +162,25 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource
} else {
this.mode = 'external';
this.warning = '';
+ let mimetype: string;
if (this.isIOS) {
this.shouldOpenInBrowser = CoreFileHelper.shouldOpenInBrowser(contents[0]);
}
- const mimetype = await CoreUtils.getMimeTypeFromUrl(CoreFileHelper.getFileUrl(contents[0]));
+ if ('contentsinfo' in this.module && this.module.contentsinfo) {
+ mimetype = this.module.contentsinfo.mimetypes[0];
+ this.readableSize = CoreTextUtils.bytesToSize(this.module.contentsinfo.filessize, 1);
+ this.timemodified = this.module.contentsinfo.lastmodified * 1000;
+ } else {
+ mimetype = await CoreUtils.getMimeTypeFromUrl(CoreFileHelper.getFileUrl(contents[0]));
+ this.readableSize = CoreTextUtils.bytesToSize(contents[0].filesize, 1);
+ this.timemodified = contents[0].timemodified * 1000;
+ }
+ this.timecreated = contents[0].timecreated * 1000;
+ this.isExternalFile = !!contents[0].isexternalfile;
+ this.type = CoreMimetypeUtils.getMimetypeDescription(mimetype);
this.isStreamedFile = CoreMimetypeUtils.isStreamedMimetype(mimetype);
}
} finally {
@@ -183,6 +204,16 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource
downloadable = await AddonModResourceHelper.isMainFileDownloadable(this.module);
if (downloadable) {
+ if (this.prefetchStatus === CoreConstants.OUTDATED && !this.isOnline) {
+ // Warn the user that the file isn't updated.
+ const alert = await CoreDomUtils.showAlert(
+ undefined,
+ Translate.instant('addon.mod_resource.resourcestatusoutdated'),
+ );
+
+ await alert.onWillDismiss();
+ }
+
return AddonModResourceHelper.openModuleFile(this.module, this.courseId, { iOSOpenFileAction });
}
}
diff --git a/src/addons/mod/resource/lang.json b/src/addons/mod/resource/lang.json
index 2449d53e3..22626646e 100644
--- a/src/addons/mod/resource/lang.json
+++ b/src/addons/mod/resource/lang.json
@@ -3,5 +3,7 @@
"modifieddate": "Modified {{$a}}",
"modulenameplural": "Files",
"openthefile": "Open",
+ "resourcestatusoutdated": "This file has changed since you last opened it.",
+ "resourcestatusoutdatedconfirm": "There is a newer version of this file. To see it, please connect your device to the internet.",
"uploadeddate": "Uploaded {{$a}}"
}
diff --git a/src/assets/mimetypes.json b/src/assets/mimetypes.json
index f94347be4..a1b422dc5 100644
--- a/src/assets/mimetypes.json
+++ b/src/assets/mimetypes.json
@@ -1,7 +1,21 @@
{
+ "application/dash_xml": "Dynamic Adaptive Streaming over HTTP (MPEG-DASH)",
"application/epub_zip": "EPUB ebook",
+ "application/json": "{{$a.MIMETYPE2}} text",
"application/msword": "Word document",
"application/pdf": "PDF document",
+ "application/vnd.google-apps.audio": "Google Drive audio",
+ "application/vnd.google-apps.document": "Google Docs",
+ "application/vnd.google-apps.drawing": "Google Drawing",
+ "application/vnd.google-apps.file": "Google Drive file",
+ "application/vnd.google-apps.folder": "Google Drive folder",
+ "application/vnd.google-apps.form": "Google Forms",
+ "application/vnd.google-apps.fusiontable": "Google Fusion Tables",
+ "application/vnd.google-apps.presentation": "Google Slides",
+ "application/vnd.google-apps.script": "Google Apps Scripts",
+ "application/vnd.google-apps.site": "Google Sites",
+ "application/vnd.google-apps.spreadsheet": "Google Sheets",
+ "application/vnd.google-apps.video": "Google Drive video",
"application/vnd.moodle.backup": "Moodle backup",
"application/vnd.ms-excel": "Excel spreadsheet",
"application/vnd.ms-excel.sheet.macroEnabled.12": "Excel 2007 macro-enabled workbook",
@@ -20,6 +34,7 @@
"application/x-iwork-numbers-sffnumbers": "iWork Numbers spreadsheet",
"application/x-iwork-pages-sffpages": "iWork Pages document",
"application/x-javascript": "JavaScript source",
+ "application/x-mpegURL": "HTTP Live Streaming (HLS)",
"application/x-mspublisher": "Publisher document",
"application/x-shockwave-flash": "Flash animation",
"application/xhtml_xml": "XHTML document",
@@ -34,6 +49,8 @@
"group:html_track": "HTML track files",
"group:html_video": "Video files natively supported by browsers",
"group:image": "Image files",
+ "group:media_source": "Streaming media",
+ "group:optimised_image": "Image files to be optimised, such as badges",
"group:presentation": "Presentation files",
"group:sourcecode": "Source code",
"group:spreadsheet": "Spreadsheet files",
@@ -51,4 +68,4 @@
"text/rtf": "RTF document",
"text/vtt": "Web Video Text Track",
"video": "Video file ({{$a.EXT}})"
-}
\ No newline at end of file
+}
diff --git a/src/core/features/course/classes/main-resource-component.ts b/src/core/features/course/classes/main-resource-component.ts
index d8ae2c48d..24964fc75 100644
--- a/src/core/features/course/classes/main-resource-component.ts
+++ b/src/core/features/course/classes/main-resource-component.ts
@@ -67,6 +67,7 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
prefetchStatus?: string; // Used when calling fillContextMenu.
prefetchText?: string; // Used when calling fillContextMenu.
size?: string; // Used when calling fillContextMenu.
+ downloadTimeReadable?: string; // Last download time in a readable format. Used when calling fillContextMenu.
isDestroyed = false; // Whether the component is destroyed, used when calling fillContextMenu.
contextMenuStatusObserver?: CoreEventObserver; // Observer of package status, used when calling fillContextMenu.
contextFileStatusObserver?: CoreEventObserver; // Observer of file status, used when calling fillContextMenu.
diff --git a/src/core/features/course/services/course-helper.ts b/src/core/features/course/services/course-helper.ts
index cfde46f6f..737a405ae 100644
--- a/src/core/features/course/services/course-helper.ts
+++ b/src/core/features/course/services/course-helper.ts
@@ -905,17 +905,30 @@ export class CoreCourseHelperProvider {
}
if (!path) {
- path = await this.downloadModuleWithMainFile(
- module,
- courseId,
- fixedUrl,
- files,
- status,
- component,
- componentId,
- siteId,
- options,
- );
+ try {
+ path = await this.downloadModuleWithMainFile(
+ module,
+ courseId,
+ fixedUrl,
+ files,
+ status,
+ component,
+ componentId,
+ siteId,
+ options,
+ );
+ } catch (error) {
+ if (status !== CoreConstants.OUTDATED) {
+ throw error;
+ }
+
+ // Use the local file even if it's outdated.
+ try {
+ path = await CoreFilepool.getInternalUrlByUrl(siteId, mainFile.fileurl);
+ } catch {
+ throw error;
+ }
+ }
}
return {
@@ -1056,6 +1069,7 @@ export class CoreCourseHelperProvider {
instance.size = moduleInfo.sizeReadable;
instance.prefetchStatusIcon = moduleInfo.statusIcon;
instance.prefetchStatus = moduleInfo.status;
+ instance.downloadTimeReadable = CoreTextUtils.ucFirst(moduleInfo.downloadTimeReadable);
if (moduleInfo.status != CoreConstants.NOT_DOWNLOADABLE) {
// Module is downloadable, get the text to display to prefetch.
@@ -1478,6 +1492,8 @@ export class CoreCourseHelperProvider {
// Currently, some modules pass invalidateCache=false because they already invalidate data in downloadResourceIfNeeded.
// If this function is changed to do more actions if invalidateCache=true, please review those modules.
CoreCourseModulePrefetchDelegate.invalidateModuleStatusCache(module);
+
+ await CoreUtils.ignoreErrors(CoreCourseModulePrefetchDelegate.invalidateCourseUpdates(courseId));
}
const results = await Promise.all([
@@ -2191,6 +2207,7 @@ type ComponentWithContextMenu = {
size?: string;
prefetchStatus?: string;
prefetchText?: string;
+ downloadTimeReadable?: string;
contextMenuStatusObserver?: CoreEventObserver;
contextFileStatusObserver?: CoreEventObserver;
};
diff --git a/src/core/lang.json b/src/core/lang.json
index 43fab245f..6690dc4bb 100644
--- a/src/core/lang.json
+++ b/src/core/lang.json
@@ -64,6 +64,7 @@
"custom": "Custom",
"datastoredoffline": "Data stored in the device because it couldn't be sent. It will be sent automatically later.",
"date": "Date",
+ "datecreated": "Created",
"day": "day",
"days": "days",
"decsep": ".",
@@ -271,6 +272,7 @@
"showmore": "Show more...",
"site": "Site",
"sitemaintenance": "The site is undergoing maintenance and is currently not available",
+ "size": "Size",
"sizeb": "bytes",
"sizegb": "GB",
"sizekb": "KB",
@@ -309,6 +311,7 @@
"toggledelete": "Toggle delete buttons",
"tryagain": "Try again",
"twoparagraphs": "{{p1}}
{{p2}}",
+ "type": "Type",
"uhoh": "Uh oh!",
"unexpectederror": "Unexpected error. Please close and reopen the application then try again.",
"unicodenotsupported": "Some emojis are not supported on this site. Such characters will be removed when the message is sent.",