commit
798d916b1f
|
@ -28,7 +28,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.userpicture {
|
.userpicture {
|
||||||
vertical-align: text-bottom;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,8 +15,6 @@
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
|
|
||||||
import { CoreSharedModule } from '@/core/shared.module';
|
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 { AddonBlockTimelineComponent } from './timeline/timeline';
|
||||||
import { AddonBlockTimelineEventsComponent } from './events/events';
|
import { AddonBlockTimelineEventsComponent } from './events/events';
|
||||||
|
@ -28,8 +26,6 @@ import { AddonBlockTimelineEventsComponent } from './events/events';
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CoreSharedModule,
|
CoreSharedModule,
|
||||||
CoreCoursesComponentsModule,
|
|
||||||
CoreCourseComponentsModule,
|
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
AddonBlockTimelineComponent,
|
AddonBlockTimelineComponent,
|
||||||
|
|
|
@ -1,50 +1,61 @@
|
||||||
|
<ion-item lines="none" *ngIf="course">
|
||||||
|
<ion-label class="ion-text-wrap">
|
||||||
|
<h3>
|
||||||
|
<span class="sr-only">{{ 'core.courses.aria:coursename' | translate }}</span>
|
||||||
|
<core-format-text [text]="course.fullname" contextLevel="course" [contextInstanceId]="course.id"></core-format-text>
|
||||||
|
</h3>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
<ion-item-group *ngFor="let dayEvents of filteredEvents">
|
<ion-item-group *ngFor="let dayEvents of filteredEvents">
|
||||||
<ion-item-divider [color]="dayEvents.color">
|
<ion-item lines="none">
|
||||||
<ion-label><h3>{{ dayEvents.dayTimestamp * 1000 | coreFormatDate:"strftimedayshort" }}</h3></ion-label>
|
<ion-label>
|
||||||
</ion-item-divider>
|
<h4 [class.core-bold]="!course">{{ dayEvents.dayTimestamp * 1000 | coreFormatDate:"strftimedayshort" }}</h4>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
<ng-container *ngFor="let event of dayEvents.events">
|
<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)"
|
<ion-item class="addon-block-timeline-activity" detail="false" (click)="action($event, event.url)" [attr.aria-label]="event.name"
|
||||||
[attr.aria-label]="event.name" button>
|
button lines="full">
|
||||||
<core-mod-icon *ngIf="event.iconUrl" slot="start" [modicon]="event.iconUrl" [componentId]="event.instance"
|
|
||||||
[modname]="event.modulename">
|
|
||||||
</core-mod-icon>
|
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<p class="item-heading">
|
<ion-row class="ion-justify-content-between ion-align-items-center ion-no-padding">
|
||||||
<core-format-text [text]="event.name" contextLevel="module" [contextInstanceId]="event.id"
|
<ion-col class="addon-block-timeline-activity-main ion-no-padding">
|
||||||
[courseId]="event.course && event.course.id">
|
<ion-row class="ion-justify-content-between ion-align-items-center ion-nowrap ion-no-padding">
|
||||||
</core-format-text>
|
<ion-col class="addon-block-timeline-activity-time ion-no-padding">
|
||||||
</p>
|
<ion-badge color="light">{{event.timesort * 1000 | coreFormatDate:"strftimetime24" }}</ion-badge>
|
||||||
<p *ngIf="showCourse && event.course">
|
<core-mod-icon *ngIf="event.iconUrl" [modicon]="event.iconUrl" [componentId]="event.instance"
|
||||||
<core-format-text [text]="event.course.fullnamedisplay" contextLevel="course"
|
[modname]="event.modulename">
|
||||||
[contextInstanceId]="event.course.id">
|
</core-mod-icon>
|
||||||
</core-format-text>
|
</ion-col>
|
||||||
</p>
|
<ion-col class="addon-block-timeline-activity-name ion-no-padding">
|
||||||
|
<p class="item-heading">
|
||||||
<ion-button fill="clear" class="ion-hide-md-up ion-text-wrap" (click)="action($event, event.action.url)"
|
<core-format-text [text]="event.activityname || event.name" contextLevel="module"
|
||||||
[title]="event.action.name" [disabled]="!event.action.actionable" *ngIf="event.action">
|
[contextInstanceId]="event.id" [courseId]="event.course && event.course.id">
|
||||||
{{event.action.name}}
|
</core-format-text>
|
||||||
<ion-badge slot="end" class="ion-margin-start" *ngIf="event.action.showitemcount">{{event.action.itemcount}}
|
<ion-badge *ngIf="event.overdue" color="danger">{{ 'addon.block_timeline.overdue' | translate }}
|
||||||
</ion-badge>
|
</ion-badge>
|
||||||
</ion-button>
|
</p>
|
||||||
|
<p *ngIf="(showCourse && event.course) || event.activitystr">
|
||||||
|
<span *ngIf="showCourse && event.course">
|
||||||
|
<core-format-text [text]="event.course.fullnamedisplay" contextLevel="course"
|
||||||
|
[contextInstanceId]="event.course.id">
|
||||||
|
</core-format-text> ·
|
||||||
|
</span>
|
||||||
|
<core-format-text [text]="event.activitystr" contextLevel="module" [contextInstanceId]="event.id">
|
||||||
|
</core-format-text>
|
||||||
|
</p>
|
||||||
|
</ion-col>
|
||||||
|
</ion-row>
|
||||||
|
</ion-col>
|
||||||
|
<ion-col class="addon-block-timeline-activity-action ion-no-padding">
|
||||||
|
<ion-button fill="clear" (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-col>
|
||||||
|
</ion-row>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
|
|
||||||
<div slot="end" class="events-info">
|
|
||||||
<div>
|
|
||||||
<ion-badge color="light">{{event.timesort * 1000 | coreFormatDate:"strftimetime24" }}</ion-badge>
|
|
||||||
</div>
|
|
||||||
<ion-button
|
|
||||||
class="ion-hide-md-down"
|
|
||||||
fill="clear"
|
|
||||||
(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>
|
|
||||||
</div>
|
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ion-item-group>
|
</ion-item-group>
|
||||||
|
@ -57,6 +68,10 @@
|
||||||
<ion-spinner *ngIf="loadingMore" [attr.aria-label]="'core.loading' | translate"></ion-spinner>
|
<ion-spinner *ngIf="loadingMore" [attr.aria-label]="'core.loading' | translate"></ion-spinner>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<core-empty-box *ngIf="empty" image="assets/img/icons/activities.svg" [message]="'addon.block_timeline.noevents' | translate"
|
<ion-item lines="none" *ngIf="empty && course">
|
||||||
inline="true">
|
<ion-label class="ion-text-wrap">
|
||||||
</core-empty-box>
|
<p>{{'addon.block_timeline.noevents' | translate}}</p>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
<core-empty-box *ngIf="empty && !course" image="assets/img/icons/activities.svg" inline="true"
|
||||||
|
[message]="'addon.block_timeline.noevents' | translate"></core-empty-box>
|
||||||
|
|
|
@ -1,6 +1,41 @@
|
||||||
.events-info {
|
@import "~theme/globals";
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
h3 {
|
||||||
text-align: end;
|
font-weight: bold;
|
||||||
padding: 10px 0;
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4.core-bold {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addon-block-timeline-activity ion-badge {
|
||||||
|
@include margin-horizontal(0.25rem, 0.5rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.addon-block-timeline-activity core-mod-icon {
|
||||||
|
--margin-end: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addon-block-timeline-activity-time,
|
||||||
|
.addon-block-timeline-activity-action {
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addon-block-timeline-activity-main,
|
||||||
|
.addon-block-timeline-activity-name {
|
||||||
|
flex-grow: 1;
|
||||||
|
p {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.addon-block-timeline-activity-name {
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,11 +17,11 @@ import { CoreSites } from '@services/sites';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { CoreTextUtils } from '@services/utils/text';
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
import { CoreTimeUtils } from '@services/utils/time';
|
import { CoreTimeUtils } from '@services/utils/time';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
|
||||||
import { CoreCourse } from '@features/course/services/course';
|
import { CoreCourse } from '@features/course/services/course';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper';
|
import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper';
|
||||||
import { AddonCalendarEvent } from '@addons/calendar/services/calendar';
|
import { AddonCalendarEvent } from '@addons/calendar/services/calendar';
|
||||||
|
import { CoreEnrolledCourseDataWithOptions } from '@features/courses/services/courses-helper';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Directive to render a list of events in course overview.
|
* Directive to render a list of events in course overview.
|
||||||
|
@ -34,34 +34,37 @@ import { AddonCalendarEvent } from '@addons/calendar/services/calendar';
|
||||||
export class AddonBlockTimelineEventsComponent implements OnChanges {
|
export class AddonBlockTimelineEventsComponent implements OnChanges {
|
||||||
|
|
||||||
@Input() events: AddonBlockTimelineEvent[] = []; // The events to render.
|
@Input() events: AddonBlockTimelineEvent[] = []; // The events to render.
|
||||||
@Input() showCourse?: boolean | string; // Whether to show the course name.
|
@Input() course?: CoreEnrolledCourseDataWithOptions; // Whether to show the course name.
|
||||||
@Input() from = 0; // Number of days from today to offset the events.
|
@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() 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.
|
@Input() canLoadMore = false; // Whether more events can be loaded.
|
||||||
@Output() loadMore: EventEmitter<void>; // Notify that more events should be loaded.
|
@Output() loadMore = new EventEmitter(); // Notify that more events should be loaded.
|
||||||
|
|
||||||
|
showCourse = false; // Whether to show the course name.
|
||||||
empty = true;
|
empty = true;
|
||||||
loadingMore = false;
|
loadingMore = false;
|
||||||
filteredEvents: AddonBlockTimelineEventFilteredEvent[] = [];
|
filteredEvents: AddonBlockTimelineEventFilteredEvent[] = [];
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.loadMore = new EventEmitter();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detect changes on input properties.
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
async ngOnChanges(changes: {[name: string]: SimpleChange}): Promise<void> {
|
async ngOnChanges(changes: {[name: string]: SimpleChange}): Promise<void> {
|
||||||
this.showCourse = CoreUtils.isTrueOrOne(this.showCourse);
|
this.showCourse = !this.course;
|
||||||
|
|
||||||
if (changes.events || changes.from || changes.to) {
|
if (changes.events || changes.from || changes.to) {
|
||||||
if (this.events && this.events.length > 0) {
|
if (this.events && this.events.length > 0) {
|
||||||
const filteredEvents = await this.filterEventsByTime(this.from, this.to);
|
const filteredEvents = await this.filterEventsByTime(this.from, this.to);
|
||||||
this.empty = !filteredEvents || filteredEvents.length <= 0;
|
this.empty = !filteredEvents || filteredEvents.length <= 0;
|
||||||
|
|
||||||
|
const now = CoreTimeUtils.timestamp();
|
||||||
|
|
||||||
const eventsByDay: Record<number, AddonCalendarEvent[]> = {};
|
const eventsByDay: Record<number, AddonCalendarEvent[]> = {};
|
||||||
filteredEvents.forEach((event) => {
|
filteredEvents.forEach((event) => {
|
||||||
const dayTimestamp = CoreTimeUtils.getMidnightForTimestamp(event.timesort);
|
const dayTimestamp = CoreTimeUtils.getMidnightForTimestamp(event.timesort);
|
||||||
|
|
||||||
|
// Already calculated on 4.0 onwards but this will be live.
|
||||||
|
event.overdue = event.timesort < now;
|
||||||
|
|
||||||
if (eventsByDay[dayTimestamp]) {
|
if (eventsByDay[dayTimestamp]) {
|
||||||
eventsByDay[dayTimestamp].push(event);
|
eventsByDay[dayTimestamp].push(event);
|
||||||
} else {
|
} else {
|
||||||
|
@ -69,15 +72,13 @@ export class AddonBlockTimelineEventsComponent implements OnChanges {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const todaysMidnight = CoreTimeUtils.getMidnightForTimestamp();
|
this.filteredEvents = Object.keys(eventsByDay).map((key) => {
|
||||||
this.filteredEvents = [];
|
|
||||||
Object.keys(eventsByDay).forEach((key) => {
|
|
||||||
const dayTimestamp = parseInt(key);
|
const dayTimestamp = parseInt(key);
|
||||||
this.filteredEvents.push({
|
|
||||||
color: dayTimestamp < todaysMidnight ? 'danger' : 'light',
|
return {
|
||||||
dayTimestamp,
|
dayTimestamp,
|
||||||
events: eventsByDay[dayTimestamp],
|
events: eventsByDay[dayTimestamp],
|
||||||
});
|
};
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.empty = true;
|
this.empty = true;
|
||||||
|
@ -94,7 +95,7 @@ export class AddonBlockTimelineEventsComponent implements OnChanges {
|
||||||
*/
|
*/
|
||||||
protected async filterEventsByTime(start: number, end?: number): Promise<AddonBlockTimelineEvent[]> {
|
protected async filterEventsByTime(start: number, end?: number): Promise<AddonBlockTimelineEvent[]> {
|
||||||
start = moment().add(start, 'days').startOf('day').unix();
|
start = moment().add(start, 'days').startOf('day').unix();
|
||||||
end = typeof end != 'undefined' ? moment().add(end, 'days').startOf('day').unix() : end;
|
end = end !== undefined ? moment().add(end, 'days').startOf('day').unix() : end;
|
||||||
|
|
||||||
return await Promise.all(this.events.filter((event) => {
|
return await Promise.all(this.events.filter((event) => {
|
||||||
if (end) {
|
if (end) {
|
||||||
|
@ -122,12 +123,12 @@ export class AddonBlockTimelineEventsComponent implements OnChanges {
|
||||||
/**
|
/**
|
||||||
* Action clicked.
|
* Action clicked.
|
||||||
*
|
*
|
||||||
* @param e Click event.
|
* @param event Click event.
|
||||||
* @param url Url of the action.
|
* @param url Url of the action.
|
||||||
*/
|
*/
|
||||||
async action(e: Event, url: string): Promise<void> {
|
async action(event: Event, url: string): Promise<void> {
|
||||||
e.preventDefault();
|
event.preventDefault();
|
||||||
e.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
// Fix URL format.
|
// Fix URL format.
|
||||||
url = CoreTextUtils.decodeHTMLEntities(url);
|
url = CoreTextUtils.decodeHTMLEntities(url);
|
||||||
|
@ -137,7 +138,7 @@ export class AddonBlockTimelineEventsComponent implements OnChanges {
|
||||||
try {
|
try {
|
||||||
const treated = await CoreContentLinksHelper.handleLink(url);
|
const treated = await CoreContentLinksHelper.handleLink(url);
|
||||||
if (!treated) {
|
if (!treated) {
|
||||||
return CoreSites.getCurrentSite()?.openInBrowserWithAutoLoginIfSameSite(url);
|
return CoreSites.getRequiredCurrentSite().openInBrowserWithAutoLoginIfSameSite(url);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
modal.dismiss();
|
modal.dismiss();
|
||||||
|
@ -154,5 +155,4 @@ type AddonBlockTimelineEvent = AddonCalendarEvent & {
|
||||||
type AddonBlockTimelineEventFilteredEvent = {
|
type AddonBlockTimelineEventFilteredEvent = {
|
||||||
events: AddonBlockTimelineEvent[];
|
events: AddonBlockTimelineEvent[];
|
||||||
dayTimestamp: number;
|
dayTimestamp: number;
|
||||||
color: string;
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
<ion-item-divider sticky="true">
|
<ion-item-divider sticky="true">
|
||||||
<ion-label><h2>{{ 'addon.block_timeline.pluginname' | translate }}</h2></ion-label>
|
<ion-label>
|
||||||
|
<h2>{{ 'addon.block_timeline.pluginname' | translate }}</h2>
|
||||||
|
</ion-label>
|
||||||
<core-context-menu slot="end">
|
<core-context-menu slot="end">
|
||||||
<core-context-menu-item *ngIf="loaded" [priority]="900" [content]="'addon.block_timeline.sortbydates' | translate"
|
<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'">
|
(action)="switchSort('sortbydates')" [iconAction]="sort == 'sortbydates' ? 'far-dot-circle' : 'far-circle'">
|
||||||
|
@ -18,7 +20,7 @@
|
||||||
<ion-select-option class="ion-text-wrap" value="overdue">
|
<ion-select-option class="ion-text-wrap" value="overdue">
|
||||||
{{ 'addon.block_timeline.overdue' | translate }}
|
{{ 'addon.block_timeline.overdue' | translate }}
|
||||||
</ion-select-option>
|
</ion-select-option>
|
||||||
<ion-select-option class="ion-text-wrap" disabled value="disabled">
|
<ion-select-option class="ion-text-wrap core-select-option-title" disabled value="disabled">
|
||||||
{{ 'addon.block_timeline.duedate' | translate }}
|
{{ 'addon.block_timeline.duedate' | translate }}
|
||||||
</ion-select-option>
|
</ion-select-option>
|
||||||
<ion-select-option class="ion-text-wrap" value="next7days">
|
<ion-select-option class="ion-text-wrap" value="next7days">
|
||||||
|
@ -36,21 +38,14 @@
|
||||||
</core-combobox>
|
</core-combobox>
|
||||||
</div>
|
</div>
|
||||||
<core-loading [hideUntil]="timeline.loaded" [hidden]="sort != 'sortbydates'" [fullscreen]="false">
|
<core-loading [hideUntil]="timeline.loaded" [hidden]="sort != 'sortbydates'" [fullscreen]="false">
|
||||||
<addon-block-timeline-events [events]="timeline.events" showCourse="true" [canLoadMore]="timeline.canLoadMore"
|
<addon-block-timeline-events [events]="timeline.events" [canLoadMore]="timeline.canLoadMore" (loadMore)="loadMore()"
|
||||||
(loadMore)="loadMoreTimeline()" [from]="dataFrom" [to]="dataTo"></addon-block-timeline-events>
|
[from]="dataFrom" [to]="dataTo"></addon-block-timeline-events>
|
||||||
</core-loading>
|
</core-loading>
|
||||||
<core-loading [hideUntil]="timelineCourses.loaded" [hidden]="sort != 'sortbycourses'"
|
<core-loading [hideUntil]="timelineCourses.loaded" [hidden]="sort != 'sortbycourses'" [fullscreen]="false" class="safe-area-page">
|
||||||
[fullscreen]="false" class="safe-area-padding">
|
<ng-container *ngFor="let course of timelineCourses.courses">
|
||||||
<ion-grid class="ion-no-padding">
|
<addon-block-timeline-events [events]="course.events" [canLoadMore]="course.canLoadMore" (loadMore)="loadMore(course)"
|
||||||
<ion-row class="ion-no-padding">
|
[course]="course" [from]="dataFrom" [to]="dataTo"></addon-block-timeline-events>
|
||||||
<ion-col *ngFor="let course of timelineCourses.courses" class="ion-no-padding" size="12" size-md="6">
|
</ng-container>
|
||||||
<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" inline="true"
|
<core-empty-box *ngIf="timelineCourses.courses.length == 0" image="assets/img/icons/courses.svg" inline="true"
|
||||||
[message]="'addon.block_timeline.nocoursesinprogress' | translate"></core-empty-box>
|
[message]="'addon.block_timeline.nocoursesinprogress' | translate"></core-empty-box>
|
||||||
</core-loading>
|
</core-loading>
|
||||||
|
|
|
@ -24,6 +24,7 @@ import { CoreCoursesHelper, CoreEnrolledCourseDataWithOptions } from '@features/
|
||||||
import { CoreSite } from '@classes/site';
|
import { CoreSite } from '@classes/site';
|
||||||
import { CoreCourses } from '@features/courses/services/courses';
|
import { CoreCourses } from '@features/courses/services/courses';
|
||||||
import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate';
|
import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate';
|
||||||
|
import { CoreNavigator } from '@services/navigator';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component to render a timeline block.
|
* Component to render a timeline block.
|
||||||
|
@ -36,7 +37,7 @@ export class AddonBlockTimelineComponent extends CoreBlockBaseComponent implemen
|
||||||
|
|
||||||
sort = 'sortbydates';
|
sort = 'sortbydates';
|
||||||
filter = 'next30days';
|
filter = 'next30days';
|
||||||
currentSite?: CoreSite;
|
currentSite!: CoreSite;
|
||||||
timeline: {
|
timeline: {
|
||||||
events: AddonCalendarEvent[];
|
events: AddonCalendarEvent[];
|
||||||
loaded: boolean;
|
loaded: boolean;
|
||||||
|
@ -66,10 +67,18 @@ export class AddonBlockTimelineComponent extends CoreBlockBaseComponent implemen
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component being initialized.
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
async ngOnInit(): Promise<void> {
|
async ngOnInit(): Promise<void> {
|
||||||
this.currentSite = CoreSites.getRequiredCurrentSite();
|
try {
|
||||||
|
this.currentSite = CoreSites.getRequiredCurrentSite();
|
||||||
|
} catch (error) {
|
||||||
|
CoreDomUtils.showErrorModal(error);
|
||||||
|
|
||||||
|
CoreNavigator.back();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.filter = await this.currentSite.getLocalSiteConfig('AddonBlockTimelineFilter', this.filter);
|
this.filter = await this.currentSite.getLocalSiteConfig('AddonBlockTimelineFilter', this.filter);
|
||||||
this.switchFilter(this.filter);
|
this.switchFilter(this.filter);
|
||||||
|
@ -117,28 +126,21 @@ export class AddonBlockTimelineComponent extends CoreBlockBaseComponent implemen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Load more events.
|
|
||||||
*/
|
|
||||||
async loadMoreTimeline(): Promise<void> {
|
|
||||||
try {
|
|
||||||
await this.fetchMyOverviewTimeline(this.timeline.canLoadMore);
|
|
||||||
} catch (error) {
|
|
||||||
CoreDomUtils.showErrorModalDefault(error, this.fetchContentDefaultError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load more events.
|
* Load more events.
|
||||||
*
|
*
|
||||||
* @param course Course.
|
* @param course Course. If defined, it will update the course events, timeline otherwise.
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
async loadMoreCourse(course: AddonBlockTimelineCourse): Promise<void> {
|
async loadMore(course?: AddonBlockTimelineCourse): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const courseEvents = await AddonBlockTimeline.getActionEventsByCourse(course.id, course.canLoadMore);
|
if (course) {
|
||||||
course.events = course.events?.concat(courseEvents.events);
|
const courseEvents = await AddonBlockTimeline.getActionEventsByCourse(course.id, course.canLoadMore);
|
||||||
course.canLoadMore = courseEvents.canLoadMore;
|
course.events = course.events?.concat(courseEvents.events);
|
||||||
|
course.canLoadMore = courseEvents.canLoadMore;
|
||||||
|
} else {
|
||||||
|
await this.fetchMyOverviewTimeline(this.timeline.canLoadMore);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CoreDomUtils.showErrorModalDefault(error, this.fetchContentDefaultError);
|
CoreDomUtils.showErrorModalDefault(error, this.fetchContentDefaultError);
|
||||||
}
|
}
|
||||||
|
@ -188,12 +190,12 @@ export class AddonBlockTimelineComponent extends CoreBlockBaseComponent implemen
|
||||||
*/
|
*/
|
||||||
switchFilter(filter: string): void {
|
switchFilter(filter: string): void {
|
||||||
this.filter = filter;
|
this.filter = filter;
|
||||||
this.currentSite?.setLocalSiteConfig('AddonBlockTimelineFilter', this.filter);
|
this.currentSite.setLocalSiteConfig('AddonBlockTimelineFilter', this.filter);
|
||||||
|
|
||||||
switch (this.filter) {
|
switch (this.filter) {
|
||||||
case 'overdue':
|
case 'overdue':
|
||||||
this.dataFrom = -14;
|
this.dataFrom = -14;
|
||||||
this.dataTo = 0;
|
this.dataTo = 1;
|
||||||
break;
|
break;
|
||||||
case 'next7days':
|
case 'next7days':
|
||||||
this.dataFrom = 0;
|
this.dataFrom = 0;
|
||||||
|
@ -226,7 +228,7 @@ export class AddonBlockTimelineComponent extends CoreBlockBaseComponent implemen
|
||||||
*/
|
*/
|
||||||
switchSort(sort: string): void {
|
switchSort(sort: string): void {
|
||||||
this.sort = sort;
|
this.sort = sort;
|
||||||
this.currentSite?.setLocalSiteConfig('AddonBlockTimelineSort', this.sort);
|
this.currentSite.setLocalSiteConfig('AddonBlockTimelineSort', this.sort);
|
||||||
|
|
||||||
if (!this.timeline.loaded && this.sort == 'sortbydates') {
|
if (!this.timeline.loaded && this.sort == 'sortbydates') {
|
||||||
this.fetchContent();
|
this.fetchContent();
|
||||||
|
@ -237,7 +239,7 @@ export class AddonBlockTimelineComponent extends CoreBlockBaseComponent implemen
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AddonBlockTimelineCourse = CoreEnrolledCourseDataWithOptions & {
|
export type AddonBlockTimelineCourse = CoreEnrolledCourseDataWithOptions & {
|
||||||
events?: AddonCalendarEvent[];
|
events?: AddonCalendarEvent[];
|
||||||
canLoadMore?: number;
|
canLoadMore?: number;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1707,14 +1707,19 @@ export type AddonCalendarEventBase = {
|
||||||
userid?: number; // Userid.
|
userid?: number; // Userid.
|
||||||
repeatid?: number; // Repeatid.
|
repeatid?: number; // Repeatid.
|
||||||
eventcount?: number; // Eventcount.
|
eventcount?: number; // Eventcount.
|
||||||
|
component?: string; // Component.
|
||||||
modulename?: string; // Modulename.
|
modulename?: string; // Modulename.
|
||||||
|
activityname?: string; // Activityname.
|
||||||
|
activitystr?: string; // Activitystr.
|
||||||
instance?: number; // Instance.
|
instance?: number; // Instance.
|
||||||
eventtype: AddonCalendarEventType; // Eventtype.
|
eventtype: AddonCalendarEventType; // Eventtype.
|
||||||
timestart: number; // Timestart.
|
timestart: number; // Timestart.
|
||||||
timeduration: number; // Timeduration.
|
timeduration: number; // Timeduration.
|
||||||
timesort: number; // Timesort.
|
timesort: number; // Timesort.
|
||||||
|
timeusermidnight: number; // Timeusermidnight.
|
||||||
visible: number; // Visible.
|
visible: number; // Visible.
|
||||||
timemodified: number; // Timemodified.
|
timemodified: number; // Timemodified.
|
||||||
|
overdue?: boolean; // Overdue.
|
||||||
icon: {
|
icon: {
|
||||||
key: string; // Key.
|
key: string; // Key.
|
||||||
component: string; // Component.
|
component: string; // Component.
|
||||||
|
|
|
@ -23,11 +23,11 @@ import { CoreSites } from '@services/sites';
|
||||||
import { CoreEvents } from '@singletons/events';
|
import { CoreEvents } from '@singletons/events';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import {
|
import {
|
||||||
CorePushNotifications,
|
|
||||||
CorePushNotificationsNotificationBasicData,
|
CorePushNotificationsNotificationBasicData,
|
||||||
} from '@features/pushnotifications/services/pushnotifications';
|
} from '@features/pushnotifications/services/pushnotifications';
|
||||||
import { CorePushNotificationsDelegate } from '@features/pushnotifications/services/push-delegate';
|
import { CorePushNotificationsDelegate } from '@features/pushnotifications/services/push-delegate';
|
||||||
import { makeSingleton } from '@singletons';
|
import { makeSingleton } from '@singletons';
|
||||||
|
import { CoreMainMenuProvider } from '@features/mainmenu/services/mainmenu';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler to inject an option into main menu.
|
* Handler to inject an option into main menu.
|
||||||
|
@ -90,7 +90,7 @@ export class AddonMessagesMainMenuHandlerService implements CoreMainMenuHandler,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Register Badge counter.
|
// Register Badge counter.
|
||||||
CorePushNotificationsDelegate.registerCounterHandler('AddonMessages');
|
CorePushNotificationsDelegate.registerCounterHandler(AddonMessagesMainMenuHandlerService.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -162,7 +162,14 @@ export class AddonMessagesMainMenuHandlerService implements CoreMainMenuHandler,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update push notifications badge.
|
// Update push notifications badge.
|
||||||
CorePushNotifications.updateAddonCounter('AddonMessages', totalCount, siteId);
|
CoreEvents.trigger(
|
||||||
|
CoreMainMenuProvider.MAIN_MENU_HANDLER_BADGE_UPDATED,
|
||||||
|
{
|
||||||
|
handler: AddonMessagesMainMenuHandlerService.name,
|
||||||
|
value: totalCount,
|
||||||
|
},
|
||||||
|
siteId,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -22,6 +22,7 @@ import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '@features/mainmenu
|
||||||
import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications';
|
import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications';
|
||||||
import { CorePushNotificationsDelegate } from '@features/pushnotifications/services/push-delegate';
|
import { CorePushNotificationsDelegate } from '@features/pushnotifications/services/push-delegate';
|
||||||
import { AddonNotifications, AddonNotificationsProvider } from '../notifications';
|
import { AddonNotifications, AddonNotificationsProvider } from '../notifications';
|
||||||
|
import { CoreMainMenuProvider } from '@features/mainmenu/services/mainmenu';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler to inject an option into main menu.
|
* Handler to inject an option into main menu.
|
||||||
|
@ -72,7 +73,7 @@ export class AddonNotificationsMainMenuHandlerService implements CoreMainMenuHan
|
||||||
});
|
});
|
||||||
|
|
||||||
// Register Badge counter.
|
// Register Badge counter.
|
||||||
CorePushNotificationsDelegate.registerCounterHandler('AddonNotifications');
|
CorePushNotificationsDelegate.registerCounterHandler(AddonNotificationsMainMenuHandlerService.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -112,10 +113,20 @@ export class AddonNotificationsMainMenuHandlerService implements CoreMainMenuHan
|
||||||
try {
|
try {
|
||||||
const unreadCountData = await AddonNotifications.getUnreadNotificationsCount(undefined, siteId);
|
const unreadCountData = await AddonNotifications.getUnreadNotificationsCount(undefined, siteId);
|
||||||
|
|
||||||
this.handlerData.badge = unreadCountData.count > 0 ?
|
this.handlerData.badge = unreadCountData.count > 0
|
||||||
unreadCountData.count + (unreadCountData.hasMore ? '+' : '') :
|
? unreadCountData.count + (unreadCountData.hasMore ? '+' : '')
|
||||||
'';
|
: '';
|
||||||
CorePushNotifications.updateAddonCounter('AddonNotifications', unreadCountData.count, siteId);
|
|
||||||
|
CorePushNotifications.updateAddonCounter(AddonNotificationsMainMenuHandlerService.name, unreadCountData.count, siteId);
|
||||||
|
|
||||||
|
CoreEvents.trigger(
|
||||||
|
CoreMainMenuProvider.MAIN_MENU_HANDLER_BADGE_UPDATED,
|
||||||
|
{
|
||||||
|
handler: AddonNotificationsMainMenuHandlerService.name,
|
||||||
|
value: unreadCountData.count,
|
||||||
|
},
|
||||||
|
siteId,
|
||||||
|
);
|
||||||
} catch {
|
} catch {
|
||||||
this.handlerData.badge = '';
|
this.handlerData.badge = '';
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
@ -7,4 +7,8 @@
|
||||||
ion-item-divider {
|
ion-item-divider {
|
||||||
min-height: var(--item-divider-min-height);
|
min-height: var(--item-divider-min-height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::ng-deep core-loading {
|
||||||
|
--loading-inline-min-height: 44px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
<ion-card [attr.course-color]="course.color ? null : course.colorNumber">
|
<ion-card [attr.course-color]="course.color ? null : course.colorNumber">
|
||||||
<div (click)="openCourse()" class="core-course-thumb" [class.core-course-color-img]="course.courseImage"
|
<div (click)="openCourse()" class="core-course-thumb" [class.core-course-color-img]="course.courseImage"
|
||||||
[style.background-color]="course.color">
|
[style.background-color]="course.color">
|
||||||
<img *ngIf="course.courseImage" [src]="course.courseImage" core-external-content alt=""/>
|
<img *ngIf="course.courseImage" [src]="course.courseImage" core-external-content alt="" />
|
||||||
</div>
|
</div>
|
||||||
<ion-item button lines="none" (click)="openCourse()" [attr.aria-label]="course.displayname || course.fullname"
|
<ion-item button lines="none" (click)="openCourse()" [attr.aria-label]="course.displayname || course.fullname"
|
||||||
class="core-course-header" [class.item-disabled]="course.visible == 0"
|
class="core-course-header" [class.item-disabled]="course.visible == 0"
|
||||||
[class.core-course-only-title]="!showAll || progress < 0 && completionUserTracked === false"
|
[class.core-course-only-title]="!showAll || progress < 0 && completionUserTracked === false" detail="false">
|
||||||
detail="false">
|
<ion-label class="ion-text-wrap core-course-title"
|
||||||
<ion-label
|
|
||||||
class="ion-text-wrap core-course-title"
|
|
||||||
[class.core-course-with-buttons]="courseOptionMenuEnabled || (downloadCourseEnabled && showDownload)"
|
[class.core-course-with-buttons]="courseOptionMenuEnabled || (downloadCourseEnabled && showDownload)"
|
||||||
[class.core-course-with-spinner]="(downloadCourseEnabled && prefetchCourseData.icon == 'spinner') || showSpinner">
|
[class.core-course-with-spinner]="(downloadCourseEnabled && prefetchCourseData.icon == 'spinner') || showSpinner">
|
||||||
<p *ngIf="course.categoryname || (course.displayname && course.shortname && course.fullname != course.displayname)"
|
<p *ngIf="course.categoryname || (course.displayname && course.shortname && course.fullname != course.displayname)"
|
||||||
|
@ -19,8 +17,7 @@
|
||||||
</span>
|
</span>
|
||||||
<span *ngIf="course.categoryname && course.displayname && course.shortname && course.fullname != course.displayname"
|
<span *ngIf="course.categoryname && course.displayname && course.shortname && course.fullname != course.displayname"
|
||||||
class="core-course-category"> | </span>
|
class="core-course-category"> | </span>
|
||||||
<span *ngIf="course.displayname && course.shortname && course.fullname != course.displayname"
|
<span *ngIf="course.displayname && course.shortname && course.fullname != course.displayname" class="core-course-shortname">
|
||||||
class="core-course-shortname">
|
|
||||||
<core-format-text [text]="course.shortname" contextLevel="course" [contextInstanceId]="course.id">
|
<core-format-text [text]="course.shortname" contextLevel="course" [contextInstanceId]="course.id">
|
||||||
</core-format-text>
|
</core-format-text>
|
||||||
</span>
|
</span>
|
||||||
|
@ -35,12 +32,8 @@
|
||||||
</ion-label>
|
</ion-label>
|
||||||
|
|
||||||
<div class="core-button-spinner" *ngIf="downloadCourseEnabled && !courseOptionMenuEnabled && showDownload" slot="end">
|
<div class="core-button-spinner" *ngIf="downloadCourseEnabled && !courseOptionMenuEnabled && showDownload" slot="end">
|
||||||
<core-download-refresh
|
<core-download-refresh [status]="prefetchCourseData.status" [enabled]="downloadCourseEnabled"
|
||||||
[status]="prefetchCourseData.status"
|
[statusTranslatable]="prefetchCourseData.statusTranslatable" canTrustDownload="false" [loading]="prefetchCourseData.loading"
|
||||||
[enabled]="downloadCourseEnabled"
|
|
||||||
[statusTranslatable]="prefetchCourseData.statusTranslatable"
|
|
||||||
canTrustDownload="false"
|
|
||||||
[loading]="prefetchCourseData.loading"
|
|
||||||
(action)="prefetchCourse()"></core-download-refresh>
|
(action)="prefetchCourse()"></core-download-refresh>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -50,9 +43,8 @@
|
||||||
[attr.aria-label]="'core.loading' | translate"></ion-spinner>
|
[attr.aria-label]="'core.loading' | translate"></ion-spinner>
|
||||||
|
|
||||||
<!-- Downloaded icon. -->
|
<!-- Downloaded icon. -->
|
||||||
<ion-icon *ngIf="downloadCourseEnabled && prefetchCourseData.downloadSucceeded && !showSpinner"
|
<ion-icon *ngIf="downloadCourseEnabled && prefetchCourseData.downloadSucceeded && !showSpinner" class="core-icon-downloaded"
|
||||||
class="core-icon-downloaded" name="cloud-done" color="success" role="status"
|
name="cloud-done" color="success" role="status" [attr.aria-label]="'core.downloaded' | translate"></ion-icon>
|
||||||
[attr.aria-label]="'core.downloaded' | translate"></ion-icon>
|
|
||||||
|
|
||||||
<!-- Options menu. -->
|
<!-- Options menu. -->
|
||||||
<ion-button fill="clear" color="dark" (click)="showCourseOptionsMenu($event)" *ngIf="!showSpinner"
|
<ion-button fill="clear" color="dark" (click)="showCourseOptionsMenu($event)" *ngIf="!showSpinner"
|
||||||
|
@ -61,11 +53,9 @@
|
||||||
</ion-button>
|
</ion-button>
|
||||||
</div>
|
</div>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item *ngIf="showAll && progress >= 0 && completionUserTracked !== false" lines="none"
|
<ion-item *ngIf="showAll && progress >= 0 && completionUserTracked !== false" lines="none" class="core-course-progress">
|
||||||
class="core-course-progress">
|
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<core-progress-bar [progress]="progress" a11yText="core.courses.aria:courseprogress"></core-progress-bar>
|
<core-progress-bar [progress]="progress" a11yText="core.courses.aria:courseprogress"></core-progress-bar>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ng-content></ng-content>
|
|
||||||
</ion-card>
|
</ion-card>
|
||||||
|
|
|
@ -9,9 +9,9 @@
|
||||||
<ion-tab-button *ngFor="let tab of tabs" (keydown)="tabAction.keyDown($event)" (keyup)="tabAction.keyUp(tab.page, $event)"
|
<ion-tab-button *ngFor="let tab of tabs" (keydown)="tabAction.keyDown($event)" (keyup)="tabAction.keyUp(tab.page, $event)"
|
||||||
[hidden]="!loaded && tab.hide" [tab]="tab.page" [disabled]="tab.hide" layout="label-hide" class="{{tab.class}}"
|
[hidden]="!loaded && tab.hide" [tab]="tab.page" [disabled]="tab.hide" layout="label-hide" class="{{tab.class}}"
|
||||||
[selected]="tab.page === selectedTab" [tabindex]="selectedTab == tab.page ? 0 : -1" [attr.aria-controls]="tab.id">
|
[selected]="tab.page === selectedTab" [tabindex]="selectedTab == tab.page ? 0 : -1" [attr.aria-controls]="tab.id">
|
||||||
<ion-icon [name]="tab.icon" aria-hidden="true"></ion-icon>
|
<ion-icon class="core-tab-icon" [name]="tab.icon" aria-hidden="true"></ion-icon>
|
||||||
<ion-label aria-hidden="true">{{ tab.title | translate }}</ion-label>
|
<ion-label aria-hidden="true">{{ tab.title | translate }}</ion-label>
|
||||||
<ion-badge *ngIf="tab.badge" aria-hidden="true">{{ tab.badge }}</ion-badge>
|
<ion-badge class="core-tab-badge" *ngIf="tab.badge" aria-hidden="true">{{ tab.badge }}</ion-badge>
|
||||||
<span class="sr-only">{{ tab.title | translate }}</span>
|
<span class="sr-only">{{ tab.title | translate }}</span>
|
||||||
<span *ngIf="tab.badge && tab.badgeA11yText" class="sr-only">
|
<span *ngIf="tab.badge && tab.badgeA11yText" class="sr-only">
|
||||||
{{ tab.badgeA11yText | translate: {$a : tab.badge } }}
|
{{ tab.badgeA11yText | translate: {$a : tab.badge } }}
|
||||||
|
@ -20,9 +20,10 @@
|
||||||
|
|
||||||
<ion-tab-button (keydown)="tabAction.keyDown($event)" (keyup)="tabAction.keyUp(morePageName, $event)" [hidden]="!loaded"
|
<ion-tab-button (keydown)="tabAction.keyDown($event)" (keyup)="tabAction.keyUp(morePageName, $event)" [hidden]="!loaded"
|
||||||
[tab]="morePageName" layout="label-hide" [tabindex]="selectedTab == morePageName ? 0 : -1" [attr.aria-controls]="morePageName">
|
[tab]="morePageName" layout="label-hide" [tabindex]="selectedTab == morePageName ? 0 : -1" [attr.aria-controls]="morePageName">
|
||||||
<ion-icon name="ellipsis-horizontal" aria-hidden="true"></ion-icon>
|
<ion-icon class="core-tab-icon" name="ellipsis-horizontal" aria-hidden="true"></ion-icon>
|
||||||
<ion-label aria-hidden="true">{{ 'core.more' | translate }}</ion-label>
|
<ion-label aria-hidden="true">{{ 'core.more' | translate }}</ion-label>
|
||||||
<span class="sr-only">{{ 'core.more' | translate }}</span>
|
<span class="sr-only">{{ 'core.more' | translate }}</span>
|
||||||
|
<ion-icon *ngIf="moreBadge" class="core-tab-badge" name="fas-circle" aria-hidden="true"></ion-icon>
|
||||||
</ion-tab-button>
|
</ion-tab-button>
|
||||||
</ion-tab-bar>
|
</ion-tab-bar>
|
||||||
</ion-tabs>
|
</ion-tabs>
|
||||||
|
|
|
@ -25,17 +25,17 @@
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ion-tab-button ion-icon {
|
ion-tab-button ion-icon.core-tab-icon {
|
||||||
text-overflow: unset;
|
text-overflow: unset;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
ion-tab-button.ios ion-icon {
|
ion-tab-button.ios ion-icon.core-tab-icon {
|
||||||
font-size: 25px;
|
font-size: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
ion-tab-button.md ion-badge {
|
ion-tab-button.md ion-badge.core-tab-badge {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
@ -48,11 +48,29 @@
|
||||||
background: var(--background-selected);
|
background: var(--background-selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ion-icon.core-tab-badge {
|
||||||
|
color: var(--core-bottom-tabs-badge-color);
|
||||||
|
padding: 3px 6px 2px;
|
||||||
|
@include position(8px, null, null, calc(50% + 6px));
|
||||||
|
min-width: 12px;
|
||||||
|
font-size: 8px;
|
||||||
|
font-weight: normal;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ion-badge.core-tab-badge {
|
||||||
|
--background: var(--core-bottom-tabs-badge-color);
|
||||||
|
--color: var(--core-bottom-tabs-badge-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
ion-tabs.placement-bottom ion-tab-button {
|
ion-tabs.placement-bottom ion-tab-button {
|
||||||
ion-icon {
|
ion-icon.core-tab-icon {
|
||||||
transition: margin 500ms ease-in-out, transform 300ms ease-in-out;
|
transition: margin 500ms ease-in-out, transform 300ms ease-in-out;
|
||||||
}
|
}
|
||||||
ion-badge {
|
ion-icon.core-tab-badge,
|
||||||
|
ion-badge.core-tab-badge {
|
||||||
top: 8px;
|
top: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,7 +94,8 @@
|
||||||
min-height: var(--menutabbar-size);
|
min-height: var(--menutabbar-size);
|
||||||
flex: 0;
|
flex: 0;
|
||||||
|
|
||||||
ion-badge {
|
ion-icon.core-tab-badge,
|
||||||
|
ion-badge.core-tab-badge {
|
||||||
top: calc(50% - 20px);
|
top: calc(50% - 20px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -114,11 +133,11 @@
|
||||||
|
|
||||||
:host-context(.core-online),
|
:host-context(.core-online),
|
||||||
:host-context(.core-offline) {
|
:host-context(.core-offline) {
|
||||||
ion-tabs.placement-bottom ion-tab-button ion-icon {
|
ion-tabs.placement-bottom ion-tab-button ion-icon.core-tab-icon {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
ion-tabs.placement-bottom ion-tab-button.ios ion-icon {
|
ion-tabs.placement-bottom ion-tab-button.ios ion-icon.core-tab-icon {
|
||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,7 @@ import { CoreNavigator } from '@services/navigator';
|
||||||
import { filter } from 'rxjs/operators';
|
import { filter } from 'rxjs/operators';
|
||||||
import { NavigationEnd } from '@angular/router';
|
import { NavigationEnd } from '@angular/router';
|
||||||
import { trigger, state, style, transition, animate } from '@angular/animations';
|
import { trigger, state, style, transition, animate } from '@angular/animations';
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page that displays the main menu of the app.
|
* Page that displays the main menu of the app.
|
||||||
|
@ -66,10 +67,12 @@ export class CoreMainMenuPage implements OnInit, OnDestroy {
|
||||||
morePageName = CoreMainMenuProvider.MORE_PAGE_NAME;
|
morePageName = CoreMainMenuProvider.MORE_PAGE_NAME;
|
||||||
selectedTab?: string;
|
selectedTab?: string;
|
||||||
isMainScreen = false;
|
isMainScreen = false;
|
||||||
|
moreBadge = false;
|
||||||
|
|
||||||
protected subscription?: Subscription;
|
protected subscription?: Subscription;
|
||||||
protected navSubscription?: Subscription;
|
protected navSubscription?: Subscription;
|
||||||
protected keyboardObserver?: CoreEventObserver;
|
protected keyboardObserver?: CoreEventObserver;
|
||||||
|
protected badgeUpdateObserver?: CoreEventObserver;
|
||||||
protected resizeFunction: () => void;
|
protected resizeFunction: () => void;
|
||||||
protected backButtonFunction: (event: BackButtonEvent) => void;
|
protected backButtonFunction: (event: BackButtonEvent) => void;
|
||||||
protected selectHistory: string[] = [];
|
protected selectHistory: string[] = [];
|
||||||
|
@ -102,11 +105,17 @@ export class CoreMainMenuPage implements OnInit, OnDestroy {
|
||||||
|
|
||||||
this.subscription = CoreMainMenuDelegate.getHandlersObservable().subscribe((handlers) => {
|
this.subscription = CoreMainMenuDelegate.getHandlersObservable().subscribe((handlers) => {
|
||||||
// Remove the handlers that should only appear in the More menu.
|
// Remove the handlers that should only appear in the More menu.
|
||||||
this.allHandlers = handlers.filter((handler) => !handler.onlyInMore);
|
this.allHandlers = handlers;
|
||||||
|
|
||||||
this.initHandlers();
|
this.initHandlers();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.badgeUpdateObserver = CoreEvents.on(CoreMainMenuProvider.MAIN_MENU_HANDLER_BADGE_UPDATED, (data) => {
|
||||||
|
if (data.siteId == CoreSites.getCurrentSiteId()) {
|
||||||
|
this.updateMoreBadge();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
window.addEventListener('resize', this.resizeFunction);
|
window.addEventListener('resize', this.resizeFunction);
|
||||||
document.addEventListener('ionBackButton', this.backButtonFunction);
|
document.addEventListener('ionBackButton', this.backButtonFunction);
|
||||||
|
|
||||||
|
@ -130,34 +139,52 @@ export class CoreMainMenuPage implements OnInit, OnDestroy {
|
||||||
* Init handlers on change (size or handlers).
|
* Init handlers on change (size or handlers).
|
||||||
*/
|
*/
|
||||||
initHandlers(): void {
|
initHandlers(): void {
|
||||||
if (this.allHandlers) {
|
if (!this.allHandlers) {
|
||||||
this.tabsPlacement = CoreMainMenu.getTabPlacement();
|
return;
|
||||||
|
|
||||||
const handlers = this.allHandlers.slice(0, CoreMainMenu.getNumItems()); // Get main handlers.
|
|
||||||
|
|
||||||
// Re-build the list of tabs. If a handler is already in the list, use existing object to prevent re-creating the tab.
|
|
||||||
const newTabs: CoreMainMenuHandlerToDisplay[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < handlers.length; i++) {
|
|
||||||
const handler = handlers[i];
|
|
||||||
|
|
||||||
// Check if the handler is already in the tabs list. If so, use it.
|
|
||||||
const tab = this.tabs.find((tab) => tab.page == handler.page);
|
|
||||||
|
|
||||||
tab ? tab.hide = false : null;
|
|
||||||
handler.hide = false;
|
|
||||||
handler.id = handler.id || 'core-mainmenu-' + CoreUtils.getUniqueId('CoreMainMenuPage');
|
|
||||||
|
|
||||||
newTabs.push(tab || handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.tabs = newTabs;
|
|
||||||
|
|
||||||
// Sort them by priority so new handlers are in the right position.
|
|
||||||
this.tabs.sort((a, b) => (b.priority || 0) - (a.priority || 0));
|
|
||||||
|
|
||||||
this.loaded = CoreMainMenuDelegate.areHandlersLoaded();
|
|
||||||
}
|
}
|
||||||
|
this.tabsPlacement = CoreMainMenu.getTabPlacement();
|
||||||
|
|
||||||
|
const handlers = this.allHandlers
|
||||||
|
.filter((handler) => !handler.onlyInMore)
|
||||||
|
.slice(0, CoreMainMenu.getNumItems()); // Get main handlers.
|
||||||
|
|
||||||
|
// Re-build the list of tabs. If a handler is already in the list, use existing object to prevent re-creating the tab.
|
||||||
|
const newTabs: CoreMainMenuHandlerToDisplay[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < handlers.length; i++) {
|
||||||
|
const handler = handlers[i];
|
||||||
|
|
||||||
|
// Check if the handler is already in the tabs list. If so, use it.
|
||||||
|
const tab = this.tabs.find((tab) => tab.page == handler.page);
|
||||||
|
|
||||||
|
tab ? tab.hide = false : null;
|
||||||
|
handler.hide = false;
|
||||||
|
handler.id = handler.id || 'core-mainmenu-' + CoreUtils.getUniqueId('CoreMainMenuPage');
|
||||||
|
|
||||||
|
newTabs.push(tab || handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tabs = newTabs;
|
||||||
|
|
||||||
|
// Sort them by priority so new handlers are in the right position.
|
||||||
|
this.tabs.sort((a, b) => (b.priority || 0) - (a.priority || 0));
|
||||||
|
|
||||||
|
this.updateMoreBadge();
|
||||||
|
|
||||||
|
this.loaded = CoreMainMenuDelegate.areHandlersLoaded();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check all non visible tab handlers for any badge text or number.
|
||||||
|
*/
|
||||||
|
updateMoreBadge(): void {
|
||||||
|
if (!this.allHandlers) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const numItems = CoreMainMenu.getNumItems();
|
||||||
|
this.moreBadge = this.allHandlers.some((handler, index) => (handler.onlyInMore || index >= numItems) && !!handler.badge);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -169,6 +196,7 @@ export class CoreMainMenuPage implements OnInit, OnDestroy {
|
||||||
window.removeEventListener('resize', this.resizeFunction);
|
window.removeEventListener('resize', this.resizeFunction);
|
||||||
document.removeEventListener('ionBackButton', this.backButtonFunction);
|
document.removeEventListener('ionBackButton', this.backButtonFunction);
|
||||||
this.keyboardObserver?.off();
|
this.keyboardObserver?.off();
|
||||||
|
this.badgeUpdateObserver?.off();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -23,6 +23,19 @@ import { Device, makeSingleton } from '@singletons';
|
||||||
import { CoreArray } from '@singletons/array';
|
import { CoreArray } from '@singletons/array';
|
||||||
import { CoreTextUtils } from '@services/utils/text';
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
|
|
||||||
|
declare module '@singletons/events' {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Augment CoreEventsData interface with events specific to this service.
|
||||||
|
*
|
||||||
|
* @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
|
||||||
|
*/
|
||||||
|
export interface CoreEventsData {
|
||||||
|
[CoreMainMenuProvider.MAIN_MENU_HANDLER_BADGE_UPDATED]: CoreMainMenuHandlerBadgeUpdatedEventData;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service that provides some features regarding Main Menu.
|
* Service that provides some features regarding Main Menu.
|
||||||
*/
|
*/
|
||||||
|
@ -32,6 +45,7 @@ export class CoreMainMenuProvider {
|
||||||
static readonly NUM_MAIN_HANDLERS = 4;
|
static readonly NUM_MAIN_HANDLERS = 4;
|
||||||
static readonly ITEM_MIN_WIDTH = 72; // Min with of every item, based on 5 items on a 360 pixel wide screen.
|
static readonly ITEM_MIN_WIDTH = 72; // Min with of every item, based on 5 items on a 360 pixel wide screen.
|
||||||
static readonly MORE_PAGE_NAME = 'more';
|
static readonly MORE_PAGE_NAME = 'more';
|
||||||
|
static readonly MAIN_MENU_HANDLER_BADGE_UPDATED = 'main_menu_handler_badge_updated';
|
||||||
|
|
||||||
protected tablet = false;
|
protected tablet = false;
|
||||||
|
|
||||||
|
@ -339,3 +353,8 @@ type CustomMenuItemsMap = Record<string, {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
export type CoreMainMenuHandlerBadgeUpdatedEventData = {
|
||||||
|
handler: string; // Handler name.
|
||||||
|
value: number; // New counter value.
|
||||||
|
};
|
||||||
|
|
|
@ -41,6 +41,7 @@ import {
|
||||||
import { CoreError } from '@classes/errors/error';
|
import { CoreError } from '@classes/errors/error';
|
||||||
import { CoreWSExternalWarning } from '@services/ws';
|
import { CoreWSExternalWarning } from '@services/ws';
|
||||||
import { CoreSitesFactory } from '@services/sites-factory';
|
import { CoreSitesFactory } from '@services/sites-factory';
|
||||||
|
import { CoreMainMenuProvider } from '@features/mainmenu/services/mainmenu';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service to handle push notifications.
|
* Service to handle push notifications.
|
||||||
|
@ -101,6 +102,10 @@ export class CorePushNotificationsProvider {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
CoreEvents.on(CoreMainMenuProvider.MAIN_MENU_HANDLER_BADGE_UPDATED, (data) => {
|
||||||
|
this.updateAddonCounter(data.handler, data.value, data.siteId);
|
||||||
|
});
|
||||||
|
|
||||||
// Listen for local notification clicks (generated by the app).
|
// Listen for local notification clicks (generated by the app).
|
||||||
CoreLocalNotifications.registerClick<CorePushNotificationsNotificationBasicData>(
|
CoreLocalNotifications.registerClick<CorePushNotificationsNotificationBasicData>(
|
||||||
CorePushNotificationsProvider.COMPONENT,
|
CorePushNotificationsProvider.COMPONENT,
|
||||||
|
|
|
@ -788,6 +788,13 @@ ion-select::part(icon) {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ion-select-popover ion-item.core-select-option-title {
|
||||||
|
cursor: pointer;
|
||||||
|
ion-radio {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ion-searchbar {
|
ion-searchbar {
|
||||||
.searchbar-search-icon.ios {
|
.searchbar-search-icon.ios {
|
||||||
top: 4px;
|
top: 4px;
|
||||||
|
|
|
@ -97,10 +97,6 @@
|
||||||
--color: var(--core-bottom-tabs-color);
|
--color: var(--core-bottom-tabs-color);
|
||||||
--color-selected: var(--core-bottom-tabs-color-selected);
|
--color-selected: var(--core-bottom-tabs-color-selected);
|
||||||
--background-selected: var(--core-bottom-tabs-background-selected);
|
--background-selected: var(--core-bottom-tabs-background-selected);
|
||||||
ion-badge {
|
|
||||||
--background: var(--core-bottom-tabs-badge-color);
|
|
||||||
--color: var(--core-bottom-tabs-badge-text-color);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
--core-link-color: var(--blue);
|
--core-link-color: var(--blue);
|
||||||
|
|
Loading…
Reference in New Issue