forked from CIT/Vmeda.Online
		
	MOBILE-3930 course: Store and display modules viewed and last
This commit is contained in:
		
							parent
							
								
									d10fa5db3c
								
							
						
					
					
						commit
						b72e247f81
					
				@ -1559,6 +1559,7 @@
 | 
			
		||||
  "core.course.highlighted": "moodle",
 | 
			
		||||
  "core.course.insufficientavailablequota": "local_moodlemobileapp",
 | 
			
		||||
  "core.course.insufficientavailablespace": "local_moodlemobileapp",
 | 
			
		||||
  "core.course.lastaccessedactivity": "local_moodlemobileapp",
 | 
			
		||||
  "core.course.manualcompletionnotsynced": "local_moodlemobileapp",
 | 
			
		||||
  "core.course.modulenotfound": "local_moodlemobileapp",
 | 
			
		||||
  "core.course.nocontentavailable": "local_moodlemobileapp",
 | 
			
		||||
 | 
			
		||||
@ -49,17 +49,41 @@ export class AddonBlockRecentlyAccessedItemsProvider {
 | 
			
		||||
            cacheKey: this.getRecentItemsCacheKey(),
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const items: AddonBlockRecentlyAccessedItemsItem[] =
 | 
			
		||||
        let items: AddonBlockRecentlyAccessedItemsItem[] =
 | 
			
		||||
            await site.read('block_recentlyaccesseditems_get_recent_items', undefined, preSets);
 | 
			
		||||
 | 
			
		||||
        return await Promise.all(items.map(async (item) => {
 | 
			
		||||
        const cmIds: number[] = [];
 | 
			
		||||
 | 
			
		||||
        items = await Promise.all(items.map(async (item) => {
 | 
			
		||||
            const modicon = item.icon && CoreDomUtils.getHTMLElementAttribute(item.icon, 'src');
 | 
			
		||||
 | 
			
		||||
            item.iconUrl = await CoreCourse.getModuleIconSrc(item.modname, modicon || undefined);
 | 
			
		||||
            item.iconTitle = item.icon && CoreDomUtils.getHTMLElementAttribute(item.icon, 'title');
 | 
			
		||||
            cmIds.push(item.cmid);
 | 
			
		||||
 | 
			
		||||
            return item;
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
        // Check if the viewed module should be updated for each activity.
 | 
			
		||||
        const lastViewedMap = await CoreCourse.getCertainModulesViewed(cmIds, site.getId());
 | 
			
		||||
 | 
			
		||||
        items.forEach((recentItem) => {
 | 
			
		||||
            const timeAccess = recentItem.timeaccess * 1000;
 | 
			
		||||
            const lastViewed = lastViewedMap[recentItem.cmid];
 | 
			
		||||
 | 
			
		||||
            if (lastViewed && lastViewed.timeaccess >= timeAccess) {
 | 
			
		||||
                return; // No need to update.
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Update access.
 | 
			
		||||
            CoreCourse.storeModuleViewed(recentItem.courseid, recentItem.cmid, {
 | 
			
		||||
                timeaccess: recentItem.timeaccess * 1000,
 | 
			
		||||
                sectionId: lastViewed && lastViewed.sectionId,
 | 
			
		||||
                siteId: site.getId(),
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return items;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -391,7 +391,7 @@ export class AddonModBookProvider {
 | 
			
		||||
    async storeLastChapterViewed(id: number, chapterId: number, courseId: number, siteId?: string): Promise<void> {
 | 
			
		||||
        const site = await CoreSites.getSite(siteId);
 | 
			
		||||
 | 
			
		||||
        await site.storeLastViewed(AddonModBookProvider.COMPONENT, id, chapterId, String(courseId));
 | 
			
		||||
        await site.storeLastViewed(AddonModBookProvider.COMPONENT, id, chapterId, { data: String(courseId) });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -204,7 +204,10 @@ export class AddonModDataEntryPage implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
            if (this.logAfterFetch) {
 | 
			
		||||
                this.logAfterFetch = false;
 | 
			
		||||
                this.logView();
 | 
			
		||||
                await CoreUtils.ignoreErrors(AddonModData.logView(this.database.id, this.database.name));
 | 
			
		||||
 | 
			
		||||
                // Store module viewed. It's done in this page because it can be reached using a link.
 | 
			
		||||
                CoreCourse.storeModuleViewed(this.courseId, this.moduleId);
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            if (!refresh) {
 | 
			
		||||
@ -402,19 +405,6 @@ export class AddonModDataEntryPage implements OnInit, OnDestroy {
 | 
			
		||||
        AddonModData.invalidateEntryData(this.database!.id, this.entryId!);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Log viewing the activity.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async logView(): Promise<void> {
 | 
			
		||||
        if (!this.database || !this.database.id) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await CoreUtils.ignoreErrors(AddonModData.logView(this.database.id, this.database.name));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Component being destroyed.
 | 
			
		||||
     */
 | 
			
		||||
 | 
			
		||||
@ -16,6 +16,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
 | 
			
		||||
import { ActivatedRouteSnapshot } from '@angular/router';
 | 
			
		||||
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
 | 
			
		||||
import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { CoreTextUtils } from '@services/utils/text';
 | 
			
		||||
@ -49,6 +50,7 @@ export class AddonModFeedbackAttemptPage implements OnInit, OnDestroy {
 | 
			
		||||
    loaded = false;
 | 
			
		||||
 | 
			
		||||
    protected attemptId: number;
 | 
			
		||||
    protected fetchSuccess = false;
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
 | 
			
		||||
@ -131,6 +133,11 @@ export class AddonModFeedbackAttemptPage implements OnInit, OnDestroy {
 | 
			
		||||
                return attemptItem;
 | 
			
		||||
            }).filter((itemData) => itemData); // Filter items with errors.
 | 
			
		||||
 | 
			
		||||
            if (!this.fetchSuccess) {
 | 
			
		||||
                this.fetchSuccess = true;
 | 
			
		||||
                // Store module viewed. It's done in this page because it can be reached using a link.
 | 
			
		||||
                CoreCourse.storeModuleViewed(this.courseId, this.cmId);
 | 
			
		||||
            }
 | 
			
		||||
        } catch (message) {
 | 
			
		||||
            // Some call failed on fetch, go back.
 | 
			
		||||
            CoreDomUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true);
 | 
			
		||||
 | 
			
		||||
@ -25,6 +25,7 @@ import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { AddonModFeedbackAttemptItem, AddonModFeedbackAttemptsSource } from '../../classes/feedback-attempts-source';
 | 
			
		||||
import { AddonModFeedbackWSAnonAttempt, AddonModFeedbackWSAttempt } from '../../services/feedback';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Page that displays feedback attempts.
 | 
			
		||||
@ -37,14 +38,14 @@ export class AddonModFeedbackAttemptsPage implements AfterViewInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
    @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
 | 
			
		||||
 | 
			
		||||
    promisedAttempts: CorePromisedValue<CoreListItemsManager<AddonModFeedbackAttemptItem, AddonModFeedbackAttemptsSource>>;
 | 
			
		||||
    promisedAttempts: CorePromisedValue<AddonModFeedbackAttemptsManager>;
 | 
			
		||||
    fetchFailed = false;
 | 
			
		||||
 | 
			
		||||
    constructor(protected route: ActivatedRoute) {
 | 
			
		||||
        this.promisedAttempts = new CorePromisedValue();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get attempts(): CoreListItemsManager<AddonModFeedbackAttemptItem, AddonModFeedbackAttemptsSource> | null {
 | 
			
		||||
    get attempts(): AddonModFeedbackAttemptsManager | null {
 | 
			
		||||
        return this.promisedAttempts.value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -95,7 +96,7 @@ export class AddonModFeedbackAttemptsPage implements AfterViewInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
            source.selectedGroup = CoreNavigator.getRouteNumberParam('group') || 0;
 | 
			
		||||
 | 
			
		||||
            this.promisedAttempts.resolve(new CoreListItemsManager(source, this.route.component));
 | 
			
		||||
            this.promisedAttempts.resolve(new AddonModFeedbackAttemptsManager(source, this.route.component));
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.showErrorModal(error);
 | 
			
		||||
 | 
			
		||||
@ -181,3 +182,18 @@ export class AddonModFeedbackAttemptsPage implements AfterViewInit, OnDestroy {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Attempts manager.
 | 
			
		||||
 */
 | 
			
		||||
class AddonModFeedbackAttemptsManager extends CoreListItemsManager<AddonModFeedbackAttemptItem, AddonModFeedbackAttemptsSource> {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected async logActivity(): Promise<void> {
 | 
			
		||||
        // Store module viewed. It's done in this page because it can be reached using a link.
 | 
			
		||||
        CoreCourse.storeModuleViewed(this.getSource().COURSE_ID, this.getSource().CM_ID);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -13,6 +13,7 @@
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Component, OnInit } from '@angular/core';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
import { IonRefresher } from '@ionic/angular';
 | 
			
		||||
import { CoreGroupInfo, CoreGroups } from '@services/groups';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
@ -34,6 +35,7 @@ export class AddonModFeedbackNonRespondentsPage implements OnInit {
 | 
			
		||||
    protected courseId!: number;
 | 
			
		||||
    protected feedback?: AddonModFeedbackWSFeedback;
 | 
			
		||||
    protected page = 0;
 | 
			
		||||
    protected fetchSuccess = false;
 | 
			
		||||
 | 
			
		||||
    selectedGroup!: number;
 | 
			
		||||
    groupInfo?: CoreGroupInfo;
 | 
			
		||||
@ -81,6 +83,12 @@ export class AddonModFeedbackNonRespondentsPage implements OnInit {
 | 
			
		||||
            this.selectedGroup = CoreGroups.validateGroupId(this.selectedGroup, this.groupInfo);
 | 
			
		||||
 | 
			
		||||
            await this.loadGroupUsers(this.selectedGroup);
 | 
			
		||||
 | 
			
		||||
            if (!this.fetchSuccess) {
 | 
			
		||||
                this.fetchSuccess = true;
 | 
			
		||||
                // Store module viewed. It's done in this page because it can be reached using a link.
 | 
			
		||||
                CoreCourse.storeModuleViewed(this.courseId, this.cmId);
 | 
			
		||||
            }
 | 
			
		||||
        } catch (message) {
 | 
			
		||||
            CoreDomUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -17,6 +17,7 @@ import { Component, OnDestroy, ViewChild, OnInit, AfterViewInit, ElementRef, Opt
 | 
			
		||||
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
 | 
			
		||||
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
 | 
			
		||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
import { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
 | 
			
		||||
import { CoreRatingInfo, CoreRatingProvider } from '@features/rating/services/rating';
 | 
			
		||||
import { CoreRatingOffline } from '@features/rating/services/rating-offline';
 | 
			
		||||
@ -118,6 +119,7 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
 | 
			
		||||
    protected ratingOfflineObserver?: CoreEventObserver;
 | 
			
		||||
    protected ratingSyncObserver?: CoreEventObserver;
 | 
			
		||||
    protected changeDiscObserver?: CoreEventObserver;
 | 
			
		||||
    protected fetchSuccess = false;
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        @Optional() protected splitView: CoreSplitViewComponent,
 | 
			
		||||
@ -547,6 +549,12 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
 | 
			
		||||
 | 
			
		||||
            this.hasOfflineRatings =
 | 
			
		||||
                await CoreRatingOffline.hasRatings('mod_forum', 'post', ContextLevel.MODULE, this.cmId, this.discussionId);
 | 
			
		||||
 | 
			
		||||
            if (!this.fetchSuccess) {
 | 
			
		||||
                this.fetchSuccess = true;
 | 
			
		||||
                // Store module viewed. It's done in this page because it can be reached using a link.
 | 
			
		||||
                this.courseId && this.cmId && CoreCourse.storeModuleViewed(this.courseId, this.cmId);
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.showErrorModal(error);
 | 
			
		||||
        } finally {
 | 
			
		||||
 | 
			
		||||
@ -17,6 +17,7 @@ import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
 | 
			
		||||
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
 | 
			
		||||
import { CoreCommentsCommentsComponent } from '@features/comments/components/comments/comments';
 | 
			
		||||
import { CoreComments } from '@features/comments/services/comments';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
import { CoreRatingInfo } from '@features/rating/services/rating';
 | 
			
		||||
import { CoreTag } from '@features/tag/services/tag';
 | 
			
		||||
import { IonRefresher } from '@ionic/angular';
 | 
			
		||||
@ -55,8 +56,10 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy {
 | 
			
		||||
    tagsEnabled = false;
 | 
			
		||||
    commentsEnabled = false;
 | 
			
		||||
    courseId!: number;
 | 
			
		||||
    cmId?: number;
 | 
			
		||||
 | 
			
		||||
    protected entryId!: number;
 | 
			
		||||
    protected fetchSuccess = false;
 | 
			
		||||
 | 
			
		||||
    constructor(protected route: ActivatedRoute) {}
 | 
			
		||||
 | 
			
		||||
@ -72,15 +75,17 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy {
 | 
			
		||||
            this.commentsEnabled = !CoreComments.areCommentsDisabledInSite();
 | 
			
		||||
 | 
			
		||||
            if (routeData.swipeEnabled ?? true) {
 | 
			
		||||
                const cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
 | 
			
		||||
                this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
 | 
			
		||||
                const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
 | 
			
		||||
                    AddonModGlossaryEntriesSource,
 | 
			
		||||
                    [this.courseId, cmId, routeData.glossaryPathPrefix ?? ''],
 | 
			
		||||
                    [this.courseId, this.cmId, routeData.glossaryPathPrefix ?? ''],
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                this.entries = new AddonModGlossaryEntryEntriesSwipeManager(source);
 | 
			
		||||
 | 
			
		||||
                await this.entries.start();
 | 
			
		||||
            } else {
 | 
			
		||||
                this.cmId = CoreNavigator.getRouteNumberParam('cmId');
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.showErrorModal(error);
 | 
			
		||||
@ -143,6 +148,12 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy {
 | 
			
		||||
            this.entry = result.entry;
 | 
			
		||||
            this.ratingInfo = result.ratinginfo;
 | 
			
		||||
 | 
			
		||||
            if (!this.fetchSuccess) {
 | 
			
		||||
                this.fetchSuccess = true;
 | 
			
		||||
                // Store module viewed. It's done in this page because it can be reached using a link.
 | 
			
		||||
                this.cmId && CoreCourse.storeModuleViewed(this.courseId, this.cmId);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (this.glossary) {
 | 
			
		||||
                // Glossary already loaded, nothing else to load.
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
@ -25,6 +25,7 @@ import {
 | 
			
		||||
    AddonModH5PActivityData,
 | 
			
		||||
    AddonModH5PActivityAttemptResults,
 | 
			
		||||
} from '../../services/h5pactivity';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Page that displays results of an attempt.
 | 
			
		||||
@ -99,6 +100,9 @@ export class AddonModH5PActivityAttemptResultsPage implements OnInit {
 | 
			
		||||
                    this.h5pActivity.name,
 | 
			
		||||
                    { attemptId: this.attemptId },
 | 
			
		||||
                ));
 | 
			
		||||
 | 
			
		||||
                // Store module viewed. It's done in this page because it can be reached using a link.
 | 
			
		||||
                CoreCourse.storeModuleViewed(this.courseId, this.cmId);
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.showErrorModalDefault(error, 'Error loading attempt.');
 | 
			
		||||
 | 
			
		||||
@ -26,6 +26,7 @@ import {
 | 
			
		||||
    AddonModH5PActivityData,
 | 
			
		||||
    AddonModH5PActivityUserAttempts,
 | 
			
		||||
} from '../../services/h5pactivity';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Page that displays user attempts of a certain user.
 | 
			
		||||
@ -101,6 +102,9 @@ export class AddonModH5PActivityUserAttemptsPage implements OnInit {
 | 
			
		||||
                    this.h5pActivity.name,
 | 
			
		||||
                    { userId: this.userId },
 | 
			
		||||
                ));
 | 
			
		||||
 | 
			
		||||
                // Store module viewed. It's done in this page because it can be reached using a link.
 | 
			
		||||
                CoreCourse.storeModuleViewed(this.courseId, this.cmId);
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.showErrorModalDefault(error, 'Error loading attempts.');
 | 
			
		||||
 | 
			
		||||
@ -13,6 +13,7 @@
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Component, OnInit } from '@angular/core';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
import { CoreUser, CoreUserProfile } from '@features/user/services/user';
 | 
			
		||||
import { IonRefresher } from '@ionic/angular';
 | 
			
		||||
 | 
			
		||||
@ -92,6 +93,9 @@ export class AddonModH5PActivityUsersAttemptsPage implements OnInit {
 | 
			
		||||
            if (!this.fetchSuccess) {
 | 
			
		||||
                this.fetchSuccess = true;
 | 
			
		||||
                CoreUtils.ignoreErrors(AddonModH5PActivity.logViewReport(this.h5pActivity.id, this.h5pActivity.name));
 | 
			
		||||
 | 
			
		||||
                // Store module viewed. It's done in this page because it can be reached using a link.
 | 
			
		||||
                CoreCourse.storeModuleViewed(this.courseId, this.cmId);
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.showErrorModalDefault(error, 'Error loading attempts.');
 | 
			
		||||
 | 
			
		||||
@ -303,7 +303,7 @@ export class AddonModImscpProvider {
 | 
			
		||||
    async storeLastItemViewed(id: number, href: string, courseId: number, siteId?: string): Promise<void> {
 | 
			
		||||
        const site = await CoreSites.getSite(siteId);
 | 
			
		||||
 | 
			
		||||
        await site.storeLastViewed(AddonModImscpProvider.COMPONENT, id, href, String(courseId));
 | 
			
		||||
        await site.storeLastViewed(AddonModImscpProvider.COMPONENT, id, href, { data: String(courseId) });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -35,6 +35,7 @@ import {
 | 
			
		||||
} from '../../services/lesson';
 | 
			
		||||
import { AddonModLessonAnswerData, AddonModLessonHelper } from '../../services/lesson-helper';
 | 
			
		||||
import { CoreTimeUtils } from '@services/utils/time';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Page that displays a retake made by a certain user.
 | 
			
		||||
@ -59,6 +60,7 @@ export class AddonModLessonUserRetakePage implements OnInit {
 | 
			
		||||
    protected userId?: number; // User ID to see the retakes.
 | 
			
		||||
    protected retakeNumber?: number; // Number of the initial retake to see.
 | 
			
		||||
    protected previousSelectedRetake?: number; // To be able to detect the previous selected retake when it has changed.
 | 
			
		||||
    protected fetchSuccess = false;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Component being initialized.
 | 
			
		||||
@ -160,6 +162,12 @@ export class AddonModLessonUserRetakePage implements OnInit {
 | 
			
		||||
            this.student.profileimageurl = user?.profileimageurl;
 | 
			
		||||
 | 
			
		||||
            await this.setRetake(this.selectedRetake);
 | 
			
		||||
 | 
			
		||||
            if (!this.fetchSuccess) {
 | 
			
		||||
                this.fetchSuccess = true;
 | 
			
		||||
                // Store module viewed. It's done in this page because it can be reached using a link.
 | 
			
		||||
                CoreCourse.storeModuleViewed(this.courseId, this.cmId);
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.showErrorModalDefault(error, 'Error getting data.', true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -21,6 +21,7 @@ import { makeSingleton } from '@singletons';
 | 
			
		||||
import { AddonModLtiHelper } from '../lti-helper';
 | 
			
		||||
import { AddonModLtiIndexComponent } from '../../components/index';
 | 
			
		||||
import { CoreModuleHandlerBase } from '@features/course/classes/module-base-handler';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Handler to support LTI modules.
 | 
			
		||||
@ -67,6 +68,8 @@ export class AddonModLtiModuleHandlerService extends CoreModuleHandlerBase imple
 | 
			
		||||
            action: (event: Event, module: CoreCourseModuleData, courseId: number): void => {
 | 
			
		||||
                // Launch the LTI.
 | 
			
		||||
                AddonModLtiHelper.getDataAndLaunch(courseId, module);
 | 
			
		||||
 | 
			
		||||
                CoreCourse.storeModuleViewed(courseId, module.id);
 | 
			
		||||
            },
 | 
			
		||||
        }];
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -13,6 +13,7 @@
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
import { CoreQuestionQuestionParsed } from '@features/question/services/question';
 | 
			
		||||
import { CoreQuestionHelper } from '@features/question/services/question-helper';
 | 
			
		||||
import { IonContent, IonRefresher } from '@ionic/angular';
 | 
			
		||||
@ -163,6 +164,9 @@ export class AddonModQuizReviewPage implements OnInit {
 | 
			
		||||
                CoreUtils.ignoreErrors(
 | 
			
		||||
                    AddonModQuiz.logViewAttemptReview(this.attemptId, this.quiz.id, this.quiz.name),
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                // Store module viewed. It's done in this page because it can be reached using a link.
 | 
			
		||||
                CoreCourse.storeModuleViewed(this.courseId, this.cmId);
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.showErrorModalDefault(error, 'addon.mod_quiz.errorgetquiz', true);
 | 
			
		||||
 | 
			
		||||
@ -90,6 +90,8 @@ export class AddonModResourceModuleHandlerService extends CoreModuleHandlerBase
 | 
			
		||||
                const hide = await this.hideOpenButton(module);
 | 
			
		||||
                if (!hide) {
 | 
			
		||||
                    AddonModResourceHelper.openModuleFile(module, courseId);
 | 
			
		||||
 | 
			
		||||
                    CoreCourse.storeModuleViewed(courseId, module.id);
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
        }];
 | 
			
		||||
 | 
			
		||||
@ -63,7 +63,7 @@ export class AddonModUrlModuleHandlerService extends CoreModuleHandlerBase imple
 | 
			
		||||
         * @param module The module object.
 | 
			
		||||
         * @param courseId The course ID.
 | 
			
		||||
         */
 | 
			
		||||
        const openUrl = async (module: CoreCourseModuleData): Promise<void> => {
 | 
			
		||||
        const openUrl = async (module: CoreCourseModuleData, courseId: number): Promise<void> => {
 | 
			
		||||
            try {
 | 
			
		||||
                if (module.instance) {
 | 
			
		||||
                    await AddonModUrl.logView(module.instance, module.name);
 | 
			
		||||
@ -73,6 +73,8 @@ export class AddonModUrlModuleHandlerService extends CoreModuleHandlerBase imple
 | 
			
		||||
                // Ignore errors.
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            CoreCourse.storeModuleViewed(courseId, module.id);
 | 
			
		||||
 | 
			
		||||
            const contents = await CoreCourse.getModuleContents(module);
 | 
			
		||||
            AddonModUrlHelper.open(contents[0].fileurl);
 | 
			
		||||
        };
 | 
			
		||||
@ -89,7 +91,7 @@ export class AddonModUrlModuleHandlerService extends CoreModuleHandlerBase imple
 | 
			
		||||
                    const shouldOpen = await this.shouldOpenLink(module);
 | 
			
		||||
 | 
			
		||||
                    if (shouldOpen) {
 | 
			
		||||
                        openUrl(module);
 | 
			
		||||
                        openUrl(module, courseId);
 | 
			
		||||
                    } else {
 | 
			
		||||
                        this.openActivityPage(module, module.course, options);
 | 
			
		||||
                    }
 | 
			
		||||
@ -101,8 +103,8 @@ export class AddonModUrlModuleHandlerService extends CoreModuleHandlerBase imple
 | 
			
		||||
                hidden: true, // Hide it until we calculate if it should be displayed or not.
 | 
			
		||||
                icon: 'fas-link',
 | 
			
		||||
                label: 'core.openmodinbrowser',
 | 
			
		||||
                action: (event: Event, module: CoreCourseModuleData): void => {
 | 
			
		||||
                    openUrl(module);
 | 
			
		||||
                action: (event: Event, module: CoreCourseModuleData, courseId: number): void => {
 | 
			
		||||
                    openUrl(module, courseId);
 | 
			
		||||
                },
 | 
			
		||||
            }],
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
@ -160,7 +160,7 @@ export class CoreSite {
 | 
			
		||||
        this.lastViewedTable = asyncInstance(() => CoreSites.getSiteTable(CoreSite.LAST_VIEWED_TABLE, {
 | 
			
		||||
            siteId: this.getId(),
 | 
			
		||||
            database: this.getDb(),
 | 
			
		||||
            config: { cachingStrategy: CoreDatabaseCachingStrategy.None },
 | 
			
		||||
            config: { cachingStrategy: CoreDatabaseCachingStrategy.Eager },
 | 
			
		||||
            primaryKeyColumns: ['component', 'id'],
 | 
			
		||||
        }));
 | 
			
		||||
        this.setInfo(infos);
 | 
			
		||||
@ -2000,21 +2000,55 @@ export class CoreSite {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get several last viewed for a certain component.
 | 
			
		||||
     *
 | 
			
		||||
     * @param component The component.
 | 
			
		||||
     * @param ids IDs. If not provided or empty, return all last viewed for a component.
 | 
			
		||||
     * @return Resolves with last viewed records, undefined if error.
 | 
			
		||||
     */
 | 
			
		||||
    async getComponentLastViewed(component: string, ids: number[] = []): Promise<CoreSiteLastViewedDBRecord[] | undefined> {
 | 
			
		||||
        try {
 | 
			
		||||
            if (!ids.length) {
 | 
			
		||||
                return await this.lastViewedTable.getMany({ component });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const whereAndParams = SQLiteDB.getInOrEqual(ids);
 | 
			
		||||
 | 
			
		||||
            whereAndParams.sql = 'id ' + whereAndParams.sql + ' AND component = ?';
 | 
			
		||||
            whereAndParams.params.push(component);
 | 
			
		||||
 | 
			
		||||
            return await this.lastViewedTable.getManyWhere({
 | 
			
		||||
                sql: whereAndParams.sql,
 | 
			
		||||
                sqlParams: whereAndParams.params,
 | 
			
		||||
                js: (record) => record.component === component && ids.includes(record.id),
 | 
			
		||||
            });
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            // Not found.
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Store a last viewed record.
 | 
			
		||||
     *
 | 
			
		||||
     * @param component The component.
 | 
			
		||||
     * @param id ID.
 | 
			
		||||
     * @param value Last viewed item value.
 | 
			
		||||
     * @param data Other data.
 | 
			
		||||
     * @param options Options.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async storeLastViewed(component: string, id: number, value: string | number, data?: string): Promise<void> {
 | 
			
		||||
    async storeLastViewed(
 | 
			
		||||
        component: string,
 | 
			
		||||
        id: number,
 | 
			
		||||
        value: string | number,
 | 
			
		||||
        options: CoreSiteStoreLastViewedOptions = {},
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        await this.lastViewedTable.insert({
 | 
			
		||||
            component,
 | 
			
		||||
            id,
 | 
			
		||||
            value: String(value),
 | 
			
		||||
            data,
 | 
			
		||||
            data: options.data,
 | 
			
		||||
            timeaccess: options.timeaccess ?? Date.now(),
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -2349,5 +2383,14 @@ export type CoreSiteLastViewedDBRecord = {
 | 
			
		||||
    component: string;
 | 
			
		||||
    id: number;
 | 
			
		||||
    value: string;
 | 
			
		||||
    timeaccess: number;
 | 
			
		||||
    data?: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Options for storeLastViewed.
 | 
			
		||||
 */
 | 
			
		||||
export type CoreSiteStoreLastViewedOptions = {
 | 
			
		||||
    data?: string; // Other data.
 | 
			
		||||
    timeaccess?: number; // Accessed time. If not set, current time.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -442,6 +442,7 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.fetchSuccess = true;
 | 
			
		||||
        CoreCourse.storeModuleViewed(this.courseId, this.module.id, { sectionId: this.module.section });
 | 
			
		||||
 | 
			
		||||
        // Log activity now.
 | 
			
		||||
        try {
 | 
			
		||||
 | 
			
		||||
@ -97,7 +97,9 @@
 | 
			
		||||
 | 
			
		||||
        <ng-container *ngFor="let module of section.modules">
 | 
			
		||||
            <core-course-module *ngIf="module.visibleoncoursepage !== 0" [module]="module" [section]="section"
 | 
			
		||||
                [showActivityDates]="course.showactivitydates" [showCompletionConditions]="course.showcompletionconditions">
 | 
			
		||||
                [showActivityDates]="course.showactivitydates" [showCompletionConditions]="course.showcompletionconditions"
 | 
			
		||||
                [isLastViewed]="lastModuleViewed && lastModuleViewed.cmId === module.id" [class.core-course-module-not-viewed]="
 | 
			
		||||
                    !viewedModules[module.id] && (!module.completiondata || module.completiondata.state === completionStatusIncomplete)">
 | 
			
		||||
            </core-course-module>
 | 
			
		||||
        </ng-container>
 | 
			
		||||
    </section>
 | 
			
		||||
 | 
			
		||||
@ -29,6 +29,7 @@ import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-comp
 | 
			
		||||
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
 | 
			
		||||
import {
 | 
			
		||||
    CoreCourse,
 | 
			
		||||
    CoreCourseModuleCompletionStatus,
 | 
			
		||||
    CoreCourseProvider,
 | 
			
		||||
} from '@features/course/services/course';
 | 
			
		||||
import {
 | 
			
		||||
@ -42,6 +43,7 @@ import { CoreCourseCourseIndexComponent, CoreCourseIndexSectionWithModule } from
 | 
			
		||||
import { CoreBlockHelper } from '@features/block/services/block-helper';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
 | 
			
		||||
import { CoreCourseViewedModulesDBRecord } from '@features/course/services/database/course';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Component to display course contents using a certain format. If the format isn't found, use default one.
 | 
			
		||||
@ -90,9 +92,14 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
    stealthModulesSectionId: number = CoreCourseProvider.STEALTH_MODULES_SECTION_ID;
 | 
			
		||||
    loaded = false;
 | 
			
		||||
    highlighted?: string;
 | 
			
		||||
    lastModuleViewed?: CoreCourseViewedModulesDBRecord;
 | 
			
		||||
    viewedModules: Record<number, boolean> = {};
 | 
			
		||||
    completionStatusIncomplete = CoreCourseModuleCompletionStatus.COMPLETION_INCOMPLETE;
 | 
			
		||||
 | 
			
		||||
    protected selectTabObserver?: CoreEventObserver;
 | 
			
		||||
    protected modViewedObserver?: CoreEventObserver;
 | 
			
		||||
    protected lastCourseFormat?: string;
 | 
			
		||||
    protected viewedModulesInitialized = false;
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        protected content: IonContent,
 | 
			
		||||
@ -133,6 +140,16 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this.modViewedObserver = CoreEvents.on(CoreEvents.COURSE_MODULE_VIEWED, (data) => {
 | 
			
		||||
            if (data.courseId !== this.course.id) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.viewedModules[data.cmId] = true;
 | 
			
		||||
            if (!this.lastModuleViewed || data.timeaccess > this.lastModuleViewed.timeaccess) {
 | 
			
		||||
                this.lastModuleViewed = data;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -179,8 +196,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
        this.lastCourseFormat = this.course.format;
 | 
			
		||||
 | 
			
		||||
        this.highlighted = CoreCourseFormatDelegate.getSectionHightlightedName(this.course);
 | 
			
		||||
        const currentSection = await CoreCourseFormatDelegate.getCurrentSection(this.course, this.sections);
 | 
			
		||||
        currentSection.highlighted = true;
 | 
			
		||||
        const currentSectionData = await CoreCourseFormatDelegate.getCurrentSection(this.course, this.sections);
 | 
			
		||||
        currentSectionData.section.highlighted = true;
 | 
			
		||||
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            this.loadCourseFormatComponent(),
 | 
			
		||||
@ -236,6 +253,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
        const hasAllSections = sections[0].id == CoreCourseProvider.ALL_SECTIONS_ID;
 | 
			
		||||
        const hasSeveralSections = sections.length > 2 || (sections.length == 2 && !hasAllSections);
 | 
			
		||||
 | 
			
		||||
        await this.initializeViewedModules();
 | 
			
		||||
 | 
			
		||||
        if (this.selectedSection) {
 | 
			
		||||
            const selectedSection = this.selectedSection;
 | 
			
		||||
            // We have a selected section, but the list has changed. Search the section in the list.
 | 
			
		||||
@ -243,7 +262,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
 | 
			
		||||
            if (!newSection) {
 | 
			
		||||
                // Section not found, calculate which one to use.
 | 
			
		||||
                newSection = await CoreCourseFormatDelegate.getCurrentSection(this.course, sections);
 | 
			
		||||
                const currentSectionData = await CoreCourseFormatDelegate.getCurrentSection(this.course, sections);
 | 
			
		||||
                newSection = currentSectionData.section;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.sectionChanged(newSection);
 | 
			
		||||
@ -269,16 +289,60 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!this.loaded) {
 | 
			
		||||
            // No section specified, not found or not visible, get current section.
 | 
			
		||||
            const section = await CoreCourseFormatDelegate.getCurrentSection(this.course, sections);
 | 
			
		||||
            // No section specified, not found or not visible, load current section or the section with last module viewed.
 | 
			
		||||
            const currentSectionData = await CoreCourseFormatDelegate.getCurrentSection(this.course, sections);
 | 
			
		||||
 | 
			
		||||
            const lastModuleViewed = this.lastModuleViewed;
 | 
			
		||||
            let section = currentSectionData.section;
 | 
			
		||||
            let moduleId: number | undefined;
 | 
			
		||||
 | 
			
		||||
            if (!currentSectionData.forceSelected && lastModuleViewed) {
 | 
			
		||||
                // Search the section with the last module viewed.
 | 
			
		||||
                let lastModuleSection: CoreCourseSection | undefined;
 | 
			
		||||
 | 
			
		||||
                if (lastModuleViewed.sectionId) {
 | 
			
		||||
                    lastModuleSection = sections.find(section => section.id === lastModuleViewed.sectionId);
 | 
			
		||||
                }
 | 
			
		||||
                if (!lastModuleSection) {
 | 
			
		||||
                    // No sectionId or section not found. Search the module.
 | 
			
		||||
                    lastModuleSection = sections.find(
 | 
			
		||||
                        section => section.modules.some(module => module.id === lastModuleViewed.cmId),
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                section = lastModuleSection || section;
 | 
			
		||||
                moduleId = lastModuleSection ? lastModuleViewed?.cmId : undefined;
 | 
			
		||||
            } else if (lastModuleViewed && currentSectionData.section.modules.some(module => module.id === lastModuleViewed.cmId)) {
 | 
			
		||||
                // Last module viewed is inside the highlighted section.
 | 
			
		||||
                moduleId = lastModuleViewed.cmId;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.loaded = true;
 | 
			
		||||
            this.sectionChanged(section);
 | 
			
		||||
            this.sectionChanged(section, moduleId);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Initialize viewed modules.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async initializeViewedModules(): Promise<void> {
 | 
			
		||||
        if (this.viewedModulesInitialized) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const viewedModules = await CoreCourse.getViewedModules(this.course.id);
 | 
			
		||||
 | 
			
		||||
        this.viewedModulesInitialized = true;
 | 
			
		||||
        this.lastModuleViewed = viewedModules[0];
 | 
			
		||||
        viewedModules.forEach(entry => {
 | 
			
		||||
            this.viewedModules[entry.cmId] = true;
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Display the course index modal.
 | 
			
		||||
     */
 | 
			
		||||
@ -345,8 +409,9 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
     * Function called when selected section changes.
 | 
			
		||||
     *
 | 
			
		||||
     * @param newSection The new selected section.
 | 
			
		||||
     * @param moduleId The module to scroll to.
 | 
			
		||||
     */
 | 
			
		||||
    sectionChanged(newSection: CoreCourseSection): void {
 | 
			
		||||
    sectionChanged(newSection: CoreCourseSection, moduleId?: number): void {
 | 
			
		||||
        const previousValue = this.selectedSection;
 | 
			
		||||
        this.selectedSection = newSection;
 | 
			
		||||
        this.data.section = this.selectedSection;
 | 
			
		||||
@ -377,12 +442,14 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
            this.showMoreActivities();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.moduleId && previousValue === undefined) {
 | 
			
		||||
        // Scroll to module if needed. Give more priority to the input.
 | 
			
		||||
        moduleId = this.moduleId && previousValue === undefined ? this.moduleId : moduleId;
 | 
			
		||||
        if (moduleId) {
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
                CoreDomUtils.scrollToElementBySelector(
 | 
			
		||||
                    this.elementRef.nativeElement,
 | 
			
		||||
                    this.content,
 | 
			
		||||
                    '#core-course-module-' + this.moduleId,
 | 
			
		||||
                    '#core-course-module-' + moduleId,
 | 
			
		||||
                );
 | 
			
		||||
            }, 200);
 | 
			
		||||
        } else {
 | 
			
		||||
@ -502,7 +569,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    ngOnDestroy(): void {
 | 
			
		||||
        this.selectTabObserver && this.selectTabObserver.off();
 | 
			
		||||
        this.selectTabObserver?.off();
 | 
			
		||||
        this.modViewedObserver?.off();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -68,11 +68,11 @@ export class CoreCourseCourseIndexComponent implements OnInit {
 | 
			
		||||
            completionEnabled = this.course.showcompletionconditions;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const currentSection = await CoreCourseFormatDelegate.getCurrentSection(this.course, this.sections);
 | 
			
		||||
        const currentSectionData = await CoreCourseFormatDelegate.getCurrentSection(this.course, this.sections);
 | 
			
		||||
 | 
			
		||||
        if (this.selectedId === undefined) {
 | 
			
		||||
            // Highlight current section if none is selected.
 | 
			
		||||
            this.selectedId = currentSection.id;
 | 
			
		||||
            this.selectedId = currentSectionData.section.id;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Clone sections to add information.
 | 
			
		||||
@ -104,7 +104,7 @@ export class CoreCourseCourseIndexComponent implements OnInit {
 | 
			
		||||
                    name: section.name,
 | 
			
		||||
                    availabilityinfo: !!section.availabilityinfo,
 | 
			
		||||
                    expanded: section.id === this.selectedId,
 | 
			
		||||
                    highlighted: currentSection?.id === section.id,
 | 
			
		||||
                    highlighted: currentSectionData.section.id === section.id,
 | 
			
		||||
                    hasVisibleModules: modules.length > 0,
 | 
			
		||||
                    modules: modules,
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
@ -99,6 +99,11 @@
 | 
			
		||||
 | 
			
		||||
            </ion-label>
 | 
			
		||||
        </ion-item>
 | 
			
		||||
 | 
			
		||||
        <div class="core-course-last-module-viewed" *ngIf="isLastViewed">
 | 
			
		||||
            <ion-icon name="fas-eye" aria-hidden="true"></ion-icon>
 | 
			
		||||
            {{ 'core.course.lastaccessedactivity' | translate }}
 | 
			
		||||
        </div>
 | 
			
		||||
    </ng-container>
 | 
			
		||||
 | 
			
		||||
    <!-- Loading. -->
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,7 @@
 | 
			
		||||
:host {
 | 
			
		||||
    --horizontal-margin: 10px;
 | 
			
		||||
    --vertical-margin: 10px;
 | 
			
		||||
    --core-course-module-not-viewed-border-color: var(--gray-500);
 | 
			
		||||
 | 
			
		||||
    ion-card {
 | 
			
		||||
        margin: var(--vertical-margin) var(--horizontal-margin);
 | 
			
		||||
@ -91,4 +92,18 @@
 | 
			
		||||
    .core-course-module-info ::ng-deep core-course-module-completion .core-module-automatic-completion-conditions .completioninfo.completion_complete {
 | 
			
		||||
        display: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.core-course-module-not-viewed {
 | 
			
		||||
        --ion-card-border-color: var(--core-course-module-not-viewed-border-color);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .core-course-last-module-viewed {
 | 
			
		||||
        padding: 8px 12px;
 | 
			
		||||
        color: var(--subdued-text-color);
 | 
			
		||||
        border-top: 1px solid var(--stroke);
 | 
			
		||||
 | 
			
		||||
        ion-icon {
 | 
			
		||||
            margin-right: 4px;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -48,6 +48,7 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy {
 | 
			
		||||
    @Input() showActivityDates = false; // Whether to show activity dates.
 | 
			
		||||
    @Input() showCompletionConditions = false; // Whether to show activity completion conditions.
 | 
			
		||||
    @Input() showLegacyCompletion?: boolean; // Whether to show module completion in the old format.
 | 
			
		||||
    @Input() isLastViewed = false; // Whether it's the last module viewed in a course.
 | 
			
		||||
    @Output() completionChanged = new EventEmitter<CoreCourseModuleCompletionData>(); // Notify when module completion changes.
 | 
			
		||||
 | 
			
		||||
    modNameTranslated = '';
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,7 @@
 | 
			
		||||
import { Injectable } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
import { CoreTimeUtils } from '@services/utils/time';
 | 
			
		||||
import { CoreCourseFormatHandler } from '@features/course/services/format-delegate';
 | 
			
		||||
import { CoreCourseFormatCurrentSectionData, CoreCourseFormatHandler } from '@features/course/services/format-delegate';
 | 
			
		||||
import { makeSingleton, Translate } from '@singletons';
 | 
			
		||||
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
 | 
			
		||||
import { CoreCourseWSSection } from '@features/course/services/course';
 | 
			
		||||
@ -41,12 +41,18 @@ export class CoreCourseFormatWeeksHandlerService implements CoreCourseFormatHand
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async getCurrentSection(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise<CoreCourseSection> {
 | 
			
		||||
    async getCurrentSection(
 | 
			
		||||
        course: CoreCourseAnyCourseData,
 | 
			
		||||
        sections: CoreCourseSection[],
 | 
			
		||||
    ): Promise<CoreCourseFormatCurrentSectionData<CoreCourseSection>> {
 | 
			
		||||
        const now = CoreTimeUtils.timestamp();
 | 
			
		||||
 | 
			
		||||
        if ((course.startdate && now < course.startdate) || (course.enddate && now > course.enddate)) {
 | 
			
		||||
            // Course hasn't started yet or it has ended already. Return all sections.
 | 
			
		||||
            return sections[0];
 | 
			
		||||
            return {
 | 
			
		||||
                section: sections[0],
 | 
			
		||||
                forceSelected: false,
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for (let i = 0; i < sections.length; i++) {
 | 
			
		||||
@ -57,12 +63,18 @@ export class CoreCourseFormatWeeksHandlerService implements CoreCourseFormatHand
 | 
			
		||||
 | 
			
		||||
            const dates = this.getSectionDates(section, course.startdate || 0);
 | 
			
		||||
            if (now >= dates.start && now < dates.end) {
 | 
			
		||||
                return section;
 | 
			
		||||
                return {
 | 
			
		||||
                    section,
 | 
			
		||||
                    forceSelected: false,
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // The section wasn't found, return all sections.
 | 
			
		||||
        return sections[0];
 | 
			
		||||
        return {
 | 
			
		||||
            section: sections[0],
 | 
			
		||||
            forceSelected: false,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -44,6 +44,7 @@
 | 
			
		||||
    "highlighted": "Highlighted",
 | 
			
		||||
    "insufficientavailablespace": "You are trying to download {{size}}. This will leave your device with insufficient space to operate normally. Please clear some storage space first.",
 | 
			
		||||
    "insufficientavailablequota": "Your device could not allocate space to save this download. It may be reserving space for app and system updates. Please clear some storage space first.",
 | 
			
		||||
    "lastaccessedactivity": "Last accessed activity",
 | 
			
		||||
    "manualcompletionnotsynced": "Manual completion not synchronised.",
 | 
			
		||||
    "modulenotfound": "Resource or activity not found, please make sure you're online and it's still available.",
 | 
			
		||||
    "nocontentavailable": "No content available at the moment.",
 | 
			
		||||
 | 
			
		||||
@ -2022,6 +2022,7 @@ export type CoreCourseModuleData = Omit<CoreCourseGetContentsWSModule, 'completi
 | 
			
		||||
    isStealth?: boolean;
 | 
			
		||||
    handlerData?: CoreCourseModuleHandlerData;
 | 
			
		||||
    completiondata?: CoreCourseModuleCompletionData;
 | 
			
		||||
    section: number;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 | 
			
		||||
@ -26,7 +26,9 @@ import { CoreConstants } from '@/core/constants';
 | 
			
		||||
import { makeSingleton, Platform, Translate } from '@singletons';
 | 
			
		||||
import { CoreStatusWithWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws';
 | 
			
		||||
 | 
			
		||||
import { CoreCourseStatusDBRecord, COURSE_STATUS_TABLE } from './database/course';
 | 
			
		||||
import {
 | 
			
		||||
    CoreCourseStatusDBRecord, CoreCourseViewedModulesDBRecord, COURSE_STATUS_TABLE, COURSE_VIEWED_MODULES_TABLE ,
 | 
			
		||||
} from './database/course';
 | 
			
		||||
import { CoreCourseOffline } from './course-offline';
 | 
			
		||||
import { CoreError } from '@classes/errors/error';
 | 
			
		||||
import {
 | 
			
		||||
@ -49,6 +51,7 @@ import { lazyMap, LazyMap } from '@/core/utils/lazy-map';
 | 
			
		||||
import { asyncInstance, AsyncInstance } from '@/core/utils/async-instance';
 | 
			
		||||
import { CoreDatabaseTable } from '@classes/database/database-table';
 | 
			
		||||
import { CoreDatabaseCachingStrategy } from '@classes/database/database-table-proxy';
 | 
			
		||||
import { SQLiteDB } from '@classes/sqlitedb';
 | 
			
		||||
 | 
			
		||||
const ROOT_CACHE_KEY = 'mmCourse:';
 | 
			
		||||
 | 
			
		||||
@ -140,6 +143,7 @@ export class CoreCourseProvider {
 | 
			
		||||
 | 
			
		||||
    protected logger: CoreLogger;
 | 
			
		||||
    protected statusTables: LazyMap<AsyncInstance<CoreDatabaseTable<CoreCourseStatusDBRecord>>>;
 | 
			
		||||
    protected viewedModulesTables: LazyMap<AsyncInstance<CoreDatabaseTable<CoreCourseViewedModulesDBRecord, 'courseId' | 'cmId'>>>;
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        this.logger = CoreLogger.getInstance('CoreCourseProvider');
 | 
			
		||||
@ -152,6 +156,17 @@ export class CoreCourseProvider {
 | 
			
		||||
                }),
 | 
			
		||||
            ),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        this.viewedModulesTables = lazyMap(
 | 
			
		||||
            siteId => asyncInstance(
 | 
			
		||||
                () => CoreSites.getSiteTable<CoreCourseViewedModulesDBRecord, 'courseId' | 'cmId'>(COURSE_VIEWED_MODULES_TABLE, {
 | 
			
		||||
                    siteId,
 | 
			
		||||
                    config: { cachingStrategy: CoreDatabaseCachingStrategy.None },
 | 
			
		||||
                    primaryKeyColumns: ['courseId', 'cmId'],
 | 
			
		||||
                    onDestroy: () => delete this.viewedModulesTables[siteId],
 | 
			
		||||
                }),
 | 
			
		||||
            ),
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -340,6 +355,31 @@ export class CoreCourseProvider {
 | 
			
		||||
        return ROOT_CACHE_KEY + 'activitiescompletion:' + courseId + ':' + userId;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get certain module viewed records in the app.
 | 
			
		||||
     *
 | 
			
		||||
     * @param ids Module IDs.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with map of last module viewed data.
 | 
			
		||||
     */
 | 
			
		||||
    async getCertainModulesViewed(ids: number[] = [], siteId?: string): Promise<Record<number, CoreCourseViewedModulesDBRecord>> {
 | 
			
		||||
        if (!ids.length) {
 | 
			
		||||
            return {};
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const site = await CoreSites.getSite(siteId);
 | 
			
		||||
 | 
			
		||||
        const whereAndParams = SQLiteDB.getInOrEqual(ids);
 | 
			
		||||
 | 
			
		||||
        const entries = await this.viewedModulesTables[site.getId()].getManyWhere({
 | 
			
		||||
            sql: 'cmId ' + whereAndParams.sql,
 | 
			
		||||
            sqlParams: whereAndParams.params,
 | 
			
		||||
            js: (record) => ids.includes(record.cmId),
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return CoreUtils.arrayToObject(entries, 'cmId');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get course blocks.
 | 
			
		||||
     *
 | 
			
		||||
@ -425,6 +465,19 @@ export class CoreCourseProvider {
 | 
			
		||||
        return entries.map((entry) => entry.id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get last module viewed in the app for a course.
 | 
			
		||||
     *
 | 
			
		||||
     * @param id Course ID.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with last module viewed data, undefined if none.
 | 
			
		||||
     */
 | 
			
		||||
    async getLastModuleViewed(id: number, siteId?: string): Promise<CoreCourseViewedModulesDBRecord | undefined> {
 | 
			
		||||
        const viewedModules = await this.getViewedModules(id, siteId);
 | 
			
		||||
 | 
			
		||||
        return viewedModules[0];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a module from Moodle.
 | 
			
		||||
     *
 | 
			
		||||
@ -548,7 +601,7 @@ export class CoreCourseProvider {
 | 
			
		||||
 | 
			
		||||
        let foundModule: CoreCourseGetContentsWSModule | undefined;
 | 
			
		||||
 | 
			
		||||
        const foundSection = sections.some((section) => {
 | 
			
		||||
        const foundSection = sections.find((section) => {
 | 
			
		||||
            if (section.id != CoreCourseProvider.STEALTH_MODULES_SECTION_ID &&
 | 
			
		||||
                sectionId !== undefined &&
 | 
			
		||||
                sectionId != section.id
 | 
			
		||||
@ -562,7 +615,7 @@ export class CoreCourseProvider {
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (foundSection && foundModule) {
 | 
			
		||||
            return this.addAdditionalModuleData(foundModule, courseId);
 | 
			
		||||
            return this.addAdditionalModuleData(foundModule, courseId, foundSection.id);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        throw new CoreError(Translate.instant('core.course.modulenotfound'));
 | 
			
		||||
@ -573,11 +626,13 @@ export class CoreCourseProvider {
 | 
			
		||||
     *
 | 
			
		||||
     * @param module Module.
 | 
			
		||||
     * @param courseId Course ID of the module.
 | 
			
		||||
     * @param sectionId Section ID of the module.
 | 
			
		||||
     * @return Module with additional info.
 | 
			
		||||
     */
 | 
			
		||||
    protected  addAdditionalModuleData(
 | 
			
		||||
    protected addAdditionalModuleData(
 | 
			
		||||
        module: CoreCourseGetContentsWSModule,
 | 
			
		||||
        courseId: number,
 | 
			
		||||
        sectionId: number,
 | 
			
		||||
    ): CoreCourseModuleData {
 | 
			
		||||
        let completionData: CoreCourseModuleCompletionData | undefined = undefined;
 | 
			
		||||
 | 
			
		||||
@ -590,13 +645,12 @@ export class CoreCourseProvider {
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const moduleWithCourse: CoreCourseModuleData = {
 | 
			
		||||
        return  {
 | 
			
		||||
            ...module,
 | 
			
		||||
            course: courseId,
 | 
			
		||||
            section: sectionId,
 | 
			
		||||
            completiondata: completionData,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return moduleWithCourse;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -873,7 +927,7 @@ export class CoreCourseProvider {
 | 
			
		||||
        // Add course to all modules.
 | 
			
		||||
        return sections.map((section) => ({
 | 
			
		||||
            ...section,
 | 
			
		||||
            modules: section.modules.map((module) => this.addAdditionalModuleData(module, courseId)),
 | 
			
		||||
            modules: section.modules.map((module) => this.addAdditionalModuleData(module, courseId, section.id)),
 | 
			
		||||
        }));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -901,6 +955,23 @@ export class CoreCourseProvider {
 | 
			
		||||
        return sections.reduce((previous: CoreCourseModuleData[], section) => previous.concat(section.modules || []), []);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get all viewed modules in a course, ordered by timeaccess in descending order.
 | 
			
		||||
     *
 | 
			
		||||
     * @param courseId Course ID.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with the list of viewed modules.
 | 
			
		||||
     */
 | 
			
		||||
    async getViewedModules(courseId: number, siteId?: string): Promise<CoreCourseViewedModulesDBRecord[]> {
 | 
			
		||||
        const site = await CoreSites.getSite(siteId);
 | 
			
		||||
 | 
			
		||||
        return await this.viewedModulesTables[site.getId()].getMany({ courseId }, {
 | 
			
		||||
            sorting: [
 | 
			
		||||
                { timeaccess: 'desc' },
 | 
			
		||||
            ],
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Invalidates course blocks WS call.
 | 
			
		||||
     *
 | 
			
		||||
@ -1348,6 +1419,34 @@ export class CoreCourseProvider {
 | 
			
		||||
        this.triggerCourseStatusChanged(courseId, status, siteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Store activity as viewed.
 | 
			
		||||
     *
 | 
			
		||||
     * @param courseId Chapter ID.
 | 
			
		||||
     * @param cmId Module ID.
 | 
			
		||||
     * @param options Other options.
 | 
			
		||||
     * @return Promise resolved with last chapter viewed, undefined if none.
 | 
			
		||||
     */
 | 
			
		||||
    async storeModuleViewed(courseId: number, cmId: number, options: CoreCourseStoreModuleViewedOptions = {}): Promise<void> {
 | 
			
		||||
        const site = await CoreSites.getSite(options.siteId);
 | 
			
		||||
 | 
			
		||||
        const timeaccess = options.timeaccess ?? Date.now();
 | 
			
		||||
 | 
			
		||||
        await this.viewedModulesTables[site.getId()].insert({
 | 
			
		||||
            courseId,
 | 
			
		||||
            cmId,
 | 
			
		||||
            sectionId: options.sectionId,
 | 
			
		||||
            timeaccess,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        CoreEvents.trigger(CoreEvents.COURSE_MODULE_VIEWED, {
 | 
			
		||||
            courseId,
 | 
			
		||||
            cmId,
 | 
			
		||||
            timeaccess,
 | 
			
		||||
            sectionId: options.sectionId,
 | 
			
		||||
        }, site.getId());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Translate a module name to current language.
 | 
			
		||||
     *
 | 
			
		||||
@ -1770,3 +1869,12 @@ type CoreCompletionUpdateActivityCompletionStatusManuallyWSParams = {
 | 
			
		||||
export type CoreCourseAnyModuleData = CoreCourseModuleData | CoreCourseModuleBasicInfo & {
 | 
			
		||||
    contents?: CoreCourseModuleContentFile[]; // If needed, calculated in the app in loadModuleContents.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Options for storeModuleViewed.
 | 
			
		||||
 */
 | 
			
		||||
export type CoreCourseStoreModuleViewedOptions = {
 | 
			
		||||
    sectionId?: number;
 | 
			
		||||
    timeaccess?: number;
 | 
			
		||||
    siteId?: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -18,9 +18,10 @@ import { CoreSiteSchema } from '@services/sites';
 | 
			
		||||
 * Database variables for CoreCourse service.
 | 
			
		||||
 */
 | 
			
		||||
export const COURSE_STATUS_TABLE = 'course_status';
 | 
			
		||||
export const COURSE_VIEWED_MODULES_TABLE = 'course_viewed_modules';
 | 
			
		||||
export const SITE_SCHEMA: CoreSiteSchema = {
 | 
			
		||||
    name: 'CoreCourseProvider',
 | 
			
		||||
    version: 1,
 | 
			
		||||
    version: 2,
 | 
			
		||||
    tables: [
 | 
			
		||||
        {
 | 
			
		||||
            name: COURSE_STATUS_TABLE,
 | 
			
		||||
@ -53,6 +54,29 @@ export const SITE_SCHEMA: CoreSiteSchema = {
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            name: COURSE_VIEWED_MODULES_TABLE,
 | 
			
		||||
            columns: [
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'courseId',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'cmId',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'timeaccess',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                    notNull: true,
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'sectionId',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
            primaryKeys: ['courseId', 'cmId'],
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -85,7 +109,7 @@ export const OFFLINE_SITE_SCHEMA: CoreSiteSchema = {
 | 
			
		||||
                    type: 'TEXT',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'timecompleted',
 | 
			
		||||
                    name: 'timeaccess',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
@ -102,6 +126,13 @@ export type CoreCourseStatusDBRecord = {
 | 
			
		||||
    previousDownloadTime: number;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type CoreCourseViewedModulesDBRecord = {
 | 
			
		||||
    courseId: number;
 | 
			
		||||
    cmId: number;
 | 
			
		||||
    timeaccess: number;
 | 
			
		||||
    sectionId?: number;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type CoreCourseManualCompletionDBRecord = {
 | 
			
		||||
    cmid: number;
 | 
			
		||||
    completed: number;
 | 
			
		||||
 | 
			
		||||
@ -98,9 +98,13 @@ export interface CoreCourseFormatHandler extends CoreDelegateHandler {
 | 
			
		||||
     *
 | 
			
		||||
     * @param course The course to get the title.
 | 
			
		||||
     * @param sections List of sections.
 | 
			
		||||
     * @return Promise resolved with current section.
 | 
			
		||||
     * @return Promise resolved with current section and whether the section should be selected. If only the section is returned,
 | 
			
		||||
     *         forceSelected will default to false.
 | 
			
		||||
     */
 | 
			
		||||
    getCurrentSection?(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise<CoreCourseSection>;
 | 
			
		||||
    getCurrentSection?(
 | 
			
		||||
        course: CoreCourseAnyCourseData,
 | 
			
		||||
        sections: CoreCourseSection[],
 | 
			
		||||
    ): Promise<CoreCourseFormatCurrentSectionData<CoreCourseSection> | CoreCourseSection>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the name for the highlighted section.
 | 
			
		||||
@ -299,21 +303,37 @@ export class CoreCourseFormatDelegateService extends CoreDelegate<CoreCourseForm
 | 
			
		||||
     *
 | 
			
		||||
     * @param course The course to get the title.
 | 
			
		||||
     * @param sections List of sections.
 | 
			
		||||
     * @return Promise resolved with current section.
 | 
			
		||||
     * @return Promise.
 | 
			
		||||
     */
 | 
			
		||||
    async getCurrentSection<T = CoreCourseSection>(course: CoreCourseAnyCourseData, sections: T[]): Promise<T> {
 | 
			
		||||
    async getCurrentSection<T = CoreCourseSection>(
 | 
			
		||||
        course: CoreCourseAnyCourseData,
 | 
			
		||||
        sections: T[],
 | 
			
		||||
    ): Promise<CoreCourseFormatCurrentSectionData<T>> {
 | 
			
		||||
        try {
 | 
			
		||||
            const section = await this.executeFunctionOnEnabled<T>(
 | 
			
		||||
            const sectionData = await this.executeFunctionOnEnabled<CoreCourseFormatCurrentSectionData<T> | T>(
 | 
			
		||||
                course.format || '',
 | 
			
		||||
                'getCurrentSection',
 | 
			
		||||
                [course, sections],
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            return section || sections[0];
 | 
			
		||||
            if (sectionData && 'forceSelected' in sectionData) {
 | 
			
		||||
                return sectionData;
 | 
			
		||||
            } else if (sectionData) {
 | 
			
		||||
                // Function just returned the section, don't force selecting it.
 | 
			
		||||
                return {
 | 
			
		||||
                    section: sectionData,
 | 
			
		||||
                    forceSelected: false,
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
        } catch {
 | 
			
		||||
            // This function should never fail. Just return the first section (usually, "All sections").
 | 
			
		||||
            return sections[0];
 | 
			
		||||
            // This function should never fail.
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Return the first section (usually, "All sections").
 | 
			
		||||
        return {
 | 
			
		||||
            section: sections[0],
 | 
			
		||||
            forceSelected: false,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -380,3 +400,8 @@ export class CoreCourseFormatDelegateService extends CoreDelegate<CoreCourseForm
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const CoreCourseFormatDelegate = makeSingleton(CoreCourseFormatDelegateService);
 | 
			
		||||
 | 
			
		||||
export type CoreCourseFormatCurrentSectionData<T = CoreCourseSection> = {
 | 
			
		||||
    section: T; // Current section.
 | 
			
		||||
    forceSelected: boolean; // If true, the app will force selecting the section when opening the course.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -18,7 +18,7 @@ import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { Translate } from '@singletons';
 | 
			
		||||
import { CoreCourseSection } from '../course-helper';
 | 
			
		||||
import { CoreCourseFormatHandler } from '../format-delegate';
 | 
			
		||||
import { CoreCourseFormatCurrentSectionData, CoreCourseFormatHandler } from '../format-delegate';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Default handler used when the course format doesn't have a specific implementation.
 | 
			
		||||
@ -74,7 +74,10 @@ export class CoreCourseFormatDefaultHandler implements CoreCourseFormatHandler {
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async getCurrentSection(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise<CoreCourseSection> {
 | 
			
		||||
    async getCurrentSection(
 | 
			
		||||
        course: CoreCourseAnyCourseData,
 | 
			
		||||
        sections: CoreCourseSection[],
 | 
			
		||||
    ): Promise<CoreCourseFormatCurrentSectionData<CoreCourseSection>> {
 | 
			
		||||
        let marker: number | undefined;
 | 
			
		||||
 | 
			
		||||
        // We need the "marker" to determine the current section.
 | 
			
		||||
@ -93,12 +96,18 @@ export class CoreCourseFormatDefaultHandler implements CoreCourseFormatHandler {
 | 
			
		||||
            const section = sections.find((sect) => sect.section == marker);
 | 
			
		||||
 | 
			
		||||
            if (section) {
 | 
			
		||||
                return section;
 | 
			
		||||
                return {
 | 
			
		||||
                    section,
 | 
			
		||||
                    forceSelected: true,
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Marked section not found or we couldn't retrieve the marker. Return all sections.
 | 
			
		||||
        return sections[0];
 | 
			
		||||
        return {
 | 
			
		||||
            section: sections[0],
 | 
			
		||||
            forceSelected: false,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -145,6 +145,10 @@ export const SITE_SCHEMA: CoreSiteSchema = {
 | 
			
		||||
                    name: 'data',
 | 
			
		||||
                    type: 'TEXT',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'timeaccess',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
            primaryKeys: ['component', 'id'],
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
@ -60,6 +60,7 @@ export interface CoreEventsData {
 | 
			
		||||
    [CoreEvents.FILE_SHARED]: CoreEventFileSharedData;
 | 
			
		||||
    [CoreEvents.APP_LAUNCHED_URL]: CoreEventAppLaunchedData;
 | 
			
		||||
    [CoreEvents.ORIENTATION_CHANGE]: CoreEventOrientationData;
 | 
			
		||||
    [CoreEvents.COURSE_MODULE_VIEWED]: CoreEventCourseModuleViewed;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
@ -110,6 +111,7 @@ export class CoreEvents {
 | 
			
		||||
    static readonly FORM_ACTION = 'form_action';
 | 
			
		||||
    static readonly ACTIVITY_DATA_SENT = 'activity_data_sent';
 | 
			
		||||
    static readonly DEVICE_REGISTERED_IN_MOODLE = 'device_registered_in_moodle';
 | 
			
		||||
    static readonly COURSE_MODULE_VIEWED = 'course_module_viewed';
 | 
			
		||||
 | 
			
		||||
    protected static logger = CoreLogger.getInstance('CoreEvents');
 | 
			
		||||
    protected static observables: { [eventName: string]: Subject<unknown> } = {};
 | 
			
		||||
@ -427,3 +429,13 @@ export type CoreEventAppLaunchedData = {
 | 
			
		||||
export type CoreEventOrientationData = {
 | 
			
		||||
    orientation: CoreScreenOrientation;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Data passed to COURSE_MODULE_VIEWED event.
 | 
			
		||||
 */
 | 
			
		||||
export type CoreEventCourseModuleViewed = {
 | 
			
		||||
    courseId: number;
 | 
			
		||||
    cmId: number;
 | 
			
		||||
    timeaccess: number;
 | 
			
		||||
    sectionId?: number;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -22,7 +22,8 @@ information provided here is intended especially for developers.
 | 
			
		||||
- The parameters of the following functions in CoreCourseHelper have changed: navigateToModuleByInstance, navigateToModule, openModule.
 | 
			
		||||
- fillContextMenu, expandDescription, gotoBlog, prefetch and removeFiles functions have been removed from CoreCourseModuleMainResourceComponent.
 | 
			
		||||
- contextMenuPrefetch and fillContextMenu have been removed from CoreCourseHelper.
 | 
			
		||||
-The variable "loaded" in CoreCourseModuleMainResourceComponent has been changed to "showLoading" to reflect its purpose better.
 | 
			
		||||
- The variable "loaded" in CoreCourseModuleMainResourceComponent has been changed to "showLoading" to reflect its purpose better.
 | 
			
		||||
- The function getCurrentSection of course formats can now return a forceSelected boolean along with the section (defaults to false if not returned).
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user