MOBILE-2308 calendar: Single event page

main
Pau Ferrer Ocaña 2017-12-28 16:10:26 +01:00
parent 3ef367db76
commit d4ddfc74a1
11 changed files with 373 additions and 23 deletions

View File

@ -2,6 +2,19 @@
"calendar": "Calendar",
"calendarevents": "Calendar events",
"defaultnotificationtime": "Default notification time",
"errorloadevent": "Error loading event.",
"errorloadevents": "Error loading events.",
"noevents": "There are no events"
"eventendtime": "End time",
"eventstarttime": "Start time",
"noevents": "There are no events",
"notifications": "Notifications",
"typeclose": "Close event",
"typecourse": "Course event",
"typecategory": "Category event",
"typedue": "Due event",
"typegradingdue": "Grading due event",
"typegroup": "Group event",
"typeopen": "Open event",
"typesite": "Site event",
"typeuser": "User event"
}

View File

@ -0,0 +1,57 @@
<ion-header>
<ion-navbar>
<ion-title><core-format-text [text]="title"></core-format-text></ion-title>
</ion-navbar>
</ion-header>
<ion-content>
<ion-refresher [enabled]="eventLoaded" (ionRefresh)="refreshEvent($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="eventLoaded" class="mm-loading-center">
<ion-card>
<ion-card-content>
<ion-card-title text-wrap>
<ion-icon *ngIf="!event.moduleIcon" name="{{event.icon}}" item-start></ion-icon>
<core-format-text [text]="event.name"></core-format-text>
</ion-card-title>
<ion-item text-wrap>
<h2>{{ 'addon.calendar.eventstarttime' | translate}}</h2>
<p>{{ event.timestart | coreToLocaleString }}</p>
</ion-item>
<ion-item text-wrap *ngIf="event.timeduration > 0">
<h2>{{ 'addon.calendar.eventendtime' | translate}}</h2>
<p>{{ (event.timestart + event.timeduration) |  coreToLocaleString }}</p>
</ion-item>
<ion-item text-wrap *ngIf="courseName">
<h2>{{ 'core.course' | translate}}</h2>
<p><core-format-text [text]="courseName"></core-format-text></p>
</ion-item>
<ion-item text-wrap *ngIf="event.moduleIcon">
<img *ngIf="event.moduleIcon" src="{{event.moduleIcon}}" item-start> {{event.moduleName}}
</ion-item>
<ion-item>
<p text-wrap *ngIf="event.description">
<core-format-text [text]="event.description"></core-format-text>
</p>
</ion-item>
</ion-card-content>
</ion-card>
<ion-card list *ngIf="notificationsEnabled">
<ion-item>
<ion-label>{{ 'addon.calendar.notifications' | translate }}</ion-label>
<ion-select [(ngModel)]="notificationTime" (ionChange)="updateNotificationTime($event)">
<ion-option value="-1">{{ 'core.defaultvalue' | translate :{$a: defaultTimeReadable} }}</ion-option>
<ion-option value="0">{{ 'core.settings.disabled' | translate }}</ion-option>
<ion-option value="10">{{ 600 | coreDuration }}</ion-option>
<ion-option value="30">{{ 1800 | coreDuration }}</ion-option>
<ion-option value="60">{{ 3600 | coreDuration }}</ion-option>
<ion-option value="120">{{ 7200 | coreDuration }}</ion-option>
<ion-option value="360">{{ 21600 | coreDuration }}</ion-option>
<ion-option value="720">{{ 43200 | coreDuration }}</ion-option>
<ion-option value="1440">{{ 86400 | coreDuration }}</ion-option>
</ion-select>
</ion-item>
</ion-card>
</core-loading>
</ion-content>

View File

@ -0,0 +1,35 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { IonicPageModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '../../../../components/components.module';
import { CoreDirectivesModule } from '../../../../directives/directives.module';
import { CorePipesModule } from '../../../../pipes/pipes.module';
import { AddonCalendarEventPage } from './event';
@NgModule({
declarations: [
AddonCalendarEventPage,
],
imports: [
CoreComponentsModule,
CoreDirectivesModule,
CorePipesModule,
IonicPageModule.forChild(AddonCalendarEventPage),
TranslateModule.forChild()
],
})
export class AddonCalendarEventPageModule {}

View File

@ -0,0 +1,3 @@
page-addon-calendar-list {
}

View File

@ -0,0 +1,142 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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, ViewChild } from '@angular/core';
import { IonicPage, Content, NavParams } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { AddonCalendarProvider } from '../../providers/calendar';
import { AddonCalendarHelperProvider } from '../../providers/helper';
import { CoreCoursesProvider } from '../../../../core/courses/providers/courses';
import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
import { CoreSitesProvider } from '../../../../providers/sites';
import { CoreLocalNotificationsProvider } from '../../../../providers/local-notifications';
//import { CoreCourseProvider } from '../../../core/course/providers/course';
import * as moment from 'moment';
/**
* Page that displays a single calendar event.
*/
@IonicPage()
@Component({
selector: 'page-addon-calendar-event',
templateUrl: 'event.html',
})
export class AddonCalendarEventPage {
@ViewChild(Content) content: Content;
protected eventId;
protected siteHomeId: number;
eventLoaded: boolean;
notificationTime: number;
defaultTimeReadable: string;
event = {};
title: string;
courseName: string;
notificationsEnabled = false;
constructor(private translate: TranslateService, private calendarProvider: AddonCalendarProvider, private navParams: NavParams,
private domUtils: CoreDomUtilsProvider, private coursesProvider: CoreCoursesProvider,
private calendarHelper: AddonCalendarHelperProvider, private sitesProvider: CoreSitesProvider,
private localNotificationsProvider: CoreLocalNotificationsProvider/*, private courseProvider: CoreCourseProvider*/) {
this.eventId = navParams.get('id');
this.notificationsEnabled = localNotificationsProvider.isAvailable();
this.siteHomeId = sitesProvider.getCurrentSite().getSiteHomeId();
if (this.notificationsEnabled) {
this.calendarProvider.getEventNotificationTimeOption(this.eventId).then((notificationTime) => {
this.notificationTime = notificationTime;
});
this.calendarProvider.getDefaultNotificationTime().then((defaultTime) => {
if (defaultTime === 0) {
// Disabled by default.
this.defaultTimeReadable = this.translate.instant('core.settings.disabled');
} else {
this.defaultTimeReadable = moment.duration(defaultTime * 60 * 1000).humanize();
}
});
}
}
/**
* View loaded.
*/
ionViewDidLoad() {
this.fetchEvent().finally(() => {
this.eventLoaded = true;
});
}
updateNotificationTime() {
if (!isNaN(this.notificationTime) && this.event && this.event.id) {
this.calendarProvider.updateNotificationTime(this.event, this.notificationTime);
}
}
/**
* Fetches the event and updates the view.
*
* @param {boolean} refresh Empty events array first.
*/
fetchEvent() {
return this.calendarProvider.getEvent(this.eventId).then((event) => {
this.calendarHelper.formatEventData(event);
this.event = event;
// Guess event title.
let title = this.translate.instant('addon.calendar.type' + event.eventtype);
if (event.moduleIcon) {
// @todo: It's a module event, translate the module name to the current language.
let name = "" //this.courseProvider.translateModuleName(event.modulename);
if (name.indexOf('core.mod_') === -1) {
event.moduleName = name;
}
if (title == 'addon.calendar.type' + event.eventtype) {
title = this.translate.instant('core.mod_'+ event.modulename + '.' + event.eventtype);
if (title == 'core.mod_'+ event.modulename + '.' + event.eventtype) {
title = name;
}
}
} else {
if (title == 'addon.calendar.type' + event.eventtype) {
title = event.name;
}
}
this.title = title;
if (event.courseid != this.siteHomeId) {
// It's a course event, retrieve the course name.
return this.coursesProvider.getUserCourse(event.courseid, true).then((course) => {
this.courseName = course.fullname;
});
}
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevent', true);
});
}
/**
* Refresh the event.
*
* @param {any} refresher Refresher.
*/
refreshEvent(refresher: any) {
this.calendarProvider.invalidateEvent(this.eventId).finally(() => {
this.fetchEvent().finally(() => {
refresher.complete();
});
});
}
}

View File

@ -21,10 +21,10 @@
</core-empty-box>
<ion-list *ngIf="filteredEvents && filteredEvents.length">
<a ion-item text-wrap *ngFor="let event of filteredEvents" [title]="event.name">
<a ion-item text-wrap *ngFor="let event of filteredEvents" [title]="event.name" (click)="gotoEvent(event.id)">
<!-- mm-split-view-link="site.calendar-event({id: event.id})" -->
<img *ngIf="event.moduleicon" src="{{event.moduleicon}}" item-start>
<ion-icon *ngIf="!event.moduleicon" name="{{event.icon}}" item-start></ion-icon>
<img *ngIf="event.moduleIcon" src="{{event.moduleIcon}}" item-start>
<ion-icon *ngIf="!event.moduleIcon" name="{{event.icon}}" item-start></ion-icon>
<h2><core-format-text [text]="event.name"></core-format-text></h2>
<p>{{ event.timestart | coreToLocaleString }}</p>
</a>

View File

@ -24,6 +24,7 @@ import { CoreSitesProvider } from '../../../../providers/sites';
import { CoreLocalNotificationsProvider } from '../../../../providers/local-notifications';
import { CoreCoursePickerMenuPopoverComponent } from '../../../../components/course-picker-menu/course-picker-menu-popover';
import { CoreEventsProvider } from '../../../../providers/events';
import { CoreAppProvider } from '../../../../providers/app';
/**
* Page that displays the list of calendar events.
@ -48,6 +49,7 @@ export class AddonCalendarListPage implements OnDestroy {
protected categories = {};
protected siteHomeId: number;
protected obsDefaultTimeChange: any;
protected eventId: number;
courses: any[];
eventsLoaded = false;
@ -64,10 +66,10 @@ export class AddonCalendarListPage implements OnDestroy {
private domUtils: CoreDomUtilsProvider, private coursesProvider: CoreCoursesProvider, private utils: CoreUtilsProvider,
private calendarHelper: AddonCalendarHelperProvider, private sitesProvider: CoreSitesProvider,
private localNotificationsProvider: CoreLocalNotificationsProvider, private popoverCtrl: PopoverController,
private eventsProvider: CoreEventsProvider, private navCtrl: NavController) {
private eventsProvider: CoreEventsProvider, private navCtrl: NavController, private appProvider: CoreAppProvider) {
this.siteHomeId = sitesProvider.getCurrentSite().getSiteHomeId();
this.notificationsEnabled = true;//localNotificationsProvider.isAvailable();
this.notificationsEnabled = localNotificationsProvider.isAvailable();
if (this.notificationsEnabled) {
// Re-schedule events if default time changes.
this.obsDefaultTimeChange = eventsProvider.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, () => {
@ -75,16 +77,24 @@ export class AddonCalendarListPage implements OnDestroy {
}, sitesProvider.getCurrentSiteId());
}
// @TODO: Split view once single event is done.
// let eventId = navParams.get('eventid') || false;
this.eventId = navParams.get('eventid') || false;
}
/**
* View loaded.
*/
ionViewDidLoad() {
this.fetchData().then(() => {
if (this.eventId && !this.appProvider.isWide()) {
// There is an event to load and it's a phone device, open the event in a new state.
this.gotoEvent(this.eventId);
}
this.fetchData().then(() => {
// @TODO: Split view once single event is done.
if (this.eventId && this.appProvider.isWide()) {
// There is an event to load and it's a phone device, open the event in a new state.
this.gotoEvent(this.eventId);
}
}).finally(() => {
this.eventsLoaded = true;
});
@ -151,7 +161,7 @@ export class AddonCalendarListPage implements OnDestroy {
}
// Resize the content so infinite loading is able to calculate if it should load more items or not.
// @TODO: Infinite loading is not working if content is not high enough.
// @todo: Infinite loading is not working if content is not high enough.
this.content.resize();
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
@ -298,7 +308,14 @@ export class AddonCalendarListPage implements OnDestroy {
*/
openSettings() {
this.navCtrl.push('AddonCalendarSettingsPage');
};
}
/**
* Navigate to a particular event.
*/
gotoEvent(eventId) {
this.navCtrl.push('AddonCalendarEventPage', {id: eventId});
}
/**
* Page destroyed.

View File

@ -19,7 +19,7 @@ import { CoreEventsProvider } from '../../../../providers/events';
import { CoreSitesProvider } from '../../../../providers/sites';
/**
* Page that displays the list of calendar events.
* Page that displays the calendar settings.
*/
@IonicPage()
@Component({

View File

@ -124,6 +124,44 @@ export class AddonCalendarProvider {
return this.configProvider.get(key, AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME);
}
/**
* Get a calendar event. If the server request fails and data is not cached, try to get it from local DB.
*
* @param {number} id Event ID.
* @param {boolean} [refresh] True when we should update the event data.
* @param {string} [siteId] ID of the site. If not defined, use current site.
* @return {Promise<any>} Promise resolved when the event data is retrieved.
*/
getEvent(id: number, siteId?: string) : Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
let presets = {
cacheKey: this.getEventCacheKey(id)
},
data = {
"options[userevents]": 0,
"options[siteevents]": 0,
"events[eventids][0]": id
};
return site.read('core_calendar_get_calendar_events', data, presets).then((response) => {
// The WebService returns all category events. Check the response to search for the event we want.
let event = response.events.find((e) => {return e.id == id});
return event || this.getEventFromLocalDb(id);
}).catch(() => {
return this.getEventFromLocalDb(id);
});
});
}
/**
* Get cache key for a single event WS call.
*
* @param {number} id Event ID.
* @return {string} Cache key.
*/
protected getEventCacheKey(id: number): string {
return 'mmaCalendar:events:' + id;
}
/**
* Get a calendar event from local Db.
*
@ -245,14 +283,14 @@ export class AddonCalendarProvider {
return 'mmaCalendar:';
}
/**
/**
* Invalidates events list and all the single events and related info.
*
* @param {any[]} courses List of courses or course ids.
* @param {string} [siteId] Site Id. If not defined, use current site.
* @return {Promise<any>} Promise resolved when the list is invalidated.
* @return {Promise<any[]>} Promise resolved when the list is invalidated.
*/
invalidateEventsList(courses: any[], siteId?: string) {
invalidateEventsList(courses: any[], siteId?: string) : Promise<any[]> {
return this.sitesProvider.getSite(siteId).then((site) => {
siteId = site.getId();
@ -265,6 +303,19 @@ export class AddonCalendarProvider {
});
}
/**
* Invalidates a single event.
*
* @param {number} eventId List of courses or course ids.
* @param {string} [siteId] Site Id. If not defined, use current site.
* @return {Promise<any>} Promise resolved when the list is invalidated.
*/
invalidateEvent(eventId: number, siteId?: string) : Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.invalidateWsCacheForKey(this.getEventCacheKey(eventId));
});
}
/**
* Check if Calendar is disabled in a certain site.
*
@ -408,4 +459,27 @@ export class AddonCalendarProvider {
return Promise.all(promises);
});
}
/**
* Updates an event notification time and schedule a new notification.
*
* @param {any} event Event to update its notification time.
* @param {number} time New notification setting time (in minutes). E.g. 10 means "notificate 10 minutes before start".
* @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site.
* @return {Promise<void>} Promise resolved when the notification is updated.
*/
updateNotificationTime(event: any, time: number, siteId?: string) : Promise<void> {
return this.sitesProvider.getSite(siteId).then((site) => {
if (!this.sitesProvider.isLoggedIn()) {
// Not logged in, we can't get the site DB. User logged out or session expired while an operation was ongoing.
return Promise.reject(null);
}
event.notificationtime = time;
return site.getDb().insertOrUpdateRecord(AddonCalendarProvider.EVENTS_TABLE, event, {id: event.id}).then(() => {
return this.scheduleEventNotification(event, time);
});
});
}
}

View File

@ -32,7 +32,7 @@ export class AddonCalendarHelperProvider {
'category': 'albums'
};
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider) {
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider/*, private courseProvider: CoreCourseProvider*/) {
this.logger = logger.getInstance('AddonCalendarHelperProvider');
}
@ -42,12 +42,11 @@ export class AddonCalendarHelperProvider {
* @param {any} e Event to format.
*/
formatEventData(e: any) {
let icon = AddonCalendarHelperProvider.eventicons[e.eventtype] || false;
if (!icon) {
// @TODO: It's a module event.
//icon = this.courseProvider.getModuleIconSrc(e.modulename);
e.moduleicon = icon;
e.icon = AddonCalendarHelperProvider.eventicons[e.eventtype] || false;
if (!e.icon) {
// @todo: It's a module event.
//e.icon = this.courseProvider.getModuleIconSrc(e.modulename);
e.moduleIcon = e.icon;
}
e.icon = icon;
};
}

View File

@ -184,7 +184,7 @@ export class CoreAppProvider {
return online;
}
/*
/**
* Check if device uses a limited connection.
*
* @return {boolean} Whether the device uses a limited connection.
@ -200,6 +200,16 @@ export class CoreAppProvider {
return limited.indexOf(type) > -1;
}
/**
* Check if device is wide enough. It's used i.e. to show split view.
*
* @return {boolean} Whether the device uses a limited connection.
*/
isWide() : boolean {
//@todo Should use media querys like splitpane
return this.platform.is('tablet');
}
/**
* Check if the app is running in a Windows environment.
*