0 || downloadEnabled">
+
-
-
- 0">
-
-
-
-
-
-
-
- 0">
-
- {{ module.totalSize | coreBytesToSize }}
-
-
+
+
+
+ 0)">
+
+
+
+
+
+
+
+ 0">
+
+ {{ module.totalSize | coreBytesToSize }}
+
+
+ {{ 'core.calculating' | translate }}
+
+
-
-
-
- 0"
- color="danger">
-
-
-
-
-
+
+
+
+ 0" color="danger">
+
+
+
+
+
+
diff --git a/src/addons/storagemanager/pages/course-storage/course-storage.ts b/src/addons/storagemanager/pages/course-storage/course-storage.ts
index aa14b5269..6aa817913 100644
--- a/src/addons/storagemanager/pages/course-storage/course-storage.ts
+++ b/src/addons/storagemanager/pages/course-storage/course-storage.ts
@@ -13,7 +13,7 @@
// limitations under the License.
import { CoreConstants } from '@/core/constants';
-import { Component, OnDestroy, OnInit } from '@angular/core';
+import { Component, ElementRef, OnDestroy, OnInit } from '@angular/core';
import { CoreCourse, CoreCourseProvider } from '@features/course/services/course';
import {
CoreCourseHelper,
@@ -30,6 +30,7 @@ import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils';
import { Translate } from '@singletons';
+import { CoreDom } from '@singletons/dom';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
/**
@@ -48,6 +49,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
loaded = false;
sections: AddonStorageManagerCourseSection[] = [];
totalSize = 0;
+ calculatingSize = true;
downloadEnabled = false;
downloadCourseEnabled = false;
@@ -61,6 +63,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
statusDownloaded = CoreConstants.DOWNLOADED;
+ protected initialSectionId?: number;
protected siteUpdatedObserver?: CoreEventObserver;
protected courseStatusObserver?: CoreEventObserver;
protected sectionStatusObserver?: CoreEventObserver;
@@ -68,7 +71,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
protected isDestroyed = false;
protected isGuest = false;
- constructor() {
+ constructor(protected elementRef: ElementRef) {
// Refresh the enabled flags if site is updated.
this.siteUpdatedObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
@@ -99,21 +102,38 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
}
this.isGuest = !!CoreNavigator.getRouteBooleanParam('isGuest');
+ this.initialSectionId = CoreNavigator.getRouteNumberParam('sectionId');
this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
this.downloadEnabled = !CoreSites.getRequiredCurrentSite().isOfflineDisabled();
- const sections = await CoreCourse.getSections(this.courseId, false, true);
+ const sections = (await CoreCourse.getSections(this.courseId, false, true))
+ .filter((section) => !CoreCourseHelper.isSectionStealth(section));
this.sections = (await CoreCourseHelper.addHandlerDataForModules(sections, this.courseId)).sections
- .map((section) => ({ ...section, totalSize: 0 }));
+ .map(section => ({
+ ...section,
+ totalSize: 0,
+ calculatingSize: true,
+ expanded: section.id === this.initialSectionId,
+ modules: section.modules.map(module => ({
+ ...module,
+ calculatingSize: true,
+ })),
+ }));
+
+ this.loaded = true;
+
+ CoreDom.scrollToElement(
+ this.elementRef.nativeElement,
+ '.core-course-storage-section-expanded',
+ { addYAxis: -10 },
+ );
await Promise.all([
- this.loadSizes(),
+ this.initSizes(),
this.initCoursePrefetch(),
this.initModulePrefetch(),
]);
-
- this.loaded = true;
}
/**
@@ -239,15 +259,9 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
/**
* Init section, course and modules sizes.
*/
- protected async loadSizes(): Promise
{
- this.totalSize = 0;
-
- const promises: Promise[] = [];
- this.sections.forEach((section) => {
- section.totalSize = 0;
- section.modules.forEach((module) => {
- module.totalSize = 0;
-
+ protected async initSizes(): Promise {
+ await Promise.all(this.sections.map(async (section) => {
+ await Promise.all(section.modules.map(async (module) => {
// Note: This function only gets the size for modules which are downloadable.
// For other modules it always returns 0, even if they have downloaded some files.
// However there is no 100% reliable way to actually track the files in this case.
@@ -255,21 +269,22 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
// But these aren't necessarily consistent, for example mod_frog vs mmaModFrog.
// There is nothing enforcing correct values.
// Most modules which have large files are downloadable, so I think this is sufficient.
- const promise = CoreCourseModulePrefetchDelegate.getModuleStoredSize(module, this.courseId).then((size) => {
- // There are some cases where the return from this is not a valid number.
- if (!isNaN(size)) {
- module.totalSize = Number(size);
- section.totalSize += size;
- this.totalSize += size;
- }
+ const size = await CoreCourseModulePrefetchDelegate.getModuleStoredSize(module, this.courseId);
- return;
- });
- promises.push(promise);
- });
- });
+ // There are some cases where the return from this is not a valid number.
+ if (!isNaN(size)) {
+ module.totalSize = Number(size);
+ section.totalSize += size;
+ this.totalSize += size;
+ }
- await Promise.all(promises);
+ module.calculatingSize = false;
+ }));
+
+ section.calculatingSize = false;
+ }));
+
+ this.calculatingSize = false;
// Mark course as not downloaded if course size is 0.
if (this.totalSize == 0) {
@@ -277,6 +292,56 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
}
}
+ /**
+ * Update the sizes of some modules.
+ *
+ * @param modules Modules.
+ * @param section Section the modules belong to.
+ * @return Promise resolved when done.
+ */
+ protected async updateModulesSizes(
+ modules: AddonStorageManagerModule[],
+ section?: AddonStorageManagerCourseSection,
+ ): Promise {
+ this.calculatingSize = true;
+
+ await Promise.all(modules.map(async (module) => {
+ if (module.calculatingSize) {
+ return;
+ }
+
+ module.calculatingSize = true;
+
+ if (!section) {
+ section = this.sections.find((section) => section.modules.some((mod) => mod.id === module.id));
+ if (section) {
+ section.calculatingSize = true;
+ }
+ }
+
+ try {
+ const size = await CoreCourseModulePrefetchDelegate.getModuleStoredSize(module, this.courseId);
+
+ const diff = (isNaN(size) ? 0 : size) - (module.totalSize ?? 0);
+
+ module.totalSize = Number(size);
+ this.totalSize += diff;
+ if (section) {
+ section.totalSize += diff;
+ }
+ } catch {
+ // Ignore errors, it shouldn't happen.
+ } finally {
+ module.calculatingSize = false;
+ }
+ }));
+
+ this.calculatingSize = false;
+ if (section) {
+ section.calculatingSize = false;
+ }
+ }
+
/**
* The user has requested a delete for the whole course data.
*
@@ -401,7 +466,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
} finally {
modal.dismiss();
- await this.loadSizes();
+ await this.updateModulesSizes(modules, section);
CoreCourseHelper.calculateSectionsStatus(this.sections, this.courseId, false, false);
}
}
@@ -450,7 +515,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
CoreDomUtils.showErrorModalDefault(error, 'core.course.errordownloadingsection', true);
}
} finally {
- await this.loadSizes();
+ await this.updateModulesSizes(section.modules, section);
}
} catch (error) {
// User cancelled or there was an error calculating the size.
@@ -496,7 +561,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
} finally {
module.spinner = false;
- await this.loadSizes();
+ await this.updateModulesSizes([module]);
}
}
@@ -587,6 +652,18 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
}
}
+ /**
+ * Toggle expand status.
+ *
+ * @param event Event object.
+ * @param section Section to expand / collapse.
+ */
+ toggleExpand(event: Event, section: AddonStorageManagerCourseSection): void {
+ section.expanded = !section.expanded;
+ event.stopPropagation();
+ event.preventDefault();
+ }
+
/**
* @inheritdoc
*/
@@ -608,11 +685,14 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
type AddonStorageManagerCourseSection = Omit & {
totalSize: number;
+ calculatingSize: boolean;
+ expanded: boolean;
modules: AddonStorageManagerModule[];
};
type AddonStorageManagerModule = CoreCourseModuleData & {
totalSize?: number;
+ calculatingSize: boolean;
prefetchHandler?: CoreCourseModulePrefetchHandler;
spinner?: boolean;
downloadStatus?: string;
diff --git a/src/core/features/contentlinks/services/contentlinks-helper.ts b/src/core/features/contentlinks/services/contentlinks-helper.ts
index 8c5ccd06c..e4d6379dd 100644
--- a/src/core/features/contentlinks/services/contentlinks-helper.ts
+++ b/src/core/features/contentlinks/services/contentlinks-helper.ts
@@ -21,6 +21,7 @@ import { makeSingleton, Translate } from '@singletons';
import { CoreNavigator } from '@services/navigator';
import { Params } from '@angular/router';
import { CoreContentLinksChooseSiteModalComponent } from '../components/choose-site-modal/choose-site-modal';
+import { CoreCustomURLSchemes } from '@services/urlschemes';
/**
* Service that provides some features regarding content links.
@@ -138,6 +139,12 @@ export class CoreContentLinksHelperProvider {
openBrowserRoot?: boolean,
): Promise {
try {
+ if (CoreCustomURLSchemes.isCustomURL(url)) {
+ await CoreCustomURLSchemes.handleCustomURL(url);
+
+ return true;
+ }
+
if (checkRoot) {
const data = await CoreSites.isStoredRootURL(url, username);
diff --git a/src/core/features/course/components/course-format/course-format.html b/src/core/features/course/components/course-format/course-format.html
index e796412d3..323c1dbfc 100644
--- a/src/core/features/course/components/course-format/course-format.html
+++ b/src/core/features/course/components/course-format/course-format.html
@@ -1,3 +1,8 @@
+
+
+
+
+
diff --git a/src/core/features/course/components/course-format/course-format.ts b/src/core/features/course/components/course-format/course-format.ts
index dca3d5443..c1bee68f6 100644
--- a/src/core/features/course/components/course-format/course-format.ts
+++ b/src/core/features/course/components/course-format/course-format.ts
@@ -391,29 +391,38 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
);
}
+ /**
+ * Get selected section ID. If viewing all sections, use current scrolled section.
+ *
+ * @return Section ID, undefined if not found.
+ */
+ protected async getSelectedSectionId(): Promise {
+ if (this.selectedSection?.id !== this.allSectionsId) {
+ return this.selectedSection?.id;
+ }
+
+ // Check current scrolled section.
+ const allSectionElements: NodeListOf =
+ this.elementRef.nativeElement.querySelectorAll('section.core-course-module-list-wrapper');
+
+ const scroll = await this.content.getScrollElement();
+ const containerTop = scroll.getBoundingClientRect().top;
+
+ const element = Array.from(allSectionElements).find((element) => {
+ const position = element.getBoundingClientRect();
+
+ // The bottom is inside the container or lower.
+ return position.bottom >= containerTop;
+ });
+
+ return Number(element?.getAttribute('id')) || undefined;
+ }
+
/**
* Display the course index modal.
*/
async openCourseIndex(): Promise {
- let selectedId = this.selectedSection?.id;
-
- if (selectedId == this.allSectionsId) {
- // Check current scrolled section.
- const allSectionElements: NodeListOf =
- this.elementRef.nativeElement.querySelectorAll('section.section-wrapper');
-
- const scroll = await this.content.getScrollElement();
- const containerTop = scroll.getBoundingClientRect().top;
-
- const element = Array.from(allSectionElements).find((element) => {
- const position = element.getBoundingClientRect();
-
- // The bottom is inside the container or lower.
- return position.bottom >= containerTop;
- });
-
- selectedId = Number(element?.getAttribute('id')) || undefined;
- }
+ const selectedId = await this.getSelectedSectionId();
const data = await CoreDomUtils.openModal({
component: CoreCourseCourseIndexComponent,
@@ -453,6 +462,23 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
this.moduleId = data.moduleId;
}
+ /**
+ * Open course downloads page.
+ */
+ async gotoCourseDownloads(): Promise {
+ const selectedId = await this.getSelectedSectionId();
+
+ CoreNavigator.navigateToSitePath(
+ `storage/${this.course.id}`,
+ {
+ params: {
+ title: this.course.fullname,
+ sectionId: selectedId,
+ },
+ },
+ );
+ }
+
/**
* Function called when selected section changes.
*
diff --git a/src/core/features/course/components/course-index/course-index.html b/src/core/features/course/components/course-index/course-index.html
index 892a63767..8de92a89b 100644
--- a/src/core/features/course/components/course-index/course-index.html
+++ b/src/core/features/course/components/course-index/course-index.html
@@ -48,34 +48,36 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
diff --git a/src/core/features/course/pages/contents/contents.html b/src/core/features/course/pages/contents/contents.html
index bc3f404f8..2d0eeb1db 100644
--- a/src/core/features/course/pages/contents/contents.html
+++ b/src/core/features/course/pages/contents/contents.html
@@ -1,8 +1,3 @@
-
-
-
-
-
diff --git a/src/core/features/course/pages/contents/contents.ts b/src/core/features/course/pages/contents/contents.ts
index 81953a209..7413a0444 100644
--- a/src/core/features/course/pages/contents/contents.ts
+++ b/src/core/features/course/pages/contents/contents.ts
@@ -366,14 +366,6 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
}
}
- gotoCourseDownloads(): void {
- CoreNavigator.navigateToSitePath(
- `storage/${this.course.id}`,
- { params: { title: this.course.fullname } },
- );
-
- }
-
/**
* @inheritdoc
*/
diff --git a/src/core/initializers/inject-ios-scripts.ts b/src/core/initializers/inject-ios-scripts.ts
index 64b26993d..cfd717666 100644
--- a/src/core/initializers/inject-ios-scripts.ts
+++ b/src/core/initializers/inject-ios-scripts.ts
@@ -17,10 +17,11 @@ import { CoreIframeUtils } from '@services/utils/iframe';
import { Platform } from '@singletons';
export default async function(): Promise {
+ await Platform.ready();
+
if (!CoreApp.isIOS() || !('WKUserScript' in window)) {
return;
}
- await Platform.ready();
CoreIframeUtils.injectiOSScripts(window);
}
diff --git a/src/core/lang.json b/src/core/lang.json
index 8dd623b4e..a66c1a6a3 100644
--- a/src/core/lang.json
+++ b/src/core/lang.json
@@ -13,6 +13,7 @@
"areyousure": "Are you sure?",
"back": "Back",
"browser": "Browser",
+ "calculating": "Calculating",
"cancel": "Cancel",
"cannotconnect": "Cannot connect",
"cannotconnecttrouble": "We're having trouble connecting to your site.",
diff --git a/src/core/services/utils/iframe.ts b/src/core/services/utils/iframe.ts
index 1faf9308e..08745cb44 100644
--- a/src/core/services/utils/iframe.ts
+++ b/src/core/services/utils/iframe.ts
@@ -446,26 +446,13 @@ export class CoreIframeUtilsProvider {
} else {
element.setAttribute('src', url);
}
- } else if (CoreUrlUtils.isLocalFileUrl(url)) {
- // It's a local file.
- const filename = url.substring(url.lastIndexOf('/') + 1);
-
- if (!CoreFileHelper.isOpenableInApp({ filename })) {
- try {
- await CoreFileHelper.showConfirmOpenUnsupportedFile();
- } catch (error) {
- return; // Cancelled, stop.
- }
- }
-
+ } else {
try {
- await CoreUtils.openFile(url);
+ // It's an external link or a local file, check if it can be opened in the app.
+ await CoreWindow.open(url, name);
} catch (error) {
CoreDomUtils.showErrorModal(error);
}
- } else {
- // It's an external link, check if it can be opened in the app.
- await CoreWindow.open(url, name);
}
}