Merge pull request #2670 from crazyserver/MOBILE-3625

Mobile 3625
main
Dani Palou 2021-02-04 15:21:39 +01:00 committed by GitHub
commit bcca120bea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1866 additions and 31 deletions

View File

@ -0,0 +1,38 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { IonicModule } from '@ionic/angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreBlockDelegate } from '@features/block/services/block-delegate';
import { AddonBlockActivityModulesHandler } from './services/block-handler';
import { AddonBlockActivityModulesComponentsModule } from './components/components.module';
@NgModule({
imports: [
IonicModule,
AddonBlockActivityModulesComponentsModule,
TranslateModule.forChild(),
],
providers: [
{
provide: APP_INITIALIZER,
multi: true,
useValue: () => {
CoreBlockDelegate.instance.registerHandler(AddonBlockActivityModulesHandler.instance);
},
},
],
})
export class AddonBlockActivityModulesModule {}

View File

@ -0,0 +1,149 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit } from '@angular/core';
import { CoreCourse } from '@features/course/services/course';
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
import { CoreBlockBaseComponent } from '@features/block/classes/base-block-component';
import { CoreSites } from '@services/sites';
import { ContextLevel, CoreConstants } from '@/core/constants';
import { Translate } from '@singletons';
import { CoreUtils } from '@services/utils/utils';
import { CoreNavigator } from '@services/navigator';
/**
* Component to render an "activity modules" block.
*/
@Component({
selector: 'addon-block-activitymodules',
templateUrl: 'addon-block-activitymodules.html',
})
export class AddonBlockActivityModulesComponent extends CoreBlockBaseComponent implements OnInit {
entries: AddonBlockActivityModuleEntry[] = [];
protected fetchContentDefaultError = 'Error getting activity modules data.';
constructor() {
super('AddonBlockActivityModulesComponent');
}
/**
* Perform the invalidate content function.
*
* @return Resolved when done.
*/
protected async invalidateContent(): Promise<void> {
await CoreCourse.instance.invalidateSections(this.instanceId);
}
/**
* Fetch the data to render the block.
*
* @return Promise resolved when done.
*/
protected async fetchContent(): Promise<void> {
const sections = await CoreCourse.instance.getSections(this.getCourseId(), false, true);
this.entries = [];
const archetypes: Record<string, number> = {};
const modIcons: Record<string, string> = {};
let modFullNames: Record<string, string> = {};
sections.forEach((section) => {
if (!section.modules) {
return;
}
section.modules.forEach((mod) => {
if (mod.uservisible === false || !CoreCourse.instance.moduleHasView(mod) ||
typeof modFullNames[mod.modname] != 'undefined') {
// Ignore this module.
return;
}
// Get the archetype of the module type.
if (typeof archetypes[mod.modname] == 'undefined') {
archetypes[mod.modname] = CoreCourseModuleDelegate.instance.supportsFeature<number>(
mod.modname,
CoreConstants.FEATURE_MOD_ARCHETYPE,
CoreConstants.MOD_ARCHETYPE_OTHER,
);
}
// Get the full name of the module type.
if (archetypes[mod.modname] == CoreConstants.MOD_ARCHETYPE_RESOURCE) {
// All resources are gathered in a single "Resources" option.
if (!modFullNames['resources']) {
modFullNames['resources'] = Translate.instance.instant('core.resources');
}
} else {
modFullNames[mod.modname] = mod.modplural;
}
modIcons[mod.modname] = mod.modicon;
});
});
// Sort the modnames alphabetically.
modFullNames = CoreUtils.instance.sortValues(modFullNames);
for (const modName in modFullNames) {
let icon: string;
if (modName === 'resources') {
icon = CoreCourse.instance.getModuleIconSrc('page', modIcons['page']);
} else {
icon = CoreCourseModuleDelegate.instance.getModuleIconSrc(modName, modIcons[modName]);
}
this.entries.push({
icon: icon,
name: modFullNames[modName],
modName,
});
}
}
/**
* Obtain the appropiate course id for the block.
*
* @return Course id.
*/
protected getCourseId(): number {
if (this.contextLevel == ContextLevel.COURSE) {
return this.instanceId;
}
return CoreSites.instance.getCurrentSiteHomeId();
}
/**
* Navigate to the activity list.
*
* @param entry Selected entry.
*/
gotoCoureListModType(entry: AddonBlockActivityModuleEntry): void {
CoreNavigator.instance.navigateToSitePath('course/list-mod-type', {
params: {
courseId: this.getCourseId(),
modName: entry.modName,
title: entry.name,
},
});
}
}
type AddonBlockActivityModuleEntry = {
icon: string;
name: string;
modName: string;
};

View File

@ -0,0 +1,11 @@
<ion-item-divider sticky="true">
<ion-label>
<h2>{{ 'addon.block_activitymodules.pluginname' | translate }}</h2>
</ion-label>
</ion-item-divider>
<core-loading [hideUntil]="loaded" class="core-loading-center">
<ion-item class="ion-text-wrap item-media" *ngFor="let entry of entries" detail="false" (click)="gotoCoureListModType(entry)">
<img slot="start" [src]="entry.icon" alt="" role="presentation" class="core-module-icon">
<ion-label>{{ entry.name }}</ion-label>
</ion-item>
</core-loading>

View File

@ -0,0 +1,43 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from '@ionic/angular';
import { FormsModule } from '@angular/forms';
import { TranslateModule } from '@ngx-translate/core';
import { CoreSharedModule } from '@/core/shared.module';
import { AddonBlockActivityModulesComponent } from './activitymodules/activitymodules';
@NgModule({
declarations: [
AddonBlockActivityModulesComponent,
],
imports: [
CommonModule,
IonicModule,
FormsModule,
TranslateModule.forChild(),
CoreSharedModule,
],
exports: [
AddonBlockActivityModulesComponent,
],
entryComponents: [
AddonBlockActivityModulesComponent,
],
})
export class AddonBlockActivityModulesComponentsModule {}

View File

@ -0,0 +1,3 @@
{
"pluginname": "Activities"
}

View File

@ -0,0 +1,46 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreBlockHandlerData } from '@features/block/services/block-delegate';
import { AddonBlockActivityModulesComponent } from '../components/activitymodules/activitymodules';
import { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler';
import { makeSingleton } from '@singletons';
/**
* Block handler.
*/
@Injectable({ providedIn: 'root' })
export class AddonBlockActivityModulesHandlerService extends CoreBlockBaseHandler {
name = 'AddonBlockActivityModules';
blockName = 'activity_modules';
/**
* Returns the data needed to render the block.
*
* @return Data or promise resolved with the data.
*/
getDisplayData(): CoreBlockHandlerData {
return {
title: 'addon.block_activitymodules.pluginname',
class: 'addon-block-activitymodules',
component: AddonBlockActivityModulesComponent,
};
}
}
export class AddonBlockActivityModulesHandler extends makeSingleton(AddonBlockActivityModulesHandlerService) {}

View File

@ -36,6 +36,9 @@ import { AddonBlockSelfCompletionModule } from './selfcompletion/selfcompletion.
import { AddonBlockSiteMainMenuModule } from './sitemainmenu/sitemainmenu.module';
import { AddonBlockStarredCoursesModule } from './starredcourses/starredcourses.module';
import { AddonBlockTagsModule } from './tags/tags.module';
import { AddonBlockActivityModulesModule } from './activitymodules/activitymodules.module';
import { AddonBlockRecentlyAccessedItemsModule } from './recentlyaccesseditems/recentlyaccesseditems.module';
import { AddonBlockTimelineModule } from './timeline/timeline.module';
@NgModule({
declarations: [],
@ -62,6 +65,9 @@ import { AddonBlockTagsModule } from './tags/tags.module';
AddonBlockSiteMainMenuModule,
AddonBlockStarredCoursesModule,
AddonBlockTagsModule,
AddonBlockActivityModulesModule,
AddonBlockRecentlyAccessedItemsModule,
AddonBlockTimelineModule,
],
providers: [],
exports: [],

View File

@ -20,15 +20,15 @@
(onClosed)="switchFilterClosed()"></core-context-menu-item>
<core-context-menu-item *ngIf="loaded && showSortFilter" [priority]="900"
content="{{('core.sortby' | translate) + ' ' + ('addon.block_myoverview.title' | translate)}}"
(action)="switchSort('fullname')" [iconAction]="sort == 'fullname' ? 'far-check-circle' : 'far-circle'">
(action)="switchSort('fullname')" [iconAction]="sort == 'fullname' ? 'far-dot-circle' : 'far-circle'">
</core-context-menu-item>
<core-context-menu-item *ngIf="loaded && showSortFilter && showSortByShortName" [priority]="800"
content="{{('core.sortby' | translate) + ' ' + ('addon.block_myoverview.shortname' | translate)}}"
(action)="switchSort('shortname')" [iconAction]="sort == 'shortname' ? 'far-check-circle' : 'far-circle'">
(action)="switchSort('shortname')" [iconAction]="sort == 'shortname' ? 'far-dot-circle' : 'far-circle'">
</core-context-menu-item>
<core-context-menu-item *ngIf="loaded && showSortFilter" [priority]="700"
content="{{('core.sortby' | translate) + ' ' + ('addon.block_myoverview.lastaccessed' | translate)}}"
(action)="switchSort('lastaccess')" [iconAction]="sort == 'lastaccess' ? 'far-check-circle' : 'far-circle'">
(action)="switchSort('lastaccess')" [iconAction]="sort == 'lastaccess' ? 'far-dot-circle' : 'far-circle'">
</core-context-menu-item>
</core-context-menu>
</ion-item-divider>

View File

@ -0,0 +1,43 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from '@ionic/angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreSharedModule } from '@/core/shared.module';
import { CoreCoursesComponentsModule } from '@features/courses/components/components.module';
import { AddonBlockRecentlyAccessedItemsComponent } from './recentlyaccesseditems/recentlyaccesseditems';
@NgModule({
declarations: [
AddonBlockRecentlyAccessedItemsComponent,
],
imports: [
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreSharedModule,
CoreCoursesComponentsModule,
],
exports: [
AddonBlockRecentlyAccessedItemsComponent,
],
entryComponents: [
AddonBlockRecentlyAccessedItemsComponent,
],
})
export class AddonBlockRecentlyAccessedItemsComponentsModule {}

View File

@ -0,0 +1,29 @@
<ion-item-divider sticky="true">
<ion-label><h2>{{ 'addon.block_recentlyaccesseditems.pluginname' | translate }}</h2></ion-label>
</ion-item-divider>
<core-loading [hideUntil]="loaded" class="core-loading-center safe-area-page">
<div class="core-horizontal-scroll" *ngIf="items && items.length > 0">
<div *ngFor="let item of items">
<ion-card>
<ion-item class="core-course-module-handler item-media ion-text-wrap" detail="false" (click)="action($event, item)"
[title]="item.name">
<img slot="start" [src]="item.iconUrl" alt="" role="presentation" *ngIf="item.iconUrl" class="core-module-icon">
<ion-label>
<h2>
<core-format-text [text]="item.name" contextLevel="module" [contextInstanceId]="item.cmid"
[courseId]="item.courseid"></core-format-text>
</h2>
<p>
<core-format-text [text]="item.coursename" contextLevel="course" [contextInstanceId]="item.courseid">
</core-format-text>
</p>
</ion-label>
</ion-item>
</ion-card>
</div>
</div>
<core-empty-box *ngIf="items.length <= 0" image="assets/img/icons/activities.svg"
[message]="'addon.block_recentlyaccesseditems.noitems' | translate"></core-empty-box>
</core-loading>

View File

@ -0,0 +1,7 @@
@import "~theme/globals";
:host {
.core-horizontal-scroll > div {
@include horizontal_scroll_item(80%, 250px, 300px);
}
}

View File

@ -0,0 +1,86 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit } from '@angular/core';
import { CoreSites } from '@services/sites';
import { CoreBlockBaseComponent } from '@features/block/classes/base-block-component';
import {
AddonBlockRecentlyAccessedItems,
AddonBlockRecentlyAccessedItemsItem,
} from '../../services/recentlyaccesseditems';
import { CoreTextUtils } from '@services/utils/text';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper';
/**
* Component to render a recently accessed items block.
*/
@Component({
selector: 'addon-block-recentlyaccesseditems',
templateUrl: 'addon-block-recentlyaccesseditems.html',
styleUrls: ['recentlyaccesseditems.scss'],
})
export class AddonBlockRecentlyAccessedItemsComponent extends CoreBlockBaseComponent implements OnInit {
items: AddonBlockRecentlyAccessedItemsItem[] = [];
protected fetchContentDefaultError = 'Error getting recently accessed items data.';
constructor() {
super('AddonBlockRecentlyAccessedItemsComponent');
}
/**
* Perform the invalidate content function.
*
* @return Resolved when done.
*/
protected async invalidateContent(): Promise<void> {
await AddonBlockRecentlyAccessedItems.instance.invalidateRecentItems();
}
/**
* Fetch the data to render the block.
*
* @return Promise resolved when done.
*/
protected async fetchContent(): Promise<void> {
this.items = await AddonBlockRecentlyAccessedItems.instance.getRecentItems();
}
/**
* Event clicked.
*
* @param e Click event.
* @param item Activity item info.
*/
async action(e: Event, item: AddonBlockRecentlyAccessedItemsItem): Promise<void> {
e.preventDefault();
e.stopPropagation();
const url = CoreTextUtils.instance.decodeHTMLEntities(item.viewurl);
const modal = await CoreDomUtils.instance.showModalLoading();
try {
const treated = await CoreContentLinksHelper.instance.handleLink(url);
if (!treated) {
return CoreSites.instance.getCurrentSite()?.openInBrowserWithAutoLoginIfSameSite(url);
}
} finally {
modal.dismiss();
}
}
}

View File

@ -0,0 +1,4 @@
{
"noitems": "No recent items",
"pluginname": "Recently accessed items"
}

View File

@ -0,0 +1,40 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { IonicModule } from '@ionic/angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreSharedModule } from '@/core/shared.module';
import { CoreBlockDelegate } from '@features/block/services/block-delegate';
import { AddonBlockRecentlyAccessedItemsComponentsModule } from './components/components.module';
import { AddonBlockRecentlyAccessedItemsHandler } from './services/block-handler';
@NgModule({
imports: [
IonicModule,
CoreSharedModule,
AddonBlockRecentlyAccessedItemsComponentsModule,
TranslateModule.forChild(),
],
providers: [
{
provide: APP_INITIALIZER,
multi: true,
useValue: () => {
CoreBlockDelegate.instance.registerHandler(AddonBlockRecentlyAccessedItemsHandler.instance);
},
},
],
})
export class AddonBlockRecentlyAccessedItemsModule {}

View File

@ -0,0 +1,50 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreBlockHandlerData } from '@features/block/services/block-delegate';
import { AddonBlockRecentlyAccessedItemsComponent } from '../components/recentlyaccesseditems/recentlyaccesseditems';
import { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler';
import { makeSingleton } from '@singletons';
/**
* Block handler.
*/
@Injectable( { providedIn: 'root' })
export class AddonBlockRecentlyAccessedItemsHandlerService extends CoreBlockBaseHandler {
name = 'AddonBlockRecentlyAccessedItems';
blockName = 'recentlyaccesseditems';
/**
* Returns the data needed to render the block.
*
* @param injector Injector.
* @param block The block to render.
* @param contextLevel The context where the block will be used.
* @param instanceId The instance ID associated with the context level.
* @return Data or promise resolved with the data.
*/
getDisplayData(): CoreBlockHandlerData{
return {
title: 'addon.block_recentlyaccesseditems.pluginname',
class: 'addon-block-recentlyaccesseditems',
component: AddonBlockRecentlyAccessedItemsComponent,
};
}
}
export class AddonBlockRecentlyAccessedItemsHandler extends makeSingleton(AddonBlockRecentlyAccessedItemsHandlerService) {}

View File

@ -0,0 +1,102 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreCourse } from '@features/course/services/course';
import { CoreSiteWSPreSets } from '@classes/site';
import { makeSingleton } from '@singletons';
const ROOT_CACHE_KEY = 'AddonBlockRecentlyAccessedItems:';
/**
* Service that provides some features regarding recently accessed items.
*/
@Injectable( { providedIn: 'root' })
export class AddonBlockRecentlyAccessedItemsProvider {
/**
* Get cache key for get last accessed items value WS call.
*
* @return Cache key.
*/
protected getRecentItemsCacheKey(): string {
return ROOT_CACHE_KEY + ':recentitems';
}
/**
* Get last accessed items.
*
* @param siteId Site ID. If not defined, use current site.
* @return Promise resolved when the info is retrieved.
*/
async getRecentItems(siteId?: string): Promise<AddonBlockRecentlyAccessedItemsItem[]> {
const site = await CoreSites.instance.getSite(siteId);
const preSets: CoreSiteWSPreSets = {
cacheKey: this.getRecentItemsCacheKey(),
};
const items: AddonBlockRecentlyAccessedItemsItem[] =
await site.read('block_recentlyaccesseditems_get_recent_items', undefined, preSets);
return items.map((item) => {
const modicon = item.icon && CoreDomUtils.instance.getHTMLElementAttribute(item.icon, 'src');
item.iconUrl = CoreCourse.instance.getModuleIconSrc(item.modname, modicon || undefined);
return item;
});
}
/**
* Invalidates get last accessed items WS call.
*
* @param siteId Site ID to invalidate. If not defined, use current site.
* @return Promise resolved when the data is invalidated.
*/
async invalidateRecentItems(siteId?: string): Promise<void> {
const site = await CoreSites.instance.getSite(siteId);
await site.invalidateWsCacheForKey(this.getRecentItemsCacheKey());
}
}
export class AddonBlockRecentlyAccessedItems extends makeSingleton(AddonBlockRecentlyAccessedItemsProvider) {}
/**
* Result of WS block_recentlyaccesseditems_get_recent_items.
*/
export type AddonBlockRecentlyAccessedItemsItem = {
id: number; // Id.
courseid: number; // Courseid.
cmid: number; // Cmid.
userid: number; // Userid.
modname: string; // Modname.
name: string; // Name.
coursename: string; // Coursename.
timeaccess: number; // Timeaccess.
viewurl: string; // Viewurl.
courseviewurl: string; // Courseviewurl.
icon: string; // Icon.
} & AddonBlockRecentlyAccessedItemsItemCalculatedData;
/**
* Calculated data for recently accessed item.
*/
export type AddonBlockRecentlyAccessedItemsItemCalculatedData = {
iconUrl: string; // Icon URL. Calculated by the app.
};

View File

@ -18,7 +18,7 @@ import { IonicModule } from '@ionic/angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreSharedModule } from '@/core/shared.module';
// import { CoreCourseComponentsModule } from '@features/course/components/components.module';
import { CoreCourseComponentsModule } from '@features/course/components/components.module';
import { AddonBlockSiteMainMenuComponent } from './sitemainmenu/sitemainmenu';
@ -32,7 +32,7 @@ import { AddonBlockSiteMainMenuComponent } from './sitemainmenu/sitemainmenu';
IonicModule,
TranslateModule.forChild(),
CoreSharedModule,
// CoreCourseComponentsModule,
CoreCourseComponentsModule,
],
exports: [
AddonBlockSiteMainMenuComponent,

View File

@ -12,7 +12,7 @@
</ion-label>
</ion-item>
<!--<core-course-module *ngFor="let module of mainMenuBlock.modules" [module]="module" [courseId]="siteHomeId"
[downloadEnabled]="downloadEnabled" [section]="mainMenuBlock"></core-course-module>-->
<core-course-module *ngFor="let module of mainMenuBlock.modules" [module]="module" [courseId]="siteHomeId"
[downloadEnabled]="downloadEnabled" [section]="mainMenuBlock"></core-course-module>
</ng-container>
</core-loading>

View File

@ -91,7 +91,7 @@ export class AddonBlockSiteMainMenuComponent extends CoreBlockBaseComponent impl
const items = config.frontpageloggedin.split(',');
const hasNewsItem = items.find((item) => parseInt(item, 10) == FrontPageItemNames['NEWS_ITEMS']);
const result = await CoreCourseHelper.instance.addHandlerDataForModules(
const result = CoreCourseHelper.instance.addHandlerDataForModules(
[mainMenuBlock],
this.siteHomeId,
undefined,

View File

@ -0,0 +1,51 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from '@ionic/angular';
import { TranslateModule } from '@ngx-translate/core';
import { FormsModule } from '@angular/forms';
import { CoreSharedModule } from '@/core/shared.module';
import { CoreCoursesComponentsModule } from '@features/courses/components/components.module';
import { CoreCourseComponentsModule } from '@features/course/components/components.module';
import { AddonBlockTimelineComponent } from './timeline/timeline';
import { AddonBlockTimelineEventsComponent } from './events/events';
@NgModule({
declarations: [
AddonBlockTimelineComponent,
AddonBlockTimelineEventsComponent,
],
imports: [
CommonModule,
IonicModule,
FormsModule,
TranslateModule.forChild(),
CoreSharedModule,
CoreCoursesComponentsModule,
CoreCourseComponentsModule,
],
exports: [
AddonBlockTimelineComponent,
AddonBlockTimelineEventsComponent,
],
entryComponents: [
AddonBlockTimelineComponent,
AddonBlockTimelineEventsComponent,
],
})
export class AddonBlockTimelineComponentsModule {}

View File

@ -0,0 +1,55 @@
<ion-item-group *ngFor="let dayEvents of filteredEvents">
<ion-item-divider [color]="dayEvents.color">
<ion-label><h2>{{ dayEvents.dayTimestamp * 1000 | coreFormatDate:"strftimedayshort" }}</h2></ion-label>
</ion-item-divider>
<ng-container *ngFor="let event of dayEvents.events">
<ion-item class="ion-text-wrap core-course-module-handler item-media" detail="false" (click)="action($event, event.url)"
[title]="event.name">
<img slot="start" [src]="event.iconUrl" alt="" role="presentation" *ngIf="event.iconUrl" class="core-module-icon">
<ion-label>
<h2>
<core-format-text [text]="event.name" contextLevel="module" [contextInstanceId]="event.id"
[courseId]="event.course && event.course.id">
</core-format-text>
</h2>
<p *ngIf="showCourse && event.course">
<core-format-text [text]="event.course.fullnamedisplay" contextLevel="course"
[contextInstanceId]="event.course.id">
</core-format-text>
</p>
<ion-button fill="clear" class="ion-hide-md-up" (click)="action($event, event.action.url)"
[title]="event.action.name" [disabled]="!event.action.actionable" *ngIf="event.action">
{{event.action.name}}
<ion-badge slot="end" class="ion-margin-start" *ngIf="event.action.showitemcount">{{event.action.itemcount}}
</ion-badge>
</ion-button>
</ion-label>
<ion-grid slot="end">
<ion-row class="ion-justify-content-end">
<ion-badge color="light">{{event.timesort * 1000 | coreFormatDate:"strftimetime24" }}</ion-badge>
</ion-row>
<ion-row class="ion-justify-content-end">
<ion-button fill="clear" class="ion-hide-md-down" (click)="action($event, event.action.url)"
[title]="event.action.name" [disabled]="!event.action.actionable" *ngIf="event.action">
{{event.action.name}}
<ion-badge slot="end" class="ion-margin-start" *ngIf="event.action.showitemcount">{{event.action.itemcount}}
</ion-badge>
</ion-button>
</ion-row>
</ion-grid>
</ion-item>
</ng-container>
</ion-item-group>
<div class="ion-padding ion-text-center" *ngIf="canLoadMore && !empty">
<!-- Button and spinner to show more attempts. -->
<ion-button expand="block" (click)="loadMoreEvents()" color="light" *ngIf="!loadingMore">
{{ 'core.loadmore' | translate }}
</ion-button>
<ion-spinner *ngIf="loadingMore"></ion-spinner>
</div>
<core-empty-box *ngIf="empty" image="assets/img/icons/activities.svg" [message]="'addon.block_timeline.noevents' | translate"
[inline]="!showCourse"></core-empty-box>

View File

@ -0,0 +1,154 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Input, Output, OnChanges, EventEmitter, SimpleChange } from '@angular/core';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreTextUtils } from '@services/utils/text';
import { CoreTimeUtils } from '@services/utils/time';
import { CoreUtils } from '@services/utils/utils';
import { CoreCourse } from '@features/course/services/course';
import moment from 'moment';
import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper';
import { AddonCalendarEvent } from '@addons/calendar/services/calendar';
/**
* Directive to render a list of events in course overview.
*/
@Component({
selector: 'addon-block-timeline-events',
templateUrl: 'addon-block-timeline-events.html',
})
export class AddonBlockTimelineEventsComponent implements OnChanges {
@Input() events: AddonBlockTimelineEvent[] = []; // The events to render.
@Input() showCourse?: boolean | string; // Whether to show the course name.
@Input() from = 0; // Number of days from today to offset the events.
@Input() to?: number; // Number of days from today to limit the events to. If not defined, no limit.
@Input() canLoadMore?: boolean; // Whether more events can be loaded.
@Output() loadMore: EventEmitter<void>; // Notify that more events should be loaded.
empty = true;
loadingMore = false;
filteredEvents: AddonBlockTimelineEventFilteredEvent[] = [];
constructor() {
this.loadMore = new EventEmitter();
}
/**
* Detect changes on input properties.
*/
ngOnChanges(changes: {[name: string]: SimpleChange}): void {
this.showCourse = CoreUtils.instance.isTrueOrOne(this.showCourse);
if (changes.events || changes.from || changes.to) {
if (this.events && this.events.length > 0) {
const filteredEvents = this.filterEventsByTime(this.from, this.to);
this.empty = !filteredEvents || filteredEvents.length <= 0;
const eventsByDay: Record<number, AddonCalendarEvent[]> = {};
filteredEvents.forEach((event) => {
const dayTimestamp = CoreTimeUtils.instance.getMidnightForTimestamp(event.timesort);
if (eventsByDay[dayTimestamp]) {
eventsByDay[dayTimestamp].push(event);
} else {
eventsByDay[dayTimestamp] = [event];
}
});
const todaysMidnight = CoreTimeUtils.instance.getMidnightForTimestamp();
this.filteredEvents = [];
Object.keys(eventsByDay).forEach((key) => {
const dayTimestamp = parseInt(key);
this.filteredEvents.push({
color: dayTimestamp < todaysMidnight ? 'danger' : 'light',
dayTimestamp,
events: eventsByDay[dayTimestamp],
});
});
} else {
this.empty = true;
}
}
}
/**
* Filter the events by time.
*
* @param start Number of days to start getting events from today. E.g. -1 will get events from yesterday.
* @param end Number of days after the start.
* @return Filtered events.
*/
protected filterEventsByTime(start: number, end?: number): AddonBlockTimelineEvent[] {
start = moment().add(start, 'days').startOf('day').unix();
end = typeof end != 'undefined' ? moment().add(end, 'days').startOf('day').unix() : end;
return this.events.filter((event) => {
if (end) {
return start <= event.timesort && event.timesort < end;
}
return start <= event.timesort;
}).map((event) => {
event.iconUrl = CoreCourse.instance.getModuleIconSrc(event.icon.component);
return event;
});
}
/**
* Load more events clicked.
*/
loadMoreEvents(): void {
this.loadingMore = true;
this.loadMore.emit();
}
/**
* Action clicked.
*
* @param e Click event.
* @param url Url of the action.
*/
async action(e: Event, url: string): Promise<void> {
e.preventDefault();
e.stopPropagation();
// Fix URL format.
url = CoreTextUtils.instance.decodeHTMLEntities(url);
const modal = await CoreDomUtils.instance.showModalLoading();
try {
const treated = await CoreContentLinksHelper.instance.handleLink(url);
if (!treated) {
return CoreSites.instance.getCurrentSite()?.openInBrowserWithAutoLoginIfSameSite(url);
}
} finally {
modal.dismiss();
}
}
}
type AddonBlockTimelineEvent = AddonCalendarEvent & {
iconUrl?: string;
};
type AddonBlockTimelineEventFilteredEvent = {
events: AddonBlockTimelineEvent[];
dayTimestamp: number;
color: string;
};

View File

@ -0,0 +1,44 @@
<ion-item-divider sticky="true">
<ion-label><h2>{{ 'addon.block_timeline.pluginname' | translate }}</h2></ion-label>
<core-context-menu slot="end">
<core-context-menu-item *ngIf="loaded" [priority]="900" [content]="'addon.block_timeline.sortbydates' | translate"
(action)="switchSort('sortbydates')" [iconAction]="sort == 'sortbydates' ? 'far-dot-circle' : 'far-circle'">
</core-context-menu-item>
<core-context-menu-item *ngIf="loaded" [priority]="800" [content]="'addon.block_timeline.sortbycourses' | translate"
(action)="switchSort('sortbycourses')" [iconAction]="sort == 'sortbycourses' ? 'far-dot-circle' : 'far-circle'">
</core-context-menu-item>
</core-context-menu>
</ion-item-divider>
<core-loading [hideUntil]="loaded" class="core-loading-center">
<div class="ion-padding safe-padding-horizontal">
<ion-select class="ion-text-start core-button-select" [(ngModel)]="filter" (ngModelChange)="switchFilter()"
interface="popover">
<ion-select-option value="all">{{ 'core.all' | translate }}</ion-select-option>
<ion-select-option value="overdue">{{ 'addon.block_timeline.overdue' | translate }}</ion-select-option>
<ion-select-option disabled value="disabled">{{ 'addon.block_timeline.duedate' | translate }}</ion-select-option>
<ion-select-option value="next7days">{{ 'addon.block_timeline.next7days' | translate }}</ion-select-option>
<ion-select-option value="next30days">{{ 'addon.block_timeline.next30days' | translate }}</ion-select-option>
<ion-select-option value="next3months">{{ 'addon.block_timeline.next3months' | translate }}</ion-select-option>
<ion-select-option value="next6months">{{ 'addon.block_timeline.next6months' | translate }}</ion-select-option>
</ion-select>
</div>
<core-loading [hideUntil]="timeline.loaded" [hidden]="sort != 'sortbydates'" class="core-loading-center">
<addon-block-timeline-events [events]="timeline.events" showCourse="true" [canLoadMore]="timeline.canLoadMore"
(loadMore)="loadMoreTimeline()" [from]="dataFrom" [to]="dataTo"></addon-block-timeline-events>
</core-loading>
<core-loading [hideUntil]="timelineCourses.loaded" [hidden]="sort != 'sortbycourses'"
class="core-loading-center safe-area-page">
<ion-grid class="ion-no-padding">
<ion-row class="ion-no-padding">
<ion-col *ngFor="let course of timelineCourses.courses" class="ion-no-padding" size="12" size-md="6">
<core-courses-course-progress [course]="course">
<addon-block-timeline-events [events]="course.events" [canLoadMore]="course.canLoadMore"
(loadMore)="loadMoreCourse(course)" [from]="dataFrom" [to]="dataTo"></addon-block-timeline-events>
</core-courses-course-progress>
</ion-col>
</ion-row>
</ion-grid>
<core-empty-box *ngIf="timelineCourses.courses.length == 0" image="assets/img/icons/courses.svg"
[message]="'addon.block_timeline.nocoursesinprogress' | translate"></core-empty-box>
</core-loading>
</core-loading>

View File

@ -0,0 +1,240 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit } from '@angular/core';
import { CoreTimeUtils } from '@services/utils/time';
import { CoreSites } from '@services/sites';
import { CoreBlockBaseComponent } from '@features/block/classes/base-block-component';
import { AddonBlockTimeline } from '../../services/timeline';
import { AddonCalendarEvent } from '@addons/calendar/services/calendar';
import { CoreUtils } from '@services/utils/utils';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreCoursesHelper, CoreEnrolledCourseDataWithOptions } from '@features/courses/services/courses-helper';
import { CoreSite } from '@classes/site';
import { CoreCourses } from '@features/courses/services/courses';
import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate';
/**
* Component to render a timeline block.
*/
@Component({
selector: 'addon-block-timeline',
templateUrl: 'addon-block-timeline.html',
})
export class AddonBlockTimelineComponent extends CoreBlockBaseComponent implements OnInit {
sort = 'sortbydates';
filter = 'next30days';
currentSite?: CoreSite;
timeline: {
events: AddonCalendarEvent[];
loaded: boolean;
canLoadMore?: number;
} = {
events: <AddonCalendarEvent[]> [],
loaded: false,
};
timelineCourses: {
courses: AddonBlockTimelineCourse[];
loaded: boolean;
canLoadMore?: number;
} = {
courses: [],
loaded: false,
};
dataFrom?: number;
dataTo?: number;
protected courseIds: number[] = [];
protected fetchContentDefaultError = 'Error getting timeline data.';
constructor() {
super('AddonBlockTimelineComponent');
}
/**
* Component being initialized.
*/
async ngOnInit(): Promise<void> {
this.currentSite = CoreSites.instance.getCurrentSite();
this.filter = await this.currentSite!.getLocalSiteConfig('AddonBlockTimelineFilter', this.filter);
this.switchFilter();
this.sort = await this.currentSite!.getLocalSiteConfig('AddonBlockTimelineSort', this.sort);
super.ngOnInit();
}
/**
* Perform the invalidate content function.
*
* @return Resolved when done.
*/
protected invalidateContent(): Promise<void> {
const promises: Promise<void>[] = [];
promises.push(AddonBlockTimeline.instance.invalidateActionEventsByTimesort());
promises.push(AddonBlockTimeline.instance.invalidateActionEventsByCourses());
promises.push(CoreCourses.instance.invalidateUserCourses());
promises.push(CoreCourseOptionsDelegate.instance.clearAndInvalidateCoursesOptions());
if (this.courseIds.length > 0) {
promises.push(CoreCourses.instance.invalidateCoursesByField('ids', this.courseIds.join(',')));
}
return CoreUtils.instance.allPromises(promises);
}
/**
* Fetch the courses for my overview.
*
* @return Promise resolved when done.
*/
protected async fetchContent(): Promise<void> {
if (this.sort == 'sortbydates') {
return this.fetchMyOverviewTimeline().finally(() => {
this.timeline.loaded = true;
});
}
if (this.sort == 'sortbycourses') {
return this.fetchMyOverviewTimelineByCourses().finally(() => {
this.timelineCourses.loaded = true;
});
}
}
/**
* Load more events.
*/
async loadMoreTimeline(): Promise<void> {
try {
await this.fetchMyOverviewTimeline(this.timeline.canLoadMore);
} catch (error) {
CoreDomUtils.instance.showErrorModalDefault(error, this.fetchContentDefaultError);
}
}
/**
* Load more events.
*
* @param course Course.
* @return Promise resolved when done.
*/
async loadMoreCourse(course: AddonBlockTimelineCourse): Promise<void> {
try {
const courseEvents = await AddonBlockTimeline.instance.getActionEventsByCourse(course.id, course.canLoadMore);
course.events = course.events?.concat(courseEvents.events);
course.canLoadMore = courseEvents.canLoadMore;
} catch (error) {
CoreDomUtils.instance.showErrorModalDefault(error, this.fetchContentDefaultError);
}
}
/**
* Fetch the timeline.
*
* @param afterEventId The last event id.
* @return Promise resolved when done.
*/
protected async fetchMyOverviewTimeline(afterEventId?: number): Promise<void> {
const events = await AddonBlockTimeline.instance.getActionEventsByTimesort(afterEventId);
this.timeline.events = events.events;
this.timeline.canLoadMore = events.canLoadMore;
}
/**
* Fetch the timeline by courses.
*
* @return Promise resolved when done.
*/
protected async fetchMyOverviewTimelineByCourses(): Promise<void> {
const courses = await CoreCoursesHelper.instance.getUserCoursesWithOptions();
const today = CoreTimeUtils.instance.timestamp();
this.timelineCourses.courses = courses.filter((course) =>
(course.startdate || 0) <= today && (!course.enddate || course.enddate >= today));
if (this.timelineCourses.courses.length > 0) {
this.courseIds = this.timelineCourses.courses.map((course) => course.id);
const courseEvents = await AddonBlockTimeline.instance.getActionEventsByCourses(this.courseIds);
this.timelineCourses.courses.forEach((course) => {
course.events = courseEvents[course.id].events;
course.canLoadMore = courseEvents[course.id].canLoadMore;
});
}
}
/**
* Change timeline filter being viewed.
*/
switchFilter(): void {
this.currentSite?.setLocalSiteConfig('AddonBlockTimelineFilter', this.filter);
switch (this.filter) {
case 'overdue':
this.dataFrom = -14;
this.dataTo = 0;
break;
case 'next7days':
this.dataFrom = 0;
this.dataTo = 7;
break;
case 'next30days':
this.dataFrom = 0;
this.dataTo = 30;
break;
case 'next3months':
this.dataFrom = 0;
this.dataTo = 90;
break;
case 'next6months':
this.dataFrom = 0;
this.dataTo = 180;
break;
default:
case 'all':
this.dataFrom = -14;
this.dataTo = undefined;
break;
}
}
/**
* Change timeline sort being viewed.
*
* @param sort New sorting.
*/
switchSort(sort: string): void {
this.sort = sort;
this.currentSite?.setLocalSiteConfig('AddonBlockTimelineSort', this.sort);
if (!this.timeline.loaded && this.sort == 'sortbydates') {
this.fetchContent();
} else if (!this.timelineCourses.loaded && this.sort == 'sortbycourses') {
this.fetchContent();
}
}
}
type AddonBlockTimelineCourse = CoreEnrolledCourseDataWithOptions & {
events?: AddonCalendarEvent[];
canLoadMore?: number;
};

View File

@ -0,0 +1,13 @@
{
"duedate": "Due date",
"next30days": "Next 30 days",
"next3months": "Next 3 months",
"next6months": "Next 6 months",
"next7days": "Next 7 days",
"nocoursesinprogress": "No in-progress courses",
"noevents": "No upcoming activities due",
"overdue": "Overdue",
"pluginname": "Timeline",
"sortbycourses": "Sort by courses",
"sortbydates": "Sort by dates"
}

View File

@ -0,0 +1,62 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreSites } from '@services/sites';
import { CoreBlockHandlerData } from '@features/block/services/block-delegate';
import { CoreCourses } from '@features/courses/services/courses';
import { AddonBlockTimelineComponent } from '@addons/block/timeline/components/timeline/timeline';
import { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler';
import { makeSingleton } from '@singletons';
import { AddonBlockTimeline } from './timeline';
/**
* Block handler.
*/
@Injectable({ providedIn: 'root' })
export class AddonBlockTimelineHandlerService extends CoreBlockBaseHandler {
name = 'AddonBlockTimeline';
blockName = 'timeline';
/**
* Check if the handler is enabled on a site level.
*
* @return Whether or not the handler is enabled on a site level.
*/
async isEnabled(): Promise<boolean> {
const enabled = await AddonBlockTimeline.instance.isAvailable();
const currentSite = CoreSites.instance.getCurrentSite();
return enabled && ((currentSite && currentSite.isVersionGreaterEqualThan('3.6')) ||
!CoreCourses.instance.isMyCoursesDisabledInSite());
}
/**
* Returns the data needed to render the block.
*
* @return Data or promise resolved with the data.
*/
getDisplayData(): CoreBlockHandlerData {
return {
title: 'addon.block_timeline.pluginname',
class: 'addon-block-timeline',
component: AddonBlockTimelineComponent,
};
}
}
export class AddonBlockTimelineHandler extends makeSingleton(AddonBlockTimelineHandlerService) {}

View File

@ -0,0 +1,290 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreSites } from '@services/sites';
import { CoreCoursesDashboard } from '@features/courses/services/dashboard';
import {
AddonCalendarEvents,
AddonCalendarEventsGroupedByCourse,
AddonCalendarEvent,
AddonCalendarGetActionEventsByCourseWSParams,
AddonCalendarGetActionEventsByTimesortWSParams,
AddonCalendarGetActionEventsByCoursesWSParams,
} from '@addons/calendar/services/calendar';
import moment from 'moment';
import { makeSingleton } from '@singletons';
import { CoreSiteWSPreSets } from '@classes/site';
import { CoreError } from '@classes/errors/error';
// Cache key was maintained from block myoverview when blocks were splitted.
const ROOT_CACHE_KEY = 'myoverview:';
/**
* Service that provides some features regarding course overview.
*/
@Injectable({ providedIn: 'root' })
export class AddonBlockTimelineProvider {
static readonly EVENTS_LIMIT = 20;
static readonly EVENTS_LIMIT_PER_COURSE = 10;
/**
* Get calendar action events for the given course.
*
* @param courseId Only events in this course.
* @param afterEventId The last seen event id.
* @param siteId Site ID. If not defined, use current site.
* @return Promise resolved when the info is retrieved.
*/
async getActionEventsByCourse(
courseId: number,
afterEventId?: number,
siteId?: string,
): Promise<{ events: AddonCalendarEvent[]; canLoadMore?: number }> {
const site = await CoreSites.instance.getSite(siteId);
const time = moment().subtract(14, 'days').unix(); // Check two weeks ago.
const data: AddonCalendarGetActionEventsByCourseWSParams = {
timesortfrom: time,
courseid: courseId,
limitnum: AddonBlockTimelineProvider.EVENTS_LIMIT_PER_COURSE,
};
if (afterEventId) {
data.aftereventid = afterEventId;
}
const preSets: CoreSiteWSPreSets = {
cacheKey: this.getActionEventsByCourseCacheKey(courseId),
};
const courseEvents = await site.read<AddonCalendarEvents>(
'core_calendar_get_action_events_by_course',
data,
preSets,
);
if (courseEvents && courseEvents.events) {
return this.treatCourseEvents(courseEvents, time);
}
throw new CoreError('No events returned on core_calendar_get_action_events_by_course.');
}
/**
* Get cache key for get calendar action events for the given course value WS call.
*
* @param courseId Only events in this course.
* @return Cache key.
*/
protected getActionEventsByCourseCacheKey(courseId: number): string {
return this.getActionEventsByCoursesCacheKey() + ':' + courseId;
}
/**
* Get calendar action events for a given list of courses.
*
* @param courseIds Course IDs.
* @param siteId Site ID. If not defined, use current site.
* @return Promise resolved when the info is retrieved.
*/
async getActionEventsByCourses(
courseIds: number[],
siteId?: string,
): Promise<{[courseId: string]: { events: AddonCalendarEvent[]; canLoadMore: number } }> {
const site = await CoreSites.instance.getSite(siteId);
const time = moment().subtract(14, 'days').unix(); // Check two weeks ago.
const data: AddonCalendarGetActionEventsByCoursesWSParams = {
timesortfrom: time,
courseids: courseIds,
limitnum: AddonBlockTimelineProvider.EVENTS_LIMIT_PER_COURSE,
};
const preSets: CoreSiteWSPreSets = {
cacheKey: this.getActionEventsByCoursesCacheKey(),
};
const events = await site.read<AddonCalendarEventsGroupedByCourse>(
'core_calendar_get_action_events_by_courses',
data,
preSets,
);
if (events && events.groupedbycourse) {
const courseEvents = {};
events.groupedbycourse.forEach((course) => {
courseEvents[course.courseid] = this.treatCourseEvents(course, time);
});
return courseEvents;
}
throw new CoreError('No events returned on core_calendar_get_action_events_by_courses.');
}
/**
* Get cache key for get calendar action events for a given list of courses value WS call.
*
* @return Cache key.
*/
protected getActionEventsByCoursesCacheKey(): string {
return ROOT_CACHE_KEY + 'bycourse';
}
/**
* Get calendar action events based on the timesort value.
*
* @param afterEventId The last seen event id.
* @param siteId Site ID. If not defined, use current site.
* @return Promise resolved when the info is retrieved.
*/
async getActionEventsByTimesort(
afterEventId?: number,
siteId?: string,
): Promise<{ events: AddonCalendarEvent[]; canLoadMore?: number }> {
const site = await CoreSites.instance.getSite(siteId);
const timesortfrom = moment().subtract(14, 'days').unix(); // Check two weeks ago.
const limitnum = AddonBlockTimelineProvider.EVENTS_LIMIT;
const data: AddonCalendarGetActionEventsByTimesortWSParams = {
timesortfrom,
limitnum,
};
if (afterEventId) {
data.aftereventid = afterEventId;
}
const preSets: CoreSiteWSPreSets = {
cacheKey: this.getActionEventsByTimesortCacheKey(afterEventId, limitnum),
getCacheUsingCacheKey: true,
uniqueCacheKey: true,
};
const result = await site.read<AddonCalendarEvents>(
'core_calendar_get_action_events_by_timesort',
data,
preSets,
);
if (result && result.events) {
const canLoadMore = result.events.length >= limitnum ? result.lastid : undefined;
// Filter events by time in case it uses cache.
const events = result.events.filter((element) => element.timesort >= timesortfrom);
return {
events,
canLoadMore,
};
}
throw new CoreError('No events returned on core_calendar_get_action_events_by_timesort.');
}
/**
* Get prefix cache key for calendar action events based on the timesort value WS calls.
*
* @return Cache key.
*/
protected getActionEventsByTimesortPrefixCacheKey(): string {
return ROOT_CACHE_KEY + 'bytimesort:';
}
/**
* Get cache key for get calendar action events based on the timesort value WS call.
*
* @param afterEventId The last seen event id.
* @param limit Limit num of the call.
* @return Cache key.
*/
protected getActionEventsByTimesortCacheKey(afterEventId?: number, limit?: number): string {
afterEventId = afterEventId || 0;
limit = limit || 0;
return this.getActionEventsByTimesortPrefixCacheKey() + afterEventId + ':' + limit;
}
/**
* Invalidates get calendar action events for a given list of courses WS call.
*
* @param siteId Site ID to invalidate. If not defined, use current site.
* @return Promise resolved when the data is invalidated.
*/
async invalidateActionEventsByCourses(siteId?: string): Promise<void> {
const site = await CoreSites.instance.getSite(siteId);
await site.invalidateWsCacheForKeyStartingWith(this.getActionEventsByCoursesCacheKey());
}
/**
* Invalidates get calendar action events based on the timesort value WS call.
*
* @param siteId Site ID to invalidate. If not defined, use current site.
* @return Promise resolved when the data is invalidated.
*/
async invalidateActionEventsByTimesort(siteId?: string): Promise<void> {
const site = await CoreSites.instance.getSite(siteId);
await site.invalidateWsCacheForKeyStartingWith(this.getActionEventsByTimesortPrefixCacheKey());
}
/**
* Returns whether or not My Overview is available for a certain site.
*
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with true if available, resolved with false or rejected otherwise.
*/
async isAvailable(siteId?: string): Promise<boolean> {
const site = await CoreSites.instance.getSite(siteId);
// First check if dashboard is disabled.
if (CoreCoursesDashboard.instance.isDisabledInSite(site)) {
return false;
}
return site.wsAvailable('core_calendar_get_action_events_by_courses') &&
site.wsAvailable('core_calendar_get_action_events_by_timesort');
}
/**
* Handles course events, filtering and treating if more can be loaded.
*
* @param course Object containing response course events info.
* @param timeFrom Current time to filter events from.
* @return Object with course events and last loaded event id if more can be loaded.
*/
protected treatCourseEvents(
course: AddonCalendarEvents,
timeFrom: number,
): { events: AddonCalendarEvent[]; canLoadMore?: number } {
const canLoadMore: number | undefined =
course.events.length >= AddonBlockTimelineProvider.EVENTS_LIMIT_PER_COURSE ? course.lastid : undefined;
// Filter events by time in case it uses cache.
course.events = course.events.filter((element) => element.timesort >= timeFrom);
return {
events: course.events,
canLoadMore,
};
}
}
export class AddonBlockTimeline extends makeSingleton(AddonBlockTimelineProvider) {}

View File

@ -0,0 +1,38 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { IonicModule } from '@ionic/angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreBlockDelegate } from '@features/block/services/block-delegate';
import { AddonBlockTimelineComponentsModule } from './components/components.module';
import { AddonBlockTimelineHandler } from './services/block-handler';
@NgModule({
imports: [
IonicModule,
AddonBlockTimelineComponentsModule,
TranslateModule.forChild(),
],
providers: [
{
provide: APP_INITIALIZER,
multi: true,
useValue: () => {
CoreBlockDelegate.instance.registerHandler(AddonBlockTimelineHandler.instance);
},
},
],
})
export class AddonBlockTimelineModule {}

View File

@ -1749,6 +1749,7 @@ export class AddonCalendar extends makeSingleton(AddonCalendarProvider) {}
/**
* Data returned by calendar's events_exporter.
* Data returned by core_calendar_get_action_events_by_course and core_calendar_get_action_events_by_timesort WS.
*/
export type AddonCalendarEvents = {
events: AddonCalendarEvent[]; // Events.
@ -1756,13 +1757,47 @@ export type AddonCalendarEvents = {
lastid: number; // Lastid.
};
/**
* Params of core_calendar_get_action_events_by_courses WS.
*/
export type AddonCalendarGetActionEventsByCoursesWSParams = {
courseids: number[];
timesortfrom?: number; // Time sort from.
timesortto?: number; // Time sort to.
limitnum?: number; // Limit number.
};
/**
* Data returned by calendar's events_grouped_by_course_exporter.
* Data returned by core_calendar_get_action_events_by_courses WS.
*/
export type AddonCalendarEventsGroupedByCourse = {
groupedbycourse: AddonCalendarEventsSameCourse[]; // Groupped by course.
};
/**
* Params of core_calendar_get_action_events_by_course WS.
*/
export type AddonCalendarGetActionEventsByCourseWSParams = {
courseid: number; // Course id.
timesortfrom?: number; // Time sort from.
timesortto?: number; // Time sort to.
aftereventid?: number; // The last seen event id.
limitnum?: number; // Limit number.
};
/**
* Params of core_calendar_get_action_events_by_timesort WS.
*/
export type AddonCalendarGetActionEventsByTimesortWSParams = {
timesortfrom?: number; // Time sort from.
timesortto?: number; // Time sort to.
aftereventid?: number; // The last seen event id.
limitnum?: number; // Limit number.
limittononsuspendedevents?: boolean; // Limit the events to courses the user is not suspended in.
userid?: number; // The user id.
};
/**
* Data returned by calendar's events_same_course_exporter.
*/

View File

@ -0,0 +1,178 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="157 -1509 148 125"
preserveAspectRatio="xMinYMid meet"
version="1.1"
id="svg23"
sodipodi:docname="activities.svg"
inkscape:version="0.92.1 r15371">
<metadata
id="metadata27">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1016"
id="namedview25"
showgrid="false"
inkscape:zoom="5.981125"
inkscape:cx="38.889548"
inkscape:cy="62.5"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="Group_42" />
<defs
id="defs7">
<style
id="style2">
.cls-1 {
clip-path: url(#clip-Activities);
}
.cls-2 {
fill: #eee;
}
.cls-3 {
fill: #c4c8cc;
}
.cls-4 {
fill: #fff;
}
</style>
<clipPath
id="clip-Activities">
<rect
x="157"
y="-1509"
width="148"
height="125"
id="rect4" />
</clipPath>
</defs>
<g
id="Activities"
class="cls-1"
clip-path="url(#clip-Activities)">
<g
id="Group_42"
data-name="Group 42"
transform="translate(-268 -1985)">
<ellipse
id="Ellipse_37"
data-name="Ellipse 37"
class="cls-2"
cx="74"
cy="14.785"
rx="74"
ry="14.785"
transform="translate(425 571.43)"
style="fill:#000000;fill-opacity:0.06666667" />
<rect
id="Rectangle_80"
data-name="Rectangle 80"
class="cls-3"
width="94.182"
height="110.215"
transform="translate(451.909 476)" />
<g
id="Group_41"
data-name="Group 41"
transform="translate(467.043 493)">
<rect
id="Rectangle_81"
data-name="Rectangle 81"
class="cls-4"
width="44.456"
height="5.625"
transform="translate(21.16 0.549)" />
<rect
id="Rectangle_82"
data-name="Rectangle 82"
class="cls-4"
width="33.342"
height="5.625"
transform="translate(21.16 11.652)" />
<rect
id="Rectangle_83"
data-name="Rectangle 83"
class="cls-4"
width="44.456"
height="5.625"
transform="translate(21.16 30.772)" />
<rect
id="Rectangle_84"
data-name="Rectangle 84"
class="cls-4"
width="33.342"
height="5.625"
transform="translate(21.16 41.875)" />
<rect
id="Rectangle_85"
data-name="Rectangle 85"
class="cls-4"
width="44.456"
height="5.625"
transform="translate(21.16 61.291)" />
<rect
id="Rectangle_86"
data-name="Rectangle 86"
class="cls-4"
width="33.342"
height="5.625"
transform="translate(21.16 72.393)" />
<ellipse
id="Ellipse_38"
data-name="Ellipse 38"
class="cls-4"
cx="7.007"
cy="7"
rx="7.007"
ry="7"
transform="translate(0 0)" />
<ellipse
id="Ellipse_39"
data-name="Ellipse 39"
class="cls-4"
cx="7.007"
cy="7"
rx="7.007"
ry="7"
transform="translate(0 31)" />
<ellipse
id="Ellipse_40"
data-name="Ellipse 40"
class="cls-4"
cx="7.007"
cy="7"
rx="7.007"
ry="7"
transform="translate(0 61)" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -20,6 +20,7 @@ import { CoreTextUtils } from '@services/utils/text';
import { CoreCourseBlock } from '../../course/services/course';
import { IonRefresher } from '@ionic/angular';
import { Params } from '@angular/router';
import { ContextLevel } from '@/core/constants';
/**
* Template class to easily create components for blocks.
@ -31,7 +32,7 @@ export abstract class CoreBlockBaseComponent implements OnInit {
@Input() title!: string; // The block title.
@Input() block!: CoreCourseBlock; // The block to render.
@Input() contextLevel!: string; // The context where the block will be used.
@Input() contextLevel!: ContextLevel; // The context where the block will be used.
@Input() instanceId!: number; // The instance ID associated with the context level.
@Input() link?: string; // Link to go when clicked.
@Input() linkParams?: Params; // Link params to go when clicked.

View File

@ -32,7 +32,7 @@ const routes: Routes = [
},
{
path: 'list-mod-type',
loadChildren: () => import('./pages/list-mod-type/list-mod-type').then( m => m.CoreCourseListModTypePage),
loadChildren: () => import('./pages/list-mod-type/list-mod-type.module').then(m => m.CoreCourseListModTypePageModule),
},
];

View File

@ -1,3 +1,5 @@
@import "~theme/globals";
:host {
ion-card {
display: flex;
@ -107,21 +109,8 @@
}
}
// @todo
:host-context(.core-horizontal-scroll) {
flex: 0 0 80%;
min-width: 250px;
max-width: 300px;
align-self: stretch;
display: block;
[text-wrap] .label {
h2, p {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
@include horizontal_scroll_item(80%, 250px, 300px);
ion-card {
.core-course-thumb {

View File

@ -20,7 +20,7 @@
<core-loading [hideUntil]="dataLoaded">
<ion-list>
<!-- Site home main contents. -->
<!-- @todo <ng-container *ngIf="section && section.hasContent">
<ng-container *ngIf="section && section.hasContent">
<ion-item class="ion-text-wrap" *ngIf="section.summary">
<core-format-text [text]="section.summary" contextLevel="course" [contextInstanceId]="siteHomeId">
</core-format-text>
@ -28,7 +28,7 @@
<core-course-module *ngFor="let module of section.modules" [module]="module" [courseId]="siteHomeId"
[downloadEnabled]="downloadEnabled" [section]="section"></core-course-module>
</ng-container> -->
</ng-container>
<!-- Site home items: news, categories, courses, etc. -->
<ng-container *ngIf="items.length > 0">
@ -71,11 +71,8 @@
</ng-template>
<ng-template #news>
<ion-item>
<ion-label>News (TODO)</ion-label>
</ion-item>
<!-- @todo <core-course-module class="core-sitehome-news" *ngIf="newsForumModule" [module]="module" [courseId]="siteHomeId">
</core-course-module> -->
<core-course-module class="core-sitehome-news" *ngIf="newsForumModule" [module]="newsForumModule" [courseId]="siteHomeId">
</core-course-module>
</ng-template>
<ng-template #categories>

View File

@ -20,6 +20,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { CoreSharedModule } from '@/core/shared.module';
import { CoreBlockComponentsModule } from '@/core/features/block/components/components.module';
import { CoreCourseComponentsModule } from '@features/course/components/components.module';
import { CoreSiteHomeIndexPage } from '.';
@ -38,6 +39,7 @@ const routes: Routes = [
TranslateModule.forChild(),
CoreSharedModule,
CoreBlockComponentsModule,
CoreCourseComponentsModule,
],
declarations: [
CoreSiteHomeIndexPage,

View File

@ -185,3 +185,32 @@
}
}
}
@mixin horizontal_scroll_item($width, $min-width, $max-width) {
flex: 0 0 $width;
min-width: $min-width;
max-width: $max-width;
align-self: stretch;
display: block;
ion-card {
height: calc(100% - 20px);
width: calc(100% - 20px);
margin-top: 10px;
margin-bottom: 10px;
@media (max-width: 360px) {
margin-left: 6px;
margin-right: 6px;
width: calc(100% - 12px);
}
}
.ion-text-wrap ion-label {
h2, p {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}