Merge pull request #1225 from crazyserver/MOBILE-2308

Mobile 2308
main
Juan Leyva 2018-01-18 15:17:35 +01:00 committed by GitHub
commit ece285db4e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
66 changed files with 1924 additions and 113 deletions

View File

@ -59,39 +59,39 @@ function treatMergedData(data) {
var mergedOrdered = {};
for (var filepath in data) {
var pathSplit = filepath.split('/');
if (filepath.indexOf('lang/') === 0 || filepath.indexOf('core/lang') === 0) {
pathSplit.pop();
addProperties(merged, data[filepath], 'core.');
switch (pathSplit[0]) {
case 'lang':
prefix = 'core';
break;
case 'core':
if (pathSplit[1] == 'lang') {
// Not used right now.
prefix = 'core';
} else {
prefix = 'core.' + pathSplit[1];
}
break;
case 'addon':
// Remove final item 'lang'.
pathSplit.pop();
// Remove first item 'addon'.
pathSplit.shift();
} else if (filepath.indexOf('core/') === 0) {
var componentName = filepath.replace('core/', '');
componentName = componentName.substr(0, componentName.indexOf('/'));
addProperties(merged, data[filepath], 'core.'+componentName+'.');
} else if (filepath.indexOf('addons') === 0) {
var split = filepath.split('/'),
pluginName = split[1],
index = 2;
// Check if it's a subplugin. If so, we'll use plugin_subfolder_subfolder2_...
// E.g. 'mod_assign_feedback_comments'.
while (split[index] && split[index] != 'lang') {
pluginName = pluginName + '_' + split[index];
index++;
}
addProperties(merged, data[filepath], 'mma.'+pluginName+'.');
} else if (filepath.indexOf('assets/countries') === 0) {
addProperties(merged, data[filepath], 'core.country-');
} else if (filepath.indexOf('assets/mimetypes') === 0) {
addProperties(merged, data[filepath], 'core.mimetype-');
// For subplugins. We'll use plugin_subfolder_subfolder2_...
// E.g. 'mod_assign_feedback_comments'.
prefix = 'addon.' + pathSplit.join('_');
break;
case 'assets':
prefix = 'assets.' + pathSplit[1];
break;
}
if (prefix) {
addProperties(merged, data[filepath], prefix + '.');
}
}
@ -181,7 +181,7 @@ var appLangFiles = ['ar.json', 'bg.json', 'ca.json', 'cs.json', 'da.json', 'de.j
lang: [
'./src/lang/',
'./src/core/**/lang/',
'./src/addons/**/lang/',
'./src/addon/**/lang/',
'./src/assets/countries/',
'./src/assets/mimetypes/'
],

View File

@ -0,0 +1,61 @@
// (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 { AddonCalendarProvider } from './providers/calendar';
import { AddonCalendarHelperProvider } from './providers/helper';
import { AddonCalendarMainMenuHandler } from './providers/handlers';
import { CoreMainMenuDelegate } from '../../core/mainmenu/providers/delegate';
import { CoreInitDelegate } from '../../providers/init';
import { CoreLocalNotificationsProvider } from '../../providers/local-notifications';
import { CoreLoginHelperProvider } from '../../core/login/providers/helper';
@NgModule({
declarations: [
],
imports: [
],
providers: [
AddonCalendarProvider,
AddonCalendarHelperProvider,
AddonCalendarMainMenuHandler
]
})
export class AddonCalendarModule {
constructor(mainMenuDelegate: CoreMainMenuDelegate, calendarHandler: AddonCalendarMainMenuHandler,
initDelegate: CoreInitDelegate, calendarProvider: AddonCalendarProvider, loginHelper: CoreLoginHelperProvider,
localNotificationsProvider: CoreLocalNotificationsProvider) {
mainMenuDelegate.registerHandler(calendarHandler);
initDelegate.ready().then(() => {
calendarProvider.scheduleAllSitesEventsNotifications();
});
localNotificationsProvider.registerClick(AddonCalendarProvider.COMPONENT, (data) => {
if (data.eventid) {
initDelegate.ready().then(() => {
calendarProvider.isDisabled(data.siteId).then(function(disabled) {
if (disabled) {
// The calendar is disabled in the site, don't open it.
return;
}
loginHelper.redirect('AddonCalendarListPage', {eventid: data.eventid}, data.siteId);
});
});
}
});
}
}

View File

@ -0,0 +1,20 @@
{
"calendar": "Calendar",
"calendarevents": "Calendar events",
"defaultnotificationtime": "Default notification time",
"errorloadevent": "Error loading event.",
"errorloadevents": "Error loading 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="core-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-event {
}

View File

@ -0,0 +1,140 @@
// (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({segment: "addon-calendar-event"})
@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: any = {};
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.
*/
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

@ -0,0 +1,37 @@
<ion-header>
<ion-navbar>
<ion-title>{{ 'addon.calendar.calendarevents' | translate }}</ion-title>
<ion-buttons end>
<button *ngIf="courses && courses.length" ion-button icon-only (click)="openCourseFilter($event)" [attr.aria-label]="'core.courses.filter' | translate">
<ion-icon name="funnel"></ion-icon>
</button>
<core-context-menu>
<core-context-menu-item [hidden]="!notificationsEnabled" [priority]="600" [content]="'core.settings.settings' | translate" (action)="openSettings()" [iconAction]="'cog'"></core-context-menu-item>
</core-context-menu>
</ion-buttons>
</ion-navbar>
</ion-header>
<core-split-view>
<ion-content>
<ion-refresher [enabled]="eventsLoaded" (ionRefresh)="refreshEvents($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="eventsLoaded" class="core-loading-center">
<core-empty-box *ngIf="!filteredEvents || !filteredEvents.length" icon="calendar" [message]="'addon.calendar.noevents' | translate">
</core-empty-box>
<ion-list *ngIf="filteredEvents && filteredEvents.length">
<a ion-item text-wrap *ngFor="let event of filteredEvents" [title]="event.name" (click)="gotoEvent(event.id)" [class.core-split-item-selected]="event.id == eventId">
<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>
</ion-list>
<ion-infinite-scroll [enabled]="canLoadMore" (ionInfinite)="$event.waitFor(fetchEvents())">
<ion-infinite-scroll-content></ion-infinite-scroll-content>
</ion-infinite-scroll>
</core-loading>
</ion-content>
</core-split-view>

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 { AddonCalendarListPage } from './list';
@NgModule({
declarations: [
AddonCalendarListPage,
],
imports: [
CoreComponentsModule,
CoreDirectivesModule,
CorePipesModule,
IonicPageModule.forChild(AddonCalendarListPage),
TranslateModule.forChild()
],
})
export class AddonCalendarListPageModule {}

View File

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

View File

@ -0,0 +1,327 @@
// (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, OnDestroy } from '@angular/core';
import { IonicPage, Content, PopoverController, NavParams, NavController } 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 { CoreUtilsProvider } from '../../../../providers/utils/utils';
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';
import { CoreSplitViewComponent } from '../../../../components/split-view/split-view';
/**
* Page that displays the list of calendar events.
*/
@IonicPage({segment: "addon-calendar-list"})
@Component({
selector: 'page-addon-calendar-list',
templateUrl: 'list.html',
})
export class AddonCalendarListPage implements OnDestroy {
@ViewChild(Content) content: Content;
@ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent;
protected daysLoaded = 0;
protected emptyEventsTimes = 0; // Variable to identify consecutive calls returning 0 events.
protected categoriesRetrieved = false;
protected getCategories = false;
protected allCourses = {
id: -1,
fullname: this.translate.instant('core.fulllistofcourses'),
category: -1
};
protected categories = {};
protected siteHomeId: number;
protected obsDefaultTimeChange: any;
protected eventId: number;
courses: any[];
eventsLoaded = false;
events = [];
notificationsEnabled = false;
filteredEvents = [];
canLoadMore = false;
filter = {
course: this.allCourses
};
constructor(private translate: TranslateService, private calendarProvider: AddonCalendarProvider, private navParams: NavParams,
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 appProvider: CoreAppProvider) {
this.siteHomeId = sitesProvider.getCurrentSite().getSiteHomeId();
this.notificationsEnabled = localNotificationsProvider.isAvailable();
if (this.notificationsEnabled) {
// Re-schedule events if default time changes.
this.obsDefaultTimeChange = eventsProvider.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, () => {
calendarProvider.scheduleEventsNotifications(this.events);
}, sitesProvider.getCurrentSiteId());
}
this.eventId = navParams.get('eventid') || false;
}
/**
* View loaded.
*/
ionViewDidLoad() {
if (this.eventId) {
// There is an event to load, open the event in a new state.
this.gotoEvent(this.eventId);
}
this.fetchData().then(() => {
if (!this.eventId && this.splitviewCtrl.isOn() && this.events.length > 0) {
// Take first and load it.
this.gotoEvent(this.events[0].id);
}
}).finally(() => {
this.eventsLoaded = true;
});
}
/**
* Fetch all the data required for the view.
*
* @param {boolean} [refresh] Empty events array first.
*/
fetchData(refresh = false) {
this.daysLoaded = 0;
this.emptyEventsTimes = 0;
// Load courses for the popover.
return this.coursesProvider.getUserCourses(false).then((courses) => {
// Add "All courses".
courses.unshift(this.allCourses);
this.courses = courses;
return this.fetchEvents(refresh);
});
}
/**
* Fetches the events and updates the view.
*
* @param {boolean} [refresh] Empty events array first.
*/
fetchEvents(refresh = false) {
return this.calendarProvider.getEventsList(this.daysLoaded, AddonCalendarProvider.DAYS_INTERVAL).then((events) => {
this.daysLoaded += AddonCalendarProvider.DAYS_INTERVAL;
if (events.length === 0) {
this.emptyEventsTimes++;
if (this.emptyEventsTimes > 5) { // Stop execution if we retrieve empty list 6 consecutive times.
this.canLoadMore = false;
if (refresh) {
this.events = [];
this.filteredEvents = [];
}
} else {
// No events returned, load next events.
return this.fetchEvents();
}
} else {
// Sort the events by timestart, they're ordered by id.
events.sort((a, b) => {
return a.timestart - b.timestart;
});
events.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper));
this.getCategories = this.shouldLoadCategories(events);
if (refresh) {
this.events = events;
} else {
// Filter events with same ID. Repeated events are returned once per WS call, show them only once.
this.events = this.utils.mergeArraysWithoutDuplicates(this.events, events, 'id');
}
this.filteredEvents = this.getFilteredEvents();
this.canLoadMore = true;
// Schedule notifications for the events retrieved (might have new events).
this.calendarProvider.scheduleEventsNotifications(this.events);
}
// 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.
this.content.resize();
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
this.canLoadMore = false; // Set to false to prevent infinite calls with infinite-loading.
}).then(() => {
// Success retrieving events. Get categories if needed.
if (this.getCategories) {
this.getCategories = false;
return this.loadCategories();
}
});
}
/**
* Get filtered events.
*/
protected getFilteredEvents() {
if (this.filter.course.id == -1) {
// No filter, display everything.
return this.events;
}
return this.events.filter(this.shouldDisplayEvent.bind(this));
}
/**
* Check if an event should be displayed based on the filter.
*
* @param {any} event Event object.
*/
protected shouldDisplayEvent(event: any) {
if (event.eventtype == 'user' || event.eventtype == 'site') {
// User or site event, display it.
return true;
}
if (event.eventtype == 'category') {
if (!event.categoryid || !Object.keys(this.categories).length) {
// We can't tell if the course belongs to the category, display them all.
return true;
}
if (event.categoryid == this.filter.course.category) {
// The event is in the same category as the course, display it.
return true;
}
// Check parent categories.
let category = this.categories[this.filter.course.category];
while (category) {
if (!category.parent) {
// Category doesn't have parent, stop.
break;
}
if (event.categoryid == category.parent) {
return true;
}
category = this.categories[category.parent];
}
return false;
}
// Show the event if it is from site home or if it matches the selected course.
return event.courseid === this.siteHomeId || event.courseid == this.filter.course.id;
}
/**
* Returns if the current state should load categories or not.
* @param {any[]} events Events to parse.
* @return {boolean} True if categories should be loaded.
*/
protected shouldLoadCategories(events: any[]) : boolean {
if (this.categoriesRetrieved || this.getCategories) {
// Use previous value
return this.getCategories;
}
// Categories not loaded yet. We should get them if there's any category event.
let found = events.some(event => event.categoryid != 'undefined' && event.categoryid > 0);
return found || this.getCategories;
}
/**
* Load categories to be able to filter events.
*/
protected loadCategories() {
return this.coursesProvider.getCategories(0, true).then((cats) => {
this.categoriesRetrieved = true;
this.categories = {};
// Index categories by ID.
cats.forEach((category) => {
this.categories[category.id] = category;
});
}).catch(() => {
// Ignore errors.
});
}
/**
* Refresh the events.
*
* @param {any} refresher Refresher.
*/
refreshEvents(refresher: any) {
let promises = [];
promises.push(this.calendarProvider.invalidateEventsList(this.courses));
if (this.categoriesRetrieved) {
promises.push(this.coursesProvider.invalidateCategories(0, true));
this.categoriesRetrieved = false;
}
Promise.all(promises).finally(() => {
this.fetchData(true).finally(() => {
refresher.complete();
});
});
}
/**
* Show the context menu.
*
* @param {MouseEvent} event Event.
*/
openCourseFilter(event: MouseEvent) : void {
let popover = this.popoverCtrl.create(CoreCoursePickerMenuPopoverComponent, {courses: this.courses,
courseId: this.filter.course.id});
popover.onDidDismiss((course) => {
if (course) {
this.filter.course = course;
this.content.scrollToTop();
this.filteredEvents = this.getFilteredEvents();
}
});
popover.present({
ev: event
});
}
/**
* Open calendar events settings.
*/
openSettings() {
this.navCtrl.push('AddonCalendarSettingsPage');
}
/**
* Navigate to a particular event.
*/
gotoEvent(eventId) {
this.eventId = eventId;
this.splitviewCtrl.push('AddonCalendarEventPage', {id: eventId});
}
/**
* Page destroyed.
*/
ngOnDestroy() {
this.obsDefaultTimeChange && this.obsDefaultTimeChange.off();
}
}

View File

@ -0,0 +1,22 @@
<ion-header>
<ion-navbar>
<ion-title>{{ 'core.settings.settings' | translate }}</ion-title>
</ion-navbar>
</ion-header>
<ion-content>
<ion-list>
<ion-item>
<ion-label>{{ 'addon.calendar.defaultnotificationtime' | translate }}</ion-label>
<ion-select [(ngModel)]="defaultTime" (ionChange)="updateDefaultTime($event)">
<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-list>
</ion-content>

View File

@ -0,0 +1,31 @@
// (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 { AddonCalendarSettingsPage } from './settings';
import { CorePipesModule } from '../../../../pipes/pipes.module';
@NgModule({
declarations: [
AddonCalendarSettingsPage,
],
imports: [
CorePipesModule,
IonicPageModule.forChild(AddonCalendarSettingsPage),
TranslateModule.forChild()
],
})
export class AddonCalendarSettingsPageModule {}

View File

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

View File

@ -0,0 +1,50 @@
// (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 } from '@angular/core';
import { IonicPage } from 'ionic-angular';
import { AddonCalendarProvider } from '../../providers/calendar';
import { CoreEventsProvider } from '../../../../providers/events';
import { CoreSitesProvider } from '../../../../providers/sites';
/**
* Page that displays the calendar settings.
*/
@IonicPage({segment: "addon-calendar-settings"})
@Component({
selector: 'page-addon-calendar-settings',
templateUrl: 'settings.html',
})
export class AddonCalendarSettingsPage {
defaultTime = 0;
constructor(private calendarProvider: AddonCalendarProvider, private eventsProvider: CoreEventsProvider,
private sitesProvider: CoreSitesProvider) {}
/**
* View loaded.
*/
ionViewDidLoad() {
this.calendarProvider.getDefaultNotificationTime().then((time) => {
this.defaultTime = time;
});
}
updateDefaultTime(newTime) {
this.calendarProvider.setDefaultNotificationTime(newTime);
this.eventsProvider.trigger(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, {time: newTime},
this.sitesProvider.getCurrentSiteId());
};
}

View File

@ -0,0 +1,530 @@
// (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 { Injectable } from '@angular/core';
import { CoreLoggerProvider } from '../../../providers/logger';
import { CoreSitesProvider } from '../../../providers/sites';
import { CoreSite } from '../../../classes/site';
import { CoreCoursesProvider } from '../../../core/courses/providers/courses';
import { CoreTimeUtilsProvider } from '../../../providers/utils/time';
import { CoreGroupsProvider } from '../../../providers/groups';
import { CoreConstants } from '../../../core/constants';
import { CoreLocalNotificationsProvider } from '../../../providers/local-notifications';
import { CoreConfigProvider } from '../../../providers/config';
/**
* Service to handle calendar events.
*/
@Injectable()
export class AddonCalendarProvider {
public static DAYS_INTERVAL = 30;
public static COMPONENT = 'AddonCalendarEvents';
public static DEFAULT_NOTIFICATION_TIME_CHANGED = 'AddonCalendarDefaultNotificationTimeChangedEvent';
protected DEFAULT_NOTIFICATION_TIME_SETTING = 'mmaCalendarDefaultNotifTime';
protected ROOT_CACHE_KEY = 'mmaCalendar:';
protected DEFAULT_NOTIFICATION_TIME = 60;
// Variables for database.
protected EVENTS_TABLE = 'calendar_events';
protected tablesSchema = [
{
name: this.EVENTS_TABLE,
columns: [
{
name: 'id',
type: 'INTEGER',
primaryKey: true
},
{
name: 'notificationtime',
type: 'INTEGER'
},
{
name: 'name',
type: 'TEXT',
notNull: true
},
{
name: 'description',
type: 'TEXT'
},
{
name: 'eventtype',
type: 'TEXT'
},
{
name: 'courseid',
type: 'INTEGER'
},
{
name: 'timestart',
type: 'INTEGER'
},
{
name: 'timeduration',
type: 'INTEGER'
},
{
name: 'categoryid',
type: 'INTEGER'
},
{
name: 'groupid',
type: 'INTEGER'
},
{
name: 'instance',
type: 'INTEGER'
},
{
name: 'modulename',
type: 'TEXT'
},
{
name: 'timemodified',
type: 'INTEGER'
},
{
name: 'repeatid',
type: 'INTEGER'
}
]
}
];
protected logger;
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private groupsProvider: CoreGroupsProvider,
private coursesProvider: CoreCoursesProvider, private timeUtils: CoreTimeUtilsProvider,
private localNotificationsProvider: CoreLocalNotificationsProvider, private configProvider: CoreConfigProvider) {
this.logger = logger.getInstance('AddonCalendarProvider');
this.sitesProvider.createTablesFromSchema(this.tablesSchema);
}
/**
* Get the configured default notification time.
*
* @param {string} [siteId] ID of the site. If not defined, use current site.
* @return {Promise<number>} Promise resolved with the default time.
*/
getDefaultNotificationTime(siteId?: string) : Promise<number> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
let key = this.DEFAULT_NOTIFICATION_TIME_SETTING + '#' + siteId;
return this.configProvider.get(key, this.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 this.ROOT_CACHE_KEY + 'events:' + id;
}
/**
* Get a calendar event from local Db.
*
* @param {number} id Event ID.
* @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site.
* @return {Promise<any>} Promise resolved when the event data is retrieved.
*/
getEventFromLocalDb(id: number, siteId?: string) : Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.getDb().getRecord(this.EVENTS_TABLE, {id: id});
});
}
/**
* Get event notification time. Always returns number of minutes (0 if disabled).
*
* @param {number} id Event ID.
* @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site.
* @return {Promise<number>} Event notification time in minutes. 0 if disabled.
*/
getEventNotificationTime(id: number, siteId?: string) : Promise<number> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
return this.getEventNotificationTimeOption(id, siteId).then((time: number) => {
if (time == -1) {
return this.getDefaultNotificationTime(siteId);
}
return time;
});
}
/**
* Get event notification time for options. Returns -1 for default time.
*
* @param {number} id Event ID.
* @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site.
* @return {Promise<number>} Promise with wvent notification time in minutes. 0 if disabled, -1 if default time.
*/
getEventNotificationTimeOption(id: number, siteId?: string) : Promise<number> {
return this.getEventFromLocalDb(id, siteId).then((e) => {
return e.notificationtime || -1;
}).catch(() => {
return -1;
});
}
/**
* Get the events in a certain period. The period is calculated like this:
* start time: now + daysToStart
* end time: start time + daysInterval
* E.g. using provider.getEventsList(30, 30) is going to get the events starting after 30 days from now
* and ending before 60 days from now.
*
* @param {number} [daysToStart=0] Number of days from now to start getting events.
* @param {number} [daysInterval=30] Number of days between timestart and timeend.
* @param {string} [siteId] Site to get the events from. If not defined, use current site.
* @return {Promise<any[]>} Promise to be resolved when the participants are retrieved.
*/
getEventsList(daysToStart = 0, daysInterval = AddonCalendarProvider.DAYS_INTERVAL, siteId?: string) : Promise<any[]> {
return this.sitesProvider.getSite(siteId).then((site) => {
siteId = site.getId();
return this.coursesProvider.getUserCourses(false, siteId).then((courses) => {
courses.push({id: site.getSiteHomeId()}); // Add front page.
return this.groupsProvider.getUserGroups(courses, siteId).then((groups) => {
let now = this.timeUtils.timestamp(),
start = now + (CoreConstants.secondsDay * daysToStart),
end = start + (CoreConstants.secondsDay * daysInterval);
// The core_calendar_get_calendar_events needs all the current user courses and groups.
let data = {
"options[userevents]": 1,
"options[siteevents]": 1,
"options[timestart]": start,
"options[timeend]": end
};
courses.forEach((course, index) => {
data["events[courseids][" + index + "]"] = course.id;
});
groups.forEach((group, index) => {
data["events[groupids][" + index + "]"] = group.id;
});
// We need to retrieve cached data using cache key because we have timestamp in the params.
let preSets = {
cacheKey: this.getEventsListCacheKey(daysToStart, daysInterval),
getCacheUsingCacheKey: true
};
return site.read('core_calendar_get_calendar_events', data, preSets).then((response) => {
this.storeEventsInLocalDB(response.events, siteId);
return response.events;
});
});
});
});
}
/**
* Get prefix cache key for events list WS calls.
*
* @return {string} Prefix Cache key.
*/
protected getEventsListPrefixCacheKey() : string {
return this.ROOT_CACHE_KEY + 'eventslist:';
}
/**
* Get cache key for events list WS calls.
*
* @param {number} daysToStart Number of days from now to start getting events.
* @param {number} daysInterval Number of days between timestart and timeend.
* @return {string} Cache key.
*/
protected getEventsListCacheKey(daysToStart: number, daysInterval: number) : string {
return this.getEventsListPrefixCacheKey() + daysToStart + ':' + daysInterval;
}
/**
* 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.
*/
invalidateEventsList(courses: any[], siteId?: string) : Promise<any[]> {
return this.sitesProvider.getSite(siteId).then((site) => {
siteId = site.getId();
let promises = [];
promises.push(this.coursesProvider.invalidateUserCourses(siteId));
promises.push(this.groupsProvider.invalidateUserGroups(courses, siteId));
promises.push(site.invalidateWsCacheForKeyStartingWith(this.getEventsListPrefixCacheKey()));
return Promise.all(promises);
});
}
/**
* 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.
*
* @param {CoreSite} [site] Site. If not defined, use current site.
* @return {boolean} Whether it's disabled.
*/
isCalendarDisabledInSite(site?: CoreSite) : boolean {
site = site || this.sitesProvider.getCurrentSite();
return site.isFeatureDisabled('$mmSideMenuDelegate_mmaCalendar');
}
/**
* Check if Calendar is disabled in a certain site.
*
* @param {string} [siteId] Site Id. If not defined, use current site.
* @return {Promise<boolean>} Promise resolved with true if disabled, rejected or resolved with false otherwise.
*/
isDisabled(siteId?: string) : Promise<boolean> {
return this.sitesProvider.getSite(siteId).then((site) => {
return this.isCalendarDisabledInSite(site);
});
}
/**
* Get the next events for all the sites and schedules their notifications.
* If an event notification time is 0, cancel its scheduled notification (if any).
* If local notification plugin is not enabled, resolve the promise.
*
* @return {Promise} Promise resolved when all the notifications have been scheduled.
*/
scheduleAllSitesEventsNotifications() : Promise<any[]> {
if (this.localNotificationsProvider.isAvailable()) {
return this.sitesProvider.getSitesIds().then((siteIds) => {
let promises = [];
siteIds.forEach((siteId) => {
// Check if calendar is disabled for the site.
promises.push(this.isDisabled(siteId).then((disabled) => {
if (!disabled) {
// Get first events.
return this.getEventsList(undefined, undefined, siteId).then((events) => {
return this.scheduleEventsNotifications(events, siteId);
});
}
}));
});
return Promise.all(promises);
});
} else {
return Promise.resolve([]);
}
}
/**
* Schedules an event notification. If time is 0, cancel scheduled notification if any.
* If local notification plugin is not enabled, resolve the promise.
*
* @param {any} event Event to schedule.
* @param {number} time Notification setting time (in minutes). E.g. 10 means "notificate 10 minutes before start".
* @param {string} [siteId] Site ID the event belongs to. If not defined, use current site.
* @return {Promise<void>} Promise resolved when the notification is scheduled.
*/
scheduleEventNotification(event: any, time: number, siteId?: string) : Promise<void> {
if (this.localNotificationsProvider.isAvailable()) {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
if (time === 0) {
// Cancel if it was scheduled.
return this.localNotificationsProvider.cancel(event.id, AddonCalendarProvider.COMPONENT, siteId);
}
// If time is -1, get event default time.
let promise = time == -1 ? this.getDefaultNotificationTime(siteId) : Promise.resolve(time);
return promise.then((time) => {
let timeEnd = (event.timestart + event.timeduration) * 1000;
if (timeEnd <= new Date().getTime()) {
// The event has finished already, don't schedule it.
return Promise.resolve();
}
let dateTriggered = new Date((event.timestart - (time * 60)) * 1000),
startDate = new Date(event.timestart * 1000),
notification = {
id: event.id,
title: event.name,
text: startDate.toLocaleString(),
at: dateTriggered,
data: {
eventid: event.id,
siteid: siteId
}
};
return this.localNotificationsProvider.schedule(notification, AddonCalendarProvider.COMPONENT, siteId);
});
} else {
return Promise.resolve();
}
}
/**
* Schedules the notifications for a list of events.
* If an event notification time is 0, cancel its scheduled notification (if any).
* If local notification plugin is not enabled, resolve the promise.
*
* @param {any[]} events Events to schedule.
* @param {string} [siteId] ID of the site the events belong to. If not defined, use current site.
* @return {Promise<any[]>} Promise resolved when all the notifications have been scheduled.
*/
scheduleEventsNotifications(events: any[], siteId?: string) : Promise<any[]> {
var promises = [];
if (this.localNotificationsProvider.isAvailable()) {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
events.forEach((e) => {
promises.push(this.getEventNotificationTime(e.id, siteId).then((time) => {
return this.scheduleEventNotification(e, time, siteId);
}));
});
}
return Promise.all(promises);
}
/**
* Set the default notification time.
*
* @param {number} time New default time.
* @param {string} [siteId] ID of the site. If not defined, use current site.
* @return {Promise<any[]>} Promise resolved when stored.
*/
setDefaultNotificationTime(time: number, siteId?: string) : Promise<any[]> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
let key = this.DEFAULT_NOTIFICATION_TIME_SETTING + '#' + siteId;
return this.configProvider.set(key, time);
}
/**
* Store events in local DB.
*
* @param {any[]} events Events to store.
* @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site.
* @return {Promise<any[]>} Promise resolved when the events are stored.
*/
protected storeEventsInLocalDB(events: any[], siteId?: string) : Promise<any[]> {
return this.sitesProvider.getSite(siteId).then((site) => {
siteId = site.getId();
let promises = [],
db = site.getDb();
events.forEach((event) => {
// Don't override event notification time if the user configured it.
promises.push(this.getEventFromLocalDb(event.id, siteId).catch(() => {
// Event not stored, return empty object.
return {};
}).then((e) => {
let eventRecord = {
id: event.id,
name: event.name,
description: event.description,
eventtype: event.eventtype,
courseid: event.courseid,
timestart: event.timestart,
timeduration: event.timeduration,
categoryid: event.categoryid,
groupid: event.groupid,
instance: event.instance,
modulename: event.modulename,
timemodified: event.timemodified,
repeatid: event.repeatid,
notificationtime: e.notificationtime || -1
};
return db.insertOrUpdateRecord(this.EVENTS_TABLE, eventRecord, {id: eventRecord.id});
}));
});
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(this.EVENTS_TABLE, event, {id: event.id}).then(() => {
return this.scheduleEventNotification(event, time);
});
});
}
}

View File

@ -0,0 +1,52 @@
// (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 { Injectable } from '@angular/core';
import { AddonCalendarProvider } from './calendar';
import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '../../../core/mainmenu/providers/delegate';
/**
* Handler to inject an option into main menu.
*/
@Injectable()
export class AddonCalendarMainMenuHandler implements CoreMainMenuHandler {
name = 'AddonCalendar';
priority = 400;
constructor(private calendarProvider: AddonCalendarProvider) {}
/**
* Check if the handler is enabled on a site level.
*
* @return {boolean} Whether or not the handler is enabled on a site level.
*/
isEnabled(): boolean|Promise<boolean> {
let isDisabled = this.calendarProvider.isCalendarDisabledInSite();
return !isDisabled;
}
/**
* Returns the data needed to render the handler.
*
* @return {CoreMainMenuHandlerData} Data needed to render the handler.
*/
getDisplayData(): CoreMainMenuHandlerData {
return {
icon: 'calendar',
title: 'addon.calendar.calendar',
page: 'AddonCalendarListPage',
class: 'addon-calendar-handler'
};
}
}

View File

@ -0,0 +1,52 @@
// (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 { Injectable } from '@angular/core';
import { CoreLoggerProvider } from '../../../providers/logger';
import { CoreSitesProvider } from '../../../providers/sites';
//import { CoreCourseProvider } from '../../../core/course/providers/course';
/**
* Service that provides some features regarding lists of courses and categories.
*/
@Injectable()
export class AddonCalendarHelperProvider {
protected logger;
private EVENTICONS = {
'course': 'ionic',
'group': 'people',
'site': 'globe',
'user': 'person',
'category': 'albums'
};
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider/*, private courseProvider: CoreCourseProvider*/) {
this.logger = logger.getInstance('AddonCalendarHelperProvider');
}
/**
* Convenience function to format some event data to be rendered.
*
* @param {any} e Event to format.
*/
formatEventData(e: any) {
e.icon = this.EVENTICONS[e.eventtype] || false;
if (!e.icon) {
// @todo: It's a module event.
//e.icon = this.courseProvider.getModuleIconSrc(e.modulename);
e.moduleIcon = e.icon;
}
};
}

View File

@ -24,6 +24,9 @@
}
}
.bar-buttons core-context-menu .button-clear-ios {
color: $toolbar-ios-button-color;
}
// Highlights inside the input element.
@if ($core-text-input-ios-show-highlight) {
@ -87,4 +90,4 @@
@include ios-input-highlight($text-input-ios-highlight-color-invalid);
}
}
}
}

View File

@ -12,6 +12,10 @@
height: calc(100% - #{($card-md-margin-end + $card-md-margin-start)});
}
.bar-buttons core-context-menu .button-clear-md {
color: $toolbar-md-button-color;
}
// Highlights inside the input element.
@if ($core-text-input-md-show-highlight) {
.card-md, .list-md {
@ -75,4 +79,4 @@
@include md-input-highlight($text-input-md-highlight-color-invalid);
}
}
}
}

View File

@ -55,7 +55,7 @@ import { CoreMainMenuModule } from '../core/mainmenu/mainmenu.module';
import { CoreCoursesModule } from '../core/courses/courses.module';
import { CoreFileUploaderModule } from '../core/fileuploader/fileuploader.module';
import { CoreSharedFilesModule } from '../core/sharedfiles/sharedfiles.module';
import { AddonCalendarModule } from '../addon/calendar/calendar.module';
// For translate loader. AoT requires an exported function for factories.
export function createTranslateLoader(http: HttpClient) {
@ -86,7 +86,8 @@ export function createTranslateLoader(http: HttpClient) {
CoreCoursesModule,
CoreFileUploaderModule,
CoreSharedFilesModule,
CoreComponentsModule
CoreComponentsModule,
AddonCalendarModule
],
bootstrap: [IonicApp],
entryComponents: [

View File

@ -11,3 +11,7 @@
.col[align-self-stretch] .card-wp {
height: calc(100% - #{($card-wp-margin-end + $card-wp-margin-start)});
}
.bar-buttons core-context-menu .button-clear-wp {
color: $toolbar-wp-button-color;
}

View File

@ -504,27 +504,27 @@ export class CoreSite {
}
// Session expired, trigger event.
this.eventsProvider.trigger(CoreEventsProvider.SESSION_EXPIRED, {siteId: this.id});
this.eventsProvider.trigger(CoreEventsProvider.SESSION_EXPIRED, {}, this.id);
// Change error message. We'll try to get data from cache.
error.message = this.translate.instant('core.lostconnection');
} else if (error.errorcode === 'userdeleted') {
// User deleted, trigger event.
this.eventsProvider.trigger(CoreEventsProvider.USER_DELETED, {siteId: this.id, params: data});
this.eventsProvider.trigger(CoreEventsProvider.USER_DELETED, {params: data}, this.id);
error.message = this.translate.instant('core.userdeleted');
return Promise.reject(error);
} else if (error.errorcode === 'forcepasswordchangenotice') {
// Password Change Forced, trigger event.
this.eventsProvider.trigger(CoreEventsProvider.PASSWORD_CHANGE_FORCED, {siteId: this.id});
this.eventsProvider.trigger(CoreEventsProvider.PASSWORD_CHANGE_FORCED, {}, this.id);
error.message = this.translate.instant('core.forcepasswordchangenotice');
return Promise.reject(error);
} else if (error.errorcode === 'usernotfullysetup') {
// User not fully setup, trigger event.
this.eventsProvider.trigger(CoreEventsProvider.USER_NOT_FULLY_SETUP, {siteId: this.id});
this.eventsProvider.trigger(CoreEventsProvider.USER_NOT_FULLY_SETUP, {}, this.id);
error.message = this.translate.instant('core.usernotfullysetup');
return Promise.reject(error);
} else if (error.errorcode === 'sitepolicynotagreed') {
// Site policy not agreed, trigger event.
this.eventsProvider.trigger(CoreEventsProvider.SITE_POLICY_NOT_AGREED, {siteId: this.id});
this.eventsProvider.trigger(CoreEventsProvider.SITE_POLICY_NOT_AGREED, {}, this.id);
error.message = this.translate.instant('core.sitepolicynotagreederror');
return Promise.reject(error);
} else if (error.errorcode === 'dmlwriteexception' && this.textUtils.hasUnicodeData(data)) {
@ -802,8 +802,8 @@ export class CoreSite {
}
this.logger.debug('Invalidate cache for key starting with: ' + key);
let sql = 'UPDATE ' + this.WS_CACHE_TABLE + ' SET expirationTime=0 WHERE key LIKE ?%';
return this.db.execute(sql, [key]);
let sql = 'UPDATE ' + this.WS_CACHE_TABLE + ' SET expirationTime=0 WHERE key LIKE ?';
return this.db.execute(sql, [key + "%"]);
}
/**

View File

@ -21,6 +21,7 @@ import { CoreLoadingComponent } from './loading/loading';
import { CoreMarkRequiredComponent } from './mark-required/mark-required';
import { CoreInputErrorsComponent } from './input-errors/input-errors';
import { CoreShowPasswordComponent } from './show-password/show-password';
import { CoreSplitViewComponent } from './split-view/split-view';
import { CoreIframeComponent } from './iframe/iframe';
import { CoreProgressBarComponent } from './progress-bar/progress-bar';
import { CoreEmptyBoxComponent } from './empty-box/empty-box';
@ -29,6 +30,7 @@ import { CoreFileComponent } from './file/file';
import { CoreContextMenuComponent } from './context-menu/context-menu';
import { CoreContextMenuItemComponent } from './context-menu/context-menu-item';
import { CoreContextMenuPopoverComponent } from './context-menu/context-menu-popover';
import { CoreCoursePickerMenuPopoverComponent } from './course-picker-menu/course-picker-menu-popover';
import { CoreChronoComponent } from './chrono/chrono';
import { CoreLocalFileComponent } from './local-file/local-file';
import { CoreSitePickerComponent } from './site-picker/site-picker';
@ -39,6 +41,7 @@ import { CoreSitePickerComponent } from './site-picker/site-picker';
CoreMarkRequiredComponent,
CoreInputErrorsComponent,
CoreShowPasswordComponent,
CoreSplitViewComponent,
CoreIframeComponent,
CoreProgressBarComponent,
CoreEmptyBoxComponent,
@ -47,12 +50,14 @@ import { CoreSitePickerComponent } from './site-picker/site-picker';
CoreContextMenuComponent,
CoreContextMenuItemComponent,
CoreContextMenuPopoverComponent,
CoreCoursePickerMenuPopoverComponent,
CoreChronoComponent,
CoreLocalFileComponent,
CoreSitePickerComponent
],
entryComponents: [
CoreContextMenuPopoverComponent
CoreContextMenuPopoverComponent,
CoreCoursePickerMenuPopoverComponent
],
imports: [
IonicModule,
@ -65,6 +70,7 @@ import { CoreSitePickerComponent } from './site-picker/site-picker';
CoreMarkRequiredComponent,
CoreInputErrorsComponent,
CoreShowPasswordComponent,
CoreSplitViewComponent,
CoreIframeComponent,
CoreProgressBarComponent,
CoreEmptyBoxComponent,

View File

@ -0,0 +1,6 @@
<ion-list radio-group [(ngModel)]="courseId">
<ion-item text-wrap *ngFor="let course of courses" >
<ion-label><core-format-text [text]="course.fullname"></core-format-text></ion-label>
<ion-radio value="{{course.id}}" (ionSelect)="coursePicked($event, course)" ></ion-radio>
</ion-item>
</ion-list>

View File

@ -0,0 +1,45 @@
// (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 } from '@angular/core';
import { NavParams, ViewController } from 'ionic-angular';
/**
* Component to display a list of courses.
*/
@Component({
selector: 'core-course-picker-menu-popover',
templateUrl: 'course-picker-menu-popover.html'
})
export class CoreCoursePickerMenuPopoverComponent {
courses: any[];
courseId = -1;
constructor(private navParams: NavParams, private viewCtrl: ViewController) {
this.courses = navParams.get('courses') || [];
this.courseId = navParams.get('courseId') || -1;
}
/**
* Function called when a course is clicked.
*
* @param {Event} event Click event.
* @param {any} course Course object clicked.
* @return {boolean} Return true if success, false if error.
*/
coursePicked(event: Event, course: any) : boolean {
this.viewCtrl.dismiss(course);
return true;
}
}

View File

@ -0,0 +1,9 @@
<ion-header>
<ion-navbar>
<ion-title>&nbsp;</ion-title>
</ion-navbar>
</ion-header>
<ion-content>
<core-empty-box icon="arrow-dropleft" [message]="'core.emptysplit' | translate"></core-empty-box>
</ion-content>

View File

@ -0,0 +1,36 @@
// (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.
// Code based on https://github.com/martinpritchardelevate/ionic-split-pane-demo
import { NgModule } from '@angular/core';
import { IonicPageModule } from 'ionic-angular';
import { CoreSplitViewPlaceholderPage } from './placeholder';
import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '../../components.module';
@NgModule({
declarations: [
CoreSplitViewPlaceholderPage,
],
imports: [
CoreComponentsModule,
IonicPageModule.forChild(CoreSplitViewPlaceholderPage),
TranslateModule.forChild()
],
exports: [
CoreSplitViewPlaceholderPage
]
})
export class CorePlaceholderPageModule { }

View File

@ -0,0 +1,3 @@
core-placeholder {
}

View File

@ -0,0 +1,29 @@
// (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.
// Code based on https://github.com/martinpritchardelevate/ionic-split-pane-demo
import { Component } from '@angular/core';
import { IonicPage } from 'ionic-angular';
@IonicPage({segment: "core-placeholder"})
@Component({
selector: 'core-placeholder',
templateUrl: 'placeholder.html',
})
export class CoreSplitViewPlaceholderPage {
constructor() { }
}

View File

@ -0,0 +1,7 @@
<ion-split-pane (ionChange)="onSplitPaneChanged($event._visible);" [when]="when">
<ion-menu [content]="detailNav" type="push">
<ion-header><ion-toolbar><ion-title></ion-title></ion-toolbar></ion-header>
<ng-content></ng-content>
</ion-menu>
<ion-nav [root]="detailPage" #detailNav main></ion-nav>
</ion-split-pane>

View File

@ -0,0 +1,39 @@
core-split-view {
ion-menu.split-pane-side {
display: block;
.menu-inner {
left: 0;
right: 0;
top: 0;
bottom: 0;
-webkit-transform: initial;
transform: initial;
width: 100%;
}
}
.split-pane-main {
display: none;
}
.split-pane-visible {
.split-pane-main {
display: block;
}
.split-pane-side .core-split-item-selected {
background-color: $gray-lighter;
border-left: 5px solid $core-color-light;
&.item-md {
padding-left: $item-md-padding-start - 5px;
}
&.item-ios {
padding-left: $item-ios-padding-start - 5px;
}
&.item-wp {
padding-left: $item-wp-padding-start - 5px;
}
}
}
}

View File

@ -0,0 +1,147 @@
// (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.
// Code based on https://github.com/martinpritchardelevate/ionic-split-pane-demo
import { Component, ViewChild, Injectable, Input, ElementRef, OnInit } from '@angular/core';
import { NavController, Nav } from 'ionic-angular';
import { CoreSplitViewPlaceholderPage } from './placeholder/placeholder';
/**
* Directive to create a split view layout.
*
* @description
* To init/change the right pane contents (content pane), inject this component in the master page.
* @ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent;
* Then use the push function to load.
*
* Accepts the following params:
*
* @param {string|boolean} [when] When the split-pane should be shown. Can be a CSS media query expression, or a shortcut
* expression. Can also be a boolean expression. Check split-pane component documentation for more information.
*
* Example:
*
* <core-split-view [when]="lg">
* <ion-content><!-- CONTENT TO SHOW ON THE LEFT PANEL (MENU) --></ion-content>
* </core-split-view>
*/
@Component({
selector: 'core-split-view',
templateUrl: 'split-view.html'
})
export class CoreSplitViewComponent implements OnInit {
// @todo Mix both panels header buttons
@ViewChild('detailNav') detailNav: Nav;
@Input() when?: string | boolean = "md"; //
protected isEnabled: boolean = false;
protected masterPageName: string = "";
protected loadDetailPage: any = false;
protected element: HTMLElement; // Current element.
// Empty placeholder for the 'detail' page.
detailPage: any = null;
constructor(private masterNav: NavController, element: ElementRef) {
this.element = element.nativeElement;
}
/**
* Component being initialized.
*/
ngOnInit() {
// Get the master page name and set an empty page as a placeholder.
this.masterPageName = this.masterNav.getActive().component.name;
this.emptyDetails();
}
/**
* Check if both panels are shown. It depends on screen width.
*
* @return {boolean} If split view is enabled.
*/
isOn(): boolean {
return this.isEnabled;
}
/**
* Push a page to the navigation stack. It will decide where to load it depending on the size of the screen.
*
* @param {any} page The component class or deeplink name you want to push onto the navigation stack.
* @param {any} params Any NavParams you want to pass along to the next view.
*/
push(page: any, params?: any, element?: HTMLElement) {
if (this.isEnabled) {
this.detailNav.setRoot(page, params);
} else {
this.loadDetailPage = {
component: page,
data: params
};
this.masterNav.push(page, params);
}
}
/**
* Set the details panel to default info.
*/
emptyDetails() {
this.loadDetailPage = false;
this.detailNav.setRoot('CoreSplitViewPlaceholderPage');
}
/**
* Splitpanel visibility has changed.
*
* @param {Boolean} isOn If it fits both panels at the same time.
*/
onSplitPaneChanged(isOn) {
this.isEnabled = isOn;
if (this.masterNav && this.detailNav) {
(isOn) ? this.activateSplitView() : this.deactivateSplitView();
}
}
/**
* Enable the split view, show both panels and do some magical navigation.
*/
activateSplitView() {
let currentView = this.masterNav.getActive(),
currentPageName = currentView.component.name;
if (currentPageName != this.masterPageName) {
// CurrentView is a 'Detail' page remove it from the 'master' nav stack.
this.masterNav.pop();
// and add it to the 'detail' nav stack.
this.detailNav.setRoot(currentView.component, currentView.data);
} else if (this.loadDetailPage) {
// MasterPage is shown, load the last detail page if found.
this.detailNav.setRoot(this.loadDetailPage.component, this.loadDetailPage.data);
}
this.loadDetailPage = false;
}
/**
* Disabled the split view, show only one panel and do some magical navigation.
*/
deactivateSplitView() {
let detailView = this.detailNav.getActive(),
currentPageName = detailView.component.name;
if (currentPageName != 'CoreSplitViewPlaceholderPage') {
// Current detail view is a 'Detail' page so, not the placeholder page, push it on 'master' nav stack.
this.masterNav.push(detailView.component, detailView.data);
}
}
}

View File

@ -21,7 +21,7 @@ import { CoreCoursesProvider } from '../../providers/courses';
/**
* Page that displays available courses in current site.
*/
@IonicPage()
@IonicPage({segment: "core-courses-available-courses"})
@Component({
selector: 'page-core-courses-available-courses',
templateUrl: 'available-courses.html',

View File

@ -23,7 +23,7 @@ import { CoreCoursesProvider } from '../../providers/courses';
/**
* Page that displays a list of categories and the courses in the current category if any.
*/
@IonicPage()
@IonicPage({segment: "core-courses-categories"})
@Component({
selector: 'page-core-courses-categories',
templateUrl: 'categories.html',

View File

@ -26,7 +26,7 @@ import { CoreCoursesDelegate } from '../../providers/delegate';
/**
* Page that allows "previewing" a course and enrolling in it if enabled and not enrolled.
*/
@IonicPage()
@IonicPage({segment: "core-courses-course-preview"})
@Component({
selector: 'page-core-courses-course-preview',
templateUrl: 'course-preview.html',
@ -307,7 +307,7 @@ export class CoreCoursesCoursePreviewPage implements OnDestroy {
this.refreshData().finally(() => {
// My courses have been updated, trigger event.
this.eventsProvider.trigger(
CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, {siteId: this.sitesProvider.getCurrentSiteId()});
CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, {}, this.sitesProvider.getCurrentSiteId());
});
});
}).catch((error) => {

View File

@ -22,7 +22,7 @@ import { CoreCoursesProvider } from '../../providers/courses';
/**
* Page that displays the list of courses the user is enrolled in.
*/
@IonicPage()
@IonicPage({segment: "core-courses-my-courses"})
@Component({
selector: 'page-core-courses-my-courses',
templateUrl: 'my-courses.html',
@ -53,17 +53,13 @@ export class CoreCoursesMyCoursesPage implements OnDestroy {
this.coursesLoaded = true;
});
this.myCoursesObserver = this.eventsProvider.on(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, (data) => {
if (data.siteId == this.sitesProvider.getCurrentSiteId()) {
this.fetchCourses();
}
});
this.myCoursesObserver = this.eventsProvider.on(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, () => {
this.fetchCourses();
}, this.sitesProvider.getCurrentSiteId());
this.siteUpdatedObserver = this.eventsProvider.on(CoreEventsProvider.SITE_UPDATED, (data) => {
if (data.siteId == this.sitesProvider.getCurrentSiteId()) {
this.searchEnabled = !this.coursesProvider.isSearchCoursesDisabledInSite();
}
});
this.siteUpdatedObserver = this.eventsProvider.on(CoreEventsProvider.SITE_UPDATED, () => {
this.searchEnabled = !this.coursesProvider.isSearchCoursesDisabledInSite();
}, this.sitesProvider.getCurrentSiteId());
}
/**

View File

@ -22,7 +22,7 @@ import * as moment from 'moment';
/**
* Page that displays My Overview.
*/
@IonicPage()
@IonicPage({segment: "core-courses-my-overview"})
@Component({
selector: 'page-core-courses-my-overview',
templateUrl: 'my-overview.html',

View File

@ -20,7 +20,7 @@ import { CoreCoursesProvider } from '../../providers/courses';
/**
* Page that allows searching for courses.
*/
@IonicPage()
@IonicPage({segment: "core-courses-search"})
@Component({
selector: 'page-core-courses-search',
templateUrl: 'search.html',

View File

@ -18,7 +18,7 @@ import { IonicPage, ViewController } from 'ionic-angular';
/**
* Page that displays a form to enter a password to self enrol in a course.
*/
@IonicPage()
@IonicPage({segment: "core-courses-self-enrol-password"})
@Component({
selector: 'page-core-courses-self-enrol-password',
templateUrl: 'self-enrol-password.html',

View File

@ -22,7 +22,7 @@ import { CoreCoursesMyOverviewProvider } from '../providers/my-overview';
*/
@Injectable()
export class CoreCoursesMainMenuHandler implements CoreMainMenuHandler {
name = 'mmCourses';
name = 'CoreCourses';
priority = 1100;
isOverviewEnabled: boolean;

View File

@ -22,7 +22,7 @@ import { CoreTimeUtilsProvider } from '../../../../providers/utils/time';
/**
* Page to capture media in browser or desktop.
*/
@IonicPage()
@IonicPage({segment: "core-emulator-capture-media"})
@Component({
selector: 'page-core-emulator-capture-media',
templateUrl: 'capture-media.html',

View File

@ -26,7 +26,7 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms';
/**
* Page to enter the user credentials.
*/
@IonicPage()
@IonicPage({segment: "core-login-credentials"})
@Component({
selector: 'page-core-login-credentials',
templateUrl: 'credentials.html',
@ -84,10 +84,7 @@ export class CoreLoginCredentialsPage {
*/
ionViewDidLeave() {
this.viewLeft = true;
this.eventsProvider.trigger(CoreEventsProvider.LOGIN_SITE_UNCHECKED, {
siteId: this.siteId,
config: this.siteConfig
});
this.eventsProvider.trigger(CoreEventsProvider.LOGIN_SITE_UNCHECKED, {config: this.siteConfig}, this.siteId);
}
/**
@ -146,9 +143,7 @@ export class CoreLoginCredentialsPage {
if (!this.eventThrown && !this.viewLeft) {
this.eventThrown = true;
this.eventsProvider.trigger(CoreEventsProvider.LOGIN_SITE_CHECKED, {
config: this.siteConfig
});
this.eventsProvider.trigger(CoreEventsProvider.LOGIN_SITE_CHECKED, {config: this.siteConfig});
}
} else {
this.siteName = null;

View File

@ -26,7 +26,7 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms';
/**
* Page to signup using email.
*/
@IonicPage()
@IonicPage({segment: "core-login-email-signup"})
@Component({
selector: 'page-core-login-email-signup',
templateUrl: 'email-signup.html',

View File

@ -22,7 +22,7 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms';
/**
* Page to recover a forgotten password.
*/
@IonicPage()
@IonicPage({segment: "core-login-forgotten-password"})
@Component({
selector: 'page-core-login-forgotten-password',
templateUrl: 'forgotten-password.html',

View File

@ -23,7 +23,7 @@ import { CoreLoginHelperProvider } from '../../providers/helper';
/**
* Page that displays a "splash screen" while the app is being initialized.
*/
@IonicPage()
@IonicPage({segment: "core-login-init"})
@Component({
selector: 'page-core-login-init',
templateUrl: 'init.html',

View File

@ -23,7 +23,7 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms';
/**
* Page to enter the user password to reconnect to a site.
*/
@IonicPage()
@IonicPage({segment: "core-login-reconnect"})
@Component({
selector: 'page-core-login-reconnect',
templateUrl: 'reconnect.html',

View File

@ -18,7 +18,7 @@ import { IonicPage, ViewController, NavParams } from 'ionic-angular';
/**
* Component that displays an error when trying to connect to a site.
*/
@IonicPage()
@IonicPage({segment: "core-login-site-error"})
@Component({
selector: 'page-core-login-site-error',
templateUrl: 'site-error.html',

View File

@ -18,7 +18,7 @@ import { IonicPage, ViewController } from 'ionic-angular';
/**
* Component that displays some help regarding the CoreLoginSitePage.
*/
@IonicPage()
@IonicPage({segment: "core-login-site-help"})
@Component({
selector: 'page-core-login-site-help',
templateUrl: 'site-help.html',

View File

@ -23,7 +23,7 @@ import { CoreSite } from '../../../../classes/site';
/**
* Page to accept a site policy.
*/
@IonicPage()
@IonicPage({segment: "core-login-site-policy"})
@Component({
selector: 'page-core-login-site-policy',
templateUrl: 'site-policy.html',

View File

@ -24,7 +24,7 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms';
/**
* Page to enter or select the site URL to connect to.
*/
@IonicPage()
@IonicPage({segment: "core-login-site"})
@Component({
selector: 'page-core-login-site',
templateUrl: 'site.html',

View File

@ -24,7 +24,7 @@ import { CoreLoginHelperProvider } from '../../providers/helper';
/**
* Page that displays the list of stored sites.
*/
@IonicPage()
@IonicPage({segment: "core-login-sites"})
@Component({
selector: 'page-core-login-sites',
templateUrl: 'sites.html',

View File

@ -574,10 +574,9 @@ export class CoreLoginHelperProvider {
if (site.isLoggedOut()) {
this.eventsProvider.trigger(CoreEventsProvider.SESSION_EXPIRED, {
siteId: site.getId(),
pageName: pageName,
params: params
});
}, site.getId());
return true;
}
return false;

View File

@ -22,7 +22,7 @@ import { CoreMainMenuDelegate, CoreMainMenuHandlerData } from '../../providers/d
/**
* Page that displays the main menu of the app.
*/
@IonicPage()
@IonicPage({segment: "core-mainmenu"})
@Component({
selector: 'page-core-mainmenu',
templateUrl: 'menu.html',

View File

@ -22,7 +22,7 @@ import { CoreMainMenuProvider, CoreMainMenuCustomItem } from '../../providers/ma
/**
* Page that displays the list of main menu options that aren't in the tabs.
*/
@IonicPage()
@IonicPage({segment: "core-mainmenu-more"})
@Component({
selector: 'page-core-mainmenu-more',
templateUrl: 'more.html',
@ -45,12 +45,8 @@ export class CoreMainMenuMorePage implements OnDestroy {
private navCtrl: NavController, private mainMenuProvider: CoreMainMenuProvider, eventsProvider: CoreEventsProvider) {
this.langObserver = eventsProvider.on(CoreEventsProvider.LANGUAGE_CHANGED, this.loadSiteInfo.bind(this));
this.updateSiteObserver = eventsProvider.on(CoreEventsProvider.SITE_UPDATED, (data) => {
if (sitesProvider.getCurrentSiteId() == data.siteId) {
this.loadSiteInfo();
}
});
this.updateSiteObserver = eventsProvider.on(CoreEventsProvider.SITE_UPDATED, this.loadSiteInfo.bind(this),
sitesProvider.getCurrentSiteId());
this.loadSiteInfo();
}

View File

@ -22,7 +22,7 @@ import { CoreSharedFilesHelperProvider } from '../../providers/helper';
/**
* Modal to display the list of sites to choose one to store a shared file.
*/
@IonicPage()
@IonicPage({segment: "core-shared-files-choose-site"})
@Component({
selector: 'page-core-shared-files-choose-site',
templateUrl: 'choose-site.html',

View File

@ -24,7 +24,7 @@ import { CoreSharedFilesProvider } from '../../providers/sharedfiles';
/**
* Modal to display the list of shared files.
*/
@IonicPage()
@IonicPage({segment: "core-shared-files-list"})
@Component({
selector: 'page-core-shared-files-list',
templateUrl: 'list.html',

View File

@ -18,7 +18,7 @@ import { IonicPage, NavParams } from 'ionic-angular';
/**
* Page to display a URL in an iframe.
*/
@IonicPage()
@IonicPage({segment: "core-viewer-iframe"})
@Component({
selector: 'page-core-viewer-iframe',
templateUrl: 'iframe.html',

View File

@ -19,7 +19,7 @@ import { CoreTextUtilsProvider } from '../../../../providers/utils/text';
/**
* Page to render a certain text. If opened as a modal, it will have a button to close the modal.
*/
@IonicPage()
@IonicPage({segment: "core-viewer-text"})
@Component({
selector: 'page-core-viewer-text',
templateUrl: 'text.html',

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.

View File

@ -64,9 +64,10 @@ export class CoreEventsProvider {
*
* @param {string} eventName Name of the event to listen to.
* @param {Function} callBack Function to call when the event is triggered.
* @param {string} [siteId] Site where to trigger the event. Undefined won't check the site.
* @return {CoreEventObserver} Observer to stop listening.
*/
on(eventName: string, callBack: (value: any) => void) : CoreEventObserver {
on(eventName: string, callBack: (value: any) => void, siteId?: string) : CoreEventObserver {
// If it's a unique event and has been triggered already, call the callBack.
// We don't need to create an observer because the event won't be triggered again.
if (this.uniqueEvents[eventName]) {
@ -84,7 +85,11 @@ export class CoreEventsProvider {
this.observables[eventName] = new Subject<any>();
}
let subscription = this.observables[eventName].subscribe(callBack);
let subscription = this.observables[eventName].subscribe((value: any) => {
if (!siteId || value.siteId == siteId) {
callBack(value);
}
});
// Create and return a CoreEventObserver.
return {
@ -100,10 +105,17 @@ export class CoreEventsProvider {
*
* @param {string} event Name of the event to trigger.
* @param {any} [data] Data to pass to the observers.
* @param {string} [siteId] Site where to trigger the event. Undefined means no Site.
*/
trigger(eventName: string, data?: any) : void {
trigger(eventName: string, data?: any, siteId?: string) : void {
this.logger.debug(`Event '${eventName}' triggered.`);
if (this.observables[eventName]) {
if (siteId) {
if (!data) {
data = {};
}
data.siteId = siteId;
}
this.observables[eventName].next(data);
}
}
@ -113,12 +125,21 @@ export class CoreEventsProvider {
*
* @param {string} event Name of the event to trigger.
* @param {any} data Data to pass to the observers.
* @param {string} [siteId] Site where to trigger the event. Undefined means no Site.
*/
triggerUnique(eventName: string, data: any) : void {
triggerUnique(eventName: string, data: any, siteId?: string) : void {
if (this.uniqueEvents[eventName]) {
this.logger.debug(`Unique event '${eventName}' ignored because it was already triggered.`);
} else {
this.logger.debug(`Unique event '${eventName}' triggered.`);
if (siteId) {
if (!data) {
data = {};
}
data.siteId = siteId;
}
// Store the data so it can be passed to observers that register from now on.
this.uniqueEvents[eventName] = {
data: data

View File

@ -2692,12 +2692,11 @@ export class CoreFilepoolProvider {
*/
protected triggerPackageStatusChanged(siteId: string, status: string, component: string, componentId?: string|number) : void {
const data = {
siteid: siteId,
component: component,
componentId: this.fixComponentId(componentId),
status: status
}
this.eventsProvider.trigger(CoreEventsProvider.PACKAGE_STATUS_CHANGED, data);
this.eventsProvider.trigger(CoreEventsProvider.PACKAGE_STATUS_CHANGED, data, siteId);
}
/**

View File

@ -398,7 +398,7 @@ export class CoreSitesProvider {
this.sites[siteId] = candidateSite;
// Store session.
this.login(siteId);
this.eventsProvider.trigger(CoreEventsProvider.SITE_ADDED, siteId);
this.eventsProvider.trigger(CoreEventsProvider.SITE_ADDED, {}, siteId);
if (this.siteTablesSchemas.length) {
// Create tables in the site's database.
@ -558,7 +558,7 @@ export class CoreSitesProvider {
// Check if local_mobile was installed to Moodle.
return site.checkIfLocalMobileInstalledAndNotUsed().then(() => {
// Local mobile was added. Throw invalid session to force reconnect and create a new token.
this.eventsProvider.trigger(CoreEventsProvider.SESSION_EXPIRED, {siteId: siteId});
this.eventsProvider.trigger(CoreEventsProvider.SESSION_EXPIRED, {}, siteId);
}, () => {
// Update site info. We don't block the UI.
this.updateSiteInfo(siteId);
@ -622,7 +622,7 @@ export class CoreSitesProvider {
// DB remove shouldn't fail, but we'll go ahead even if it does.
return site.deleteFolder();
}).then(() => {
this.eventsProvider.trigger(CoreEventsProvider.SITE_DELETED, site);
this.eventsProvider.trigger(CoreEventsProvider.SITE_DELETED, site, siteId);
});
});
});
@ -739,7 +739,7 @@ export class CoreSitesProvider {
* @param {number} [siteId] The site ID. If not defined, current site (if available).
* @return {Promise} Promise resolved with site home ID.
*/
getSiteHomeId(siteId: string) : Promise<number> {
getSiteHomeId(siteId?: string) : Promise<number> {
return this.getSite(siteId).then((site) => {
return site.getSiteHomeId();
});
@ -801,7 +801,7 @@ export class CoreSitesProvider {
siteId: siteId
};
return this.appDB.insertOrUpdateRecord(this.CURRENT_SITE_TABLE, entry, {id: 1}).then(() => {
this.eventsProvider.trigger(CoreEventsProvider.LOGIN, {siteId: siteId});
this.eventsProvider.trigger(CoreEventsProvider.LOGIN, {}, siteId);
});
}
@ -829,7 +829,7 @@ export class CoreSitesProvider {
promises.push(this.appDB.deleteRecords(this.CURRENT_SITE_TABLE, {id: 1}));
return Promise.all(promises).finally(() => {
this.eventsProvider.trigger(CoreEventsProvider.LOGOUT, {siteId: siteId});
this.eventsProvider.trigger(CoreEventsProvider.LOGOUT, {}, siteId);
});
}
@ -936,7 +936,7 @@ export class CoreSitesProvider {
}
return this.appDB.updateRecords(this.SITES_TABLE, newValues, {id: siteId}).finally(() => {
this.eventsProvider.trigger(CoreEventsProvider.SITE_UPDATED, {siteId: siteId});
this.eventsProvider.trigger(CoreEventsProvider.SITE_UPDATED, {}, siteId);
});
});
});

View File

@ -323,7 +323,7 @@ export class CoreMimetypeUtilsProvider {
let filename = '',
mimetype = '',
extension = '',
langPrefix = 'core.mimetype-';
langPrefix = 'assets.mimetypes.';
if (typeof obj == 'object' && typeof obj.file == 'function') {
// It's a FileEntry. Don't use the file function because it's asynchronous and the type isn't reliable.
@ -422,7 +422,7 @@ export class CoreMimetypeUtilsProvider {
* @return {string} Translated name.
*/
getTranslatedGroupName(name: string) : string {
let key = 'core.mimetype-group:' + name,
let key = 'assets.mimetypes.group:' + name,
translated = this.translate.instant(key);
return translated != key ? translated : name;
}

View File

@ -552,7 +552,7 @@ export class CoreUtilsProvider {
* @return {string} Country name. If the country is not found, return the country code.
*/
getCountryName(code: string) : string {
let countryKey = 'core.country-' + code,
let countryKey = 'assets.countries.' + code,
countryName = this.translate.instant(countryKey);
return countryName !== countryKey ? countryName : code;
@ -580,8 +580,8 @@ export class CoreUtilsProvider {
let countries = {};
for (let name in table) {
if (name.indexOf('core.country-') === 0) {
let code = name.replace('core.country-', '');
if (name.indexOf('assets.countries.') === 0) {
let code = name.replace('assets.countries.', '');
countries[code] = table[name];
}
}

View File

@ -3,7 +3,15 @@
"no-duplicate-variable": true,
"no-unused-variable": [
true
]
],
"max-line-length": {
"options": [132]
},
},
"jsRules": {
"max-line-length": {
"options": [132]
}
},
"rulesDirectory": [
"node_modules/tslint-eslint-rules/dist/rules"