@ -40,10 +40,12 @@ import { AddonPrivateFilesModule } from './privatefiles/privatefiles.module';
 | 
			
		||||
import { AddonFilterModule } from './filter/filter.module';
 | 
			
		||||
import { AddonUserProfileFieldModule } from './userprofilefield/userprofilefield.module';
 | 
			
		||||
import { AddonBadgesModule } from './badges/badges.module';
 | 
			
		||||
import { AddonCalendarModule } from './calendar/calendar.module';
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    imports: [
 | 
			
		||||
        AddonBadgesModule,
 | 
			
		||||
        AddonCalendarModule,
 | 
			
		||||
        AddonPrivateFilesModule,
 | 
			
		||||
        AddonFilterModule,
 | 
			
		||||
        AddonBlockActivityResultsModule,
 | 
			
		||||
 | 
			
		||||
@ -24,7 +24,7 @@ import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-ro
 | 
			
		||||
import { CorePushNotificationsDelegate } from '@features/pushnotifications/services/push-delegate';
 | 
			
		||||
import { AddonBadgesPushClickHandler } from './services/handlers/push-click';
 | 
			
		||||
 | 
			
		||||
const mainMenuHomeSiblingRoutes: Routes = [
 | 
			
		||||
const mainMenuRoutes: Routes = [
 | 
			
		||||
    {
 | 
			
		||||
        path: 'badges',
 | 
			
		||||
        loadChildren: () => import('./badges-lazy.module').then(m => m.AddonBadgesLazyModule),
 | 
			
		||||
@ -33,7 +33,7 @@ const mainMenuHomeSiblingRoutes: Routes = [
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    imports: [
 | 
			
		||||
        CoreMainMenuTabRoutingModule.forChild(mainMenuHomeSiblingRoutes),
 | 
			
		||||
        CoreMainMenuTabRoutingModule.forChild(mainMenuRoutes),
 | 
			
		||||
    ],
 | 
			
		||||
    providers: [
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
@ -50,7 +50,7 @@ export class AddonBadgesIssuedBadgePage implements OnInit {
 | 
			
		||||
     * View loaded.
 | 
			
		||||
     */
 | 
			
		||||
    ngOnInit(): void {
 | 
			
		||||
        this.courseId =  this.route.snapshot.queryParams['courseId'] || this.courseId; // Use 0 for site badges.
 | 
			
		||||
        this.courseId =  parseInt(this.route.snapshot.queryParams['courseId'], 10) || this.courseId; // Use 0 for site badges.
 | 
			
		||||
        this.userId = this.route.snapshot.queryParams['userId'] ||
 | 
			
		||||
            CoreSites.instance.getCurrentSite()?.getUserId();
 | 
			
		||||
        this.badgeHash = this.route.snapshot.queryParams['badgeHash'];
 | 
			
		||||
 | 
			
		||||
@ -51,7 +51,7 @@ export class AddonBadgesUserBadgesPage implements OnInit {
 | 
			
		||||
     */
 | 
			
		||||
    ngOnInit(): void {
 | 
			
		||||
 | 
			
		||||
        this.courseId =  this.route.snapshot.queryParams['courseId'] || this.courseId; // Use 0 for site badges.
 | 
			
		||||
        this.courseId =  parseInt(this.route.snapshot.queryParams['courseId'], 10) || this.courseId; // Use 0 for site badges.
 | 
			
		||||
        this.userId = this.route.snapshot.queryParams['userId'] ||
 | 
			
		||||
            CoreSites.instance.getCurrentSite()?.getUserId();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,7 @@ import { Injectable } from '@angular/core';
 | 
			
		||||
import { CoreBlockHandlerData } from '@features/block/services/block-delegate';
 | 
			
		||||
import { CoreBlockOnlyTitleComponent } from '@features/block/components/only-title-block/only-title-block';
 | 
			
		||||
import { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler';
 | 
			
		||||
// import { AddonCalendar } from '@addon/calendar/services/calendar';
 | 
			
		||||
import { AddonCalendar } from '@/addons/calendar/services/calendar';
 | 
			
		||||
import { CoreCourseBlock } from '@features/course/services/course';
 | 
			
		||||
import { Params } from '@angular/router';
 | 
			
		||||
import { makeSingleton } from '@singletons';
 | 
			
		||||
@ -39,19 +39,13 @@ export class AddonBlockCalendarMonthHandlerService extends CoreBlockBaseHandler
 | 
			
		||||
     * @return Data or promise resolved with the data.
 | 
			
		||||
     */
 | 
			
		||||
    getDisplayData(block: CoreCourseBlock, contextLevel: string, instanceId: number): CoreBlockHandlerData {
 | 
			
		||||
        // @todo
 | 
			
		||||
        const link = 'AddonCalendarListPage';
 | 
			
		||||
        const linkParams: Params = contextLevel == 'course' ? { courseId: instanceId } : {};
 | 
			
		||||
 | 
			
		||||
        /* if (AddonCalendar.instance.canViewMonthInSite()) {
 | 
			
		||||
            link = 'AddonCalendarIndexPage';
 | 
			
		||||
        }*/
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            title: 'addon.block_calendarmonth.pluginname',
 | 
			
		||||
            class: 'addon-block-calendar-month',
 | 
			
		||||
            component: CoreBlockOnlyTitleComponent,
 | 
			
		||||
            link: link,
 | 
			
		||||
            link: AddonCalendar.instance.getMainCalendarPagePath(),
 | 
			
		||||
            linkParams: linkParams,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,7 @@ import { Injectable } from '@angular/core';
 | 
			
		||||
import { CoreBlockHandlerData } from '@features/block/services/block-delegate';
 | 
			
		||||
import { CoreBlockOnlyTitleComponent } from '@features/block/components/only-title-block/only-title-block';
 | 
			
		||||
import { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler';
 | 
			
		||||
// import { AddonCalendar } from '@addon/calendar/services/calendar';
 | 
			
		||||
import { AddonCalendar } from '@/addons/calendar/services/calendar';
 | 
			
		||||
import { CoreCourseBlock } from '@features/course/services/course';
 | 
			
		||||
import { Params } from '@angular/router';
 | 
			
		||||
import { makeSingleton } from '@singletons';
 | 
			
		||||
@ -39,20 +39,13 @@ export class AddonBlockCalendarUpcomingHandlerService extends CoreBlockBaseHandl
 | 
			
		||||
     * @return Data or promise resolved with the data.
 | 
			
		||||
     */
 | 
			
		||||
    getDisplayData(block: CoreCourseBlock, contextLevel: string, instanceId: number): CoreBlockHandlerData {
 | 
			
		||||
        // @todo
 | 
			
		||||
        const link = 'AddonCalendarListPage';
 | 
			
		||||
        const linkParams: Params = contextLevel == 'course' ? { courseId: instanceId } : {};
 | 
			
		||||
 | 
			
		||||
        /* if (AddonCalendar.instance.canViewMonthInSite()) {
 | 
			
		||||
            link = 'AddonCalendarIndexPage';
 | 
			
		||||
            linkParams.upcoming = true;
 | 
			
		||||
        }*/
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            title: 'addon.block_calendarupcoming.pluginname',
 | 
			
		||||
            class: 'addon-block-calendar-upcoming',
 | 
			
		||||
            component: CoreBlockOnlyTitleComponent,
 | 
			
		||||
            link: link,
 | 
			
		||||
            link: AddonCalendar.instance.getMainCalendarPagePath(),
 | 
			
		||||
            linkParams: linkParams,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -45,7 +45,7 @@ export class AddonBlockSiteMainMenuComponent extends CoreBlockBaseComponent impl
 | 
			
		||||
     * Component being initialized.
 | 
			
		||||
     */
 | 
			
		||||
    async ngOnInit(): Promise<void> {
 | 
			
		||||
        this.siteHomeId = CoreSites.instance.getCurrentSite()?.getSiteHomeId() || 1;
 | 
			
		||||
        this.siteHomeId = CoreSites.instance.getCurrentSiteHomeId();
 | 
			
		||||
 | 
			
		||||
        super.ngOnInit();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										28
									
								
								src/addons/calendar/calendar-common.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,28 @@
 | 
			
		||||
:host {
 | 
			
		||||
 | 
			
		||||
    --addon-calendar-blank-day-background-color: var(--gray-lighter);
 | 
			
		||||
 | 
			
		||||
    .item.addon-calendar-event {
 | 
			
		||||
        > ion-icon {
 | 
			
		||||
            color: white;
 | 
			
		||||
            border-radius: 50%;
 | 
			
		||||
            padding: 6px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        &.addon-calendar-eventtype-category > ion-icon {
 | 
			
		||||
            background-color: var(--addon-calendar-event-category-color);
 | 
			
		||||
        }
 | 
			
		||||
        &.addon-calendar-eventtype-course > ion-icon {
 | 
			
		||||
            background-color: var(--addon-calendar-event-course-color);
 | 
			
		||||
        }
 | 
			
		||||
        &.addon-calendar-eventtype-group > ion-icon {
 | 
			
		||||
            background-color: var(--addon-calendar-event-group-color);
 | 
			
		||||
        }
 | 
			
		||||
        &.addon-calendar-eventtype-user > ion-icon {
 | 
			
		||||
            background-color: var(--addon-calendar-event-user-color);
 | 
			
		||||
        }
 | 
			
		||||
        &.addon-calendar-eventtype-site > ion-icon {
 | 
			
		||||
            background-color: var(--addon-calendar-event-site-color);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										68
									
								
								src/addons/calendar/calendar-lazy.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,68 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Injector, NgModule } from '@angular/core';
 | 
			
		||||
import { RouterModule, ROUTES, Routes } from '@angular/router';
 | 
			
		||||
 | 
			
		||||
import { buildTabMainRoutes } from '@features/mainmenu/mainmenu-tab-routing.module';
 | 
			
		||||
 | 
			
		||||
function buildRoutes(injector: Injector): Routes {
 | 
			
		||||
    return [
 | 
			
		||||
        {
 | 
			
		||||
            path: 'index',
 | 
			
		||||
            loadChildren: () => import('@/addons/calendar/pages/index/index.module').then(m => m.AddonCalendarIndexPageModule),
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            path: 'list',
 | 
			
		||||
            loadChildren: () => import('@/addons/calendar/pages/list/list.module').then(m => m.AddonCalendarListPageModule),
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            path: 'settings',
 | 
			
		||||
            loadChildren: () =>
 | 
			
		||||
                import('@/addons/calendar/pages/settings/settings.module').then(m => m.AddonCalendarSettingsPageModule),
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            path: 'day',
 | 
			
		||||
            loadChildren: () =>
 | 
			
		||||
                import('@/addons/calendar/pages/day/day.module').then(m => m.AddonCalendarDayPageModule),
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            path: 'event',
 | 
			
		||||
            loadChildren: () =>
 | 
			
		||||
                import('@/addons/calendar/pages/event/event.module').then(m => m.AddonCalendarEventPageModule),
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            path: 'edit',
 | 
			
		||||
            loadChildren: () =>
 | 
			
		||||
                import('@/addons/calendar/pages/edit-event/edit-event.module').then(m => m.AddonCalendarEditEventPageModule),
 | 
			
		||||
        },
 | 
			
		||||
        ...buildTabMainRoutes(injector, {
 | 
			
		||||
            redirectTo: 'index',
 | 
			
		||||
            pathMatch: 'full',
 | 
			
		||||
        }),
 | 
			
		||||
    ];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    exports: [RouterModule],
 | 
			
		||||
    providers: [
 | 
			
		||||
        {
 | 
			
		||||
            provide: ROUTES,
 | 
			
		||||
            multi: true,
 | 
			
		||||
            deps: [Injector],
 | 
			
		||||
            useFactory: buildRoutes,
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
})
 | 
			
		||||
export class AddonCalendarLazyModule { }
 | 
			
		||||
							
								
								
									
										67
									
								
								src/addons/calendar/calendar.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,67 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { APP_INITIALIZER, NgModule } from '@angular/core';
 | 
			
		||||
import { Routes } from '@angular/router';
 | 
			
		||||
import { CoreMainMenuRoutingModule } from '@features/mainmenu/mainmenu-routing.module';
 | 
			
		||||
 | 
			
		||||
import { CoreMainMenuDelegate } from '@features/mainmenu/services/mainmenu-delegate';
 | 
			
		||||
import { CoreCronDelegate } from '@services/cron';
 | 
			
		||||
import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate';
 | 
			
		||||
import { AddonCalendarViewLinkHandler } from './services/handlers/view-link';
 | 
			
		||||
import { AddonCalendarMainMenuHandler, AddonCalendarMainMenuHandlerService } from './services/handlers/mainmenu';
 | 
			
		||||
import { AddonCalendarSyncCronHandler } from './services/handlers/sync-cron';
 | 
			
		||||
 | 
			
		||||
import { CORE_SITE_SCHEMAS } from '@services/sites';
 | 
			
		||||
import { CALENDAR_SITE_SCHEMA } from './services/database/calendar';
 | 
			
		||||
import { CALENDAR_OFFLINE_SITE_SCHEMA } from './services/database/calendar-offline';
 | 
			
		||||
import { AddonCalendarComponentsModule } from './components/components.module';
 | 
			
		||||
import { AddonCalendar } from './services/calendar';
 | 
			
		||||
 | 
			
		||||
const mainMenuChildrenRoutes: Routes = [
 | 
			
		||||
    {
 | 
			
		||||
        path: AddonCalendarMainMenuHandlerService.PAGE_NAME,
 | 
			
		||||
        loadChildren: () => import('./calendar-lazy.module').then(m => m.AddonCalendarLazyModule),
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    imports: [
 | 
			
		||||
        CoreMainMenuRoutingModule.forChild({ children: mainMenuChildrenRoutes }),
 | 
			
		||||
        AddonCalendarComponentsModule,
 | 
			
		||||
    ],
 | 
			
		||||
    exports: [CoreMainMenuRoutingModule],
 | 
			
		||||
    providers: [
 | 
			
		||||
        {
 | 
			
		||||
            provide: CORE_SITE_SCHEMAS,
 | 
			
		||||
            useValue: [CALENDAR_SITE_SCHEMA, CALENDAR_OFFLINE_SITE_SCHEMA],
 | 
			
		||||
            multi: true,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            provide: APP_INITIALIZER,
 | 
			
		||||
            multi: true,
 | 
			
		||||
            deps: [],
 | 
			
		||||
            useFactory: () => async () => {
 | 
			
		||||
                CoreContentLinksDelegate.instance.registerHandler(AddonCalendarViewLinkHandler.instance);
 | 
			
		||||
                CoreMainMenuDelegate.instance.registerHandler(AddonCalendarMainMenuHandler.instance);
 | 
			
		||||
                CoreCronDelegate.instance.register(AddonCalendarSyncCronHandler.instance);
 | 
			
		||||
 | 
			
		||||
                await AddonCalendar.instance.initialize();
 | 
			
		||||
 | 
			
		||||
                AddonCalendar.instance.scheduleAllSitesEventsNotifications();
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
})
 | 
			
		||||
export class AddonCalendarModule {}
 | 
			
		||||
@ -0,0 +1,79 @@
 | 
			
		||||
 | 
			
		||||
<!-- Add buttons to the nav bar. -->
 | 
			
		||||
<core-navbar-buttons slot="end" prepend>
 | 
			
		||||
    <core-context-menu>
 | 
			
		||||
        <core-context-menu-item *ngIf="canNavigate && !isCurrentMonth && displayNavButtons" [priority]="900"
 | 
			
		||||
        [content]="'addon.calendar.currentmonth' | translate" iconAction="fas-calendar-day"
 | 
			
		||||
        (action)="goToCurrentMonth()"></core-context-menu-item>
 | 
			
		||||
    </core-context-menu>
 | 
			
		||||
</core-navbar-buttons>
 | 
			
		||||
 | 
			
		||||
<core-loading [hideUntil]="loaded" class="core-loading-center safe-area-page">
 | 
			
		||||
    <!-- Period name and arrows to navigate. -->
 | 
			
		||||
    <ion-grid class="ion-no-padding addon-calendar-navigation">
 | 
			
		||||
        <ion-row class="ion-align-items-center">
 | 
			
		||||
            <ion-col class="ion-text-start" *ngIf="canNavigate">
 | 
			
		||||
                <ion-button fill="clear" (click)="loadPrevious()" [title]="'core.previous' | translate">
 | 
			
		||||
                    <ion-icon name="fas-chevron-left" slot="icon-only"></ion-icon>
 | 
			
		||||
                </ion-button>
 | 
			
		||||
            </ion-col>
 | 
			
		||||
            <ion-col class="ion-text-center addon-calendar-period">
 | 
			
		||||
                <h3>{{ periodName }}</h3>
 | 
			
		||||
            </ion-col>
 | 
			
		||||
            <ion-col class="ion-text-end" *ngIf="canNavigate">
 | 
			
		||||
                <ion-button fill="clear" (click)="loadNext()" [title]="'core.next' | translate">
 | 
			
		||||
                    <ion-icon name="fas-chevron-right" slot="icon-only"></ion-icon>
 | 
			
		||||
                </ion-button>
 | 
			
		||||
            </ion-col>
 | 
			
		||||
        </ion-row>
 | 
			
		||||
    </ion-grid>
 | 
			
		||||
 | 
			
		||||
    <!-- Calendar view. -->
 | 
			
		||||
    <ion-grid class="addon-calendar-months">
 | 
			
		||||
        <!-- List of days. -->
 | 
			
		||||
        <ion-row>
 | 
			
		||||
            <ion-col class="ion-text-center" *ngFor="let day of weekDays" class="addon-calendar-weekday">
 | 
			
		||||
                <span class="ion-hide-md-up" [title]="day.fullname | translate">{{ day.shortname | translate }}</span>
 | 
			
		||||
                <span class="ion-hide-md-down">{{ day.fullname | translate }}</span>
 | 
			
		||||
            </ion-col>
 | 
			
		||||
        </ion-row>
 | 
			
		||||
 | 
			
		||||
        <!-- Weeks. -->
 | 
			
		||||
        <ion-row *ngFor="let week of weeks" class="addon-calendar-week">
 | 
			
		||||
            <!-- Empty slots (first week). -->
 | 
			
		||||
            <ion-col *ngFor="let value of week.prepadding" class="dayblank addon-calendar-day"></ion-col>
 | 
			
		||||
            <ion-col class="ion-text-center" *ngFor="let day of week.days"  (click)="dayClicked(day.mday)"
 | 
			
		||||
            [ngClass]='{"hasevents": day.hasevents, "today": isCurrentMonth && day.istoday,
 | 
			
		||||
                "weekend": day.isweekend, "duration_finish": day.haslastdayofevent}'
 | 
			
		||||
            class="addon-calendar-day" [class.addon-calendar-event-past-day]="isPastMonth || day.ispast">
 | 
			
		||||
                <p class="addon-calendar-day-number"><span>{{ day.mday }}</span></p>
 | 
			
		||||
 | 
			
		||||
                <!-- In phone, display some dots to indicate the type of events. -->
 | 
			
		||||
                <p class="ion-hide-md-up addon-calendar-dot-types"><span *ngFor="let type of day.calendareventtypes"
 | 
			
		||||
                    class="calendar_event_type calendar_event_{{type}}"></span></p>
 | 
			
		||||
 | 
			
		||||
                <!-- In tablet, display list of events. -->
 | 
			
		||||
                <div class="ion-hide-md-down addon-calendar-day-events">
 | 
			
		||||
                    <ng-container *ngFor="let event of day.filteredEvents | slice:0:4; let index = index">
 | 
			
		||||
                        <p *ngIf="index < 3 || day.filteredEvents.length == 4" class="addon-calendar-event"
 | 
			
		||||
                        (click)="eventClicked(event, $event)" [class.addon-calendar-event-past]="event.ispast">
 | 
			
		||||
                            <span class="calendar_event_type calendar_event_{{event.formattedType}}"></span>
 | 
			
		||||
                            <ion-icon *ngIf="event.offline && !event.deleted" name="far-clock"></ion-icon>
 | 
			
		||||
                            <ion-icon *ngIf="event.deleted" name="fas-trash"></ion-icon>
 | 
			
		||||
                            <span class="addon-calendar-event-time">{{ event.timestart * 1000 | coreFormatDate: timeFormat }}</span>
 | 
			
		||||
                            <img *ngIf="event.moduleIcon" src="{{event.moduleIcon}}" alt="" role="presentation"
 | 
			
		||||
                                class="core-module-icon">
 | 
			
		||||
                            <span class="addon-calendar-event-name">{{event.name}}</span>
 | 
			
		||||
                        </p>
 | 
			
		||||
                    </ng-container>
 | 
			
		||||
                    <p *ngIf="day.filteredEvents.length > 4" class="addon-calendar-day-more">
 | 
			
		||||
                        <b>{{ 'core.nummore' | translate:{$a: day.filteredEvents.length - 3} }}</b>
 | 
			
		||||
                    </p>
 | 
			
		||||
                </div>
 | 
			
		||||
            </ion-col>
 | 
			
		||||
            <!-- Empty slots (last week). -->
 | 
			
		||||
            <ion-col *ngFor="let value of week.postpadding" class="dayblank addon-calendar-day"></ion-col>
 | 
			
		||||
        </ion-row>
 | 
			
		||||
    </ion-grid>
 | 
			
		||||
 | 
			
		||||
</core-loading>
 | 
			
		||||
							
								
								
									
										187
									
								
								src/addons/calendar/components/calendar/calendar.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,187 @@
 | 
			
		||||
:host {
 | 
			
		||||
    --addon-calendar-blank-day-background-color: var(--gray-lighter);
 | 
			
		||||
 | 
			
		||||
    .addon-calendar-navigation {
 | 
			
		||||
        padding-top: 5px;
 | 
			
		||||
        padding-left:  10px;
 | 
			
		||||
        padding-right:  10px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .addon-calendar-months {
 | 
			
		||||
        background-color: var(--contrast-background);
 | 
			
		||||
        padding: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .addon-calendar-day {
 | 
			
		||||
        border-bottom: 1px solid var(--addon-calendar-border-color);
 | 
			
		||||
        border-right: 1px solid var(--addon-calendar-border-color);
 | 
			
		||||
        overflow: hidden;
 | 
			
		||||
        min-height: 60px;
 | 
			
		||||
        cursor: pointer;
 | 
			
		||||
 | 
			
		||||
        &:first-child {
 | 
			
		||||
            padding-left: 10px;
 | 
			
		||||
        }
 | 
			
		||||
        &:last-child {
 | 
			
		||||
            border-right: 0;
 | 
			
		||||
            padding-left: 8px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        &.addon-calendar-event-past-day > .addon-calendar-dot-types,
 | 
			
		||||
        &.addon-calendar-event-past-day > .addon-calendar-day-events {
 | 
			
		||||
            opacity: 0.5;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .addon-calendar-day-number {
 | 
			
		||||
            margin: 0;
 | 
			
		||||
 | 
			
		||||
            span {
 | 
			
		||||
                line-height: 24px;
 | 
			
		||||
                font-weight: 500;
 | 
			
		||||
                display: inline-block;
 | 
			
		||||
                margin: 3px;
 | 
			
		||||
                width: max-content;
 | 
			
		||||
                width: 24px;
 | 
			
		||||
                height: 24px;
 | 
			
		||||
                text-align: center;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @media (min-width: 768px) {
 | 
			
		||||
            .addon-calendar-day-number {
 | 
			
		||||
                text-align: start;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        &.today .addon-calendar-day-number span {
 | 
			
		||||
            background-color: var(--addon-calendar-today-bgcolor);
 | 
			
		||||
            color: var(--addon-calendar-today-color);
 | 
			
		||||
 | 
			
		||||
            border-radius: 50%;
 | 
			
		||||
        }
 | 
			
		||||
        &.dayblank {
 | 
			
		||||
            cursor: auto;
 | 
			
		||||
            background-color: var(--addon-calendar-blank-day-background-color);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .addon-calendar-event {
 | 
			
		||||
            margin-top: 0.6em;
 | 
			
		||||
            margin-bottom: 0.6em;
 | 
			
		||||
            overflow: hidden;
 | 
			
		||||
            white-space: nowrap;
 | 
			
		||||
 | 
			
		||||
            &.addon-calendar-event-past {
 | 
			
		||||
                opacity: 0.5;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .addon-calendar-event-name {
 | 
			
		||||
                font-weight: 500;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .addon-calendar-day-more {
 | 
			
		||||
            margin-top: 0.6em;
 | 
			
		||||
            margin-bottom: 0.6em;
 | 
			
		||||
            margin-right: 4px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .addon-calendar-dot-types {
 | 
			
		||||
            margin: 0;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .addon-calendar-period {
 | 
			
		||||
        flex-grow: 3;
 | 
			
		||||
        h3 {
 | 
			
		||||
            margin-top: 10px;
 | 
			
		||||
            font-size: 1.2rem;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .addon-calendar-weekday {
 | 
			
		||||
        border-bottom: 1px solid var(--addon-calendar-border-color);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .addon-calendar-day-events {
 | 
			
		||||
        text-align: left;
 | 
			
		||||
 | 
			
		||||
        ion-icon {
 | 
			
		||||
            margin-right: 2px;
 | 
			
		||||
            font-size: 1em;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .addon-calendar-event, .addon-calendar-day-number, .addon-calendar-day-more {
 | 
			
		||||
        cursor: pointer;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .calendar_event_type {
 | 
			
		||||
        display: inline-block;
 | 
			
		||||
        width: 8px;
 | 
			
		||||
        height: 8px;
 | 
			
		||||
        border-radius: 50%;
 | 
			
		||||
        border: 1px solid white;
 | 
			
		||||
        margin-right: 1px;
 | 
			
		||||
        margin-left: 1px;
 | 
			
		||||
 | 
			
		||||
        &.calendar_event_category {
 | 
			
		||||
            background-color: var(--addon-calendar-event-category-color);
 | 
			
		||||
        }
 | 
			
		||||
        &.calendar_event_course {
 | 
			
		||||
            background-color: var(--addon-calendar-event-course-color);
 | 
			
		||||
        }
 | 
			
		||||
        &.calendar_event_group {
 | 
			
		||||
            background-color: var(--addon-calendar-event-group-color);
 | 
			
		||||
        }
 | 
			
		||||
        &.calendar_event_user {
 | 
			
		||||
            background-color: var(--addon-calendar-event-user-color);
 | 
			
		||||
        }
 | 
			
		||||
        &.calendar_event_site {
 | 
			
		||||
            background-color: var(--addon-calendar-event-site-color);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .core-module-icon {
 | 
			
		||||
        margin-right: 1px;
 | 
			
		||||
        margin-left: 1px;
 | 
			
		||||
        width: 16px;
 | 
			
		||||
        height: 16px;
 | 
			
		||||
        display: inline-block;
 | 
			
		||||
        vertical-align: bottom;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:host-context([dir=rtl]) {
 | 
			
		||||
    .addon-calendar-day-events {
 | 
			
		||||
        text-align: right;
 | 
			
		||||
 | 
			
		||||
        ion-icon {
 | 
			
		||||
            margin-right: unset;
 | 
			
		||||
            margin-left: 2px;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .addon-calendar-day {
 | 
			
		||||
        border-left: 1px solid var(--addon-calendar-border-color);
 | 
			
		||||
        border-right: unset;
 | 
			
		||||
 | 
			
		||||
        &:first-child {
 | 
			
		||||
            padding-right: 10px;
 | 
			
		||||
            padding-left: unset;
 | 
			
		||||
        }
 | 
			
		||||
        &:last-child {
 | 
			
		||||
            border-left: 0;
 | 
			
		||||
            border-right: unset;
 | 
			
		||||
            padding-right: 8px;
 | 
			
		||||
            padding-left: unset;
 | 
			
		||||
        }
 | 
			
		||||
        .addon-calendar-day-more {
 | 
			
		||||
            margin-left: 4px;
 | 
			
		||||
            margin-right: unset;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:host-context(body.dark) {
 | 
			
		||||
    --addon-calendar-blank-day-background-color: var(--black);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										529
									
								
								src/addons/calendar/components/calendar/calendar.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,529 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
    Component,
 | 
			
		||||
    OnDestroy,
 | 
			
		||||
    OnInit,
 | 
			
		||||
    Input,
 | 
			
		||||
    DoCheck,
 | 
			
		||||
    Output,
 | 
			
		||||
    EventEmitter,
 | 
			
		||||
    KeyValueDiffers,
 | 
			
		||||
    KeyValueDiffer,
 | 
			
		||||
} from '@angular/core';
 | 
			
		||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { CoreTimeUtils } from '@services/utils/time';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import {
 | 
			
		||||
    AddonCalendar,
 | 
			
		||||
    AddonCalendarProvider,
 | 
			
		||||
    AddonCalendarWeek,
 | 
			
		||||
    AddonCalendarWeekDaysTranslationKeys,
 | 
			
		||||
    AddonCalendarEventToDisplay,
 | 
			
		||||
    AddonCalendarUpdatedEventEvent,
 | 
			
		||||
    AddonCalendarDayName,
 | 
			
		||||
} from '../../services/calendar';
 | 
			
		||||
import { AddonCalendarFilter, AddonCalendarHelper } from '../../services/calendar-helper';
 | 
			
		||||
import { AddonCalendarOffline } from '../../services/calendar-offline';
 | 
			
		||||
import { CoreCategoryData, CoreCourses } from '@features/courses/services/courses';
 | 
			
		||||
import { CoreApp } from '@services/app';
 | 
			
		||||
import { CoreLocalNotifications } from '@services/local-notifications';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Component that displays a calendar.
 | 
			
		||||
 */
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'addon-calendar-calendar',
 | 
			
		||||
    templateUrl: 'addon-calendar-calendar.html',
 | 
			
		||||
    styleUrls: ['calendar.scss'],
 | 
			
		||||
})
 | 
			
		||||
export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestroy {
 | 
			
		||||
 | 
			
		||||
    @Input() initialYear?: number; // Initial year to load.
 | 
			
		||||
    @Input() initialMonth?: number; // Initial month to load.
 | 
			
		||||
    @Input() filter?: AddonCalendarFilter; // Filter to apply.
 | 
			
		||||
    @Input() canNavigate?: string | boolean; // Whether to include arrows to change the month. Defaults to true.
 | 
			
		||||
    @Input() displayNavButtons?: string | boolean; // Whether to display nav buttons created by this component. Defaults to true.
 | 
			
		||||
    @Output() onEventClicked = new EventEmitter<number>();
 | 
			
		||||
    @Output() onDayClicked = new EventEmitter<{day: number; month: number; year: number}>();
 | 
			
		||||
 | 
			
		||||
    periodName?: string;
 | 
			
		||||
    weekDays: AddonCalendarWeekDaysTranslationKeys[] = [];
 | 
			
		||||
    weeks: AddonCalendarWeek[] = [];
 | 
			
		||||
    loaded = false;
 | 
			
		||||
    timeFormat?: string;
 | 
			
		||||
    isCurrentMonth = false;
 | 
			
		||||
    isPastMonth = false;
 | 
			
		||||
 | 
			
		||||
    protected year?: number;
 | 
			
		||||
    protected month?: number;
 | 
			
		||||
    protected categoriesRetrieved = false;
 | 
			
		||||
    protected categories: { [id: number]: CoreCategoryData } = {};
 | 
			
		||||
    protected currentSiteId: string;
 | 
			
		||||
    protected offlineEvents: { [monthId: string]: { [day: number]: AddonCalendarEventToDisplay[] } } =
 | 
			
		||||
        {}; // Offline events classified in month & day.
 | 
			
		||||
 | 
			
		||||
    protected offlineEditedEventsIds: number[] = []; // IDs of events edited in offline.
 | 
			
		||||
    protected deletedEvents: number[] = []; // Events deleted in offline.
 | 
			
		||||
    protected currentTime?: number;
 | 
			
		||||
    protected differ: KeyValueDiffer<unknown, unknown>; // To detect changes in the data input.
 | 
			
		||||
    // Observers.
 | 
			
		||||
    protected undeleteEventObserver: CoreEventObserver;
 | 
			
		||||
    protected obsDefaultTimeChange?: CoreEventObserver;
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        differs: KeyValueDiffers,
 | 
			
		||||
    ) {
 | 
			
		||||
 | 
			
		||||
        this.currentSiteId = CoreSites.instance.getCurrentSiteId();
 | 
			
		||||
 | 
			
		||||
        if (CoreLocalNotifications.instance.isAvailable()) {
 | 
			
		||||
            // Re-schedule events if default time changes.
 | 
			
		||||
            this.obsDefaultTimeChange = CoreEvents.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, () => {
 | 
			
		||||
                this.weeks.forEach((week) => {
 | 
			
		||||
                    week.days.forEach((day) => {
 | 
			
		||||
                        AddonCalendar.instance.scheduleEventsNotifications(day.eventsFormated!);
 | 
			
		||||
                    });
 | 
			
		||||
                });
 | 
			
		||||
            }, this.currentSiteId);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Listen for events "undeleted" (offline).
 | 
			
		||||
        this.undeleteEventObserver = CoreEvents.on(
 | 
			
		||||
            AddonCalendarProvider.UNDELETED_EVENT_EVENT,
 | 
			
		||||
            (data: AddonCalendarUpdatedEventEvent) => {
 | 
			
		||||
                if (!data || !data.eventId) {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Mark it as undeleted, no need to refresh.
 | 
			
		||||
                this.undeleteEvent(data.eventId);
 | 
			
		||||
 | 
			
		||||
                // Remove it from the list of deleted events if it's there.
 | 
			
		||||
                const index = this.deletedEvents.indexOf(data.eventId);
 | 
			
		||||
                if (index != -1) {
 | 
			
		||||
                    this.deletedEvents.splice(index, 1);
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            this.currentSiteId,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        this.differ = differs.find([]).create();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Component loaded.
 | 
			
		||||
     */
 | 
			
		||||
    ngOnInit(): void {
 | 
			
		||||
        const now = new Date();
 | 
			
		||||
 | 
			
		||||
        this.year = this.initialYear ? this.initialYear : now.getFullYear();
 | 
			
		||||
        this.month = this.initialMonth ? this.initialMonth : now.getMonth() + 1;
 | 
			
		||||
 | 
			
		||||
        this.calculateIsCurrentMonth();
 | 
			
		||||
 | 
			
		||||
        this.fetchData();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Detect and act upon changes that Angular can’t or won’t detect on its own (objects and arrays).
 | 
			
		||||
     */
 | 
			
		||||
    ngDoCheck(): void {
 | 
			
		||||
        this.canNavigate = typeof this.canNavigate == 'undefined' ? true : CoreUtils.instance.isTrueOrOne(this.canNavigate);
 | 
			
		||||
        this.displayNavButtons = typeof this.displayNavButtons == 'undefined' ? true :
 | 
			
		||||
            CoreUtils.instance.isTrueOrOne(this.displayNavButtons);
 | 
			
		||||
 | 
			
		||||
        if (this.weeks) {
 | 
			
		||||
            // Check if there's any change in the filter object.
 | 
			
		||||
            const changes = this.differ.diff(this.filter!);
 | 
			
		||||
            if (changes) {
 | 
			
		||||
                this.filterEvents();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Fetch contacts.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async fetchData(): Promise<void> {
 | 
			
		||||
        const promises: Promise<void>[] = [];
 | 
			
		||||
 | 
			
		||||
        promises.push(this.loadCategories());
 | 
			
		||||
 | 
			
		||||
        // Get offline events.
 | 
			
		||||
        promises.push(AddonCalendarOffline.instance.getAllEditedEvents().then((events) => {
 | 
			
		||||
            // Classify them by month.
 | 
			
		||||
            this.offlineEvents = AddonCalendarHelper.instance.classifyIntoMonths(events);
 | 
			
		||||
 | 
			
		||||
            // Get the IDs of events edited in offline.
 | 
			
		||||
            const filtered = events.filter((event) => event.id! > 0);
 | 
			
		||||
            this.offlineEditedEventsIds = filtered.map((event) => event.id!);
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
        // Get events deleted in offline.
 | 
			
		||||
        promises.push(AddonCalendarOffline.instance.getAllDeletedEventsIds().then((ids) => {
 | 
			
		||||
            this.deletedEvents = ids;
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
        // Get time format to use.
 | 
			
		||||
        promises.push(AddonCalendar.instance.getCalendarTimeFormat().then((value) => {
 | 
			
		||||
            this.timeFormat = value;
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await Promise.all(promises);
 | 
			
		||||
 | 
			
		||||
            await this.fetchEvents();
 | 
			
		||||
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.instance.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.loaded = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Fetch the events for current month.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async fetchEvents(): Promise<void> {
 | 
			
		||||
        // Don't pass courseId and categoryId, we'll filter them locally.
 | 
			
		||||
        let result: { daynames: Partial<AddonCalendarDayName>[]; weeks: Partial<AddonCalendarWeek>[] };
 | 
			
		||||
        try {
 | 
			
		||||
            result = await AddonCalendar.instance.getMonthlyEvents(this.year!, this.month!);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            if (!CoreApp.instance.isOnline()) {
 | 
			
		||||
                // Allow navigating to non-cached months in offline (behave as if using emergency cache).
 | 
			
		||||
                result = await AddonCalendarHelper.instance.getOfflineMonthWeeks(this.year!, this.month!);
 | 
			
		||||
            } else {
 | 
			
		||||
                throw error;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Calculate the period name. We don't use the one in result because it's in server's language.
 | 
			
		||||
        this.periodName = CoreTimeUtils.instance.userDate(
 | 
			
		||||
            new Date(this.year!, this.month! - 1).getTime(),
 | 
			
		||||
            'core.strftimemonthyear',
 | 
			
		||||
        );
 | 
			
		||||
        this.weekDays = AddonCalendar.instance.getWeekDays(result.daynames[0].dayno);
 | 
			
		||||
        this.weeks = result.weeks as AddonCalendarWeek[];
 | 
			
		||||
        this.calculateIsCurrentMonth();
 | 
			
		||||
 | 
			
		||||
        this.weeks.forEach((week) => {
 | 
			
		||||
            week.days.forEach((day) => {
 | 
			
		||||
                day.eventsFormated = day.eventsFormated || [];
 | 
			
		||||
                day.filteredEvents = day.filteredEvents || [];
 | 
			
		||||
                day.events.forEach((event) => {
 | 
			
		||||
                    /// Format online events.
 | 
			
		||||
                    day.eventsFormated!.push(AddonCalendarHelper.instance.formatEventData(event));
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (this.isCurrentMonth) {
 | 
			
		||||
            const currentDay = new Date().getDate();
 | 
			
		||||
            let isPast = true;
 | 
			
		||||
 | 
			
		||||
            this.weeks.forEach((week) => {
 | 
			
		||||
                week.days.forEach((day) => {
 | 
			
		||||
                    day.istoday = day.mday == currentDay;
 | 
			
		||||
                    day.ispast = isPast && !day.istoday;
 | 
			
		||||
                    isPast = day.ispast;
 | 
			
		||||
 | 
			
		||||
                    if (day.istoday) {
 | 
			
		||||
                        day.eventsFormated!.forEach((event) => {
 | 
			
		||||
                            event.ispast = this.isEventPast(event);
 | 
			
		||||
                        });
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        // Merge the online events with offline data.
 | 
			
		||||
        this.mergeEvents();
 | 
			
		||||
        // Filter events by course.
 | 
			
		||||
        this.filterEvents();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Load categories to be able to filter events.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async loadCategories(): Promise<void> {
 | 
			
		||||
        if (this.categoriesRetrieved) {
 | 
			
		||||
            // Already retrieved, stop.
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const cats = await CoreCourses.instance.getCategories(0, true);
 | 
			
		||||
            this.categoriesRetrieved = true;
 | 
			
		||||
            this.categories = {};
 | 
			
		||||
 | 
			
		||||
            // Index categories by ID.
 | 
			
		||||
            cats.forEach((category) => {
 | 
			
		||||
                this.categories[category.id] = category;
 | 
			
		||||
            });
 | 
			
		||||
        } catch {
 | 
			
		||||
            // Ignore errors.
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Filter events based on the filter popover.
 | 
			
		||||
     */
 | 
			
		||||
    filterEvents(): void {
 | 
			
		||||
        this.weeks.forEach((week) => {
 | 
			
		||||
            week.days.forEach((day) => {
 | 
			
		||||
                day.filteredEvents = AddonCalendarHelper.instance.getFilteredEvents(
 | 
			
		||||
                    day.eventsFormated!,
 | 
			
		||||
                    this.filter!,
 | 
			
		||||
                    this.categories,
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                // Re-calculate some properties.
 | 
			
		||||
                AddonCalendarHelper.instance.calculateDayData(day, day.filteredEvents);
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Refresh events.
 | 
			
		||||
     *
 | 
			
		||||
     * @param afterChange Whether the refresh is done after an event has changed or has been synced.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async refreshData(afterChange?: boolean): Promise<void> {
 | 
			
		||||
        const promises: Promise<void>[] = [];
 | 
			
		||||
 | 
			
		||||
        // Don't invalidate monthly events after a change, it has already been handled.
 | 
			
		||||
        if (!afterChange) {
 | 
			
		||||
            promises.push(AddonCalendar.instance.invalidateMonthlyEvents(this.year!, this.month!));
 | 
			
		||||
        }
 | 
			
		||||
        promises.push(CoreCourses.instance.invalidateCategories(0, true));
 | 
			
		||||
        promises.push(AddonCalendar.instance.invalidateTimeFormat());
 | 
			
		||||
 | 
			
		||||
        this.categoriesRetrieved = false; // Get categories again.
 | 
			
		||||
 | 
			
		||||
        await Promise.all(promises);
 | 
			
		||||
 | 
			
		||||
        this.fetchData();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Load next month.
 | 
			
		||||
     */
 | 
			
		||||
    async loadNext(): Promise<void> {
 | 
			
		||||
        this.increaseMonth();
 | 
			
		||||
 | 
			
		||||
        this.loaded = false;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await this.fetchEvents();
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.instance.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
 | 
			
		||||
            this.decreaseMonth();
 | 
			
		||||
        }
 | 
			
		||||
        this.loaded = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Load previous month.
 | 
			
		||||
     */
 | 
			
		||||
    async loadPrevious(): Promise<void> {
 | 
			
		||||
        this.decreaseMonth();
 | 
			
		||||
 | 
			
		||||
        this.loaded = false;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await this.fetchEvents();
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.instance.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
 | 
			
		||||
            this.increaseMonth();
 | 
			
		||||
        }
 | 
			
		||||
        this.loaded = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * An event was clicked.
 | 
			
		||||
     *
 | 
			
		||||
     * @param calendarEvent Calendar event..
 | 
			
		||||
     * @param event Mouse event.
 | 
			
		||||
     */
 | 
			
		||||
    eventClicked(calendarEvent: AddonCalendarEventToDisplay, event: MouseEvent): void {
 | 
			
		||||
        this.onEventClicked.emit(calendarEvent.id);
 | 
			
		||||
        event.stopPropagation();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * A day was clicked.
 | 
			
		||||
     *
 | 
			
		||||
     * @param day Day.
 | 
			
		||||
     */
 | 
			
		||||
    dayClicked(day: number): void {
 | 
			
		||||
        this.onDayClicked.emit({ day: day, month: this.month!, year: this.year! });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if user is viewing the current month.
 | 
			
		||||
     */
 | 
			
		||||
    calculateIsCurrentMonth(): void {
 | 
			
		||||
        const now = new Date();
 | 
			
		||||
 | 
			
		||||
        this.currentTime = CoreTimeUtils.instance.timestamp();
 | 
			
		||||
 | 
			
		||||
        this.isCurrentMonth = this.year == now.getFullYear() && this.month == now.getMonth() + 1;
 | 
			
		||||
        this.isPastMonth = this.year! < now.getFullYear() || (this.year == now.getFullYear() && this.month! < now.getMonth() + 1);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Go to current month.
 | 
			
		||||
     */
 | 
			
		||||
    async goToCurrentMonth(): Promise<void> {
 | 
			
		||||
        const now = new Date();
 | 
			
		||||
        const initialMonth = this.month;
 | 
			
		||||
        const initialYear = this.year;
 | 
			
		||||
 | 
			
		||||
        this.month = now.getMonth() + 1;
 | 
			
		||||
        this.year = now.getFullYear();
 | 
			
		||||
 | 
			
		||||
        this.loaded = false;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await this.fetchEvents();
 | 
			
		||||
            this.isCurrentMonth = true;
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.instance.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
 | 
			
		||||
            this.year = initialYear;
 | 
			
		||||
            this.month = initialMonth;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.loaded = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Decrease the current month.
 | 
			
		||||
     */
 | 
			
		||||
    protected decreaseMonth(): void {
 | 
			
		||||
        if (this.month === 1) {
 | 
			
		||||
            this.month = 12;
 | 
			
		||||
            this.year!--;
 | 
			
		||||
        } else {
 | 
			
		||||
            this.month!--;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Increase the current month.
 | 
			
		||||
     */
 | 
			
		||||
    protected increaseMonth(): void {
 | 
			
		||||
        if (this.month === 12) {
 | 
			
		||||
            this.month = 1;
 | 
			
		||||
            this.year!++;
 | 
			
		||||
        } else {
 | 
			
		||||
            this.month!++;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Merge online events with the offline events of that period.
 | 
			
		||||
     */
 | 
			
		||||
    protected mergeEvents(): void {
 | 
			
		||||
        const monthOfflineEvents: { [day: number]: AddonCalendarEventToDisplay[] } =
 | 
			
		||||
            this.offlineEvents[AddonCalendarHelper.instance.getMonthId(this.year!, this.month!)];
 | 
			
		||||
 | 
			
		||||
        this.weeks.forEach((week) => {
 | 
			
		||||
            week.days.forEach((day) => {
 | 
			
		||||
 | 
			
		||||
                // Schedule notifications for the events retrieved (only future events will be scheduled).
 | 
			
		||||
                AddonCalendar.instance.scheduleEventsNotifications(day.eventsFormated!);
 | 
			
		||||
 | 
			
		||||
                if (monthOfflineEvents || this.deletedEvents.length) {
 | 
			
		||||
                    // There is offline data, merge it.
 | 
			
		||||
 | 
			
		||||
                    if (this.deletedEvents.length) {
 | 
			
		||||
                        // Mark as deleted the events that were deleted in offline.
 | 
			
		||||
                        day.eventsFormated!.forEach((event) => {
 | 
			
		||||
                            event.deleted = this.deletedEvents.indexOf(event.id) != -1;
 | 
			
		||||
                        });
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if (this.offlineEditedEventsIds.length) {
 | 
			
		||||
                        // Remove the online events that were modified in offline.
 | 
			
		||||
                        day.events = day.events.filter((event) => this.offlineEditedEventsIds.indexOf(event.id) == -1);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if (monthOfflineEvents && monthOfflineEvents[day.mday]) {
 | 
			
		||||
                        // Add the offline events (either new or edited).
 | 
			
		||||
                        day.eventsFormated =
 | 
			
		||||
                            AddonCalendarHelper.instance.sortEvents(day.eventsFormated!.concat(monthOfflineEvents[day.mday]));
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Undelete a certain event.
 | 
			
		||||
     *
 | 
			
		||||
     * @param eventId Event ID.
 | 
			
		||||
     */
 | 
			
		||||
    protected undeleteEvent(eventId: number): void {
 | 
			
		||||
        if (!this.weeks) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.weeks.forEach((week) => {
 | 
			
		||||
            week.days.forEach((day) => {
 | 
			
		||||
                const event = day.eventsFormated!.find((event) => event.id == eventId);
 | 
			
		||||
 | 
			
		||||
                if (event) {
 | 
			
		||||
                    event.deleted = false;
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns if the event is in the past or not.
 | 
			
		||||
     *
 | 
			
		||||
     * @param event Event object.
 | 
			
		||||
     * @return True if it's in the past.
 | 
			
		||||
     */
 | 
			
		||||
    protected isEventPast(event: { timestart: number; timeduration: number}): boolean {
 | 
			
		||||
        return (event.timestart + event.timeduration) < this.currentTime!;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Component destroyed.
 | 
			
		||||
     */
 | 
			
		||||
    ngOnDestroy(): void {
 | 
			
		||||
        this.undeleteEventObserver?.off();
 | 
			
		||||
        this.obsDefaultTimeChange?.off();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										55
									
								
								src/addons/calendar/components/components.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,55 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { NgModule } from '@angular/core';
 | 
			
		||||
import { CommonModule } from '@angular/common';
 | 
			
		||||
import { IonicModule } from '@ionic/angular';
 | 
			
		||||
import { FormsModule } from '@angular/forms';
 | 
			
		||||
import { TranslateModule } from '@ngx-translate/core';
 | 
			
		||||
 | 
			
		||||
import { CoreComponentsModule } from '@components/components.module';
 | 
			
		||||
import { CoreDirectivesModule } from '@directives/directives.module';
 | 
			
		||||
import { CorePipesModule } from '@pipes/pipes.module';
 | 
			
		||||
 | 
			
		||||
import { AddonCalendarCalendarComponent } from './calendar/calendar';
 | 
			
		||||
import { AddonCalendarUpcomingEventsComponent } from './upcoming-events/upcoming-events';
 | 
			
		||||
import { AddonCalendarFilterPopoverComponent } from './filter/filter';
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    declarations: [
 | 
			
		||||
        AddonCalendarCalendarComponent,
 | 
			
		||||
        AddonCalendarUpcomingEventsComponent,
 | 
			
		||||
        AddonCalendarFilterPopoverComponent,
 | 
			
		||||
    ],
 | 
			
		||||
    imports: [
 | 
			
		||||
        CommonModule,
 | 
			
		||||
        IonicModule,
 | 
			
		||||
        FormsModule,
 | 
			
		||||
        TranslateModule.forChild(),
 | 
			
		||||
        CoreComponentsModule,
 | 
			
		||||
        CoreDirectivesModule,
 | 
			
		||||
        CorePipesModule,
 | 
			
		||||
    ],
 | 
			
		||||
    providers: [
 | 
			
		||||
    ],
 | 
			
		||||
    exports: [
 | 
			
		||||
        AddonCalendarCalendarComponent,
 | 
			
		||||
        AddonCalendarUpcomingEventsComponent,
 | 
			
		||||
        AddonCalendarFilterPopoverComponent,
 | 
			
		||||
    ],
 | 
			
		||||
    entryComponents: [
 | 
			
		||||
        AddonCalendarFilterPopoverComponent,
 | 
			
		||||
    ],
 | 
			
		||||
})
 | 
			
		||||
export class AddonCalendarComponentsModule {}
 | 
			
		||||
@ -0,0 +1,20 @@
 | 
			
		||||
<ion-list>
 | 
			
		||||
    <ion-radio-group>
 | 
			
		||||
        <ion-item *ngFor="let type of types" class="addon-calendar-event" [ngClass]="['addon-calendar-eventtype-'+type]">
 | 
			
		||||
            <ion-icon [name]="typeIcons[type]" slot="start"></ion-icon>
 | 
			
		||||
            <ion-label>{{ 'addon.calendar.' + type + 'events' | translate}}</ion-label>
 | 
			
		||||
            <ion-toggle [(ngModel)]="filter[type]" (ionChange)="onChange()" slot="end"></ion-toggle>
 | 
			
		||||
        </ion-item>
 | 
			
		||||
        <ion-item-divider *ngIf="filter.course || filter.category || filter.group">
 | 
			
		||||
            <ion-label></ion-label>
 | 
			
		||||
        </ion-item-divider>
 | 
			
		||||
        <ion-list *ngIf="filter.course || filter.category || filter.group">
 | 
			
		||||
            <ion-radio-group [(ngModel)]="courseId" (ionChange)="onChange()">
 | 
			
		||||
                <ion-item class="ion-text-wrap" *ngFor="let course of courses">
 | 
			
		||||
                    <ion-label><core-format-text [text]="course.fullname"></core-format-text></ion-label>
 | 
			
		||||
                    <ion-radio slot="start" value="{{course.id}}"></ion-radio>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
            </ion-radio-group>
 | 
			
		||||
        </ion-list>
 | 
			
		||||
    </ion-radio-group>
 | 
			
		||||
</ion-list>
 | 
			
		||||
							
								
								
									
										21
									
								
								src/addons/calendar/components/filter/filter-popover.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,21 @@
 | 
			
		||||
:host {
 | 
			
		||||
    ion-item {
 | 
			
		||||
        ion-icon, ion-radio {
 | 
			
		||||
            margin-right: 8px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        > ion-icon {
 | 
			
		||||
            padding: 4px;
 | 
			
		||||
            font-size: 20px;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:host-context([dir=rtl]) {
 | 
			
		||||
    ion-item {
 | 
			
		||||
        ion-icon, ion-radio {
 | 
			
		||||
            margin-left: 8px;
 | 
			
		||||
            margin-right: unset;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										86
									
								
								src/addons/calendar/components/filter/filter.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,86 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Component, Input, OnInit } from '@angular/core';
 | 
			
		||||
import { CoreEnrolledCourseData } from '@features/courses/services/courses';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { CoreEvents } from '@singletons/events';
 | 
			
		||||
import { AddonCalendarEventType, AddonCalendarProvider } from '../../services/calendar';
 | 
			
		||||
import { AddonCalendarFilter, AddonCalendarEventIcons } from '../../services/calendar-helper';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Component to display the events filter that includes events types and a list of courses.
 | 
			
		||||
 */
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'addon-calendar-filter-popover',
 | 
			
		||||
    templateUrl: 'addon-calendar-filter-popover.html',
 | 
			
		||||
    styleUrls: ['../../calendar-common.scss', 'filter-popover.scss'],
 | 
			
		||||
})
 | 
			
		||||
export class AddonCalendarFilterPopoverComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @Input() filter: AddonCalendarFilter = {
 | 
			
		||||
        filtered: false,
 | 
			
		||||
        courseId: -1,
 | 
			
		||||
        categoryId: undefined,
 | 
			
		||||
        course: true,
 | 
			
		||||
        group: true,
 | 
			
		||||
        site: true,
 | 
			
		||||
        user: true,
 | 
			
		||||
        category: true,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    courseId = '-1';
 | 
			
		||||
 | 
			
		||||
    @Input() courses: Partial<CoreEnrolledCourseData>[] = [];
 | 
			
		||||
    typeIcons: AddonCalendarEventIcons[] = [];
 | 
			
		||||
    types: string[] = [];
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        CoreUtils.instance.enumKeys(AddonCalendarEventType).forEach((name) => {
 | 
			
		||||
            const value = AddonCalendarEventType[name];
 | 
			
		||||
            this.typeIcons[value] = AddonCalendarEventIcons[name];
 | 
			
		||||
            this.types.push(value);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Init the component.
 | 
			
		||||
     */
 | 
			
		||||
    ngOnInit(): void {
 | 
			
		||||
        this.courseId = this.filter.courseId + '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Function called when an item is clicked.
 | 
			
		||||
     */
 | 
			
		||||
    onChange(): void {
 | 
			
		||||
        const courseId = parseInt(this.courseId, 10);
 | 
			
		||||
        if (courseId > 0) {
 | 
			
		||||
            const course = this.courses.find((course) => courseId == course.id);
 | 
			
		||||
            this.filter.courseId = course?.id || -1;
 | 
			
		||||
            this.filter.categoryId = course?.categoryid;
 | 
			
		||||
        } else {
 | 
			
		||||
            this.filter.courseId = -1;
 | 
			
		||||
            this.filter.categoryId = undefined;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.filter.filtered = this.filter.courseId > 0 || this.types.some((name) => !this.filter[name]);
 | 
			
		||||
 | 
			
		||||
        CoreEvents.trigger<AddonCalendarFilter>(AddonCalendarProvider.FILTER_CHANGED_EVENT, this.filter);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,29 @@
 | 
			
		||||
<core-loading [hideUntil]="loaded" class="core-loading-center">
 | 
			
		||||
    <core-empty-box *ngIf="!filteredEvents || !filteredEvents.length" icon="fas-calendar" [message]="'addon.calendar.noevents' | translate">
 | 
			
		||||
    </core-empty-box>
 | 
			
		||||
 | 
			
		||||
    <ion-list *ngIf="filteredEvents && filteredEvents.length"  class="ion-no-margin">
 | 
			
		||||
        <ng-container *ngFor="let event of filteredEvents">
 | 
			
		||||
            <ion-item class="ion-text-wrap" [title]="event.name" (click)="eventClicked(event)" class="addon-calendar-event"
 | 
			
		||||
                [ngClass]="['addon-calendar-eventtype-'+event.eventtype]">
 | 
			
		||||
                <img *ngIf="event.moduleIcon" src="{{event.moduleIcon}}" slot="start" class="core-module-icon">
 | 
			
		||||
                <ion-icon *ngIf="event.eventIcon && !event.moduleIcon" [name]="event.eventIcon" slot="start">
 | 
			
		||||
                </ion-icon>
 | 
			
		||||
                <ion-label>
 | 
			
		||||
                    <h2><core-format-text [text]="event.name" [contextLevel]="event.contextLevel"
 | 
			
		||||
                        [contextInstanceId]="event.contextInstanceId"></core-format-text></h2>
 | 
			
		||||
                    <p [innerHTML]="event.formattedtime"></p>
 | 
			
		||||
                </ion-label>
 | 
			
		||||
                <ion-note *ngIf="event.offline && !event.deleted" slot="end">
 | 
			
		||||
                    <ion-icon name="far-clock"></ion-icon>
 | 
			
		||||
                    <span class="ion-text-wrap">{{ 'core.notsent' | translate }}</span>
 | 
			
		||||
                </ion-note>
 | 
			
		||||
                <ion-note *ngIf="event.deleted" slot="end">
 | 
			
		||||
                    <ion-icon name="fas-trash"></ion-icon>
 | 
			
		||||
                    <span class="ion-text-wrap">{{ 'core.deletedoffline' | translate }}</span>
 | 
			
		||||
                </ion-note>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
        </ng-container>
 | 
			
		||||
    </ion-list>
 | 
			
		||||
 | 
			
		||||
</core-loading>
 | 
			
		||||
@ -0,0 +1,5 @@
 | 
			
		||||
:host {
 | 
			
		||||
    .addon-calendar-event {
 | 
			
		||||
        cursor: pointer;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,324 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Component, OnDestroy, OnInit, Input, DoCheck, Output, EventEmitter, KeyValueDiffers, KeyValueDiffer } from '@angular/core';
 | 
			
		||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import {
 | 
			
		||||
    AddonCalendarProvider,
 | 
			
		||||
    AddonCalendarEventToDisplay,
 | 
			
		||||
    AddonCalendar,
 | 
			
		||||
    AddonCalendarUpdatedEventEvent,
 | 
			
		||||
} from '../../services/calendar';
 | 
			
		||||
import { AddonCalendarHelper, AddonCalendarFilter } from '../../services/calendar-helper';
 | 
			
		||||
import { AddonCalendarOffline } from '../../services/calendar-offline';
 | 
			
		||||
import { CoreCategoryData, CoreCourses } from '@features/courses/services/courses';
 | 
			
		||||
import { CoreConstants } from '@/core/constants';
 | 
			
		||||
import { CoreLocalNotifications } from '@services/local-notifications';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Component that displays upcoming events.
 | 
			
		||||
 */
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'addon-calendar-upcoming-events',
 | 
			
		||||
    templateUrl: 'addon-calendar-upcoming-events.html',
 | 
			
		||||
    styleUrls: ['../../calendar-common.scss', 'upcoming-events.scss'],
 | 
			
		||||
})
 | 
			
		||||
export class AddonCalendarUpcomingEventsComponent implements OnInit, DoCheck, OnDestroy {
 | 
			
		||||
 | 
			
		||||
    @Input() filter?: AddonCalendarFilter; // Filter to apply.
 | 
			
		||||
    @Output() onEventClicked = new EventEmitter<number>();
 | 
			
		||||
 | 
			
		||||
    filteredEvents: AddonCalendarEventToDisplay[] = [];
 | 
			
		||||
    loaded = false;
 | 
			
		||||
 | 
			
		||||
    protected year?: number;
 | 
			
		||||
    protected month?: number;
 | 
			
		||||
    protected categoriesRetrieved = false;
 | 
			
		||||
    protected categories: { [id: number]: CoreCategoryData } = {};
 | 
			
		||||
    protected currentSiteId: string;
 | 
			
		||||
    protected events: AddonCalendarEventToDisplay[] = []; // Events (both online and offline).
 | 
			
		||||
    protected onlineEvents: AddonCalendarEventToDisplay[] = [];
 | 
			
		||||
    protected offlineEvents: AddonCalendarEventToDisplay[] = []; // Offline events.
 | 
			
		||||
    protected deletedEvents: number[] = []; // Events deleted in offline.
 | 
			
		||||
    protected lookAhead = 0;
 | 
			
		||||
    protected timeFormat?: string;
 | 
			
		||||
    protected differ: KeyValueDiffer<unknown, unknown>; // To detect changes in the data input.
 | 
			
		||||
 | 
			
		||||
    // Observers.
 | 
			
		||||
    protected undeleteEventObserver: CoreEventObserver;
 | 
			
		||||
    protected obsDefaultTimeChange?: CoreEventObserver;
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        differs: KeyValueDiffers,
 | 
			
		||||
    ) {
 | 
			
		||||
        this.currentSiteId = CoreSites.instance.getCurrentSiteId();
 | 
			
		||||
 | 
			
		||||
        if (CoreLocalNotifications.instance.isAvailable()) {            // Re-schedule events if default time changes.
 | 
			
		||||
            this.obsDefaultTimeChange = CoreEvents.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, () => {
 | 
			
		||||
                AddonCalendar.instance.scheduleEventsNotifications(this.onlineEvents);
 | 
			
		||||
            }, this.currentSiteId);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Listen for events "undeleted" (offline).
 | 
			
		||||
        this.undeleteEventObserver = CoreEvents.on(
 | 
			
		||||
            AddonCalendarProvider.UNDELETED_EVENT_EVENT,
 | 
			
		||||
            (data: AddonCalendarUpdatedEventEvent) => {
 | 
			
		||||
                if (!data || !data.eventId) {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Mark it as undeleted, no need to refresh.
 | 
			
		||||
                this.undeleteEvent(data.eventId);
 | 
			
		||||
 | 
			
		||||
                // Remove it from the list of deleted events if it's there.
 | 
			
		||||
                const index = this.deletedEvents.indexOf(data.eventId);
 | 
			
		||||
                if (index != -1) {
 | 
			
		||||
                    this.deletedEvents.splice(index, 1);
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            this.currentSiteId,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        this.differ = differs.find([]).create();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Component loaded.
 | 
			
		||||
     */
 | 
			
		||||
    ngOnInit(): void {
 | 
			
		||||
        this.fetchData();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Detect and act upon changes that Angular can’t or won’t detect on its own (objects and arrays).
 | 
			
		||||
     */
 | 
			
		||||
    ngDoCheck(): void {
 | 
			
		||||
        // Check if there's any change in the filter object.
 | 
			
		||||
        const changes = this.differ.diff(this.filter!);
 | 
			
		||||
        if (changes) {
 | 
			
		||||
            this.filterEvents();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Fetch data.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async fetchData(): Promise<void> {
 | 
			
		||||
        const promises: Promise<void>[] = [];
 | 
			
		||||
 | 
			
		||||
        promises.push(this.loadCategories());
 | 
			
		||||
 | 
			
		||||
        // Get offline events.
 | 
			
		||||
        promises.push(AddonCalendarOffline.instance.getAllEditedEvents().then((offlineEvents) => {
 | 
			
		||||
            // Format data.
 | 
			
		||||
            const events: AddonCalendarEventToDisplay[] = offlineEvents.map((event) =>
 | 
			
		||||
                AddonCalendarHelper.instance.formatOfflineEventData(event));
 | 
			
		||||
 | 
			
		||||
            this.offlineEvents = AddonCalendarHelper.instance.sortEvents(events);
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
        // Get events deleted in offline.
 | 
			
		||||
        promises.push(AddonCalendarOffline.instance.getAllDeletedEventsIds().then((ids) => {
 | 
			
		||||
            this.deletedEvents = ids;
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
        // Get user preferences.
 | 
			
		||||
        promises.push(AddonCalendar.instance.getCalendarLookAhead().then((value) => {
 | 
			
		||||
            this.lookAhead = value;
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
        promises.push(AddonCalendar.instance.getCalendarTimeFormat().then((value) => {
 | 
			
		||||
            this.timeFormat = value;
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await Promise.all(promises);
 | 
			
		||||
 | 
			
		||||
            this.fetchEvents();
 | 
			
		||||
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.instance.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.loaded = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Fetch upcoming events.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async fetchEvents(): Promise<void> {
 | 
			
		||||
        // Don't pass courseId and categoryId, we'll filter them locally.
 | 
			
		||||
        const result = await AddonCalendar.instance.getUpcomingEvents();
 | 
			
		||||
        this.onlineEvents = result.events.map((event) => AddonCalendarHelper.instance.formatEventData(event));
 | 
			
		||||
        // Schedule notifications for the events retrieved.
 | 
			
		||||
        AddonCalendar.instance.scheduleEventsNotifications(this.onlineEvents);
 | 
			
		||||
        // Merge the online events with offline data.
 | 
			
		||||
        this.events = this.mergeEvents();
 | 
			
		||||
        // Filter events by course.
 | 
			
		||||
        this.filterEvents();
 | 
			
		||||
 | 
			
		||||
        // Re-calculate the formatted time so it uses the device date.
 | 
			
		||||
        const promises = this.events.map((event) =>
 | 
			
		||||
            AddonCalendar.instance.formatEventTime(event, this.timeFormat!).then((time) => {
 | 
			
		||||
                event.formattedtime = time;
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }));
 | 
			
		||||
 | 
			
		||||
        await Promise.all(promises);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Load categories to be able to filter events.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async loadCategories(): Promise<void> {
 | 
			
		||||
        if (this.categoriesRetrieved) {
 | 
			
		||||
            // Already retrieved, stop.
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const cats = await CoreCourses.instance.getCategories(0, true);
 | 
			
		||||
            this.categoriesRetrieved = true;
 | 
			
		||||
            this.categories = {};
 | 
			
		||||
 | 
			
		||||
            // Index categories by ID.
 | 
			
		||||
            cats.forEach((category) => {
 | 
			
		||||
                this.categories[category.id] = category;
 | 
			
		||||
            });
 | 
			
		||||
        } catch {
 | 
			
		||||
            // Ignore errors.
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Filter events based on the filter popover.
 | 
			
		||||
     */
 | 
			
		||||
    protected filterEvents(): void {
 | 
			
		||||
        this.filteredEvents = AddonCalendarHelper.instance.getFilteredEvents(this.events, this.filter!, this.categories);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Refresh events.
 | 
			
		||||
     *
 | 
			
		||||
     * @param afterChange Whether the refresh is done after an event has changed or has been synced.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async refreshData(afterChange?: boolean): Promise<void> {
 | 
			
		||||
        const promises: Promise<void>[] = [];
 | 
			
		||||
 | 
			
		||||
        // Don't invalidate upcoming events after a change, it has already been handled.
 | 
			
		||||
        if (!afterChange) {
 | 
			
		||||
            promises.push(AddonCalendar.instance.invalidateAllUpcomingEvents());
 | 
			
		||||
        }
 | 
			
		||||
        promises.push(CoreCourses.instance.invalidateCategories(0, true));
 | 
			
		||||
        promises.push(AddonCalendar.instance.invalidateLookAhead());
 | 
			
		||||
        promises.push(AddonCalendar.instance.invalidateTimeFormat());
 | 
			
		||||
 | 
			
		||||
        this.categoriesRetrieved = false; // Get categories again.
 | 
			
		||||
 | 
			
		||||
        await Promise.all(promises);
 | 
			
		||||
 | 
			
		||||
        await this.fetchData();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * An event was clicked.
 | 
			
		||||
     *
 | 
			
		||||
     * @param event Event.
 | 
			
		||||
     */
 | 
			
		||||
    eventClicked(event: AddonCalendarEventToDisplay): void {
 | 
			
		||||
        this.onEventClicked.emit(event.id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Merge online events with the offline events of that period.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Merged events.
 | 
			
		||||
     */
 | 
			
		||||
    protected mergeEvents(): AddonCalendarEventToDisplay[] {
 | 
			
		||||
        if (!this.offlineEvents.length && !this.deletedEvents.length) {
 | 
			
		||||
            // No offline events, nothing to merge.
 | 
			
		||||
            return this.onlineEvents;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const start = Date.now() / 1000;
 | 
			
		||||
        const end = start + (CoreConstants.SECONDS_DAY * this.lookAhead);
 | 
			
		||||
        let result: AddonCalendarEventToDisplay[] = this.onlineEvents;
 | 
			
		||||
 | 
			
		||||
        if (this.deletedEvents.length) {
 | 
			
		||||
            // Mark as deleted the events that were deleted in offline.
 | 
			
		||||
            result.forEach((event) => {
 | 
			
		||||
                event.deleted = this.deletedEvents.indexOf(event.id) != -1;
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.offlineEvents.length) {
 | 
			
		||||
            // Remove the online events that were modified in offline.
 | 
			
		||||
            result = result.filter((event) => {
 | 
			
		||||
                const offlineEvent = this.offlineEvents.find((ev) => ev.id == event.id);
 | 
			
		||||
 | 
			
		||||
                return !offlineEvent;
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Now get the offline events that belong to this period.
 | 
			
		||||
        const periodOfflineEvents =
 | 
			
		||||
            this.offlineEvents.filter((event) =>
 | 
			
		||||
                (event.timestart >= start || event.timestart + event.timeduration >= start) && event.timestart <= end);
 | 
			
		||||
 | 
			
		||||
        // Merge both arrays and sort them.
 | 
			
		||||
        result = result.concat(periodOfflineEvents);
 | 
			
		||||
 | 
			
		||||
        return AddonCalendarHelper.instance.sortEvents(result);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Undelete a certain event.
 | 
			
		||||
     *
 | 
			
		||||
     * @param eventId Event ID.
 | 
			
		||||
     */
 | 
			
		||||
    protected undeleteEvent(eventId: number): void {
 | 
			
		||||
        const event = this.onlineEvents.find((event) => event.id == eventId);
 | 
			
		||||
 | 
			
		||||
        if (event) {
 | 
			
		||||
            event.deleted = false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Component destroyed.
 | 
			
		||||
     */
 | 
			
		||||
    ngOnDestroy(): void {
 | 
			
		||||
        this.undeleteEventObserver?.off();
 | 
			
		||||
        this.obsDefaultTimeChange?.off();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										76
									
								
								src/addons/calendar/lang.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,76 @@
 | 
			
		||||
{
 | 
			
		||||
    "allday": "All day",
 | 
			
		||||
    "calendar": "Calendar",
 | 
			
		||||
    "calendarevent": "Calendar event",
 | 
			
		||||
    "calendarevents": "Calendar events",
 | 
			
		||||
    "calendarreminders": "Calendar reminders",
 | 
			
		||||
    "categoryevents": "Category events",
 | 
			
		||||
    "confirmeventdelete": "Are you sure you want to delete the \"{{$a}}\" event?",
 | 
			
		||||
    "confirmeventseriesdelete": "The \"{{$a.name}}\" event is part of a series. Do you want to delete just this event, or all {{$a.count}} events in the series?",
 | 
			
		||||
    "courseevents": "Course events",
 | 
			
		||||
    "currentmonth": "Current Month",
 | 
			
		||||
    "daynext": "Next day",
 | 
			
		||||
    "dayprev": "Previous day",
 | 
			
		||||
    "defaultnotificationtime": "Default notification time",
 | 
			
		||||
    "deleteallevents": "Delete all events",
 | 
			
		||||
    "deleteevent": "Delete event",
 | 
			
		||||
    "deleteoneevent": "Delete this event",
 | 
			
		||||
    "durationminutes": "Duration in minutes",
 | 
			
		||||
    "durationnone": "Without duration",
 | 
			
		||||
    "durationuntil": "Until",
 | 
			
		||||
    "editevent": "Editing event",
 | 
			
		||||
    "errorloadevent": "Error loading event.",
 | 
			
		||||
    "errorloadevents": "Error loading events.",
 | 
			
		||||
    "eventcalendareventdeleted": "Calendar event deleted",
 | 
			
		||||
    "eventduration": "Duration",
 | 
			
		||||
    "eventendtime": "End time",
 | 
			
		||||
    "eventkind": "Type of event",
 | 
			
		||||
    "eventname": "Event title",
 | 
			
		||||
    "eventstarttime": "Start time",
 | 
			
		||||
    "eventtype": "Event type",
 | 
			
		||||
    "fri": "Fri",
 | 
			
		||||
    "friday": "Friday",
 | 
			
		||||
    "gotoactivity": "Go to activity",
 | 
			
		||||
    "groupevents": "Group events",
 | 
			
		||||
    "invalidtimedurationminutes": "The duration in minutes you have entered is invalid. Please enter the duration in minutes greater than 0 or select no duration.",
 | 
			
		||||
    "invalidtimedurationuntil": "The date and time you selected for duration until is before the start time of the event. Please correct this before proceeding.",
 | 
			
		||||
    "mon": "Mon",
 | 
			
		||||
    "monday": "Monday",
 | 
			
		||||
    "monthlyview": "Monthly view",
 | 
			
		||||
    "newevent": "New event",
 | 
			
		||||
    "noevents": "There are no events",
 | 
			
		||||
    "nopermissiontoupdatecalendar": "Sorry, but you do not have permission to update the calendar event.",
 | 
			
		||||
    "reminders": "Reminders",
 | 
			
		||||
    "repeatedevents": "Repeated events",
 | 
			
		||||
    "repeateditall": "Also apply changes to the other {{$a}} events in this repeat series",
 | 
			
		||||
    "repeateditthis": "Apply changes to this event only",
 | 
			
		||||
    "repeatevent": "Repeat this event",
 | 
			
		||||
    "repeatweeksl": "Repeat weekly, creating altogether",
 | 
			
		||||
    "sat": "Sat",
 | 
			
		||||
    "saturday": "Saturday",
 | 
			
		||||
    "setnewreminder": "Set a new reminder",
 | 
			
		||||
    "siteevents": "Site events",
 | 
			
		||||
    "sun": "Sun",
 | 
			
		||||
    "sunday": "Sunday",
 | 
			
		||||
    "thu": "Thu",
 | 
			
		||||
    "thursday": "Thursday",
 | 
			
		||||
    "today": "Today",
 | 
			
		||||
    "tomorrow": "Tomorrow",
 | 
			
		||||
    "tue": "Tue",
 | 
			
		||||
    "tuesday": "Tuesday",
 | 
			
		||||
    "typecategory": "Category event",
 | 
			
		||||
    "typeclose": "Close event",
 | 
			
		||||
    "typecourse": "Course event",
 | 
			
		||||
    "typedue": "Due event",
 | 
			
		||||
    "typegradingdue": "Grading due event",
 | 
			
		||||
    "typegroup": "Group event",
 | 
			
		||||
    "typeopen": "Open event",
 | 
			
		||||
    "typesite": "Site event",
 | 
			
		||||
    "typeuser": "User event",
 | 
			
		||||
    "upcomingevents": "Upcoming events",
 | 
			
		||||
    "userevents": "User events",
 | 
			
		||||
    "wed": "Wed",
 | 
			
		||||
    "wednesday": "Wednesday",
 | 
			
		||||
    "when": "When",
 | 
			
		||||
    "yesterday": "Yesterday"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										92
									
								
								src/addons/calendar/pages/day/day.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,92 @@
 | 
			
		||||
<ion-header>
 | 
			
		||||
    <ion-toolbar>
 | 
			
		||||
        <ion-buttons slot="start">
 | 
			
		||||
            <ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
        <ion-title>{{ 'addon.calendar.calendarevents' | translate }}</ion-title>
 | 
			
		||||
        <ion-buttons slot="end">
 | 
			
		||||
            <ion-button (click)="openFilter($event)" [attr.aria-label]="'core.filter' | translate">
 | 
			
		||||
                <ion-icon slot="icon-only" name="fas-filter"></ion-icon>
 | 
			
		||||
            </ion-button>
 | 
			
		||||
            <core-context-menu>
 | 
			
		||||
                <core-context-menu-item *ngIf="!isCurrentDay" [priority]="900" [content]="'addon.calendar.today' | translate"
 | 
			
		||||
                    iconAction="fas-calendar-day" (action)="goToCurrentDay()">
 | 
			
		||||
                </core-context-menu-item>
 | 
			
		||||
                <core-context-menu-item [hidden]="!loaded || !hasOffline || !isOnline"  [priority]="400"
 | 
			
		||||
                    [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(undefined, $event)"
 | 
			
		||||
                    [iconAction]="syncIcon" [closeOnClick]="false">
 | 
			
		||||
                </core-context-menu-item>
 | 
			
		||||
            </core-context-menu>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
    </ion-toolbar>
 | 
			
		||||
</ion-header>
 | 
			
		||||
<ion-content>
 | 
			
		||||
    <ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event)">
 | 
			
		||||
        <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
 | 
			
		||||
    </ion-refresher>
 | 
			
		||||
 | 
			
		||||
    <!-- Period name and arrows to navigate. -->
 | 
			
		||||
    <ion-grid class="ion-no-padding safe-area-page">
 | 
			
		||||
        <ion-row class="ion-align-items-center">
 | 
			
		||||
            <ion-col class="ion-text-start" *ngIf="currentMoment">
 | 
			
		||||
                <ion-button fill="clear" (click)="loadPrevious()" [title]="'addon.calendar.dayprev' | translate">
 | 
			
		||||
                    <ion-icon name="fas-chevron-left" slot="icon-only"></ion-icon>
 | 
			
		||||
                </ion-button>
 | 
			
		||||
            </ion-col>
 | 
			
		||||
            <ion-col class="ion-text-center addon-calendar-period">
 | 
			
		||||
                <h3>{{ periodName }}</h3>
 | 
			
		||||
            </ion-col>
 | 
			
		||||
            <ion-col class="ion-text-end" *ngIf="currentMoment">
 | 
			
		||||
                <ion-button fill="clear" (click)="loadNext()" [title]="'addon.calendar.daynext' | translate">
 | 
			
		||||
                    <ion-icon name="fas-chevron-right" slot="icon-only"></ion-icon>
 | 
			
		||||
                </ion-button>
 | 
			
		||||
            </ion-col>
 | 
			
		||||
        </ion-row>
 | 
			
		||||
    </ion-grid>
 | 
			
		||||
 | 
			
		||||
    <core-loading [hideUntil]="loaded" class="safe-area-page">
 | 
			
		||||
        <!-- There is data to be synchronized -->          <!-- There is data to be synchronized -->
 | 
			
		||||
        <ion-card class="core-warning-card" *ngIf="hasOffline">
 | 
			
		||||
            <ion-item>
 | 
			
		||||
                <ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon>
 | 
			
		||||
                <ion-label>{{ 'core.hasdatatosync' | translate:{$a: 'core.day' | translate} }}</ion-label>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
        </ion-card>
 | 
			
		||||
 | 
			
		||||
        <core-empty-box *ngIf="!filteredEvents || !filteredEvents.length" icon="fas-calendar" inline="true"
 | 
			
		||||
            [message]="'addon.calendar.noevents' | translate">
 | 
			
		||||
        </core-empty-box>
 | 
			
		||||
 | 
			
		||||
        <ion-list *ngIf="filteredEvents && filteredEvents.length"  class="ion-no-margin">
 | 
			
		||||
            <ng-container *ngFor="let event of filteredEvents">
 | 
			
		||||
                <ion-item class="ion-text-wrap" [title]="event.name" (click)="gotoEvent(event.id)"
 | 
			
		||||
                [class.item-dimmed]="event.ispast" class="addon-calendar-event"
 | 
			
		||||
                [ngClass]="['addon-calendar-eventtype-'+event.eventtype]">
 | 
			
		||||
                    <img *ngIf="event.moduleIcon" src="{{event.moduleIcon}}" slot="start" class="core-module-icon">
 | 
			
		||||
                    <ion-icon *ngIf="event.eventIcon && !event.moduleIcon" [name]="event.eventIcon" slot="start">
 | 
			
		||||
                    </ion-icon>
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <h2><core-format-text [text]="event.name" [contextLevel]="event.contextLevel"
 | 
			
		||||
                            [contextInstanceId]="event.contextInstanceId"></core-format-text></h2>
 | 
			
		||||
                        <p [innerHTML]="event.formattedtime"></p>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                    <ion-note *ngIf="event.offline && !event.deleted" slot="end">
 | 
			
		||||
                        <ion-icon name="far-clock"></ion-icon>
 | 
			
		||||
                        <span class="ion-text-wrap">{{ 'core.notsent' | translate }}</span>
 | 
			
		||||
                    </ion-note>
 | 
			
		||||
                    <ion-note *ngIf="event.deleted" slot="end">
 | 
			
		||||
                        <ion-icon name="fas-trash"></ion-icon>
 | 
			
		||||
                        <span class="ion-text-wrap">{{ 'core.deletedoffline' | translate }}</span>
 | 
			
		||||
                    </ion-note>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
            </ng-container>
 | 
			
		||||
        </ion-list>
 | 
			
		||||
    </core-loading>
 | 
			
		||||
 | 
			
		||||
    <!-- Create a calendar event. -->
 | 
			
		||||
    <ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="canCreate && loaded">
 | 
			
		||||
        <ion-fab-button (click)="openEdit()" [attr.aria-label]="'addon.calendar.newevent' | translate">
 | 
			
		||||
            <ion-icon name="fas-plus"></ion-icon>
 | 
			
		||||
        </ion-fab-button>
 | 
			
		||||
    </ion-fab>
 | 
			
		||||
</ion-content>
 | 
			
		||||
							
								
								
									
										51
									
								
								src/addons/calendar/pages/day/day.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,51 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { NgModule } from '@angular/core';
 | 
			
		||||
import { IonicModule } from '@ionic/angular';
 | 
			
		||||
import { TranslateModule } from '@ngx-translate/core';
 | 
			
		||||
import { RouterModule, Routes } from '@angular/router';
 | 
			
		||||
import { CommonModule } from '@angular/common';
 | 
			
		||||
 | 
			
		||||
import { CoreComponentsModule } from '@components/components.module';
 | 
			
		||||
import { CoreDirectivesModule } from '@directives/directives.module';
 | 
			
		||||
import { CorePipesModule } from '@pipes/pipes.module';
 | 
			
		||||
import { AddonCalendarComponentsModule } from '../../components/components.module';
 | 
			
		||||
 | 
			
		||||
import { AddonCalendarDayPage } from './day.page';
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
    {
 | 
			
		||||
        path: '',
 | 
			
		||||
        component: AddonCalendarDayPage,
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    imports: [
 | 
			
		||||
        RouterModule.forChild(routes),
 | 
			
		||||
        CommonModule,
 | 
			
		||||
        IonicModule,
 | 
			
		||||
        TranslateModule.forChild(),
 | 
			
		||||
        CoreComponentsModule,
 | 
			
		||||
        CoreDirectivesModule,
 | 
			
		||||
        CorePipesModule,
 | 
			
		||||
        AddonCalendarComponentsModule,
 | 
			
		||||
    ],
 | 
			
		||||
    declarations: [
 | 
			
		||||
        AddonCalendarDayPage,
 | 
			
		||||
    ],
 | 
			
		||||
    exports: [RouterModule],
 | 
			
		||||
})
 | 
			
		||||
export class AddonCalendarDayPageModule {}
 | 
			
		||||
							
								
								
									
										728
									
								
								src/addons/calendar/pages/day/day.page.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,728 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Component, OnInit, OnDestroy } from '@angular/core';
 | 
			
		||||
import { PopoverController, IonRefresher } from '@ionic/angular';
 | 
			
		||||
import { CoreApp } from '@services/app';
 | 
			
		||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
 | 
			
		||||
import { CoreLocalNotifications } from '@services/local-notifications';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { CoreTimeUtils } from '@services/utils/time';
 | 
			
		||||
import {
 | 
			
		||||
    AddonCalendarProvider,
 | 
			
		||||
    AddonCalendar,
 | 
			
		||||
    AddonCalendarEventToDisplay,
 | 
			
		||||
    AddonCalendarCalendarDay,
 | 
			
		||||
    AddonCalendarEventType,
 | 
			
		||||
    AddonCalendarUpdatedEventEvent,
 | 
			
		||||
} from '../../services/calendar';
 | 
			
		||||
import { AddonCalendarOffline } from '../../services/calendar-offline';
 | 
			
		||||
import { AddonCalendarFilter, AddonCalendarHelper } from '../../services/calendar-helper';
 | 
			
		||||
import { AddonCalendarSync, AddonCalendarSyncEvents, AddonCalendarSyncProvider } from '../../services/calendar-sync';
 | 
			
		||||
import { CoreCategoryData, CoreCourses, CoreEnrolledCourseData } from '@features/courses/services/courses';
 | 
			
		||||
import { CoreCoursesHelper } from '@features/courses/services/courses-helper';
 | 
			
		||||
import { AddonCalendarFilterPopoverComponent } from '../../components/filter/filter';
 | 
			
		||||
import moment from 'moment';
 | 
			
		||||
import { Network, NgZone } from '@singletons';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { ActivatedRoute, Params } from '@angular/router';
 | 
			
		||||
import { Subscription } from 'rxjs';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Page that displays the calendar events for a certain day.
 | 
			
		||||
 */
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'page-addon-calendar-day',
 | 
			
		||||
    templateUrl: 'day.html',
 | 
			
		||||
    styleUrls: ['../../calendar-common.scss', 'day.scss'],
 | 
			
		||||
})
 | 
			
		||||
export class AddonCalendarDayPage implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
    protected currentSiteId: string;
 | 
			
		||||
    protected year!: number;
 | 
			
		||||
    protected month!: number;
 | 
			
		||||
    protected day!: number;
 | 
			
		||||
    protected categories: { [id: number]: CoreCategoryData } = {};
 | 
			
		||||
    protected events: AddonCalendarEventToDisplay[] = []; // Events (both online and offline).
 | 
			
		||||
    protected onlineEvents: AddonCalendarEventToDisplay[] = [];
 | 
			
		||||
    protected offlineEvents: { [monthId: string]: { [day: number]: AddonCalendarEventToDisplay[] } } =
 | 
			
		||||
        {}; // Offline events classified in month & day.
 | 
			
		||||
 | 
			
		||||
    protected offlineEditedEventsIds: number[] = []; // IDs of events edited in offline.
 | 
			
		||||
    protected deletedEvents: number[] = []; // Events deleted in offline.
 | 
			
		||||
    protected timeFormat?: string;
 | 
			
		||||
    protected currentTime!: number;
 | 
			
		||||
 | 
			
		||||
    // Observers.
 | 
			
		||||
    protected newEventObserver: CoreEventObserver;
 | 
			
		||||
    protected discardedObserver: CoreEventObserver;
 | 
			
		||||
    protected editEventObserver: CoreEventObserver;
 | 
			
		||||
    protected deleteEventObserver: CoreEventObserver;
 | 
			
		||||
    protected undeleteEventObserver: CoreEventObserver;
 | 
			
		||||
    protected syncObserver: CoreEventObserver;
 | 
			
		||||
    protected manualSyncObserver: CoreEventObserver;
 | 
			
		||||
    protected onlineObserver: Subscription;
 | 
			
		||||
    protected obsDefaultTimeChange?: CoreEventObserver;
 | 
			
		||||
    protected filterChangedObserver: CoreEventObserver;
 | 
			
		||||
 | 
			
		||||
    periodName?: string;
 | 
			
		||||
    filteredEvents: AddonCalendarEventToDisplay [] = [];
 | 
			
		||||
    canCreate = false;
 | 
			
		||||
    courses: Partial<CoreEnrolledCourseData>[] = [];
 | 
			
		||||
    loaded = false;
 | 
			
		||||
    hasOffline = false;
 | 
			
		||||
    isOnline = false;
 | 
			
		||||
    syncIcon = 'spinner';
 | 
			
		||||
    isCurrentDay = false;
 | 
			
		||||
    isPastDay = false;
 | 
			
		||||
    currentMoment!: moment.Moment;
 | 
			
		||||
    filter: AddonCalendarFilter = {
 | 
			
		||||
        filtered: false,
 | 
			
		||||
        courseId: -1,
 | 
			
		||||
        categoryId: undefined,
 | 
			
		||||
        course: true,
 | 
			
		||||
        group: true,
 | 
			
		||||
        site: true,
 | 
			
		||||
        user: true,
 | 
			
		||||
        category: true,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        protected route: ActivatedRoute,
 | 
			
		||||
        private popoverCtrl: PopoverController,
 | 
			
		||||
    ) {
 | 
			
		||||
        this.currentSiteId = CoreSites.instance.getCurrentSiteId();
 | 
			
		||||
 | 
			
		||||
        if (CoreLocalNotifications.instance.isAvailable()) {
 | 
			
		||||
            // Re-schedule events if default time changes.
 | 
			
		||||
            this.obsDefaultTimeChange = CoreEvents.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, () => {
 | 
			
		||||
                AddonCalendar.instance.scheduleEventsNotifications(this.onlineEvents);
 | 
			
		||||
            }, this.currentSiteId);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Listen for events added. When an event is added, reload the data.
 | 
			
		||||
        this.newEventObserver = CoreEvents.on(
 | 
			
		||||
            AddonCalendarProvider.NEW_EVENT_EVENT,
 | 
			
		||||
            (data: AddonCalendarUpdatedEventEvent) => {
 | 
			
		||||
                if (data && data.eventId) {
 | 
			
		||||
                    this.loaded = false;
 | 
			
		||||
                    this.refreshData(true, true);
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            this.currentSiteId,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Listen for new event discarded event. When it does, reload the data.
 | 
			
		||||
        this.discardedObserver = CoreEvents.on(AddonCalendarProvider.NEW_EVENT_DISCARDED_EVENT, () => {
 | 
			
		||||
            this.loaded = false;
 | 
			
		||||
            this.refreshData(true, true);
 | 
			
		||||
        }, this.currentSiteId);
 | 
			
		||||
 | 
			
		||||
        // Listen for events edited. When an event is edited, reload the data.
 | 
			
		||||
        this.editEventObserver = CoreEvents.on(
 | 
			
		||||
            AddonCalendarProvider.EDIT_EVENT_EVENT,
 | 
			
		||||
            (data: AddonCalendarUpdatedEventEvent) => {
 | 
			
		||||
                if (data && data.eventId) {
 | 
			
		||||
                    this.loaded = false;
 | 
			
		||||
                    this.refreshData(true, true);
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            this.currentSiteId,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Refresh data if calendar events are synchronized automatically.
 | 
			
		||||
        this.syncObserver = CoreEvents.on(AddonCalendarSyncProvider.AUTO_SYNCED, () => {
 | 
			
		||||
            this.loaded = false;
 | 
			
		||||
            this.refreshData(false, true);
 | 
			
		||||
        }, this.currentSiteId);
 | 
			
		||||
 | 
			
		||||
        // Refresh data if calendar events are synchronized manually but not by this page.
 | 
			
		||||
        this.manualSyncObserver = CoreEvents.on(AddonCalendarSyncProvider.MANUAL_SYNCED, (data: AddonCalendarSyncEvents) => {
 | 
			
		||||
            if (data && (data.source != 'day' || data.year != this.year || data.month != this.month || data.day != this.day)) {
 | 
			
		||||
                this.loaded = false;
 | 
			
		||||
                this.refreshData(false, true);
 | 
			
		||||
            }
 | 
			
		||||
        }, this.currentSiteId);
 | 
			
		||||
 | 
			
		||||
        // Update the events when an event is deleted.
 | 
			
		||||
        this.deleteEventObserver = CoreEvents.on(
 | 
			
		||||
            AddonCalendarProvider.DELETED_EVENT_EVENT,
 | 
			
		||||
            (data: AddonCalendarUpdatedEventEvent) => {
 | 
			
		||||
                if (data && !data.sent) {
 | 
			
		||||
                    // Event was deleted in offline. Just mark it as deleted, no need to refresh.
 | 
			
		||||
                    this.hasOffline = this.markAsDeleted(data.eventId, true) || this.hasOffline;
 | 
			
		||||
                    this.deletedEvents.push(data.eventId);
 | 
			
		||||
                } else {
 | 
			
		||||
                    this.loaded = false;
 | 
			
		||||
                    this.refreshData(false, true);
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            this.currentSiteId,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Listen for events "undeleted" (offline).
 | 
			
		||||
        this.undeleteEventObserver = CoreEvents.on(
 | 
			
		||||
            AddonCalendarProvider.UNDELETED_EVENT_EVENT,
 | 
			
		||||
            (data: AddonCalendarUpdatedEventEvent) => {
 | 
			
		||||
                if (!data || !data.eventId) {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Mark it as undeleted, no need to refresh.
 | 
			
		||||
                const found = this.markAsDeleted(data.eventId, false);
 | 
			
		||||
 | 
			
		||||
                // Remove it from the list of deleted events if it's there.
 | 
			
		||||
                const index = this.deletedEvents.indexOf(data.eventId);
 | 
			
		||||
                if (index != -1) {
 | 
			
		||||
                    this.deletedEvents.splice(index, 1);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (found) {
 | 
			
		||||
                // The deleted event belongs to current list. Re-calculate "hasOffline".
 | 
			
		||||
                    this.hasOffline = false;
 | 
			
		||||
 | 
			
		||||
                    if (this.events.length != this.onlineEvents.length) {
 | 
			
		||||
                        this.hasOffline = true;
 | 
			
		||||
                    } else {
 | 
			
		||||
                        const event = this.events.find((event) => event.deleted || event.offline);
 | 
			
		||||
 | 
			
		||||
                        this.hasOffline = !!event;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            this.currentSiteId,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        this.filterChangedObserver = CoreEvents.on(
 | 
			
		||||
            AddonCalendarProvider.FILTER_CHANGED_EVENT,
 | 
			
		||||
            async (data: AddonCalendarFilter) => {
 | 
			
		||||
                this.filter = data;
 | 
			
		||||
 | 
			
		||||
                // Course viewed has changed, check if the user can create events for this course calendar.
 | 
			
		||||
                this.canCreate = await AddonCalendarHelper.instance.canEditEvents(this.filter.courseId);
 | 
			
		||||
 | 
			
		||||
                this.filterEvents();
 | 
			
		||||
            },
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Refresh online status when changes.
 | 
			
		||||
        this.onlineObserver = Network.instance.onChange().subscribe(() => {
 | 
			
		||||
            // Execute the callback in the Angular zone, so change detection doesn't stop working.
 | 
			
		||||
            NgZone.instance.run(() => {
 | 
			
		||||
                this.isOnline = CoreApp.instance.isOnline();
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * View loaded.
 | 
			
		||||
     */
 | 
			
		||||
    ngOnInit(): void {
 | 
			
		||||
        const types: string[] = [];
 | 
			
		||||
 | 
			
		||||
        CoreUtils.instance.enumKeys(AddonCalendarEventType).forEach((name) => {
 | 
			
		||||
            const value = AddonCalendarEventType[name];
 | 
			
		||||
            const filter = this.route.snapshot.queryParams[name];
 | 
			
		||||
            this.filter[name] = typeof filter == 'undefined' ? true : filter;
 | 
			
		||||
            types.push(value);
 | 
			
		||||
        });
 | 
			
		||||
        this.filter.courseId = parseInt(this.route.snapshot.queryParams['courseId'], 10) || -1;
 | 
			
		||||
        this.filter.categoryId = parseInt(this.route.snapshot.queryParams['categoryId'], 10) || undefined;
 | 
			
		||||
 | 
			
		||||
        this.filter.filtered = typeof this.filter.courseId != 'undefined' || types.some((name) => !this.filter[name]);
 | 
			
		||||
 | 
			
		||||
        const now = new Date();
 | 
			
		||||
        this.year = this.route.snapshot.queryParams['year'] || now.getFullYear();
 | 
			
		||||
        this.month = this.route.snapshot.queryParams['month'] || (now.getMonth() + 1);
 | 
			
		||||
        this.day = this.route.snapshot.queryParams['day'] || now.getDate();
 | 
			
		||||
 | 
			
		||||
        this.calculateCurrentMoment();
 | 
			
		||||
        this.calculateIsCurrentDay();
 | 
			
		||||
 | 
			
		||||
        this.fetchData(true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Fetch all the data required for the view.
 | 
			
		||||
     *
 | 
			
		||||
     * @param sync Whether it should try to synchronize offline events.
 | 
			
		||||
     * @param showErrors Whether to show sync errors to the user.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async fetchData(sync?: boolean): Promise<void> {
 | 
			
		||||
 | 
			
		||||
        this.syncIcon = 'spinner';
 | 
			
		||||
        this.isOnline = CoreApp.instance.isOnline();
 | 
			
		||||
 | 
			
		||||
        if (sync) {
 | 
			
		||||
            await this.sync();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const promises: Promise<void>[] = [];
 | 
			
		||||
 | 
			
		||||
            // Load courses for the popover.
 | 
			
		||||
            promises.push(CoreCoursesHelper.instance.getCoursesForPopover(this.filter.courseId).then((data) => {
 | 
			
		||||
                this.courses = data.courses;
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }));
 | 
			
		||||
 | 
			
		||||
            // Get categories.
 | 
			
		||||
            promises.push(this.loadCategories());
 | 
			
		||||
 | 
			
		||||
            // Get offline events.
 | 
			
		||||
            promises.push(AddonCalendarOffline.instance.getAllEditedEvents().then((offlineEvents) => {
 | 
			
		||||
                // Classify them by month & day.
 | 
			
		||||
                this.offlineEvents = AddonCalendarHelper.instance.classifyIntoMonths(offlineEvents);
 | 
			
		||||
 | 
			
		||||
                // Get the IDs of events edited in offline.
 | 
			
		||||
                this.offlineEditedEventsIds = offlineEvents.filter((event) => event.id! > 0).map((event) => event.id!);
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }));
 | 
			
		||||
 | 
			
		||||
            // Get events deleted in offline.
 | 
			
		||||
            promises.push(AddonCalendarOffline.instance.getAllDeletedEventsIds().then((ids) => {
 | 
			
		||||
                this.deletedEvents = ids;
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }));
 | 
			
		||||
 | 
			
		||||
            // Check if user can create events.
 | 
			
		||||
            promises.push(AddonCalendarHelper.instance.canEditEvents(this.filter.courseId).then((canEdit) => {
 | 
			
		||||
                this.canCreate = canEdit;
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }));
 | 
			
		||||
 | 
			
		||||
            // Get user preferences.
 | 
			
		||||
            promises.push(AddonCalendar.instance.getCalendarTimeFormat().then((value) => {
 | 
			
		||||
                this.timeFormat = value;
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }));
 | 
			
		||||
 | 
			
		||||
            await Promise.all(promises);
 | 
			
		||||
 | 
			
		||||
            await this.fetchEvents();
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.instance.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.loaded = true;
 | 
			
		||||
        this.syncIcon = 'fas-sync-alt';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Fetch the events for current day.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async fetchEvents(): Promise<void> {
 | 
			
		||||
        let result: AddonCalendarCalendarDay;
 | 
			
		||||
        try {
 | 
			
		||||
            // Don't pass courseId and categoryId, we'll filter them locally.
 | 
			
		||||
            result = await AddonCalendar.instance.getDayEvents(this.year, this.month, this.day);
 | 
			
		||||
            this.onlineEvents = result.events.map((event) => AddonCalendarHelper.instance.formatEventData(event));
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            if (CoreApp.instance.isOnline()) {
 | 
			
		||||
                throw error;
 | 
			
		||||
            }
 | 
			
		||||
            // Allow navigating to non-cached days in offline (behave as if using emergency cache).
 | 
			
		||||
            this.onlineEvents = [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Calculate the period name. We don't use the one in result because it's in server's language.
 | 
			
		||||
        this.periodName = CoreTimeUtils.instance.userDate(
 | 
			
		||||
            new Date(this.year, this.month - 1, this.day).getTime(),
 | 
			
		||||
            'core.strftimedaydate',
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Schedule notifications for the events retrieved (only future events will be scheduled).
 | 
			
		||||
        AddonCalendar.instance.scheduleEventsNotifications(this.onlineEvents);
 | 
			
		||||
        // Merge the online events with offline data.
 | 
			
		||||
        this.events = this.mergeEvents();
 | 
			
		||||
        // Filter events by course.
 | 
			
		||||
        this.filterEvents();
 | 
			
		||||
        this.calculateIsCurrentDay();
 | 
			
		||||
        // Re-calculate the formatted time so it uses the device date.
 | 
			
		||||
        const dayTime = this.currentMoment.unix() * 1000;
 | 
			
		||||
 | 
			
		||||
        const promises = this.events.map((event) => {
 | 
			
		||||
            event.ispast = this.isPastDay || (this.isCurrentDay && this.isEventPast(event));
 | 
			
		||||
 | 
			
		||||
            return AddonCalendar.instance.formatEventTime(event, this.timeFormat!, true, dayTime).then((time) => {
 | 
			
		||||
                event.formattedtime = time;
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        await Promise.all(promises);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Merge online events with the offline events of that period.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Merged events.
 | 
			
		||||
     */
 | 
			
		||||
    protected mergeEvents(): AddonCalendarEventToDisplay[] {
 | 
			
		||||
        this.hasOffline = false;
 | 
			
		||||
 | 
			
		||||
        if (!Object.keys(this.offlineEvents).length && !this.deletedEvents.length) {
 | 
			
		||||
            // No offline events, nothing to merge.
 | 
			
		||||
            return this.onlineEvents;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const monthOfflineEvents = this.offlineEvents[AddonCalendarHelper.instance.getMonthId(this.year, this.month)];
 | 
			
		||||
        const dayOfflineEvents = monthOfflineEvents && monthOfflineEvents[this.day];
 | 
			
		||||
        let result = this.onlineEvents;
 | 
			
		||||
 | 
			
		||||
        if (this.deletedEvents.length) {
 | 
			
		||||
            // Mark as deleted the events that were deleted in offline.
 | 
			
		||||
            result.forEach((event) => {
 | 
			
		||||
                event.deleted = this.deletedEvents.indexOf(event.id) != -1;
 | 
			
		||||
 | 
			
		||||
                if (event.deleted) {
 | 
			
		||||
                    this.hasOffline = true;
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.offlineEditedEventsIds.length) {
 | 
			
		||||
            // Remove the online events that were modified in offline.
 | 
			
		||||
            result = result.filter((event) => this.offlineEditedEventsIds.indexOf(event.id) == -1);
 | 
			
		||||
 | 
			
		||||
            if (result.length != this.onlineEvents.length) {
 | 
			
		||||
                this.hasOffline = true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (dayOfflineEvents && dayOfflineEvents.length) {
 | 
			
		||||
            // Add the offline events (either new or edited).
 | 
			
		||||
            this.hasOffline = true;
 | 
			
		||||
            result = AddonCalendarHelper.instance.sortEvents(result.concat(dayOfflineEvents));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Filter events based on the filter popover.
 | 
			
		||||
     */
 | 
			
		||||
    protected filterEvents(): void {
 | 
			
		||||
        this.filteredEvents = AddonCalendarHelper.instance.getFilteredEvents(this.events, this.filter, this.categories);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Refresh the data.
 | 
			
		||||
     *
 | 
			
		||||
     * @param refresher Refresher.
 | 
			
		||||
     * @param done Function to call when done.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async doRefresh(refresher?: CustomEvent<IonRefresher>, done?: () => void): Promise<void> {
 | 
			
		||||
        if (!this.loaded) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await this.refreshData(true).finally(() => {
 | 
			
		||||
            refresher?.detail.complete();
 | 
			
		||||
            done && done();
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Refresh the data.
 | 
			
		||||
     *
 | 
			
		||||
     * @param sync Whether it should try to synchronize offline events.
 | 
			
		||||
     * @param afterChange Whether the refresh is done after an event has changed or has been synced.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async refreshData(sync?: boolean, afterChange?: boolean): Promise<void> {
 | 
			
		||||
        this.syncIcon = 'spinner';
 | 
			
		||||
 | 
			
		||||
        const promises: Promise<void>[] = [];
 | 
			
		||||
 | 
			
		||||
        // Don't invalidate day events after a change, it has already been handled.
 | 
			
		||||
        if (!afterChange) {
 | 
			
		||||
            promises.push(AddonCalendar.instance.invalidateDayEvents(this.year, this.month, this.day));
 | 
			
		||||
        }
 | 
			
		||||
        promises.push(AddonCalendar.instance.invalidateAllowedEventTypes());
 | 
			
		||||
        promises.push(CoreCourses.instance.invalidateCategories(0, true));
 | 
			
		||||
        promises.push(AddonCalendar.instance.invalidateTimeFormat());
 | 
			
		||||
 | 
			
		||||
        await Promise.all(promises).finally(() =>
 | 
			
		||||
            this.fetchData(sync));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Load categories to be able to filter events.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async loadCategories(): Promise<void> {
 | 
			
		||||
        try {
 | 
			
		||||
            const cats = await CoreCourses.instance.getCategories(0, true);
 | 
			
		||||
            this.categories = {};
 | 
			
		||||
 | 
			
		||||
            // Index categories by ID.
 | 
			
		||||
            cats.forEach((category) => {
 | 
			
		||||
                this.categories[category.id] = category;
 | 
			
		||||
            });
 | 
			
		||||
        } catch {
 | 
			
		||||
            // Ignore errors.
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Try to synchronize offline events.
 | 
			
		||||
     *
 | 
			
		||||
     * @param showErrors Whether to show sync errors to the user.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async sync(showErrors?: boolean): Promise<void> {
 | 
			
		||||
        try {
 | 
			
		||||
            const result = await AddonCalendarSync.instance.syncEvents();
 | 
			
		||||
 | 
			
		||||
            if (result.warnings && result.warnings.length) {
 | 
			
		||||
                CoreDomUtils.instance.showErrorModal(result.warnings[0]);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (result.updated) {
 | 
			
		||||
                // Trigger a manual sync event.
 | 
			
		||||
                result.source = 'day';
 | 
			
		||||
                result.day = this.day;
 | 
			
		||||
                result.month = this.month;
 | 
			
		||||
                result.year = this.year;
 | 
			
		||||
 | 
			
		||||
                CoreEvents.trigger<AddonCalendarSyncEvents>(AddonCalendarSyncProvider.MANUAL_SYNCED, result, this.currentSiteId);
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            if (showErrors) {
 | 
			
		||||
                CoreDomUtils.instance.showErrorModalDefault(error, 'core.errorsync', true);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Navigate to a particular event.
 | 
			
		||||
     *
 | 
			
		||||
     * @param eventId Event to load.
 | 
			
		||||
     */
 | 
			
		||||
    gotoEvent(eventId: number): void {
 | 
			
		||||
        if (eventId < 0) {
 | 
			
		||||
            // It's an offline event, go to the edit page.
 | 
			
		||||
            this.openEdit(eventId);
 | 
			
		||||
        } else {
 | 
			
		||||
            CoreNavigator.instance.navigateToSitePath('/calendar/event', { params: { id: eventId } });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Show the context menu.
 | 
			
		||||
     *
 | 
			
		||||
     * @param event Event.
 | 
			
		||||
     */
 | 
			
		||||
    async openFilter(event: MouseEvent): Promise<void> {
 | 
			
		||||
        const popover = await this.popoverCtrl.create({
 | 
			
		||||
            component: AddonCalendarFilterPopoverComponent,
 | 
			
		||||
            componentProps: {
 | 
			
		||||
                courses: this.courses,
 | 
			
		||||
                filter: this.filter,
 | 
			
		||||
            },
 | 
			
		||||
            event,
 | 
			
		||||
        });
 | 
			
		||||
        await popover.present();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Open page to create/edit an event.
 | 
			
		||||
     *
 | 
			
		||||
     * @param eventId Event ID to edit.
 | 
			
		||||
     */
 | 
			
		||||
    openEdit(eventId?: number): void {
 | 
			
		||||
        const params: Params = {};
 | 
			
		||||
 | 
			
		||||
        if (eventId) {
 | 
			
		||||
            params.eventId = eventId;
 | 
			
		||||
        } else {
 | 
			
		||||
            // It's a new event, set the time.
 | 
			
		||||
            params.timestamp = moment().year(this.year).month(this.month - 1).date(this.day).unix() * 1000;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.filter.courseId) {
 | 
			
		||||
            params.courseId = this.filter.courseId;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        CoreNavigator.instance.navigateToSitePath('/calendar/edit', { params });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Calculate current moment.
 | 
			
		||||
     */
 | 
			
		||||
    calculateCurrentMoment(): void {
 | 
			
		||||
        this.currentMoment = moment().year(this.year).month(this.month - 1).date(this.day);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if user is viewing the current day.
 | 
			
		||||
     */
 | 
			
		||||
    calculateIsCurrentDay(): void {
 | 
			
		||||
        const now = new Date();
 | 
			
		||||
 | 
			
		||||
        this.currentTime = CoreTimeUtils.instance.timestamp();
 | 
			
		||||
 | 
			
		||||
        this.isCurrentDay = this.year == now.getFullYear() && this.month == now.getMonth() + 1 && this.day == now.getDate();
 | 
			
		||||
        this.isPastDay = this.year < now.getFullYear() || (this.year == now.getFullYear() && this.month < now.getMonth()) ||
 | 
			
		||||
            (this.year == now.getFullYear() && this.month == now.getMonth() + 1 && this.day < now.getDate());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Go to current day.
 | 
			
		||||
     */
 | 
			
		||||
    async goToCurrentDay(): Promise<void> {
 | 
			
		||||
        const now = new Date();
 | 
			
		||||
        const initialDay = this.day;
 | 
			
		||||
        const initialMonth = this.month;
 | 
			
		||||
        const initialYear = this.year;
 | 
			
		||||
 | 
			
		||||
        this.day = now.getDate();
 | 
			
		||||
        this.month = now.getMonth() + 1;
 | 
			
		||||
        this.year = now.getFullYear();
 | 
			
		||||
        this.calculateCurrentMoment();
 | 
			
		||||
 | 
			
		||||
        this.loaded = false;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await this.fetchEvents();
 | 
			
		||||
 | 
			
		||||
            this.isCurrentDay = true;
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.instance.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
 | 
			
		||||
 | 
			
		||||
            this.year = initialYear;
 | 
			
		||||
            this.month = initialMonth;
 | 
			
		||||
            this.day = initialDay;
 | 
			
		||||
            this.calculateCurrentMoment();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.loaded = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Load next day.
 | 
			
		||||
     */
 | 
			
		||||
    async loadNext(): Promise<void> {
 | 
			
		||||
        this.increaseDay();
 | 
			
		||||
 | 
			
		||||
        this.loaded = false;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await this.fetchEvents();
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.instance.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
 | 
			
		||||
            this.decreaseDay();
 | 
			
		||||
        }
 | 
			
		||||
        this.loaded = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Load previous day.
 | 
			
		||||
     */
 | 
			
		||||
    async loadPrevious(): Promise<void> {
 | 
			
		||||
        this.decreaseDay();
 | 
			
		||||
 | 
			
		||||
        this.loaded = false;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await this.fetchEvents();
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.instance.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
 | 
			
		||||
            this.increaseDay();
 | 
			
		||||
        }
 | 
			
		||||
        this.loaded = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Decrease the current day.
 | 
			
		||||
     */
 | 
			
		||||
    protected decreaseDay(): void {
 | 
			
		||||
        this.currentMoment.subtract(1, 'day');
 | 
			
		||||
 | 
			
		||||
        this.year = this.currentMoment.year();
 | 
			
		||||
        this.month = this.currentMoment.month() + 1;
 | 
			
		||||
        this.day = this.currentMoment.date();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Increase the current day.
 | 
			
		||||
     */
 | 
			
		||||
    protected increaseDay(): void {
 | 
			
		||||
        this.currentMoment.add(1, 'day');
 | 
			
		||||
 | 
			
		||||
        this.year = this.currentMoment.year();
 | 
			
		||||
        this.month = this.currentMoment.month() + 1;
 | 
			
		||||
        this.day = this.currentMoment.date();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Find an event and mark it as deleted.
 | 
			
		||||
     *
 | 
			
		||||
     * @param eventId Event ID.
 | 
			
		||||
     * @param deleted Whether to mark it as deleted or not.
 | 
			
		||||
     * @return Whether the event was found.
 | 
			
		||||
     */
 | 
			
		||||
    protected markAsDeleted(eventId: number, deleted: boolean): boolean {
 | 
			
		||||
        const event = this.onlineEvents.find((event) => event.id == eventId);
 | 
			
		||||
 | 
			
		||||
        if (event) {
 | 
			
		||||
            event.deleted = deleted;
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns if the event is in the past or not.
 | 
			
		||||
     *
 | 
			
		||||
     * @param event Event object.
 | 
			
		||||
     * @return True if it's in the past.
 | 
			
		||||
     */
 | 
			
		||||
    isEventPast(event: AddonCalendarEventToDisplay): boolean {
 | 
			
		||||
        return (event.timestart + event.timeduration) < this.currentTime;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Page destroyed.
 | 
			
		||||
     */
 | 
			
		||||
    ngOnDestroy(): void {
 | 
			
		||||
        this.newEventObserver?.off();
 | 
			
		||||
        this.discardedObserver?.off();
 | 
			
		||||
        this.editEventObserver?.off();
 | 
			
		||||
        this.deleteEventObserver?.off();
 | 
			
		||||
        this.undeleteEventObserver?.off();
 | 
			
		||||
        this.syncObserver?.off();
 | 
			
		||||
        this.manualSyncObserver?.off();
 | 
			
		||||
        this.onlineObserver?.unsubscribe();
 | 
			
		||||
        this.filterChangedObserver?.off();
 | 
			
		||||
        this.obsDefaultTimeChange?.off();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										9
									
								
								src/addons/calendar/pages/day/day.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,9 @@
 | 
			
		||||
:host {
 | 
			
		||||
    .addon-calendar-period {
 | 
			
		||||
        flex-grow: 3;
 | 
			
		||||
        h3 {
 | 
			
		||||
            margin-top: 10px;
 | 
			
		||||
            font-size: 1.2rem;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										232
									
								
								src/addons/calendar/pages/edit-event/edit-event.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,232 @@
 | 
			
		||||
<ion-header>
 | 
			
		||||
    <ion-toolbar>
 | 
			
		||||
        <ion-buttons slot="start">
 | 
			
		||||
            <ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
        <ion-title>{{ title | translate }}</ion-title>
 | 
			
		||||
    </ion-toolbar>
 | 
			
		||||
</ion-header>
 | 
			
		||||
<ion-content>
 | 
			
		||||
    <ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshData($event)">
 | 
			
		||||
        <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
 | 
			
		||||
    </ion-refresher>
 | 
			
		||||
 | 
			
		||||
    <core-loading [hideUntil]="loaded">
 | 
			
		||||
        <form [formGroup]="form" *ngIf="!error" #editEventForm>
 | 
			
		||||
            <!-- Event name. -->
 | 
			
		||||
            <ion-item class="ion-text-wrap">
 | 
			
		||||
                    <ion-label position="stacked"><h2 [core-mark-required]="true">
 | 
			
		||||
                        {{ 'addon.calendar.eventname' | translate }}
 | 
			
		||||
                    </h2>
 | 
			
		||||
                </ion-label>
 | 
			
		||||
                <ion-input type="text" name="name" [placeholder]="'addon.calendar.eventname' | translate" formControlName="name">
 | 
			
		||||
                </ion-input>
 | 
			
		||||
                <core-input-errors item-content [control]="form.controls.name" [errorMessages]="errors"></core-input-errors>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
 | 
			
		||||
            <!-- Date. -->
 | 
			
		||||
            <ion-item class="ion-text-wrap">
 | 
			
		||||
                <ion-label position="stacked">
 | 
			
		||||
                    <h2 [core-mark-required]="true">
 | 
			
		||||
                        {{ 'core.date' | translate }}
 | 
			
		||||
                    </h2>
 | 
			
		||||
                </ion-label>
 | 
			
		||||
                <ion-datetime formControlName="timestart" [placeholder]="'core.date' | translate" [displayFormat]="dateFormat">
 | 
			
		||||
                </ion-datetime>
 | 
			
		||||
                <core-input-errors item-content [control]="form.controls.timestart" [errorMessages]="errors"></core-input-errors>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
 | 
			
		||||
            <!-- Type. -->
 | 
			
		||||
            <ion-item class="ion-text-wrap addon-calendar-eventtype-container">
 | 
			
		||||
                <ion-label id="addon-calendar-eventtype-label">
 | 
			
		||||
                    <h2 [core-mark-required]="true">
 | 
			
		||||
                        {{ 'addon.calendar.eventkind' | translate }}
 | 
			
		||||
                    </h2>
 | 
			
		||||
                </ion-label>
 | 
			
		||||
                <ion-select formControlName="eventtype" aria-labelledby="addon-calendar-eventtype-label" interface="action-sheet"
 | 
			
		||||
                    [disabled]="eventTypes.length == 1">
 | 
			
		||||
                    <ion-select-option *ngFor="let type of eventTypes" [value]="type.value">
 | 
			
		||||
                        {{ type.name | translate }}
 | 
			
		||||
                    </ion-select-option>
 | 
			
		||||
                </ion-select>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
 | 
			
		||||
            <!-- Category. -->
 | 
			
		||||
            <ion-item class="ion-text-wrap" *ngIf="typeControl.value == 'category'">
 | 
			
		||||
                <ion-label id="addon-calendar-category-label">
 | 
			
		||||
                    <h2 [core-mark-required]="true">
 | 
			
		||||
                        {{ 'core.category' | translate }}
 | 
			
		||||
                    </h2>
 | 
			
		||||
                </ion-label>
 | 
			
		||||
                <ion-select formControlName="categoryid" aria-labelledby="addon-calendar-category-label" interface="action-sheet"
 | 
			
		||||
                    [placeholder]="'core.noselection' | translate">
 | 
			
		||||
                    <ion-select-option *ngFor="let category of categories" [value]="category.id">
 | 
			
		||||
                        {{ category.name }}
 | 
			
		||||
                    </ion-select-option>
 | 
			
		||||
                </ion-select>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
 | 
			
		||||
            <!-- Course. -->
 | 
			
		||||
            <ion-item class="ion-text-wrap" *ngIf="typeControl.value == 'course'">
 | 
			
		||||
                <ion-label id="addon-calendar-course-label">
 | 
			
		||||
                    <h2 [core-mark-required]="true">
 | 
			
		||||
                        {{ 'core.course' | translate }}
 | 
			
		||||
                    </h2>
 | 
			
		||||
                </ion-label>
 | 
			
		||||
                <ion-select formControlName="courseid" aria-labelledby="addon-calendar-course-label" interface="action-sheet"
 | 
			
		||||
                    [placeholder]="'core.noselection' | translate">
 | 
			
		||||
                    <ion-select-option *ngFor="let course of courses" [value]="course.id">{{ course.fullname }}</ion-select-option>
 | 
			
		||||
                </ion-select>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
 | 
			
		||||
            <!-- Group. -->
 | 
			
		||||
            <ng-container *ngIf="typeControl.value == 'group'">
 | 
			
		||||
                <!-- Select the course. -->
 | 
			
		||||
                <ion-item class="ion-text-wrap">
 | 
			
		||||
                    <ion-label id="addon-calendar-groupcourse-label">
 | 
			
		||||
                        <h2 [core-mark-required]="true">
 | 
			
		||||
                            {{ 'core.course' | translate }}
 | 
			
		||||
                        </h2>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                    <ion-select formControlName="groupcourseid" aria-labelledby="addon-calendar-groupcourse-label"
 | 
			
		||||
                        interface="action-sheet" [placeholder]="'core.noselection' | translate"
 | 
			
		||||
                        (ionChange)="groupCourseSelected($event)">
 | 
			
		||||
                        <ion-select-option *ngFor="let course of courses" [value]="course.id">
 | 
			
		||||
                            {{ course.fullname }}
 | 
			
		||||
                        </ion-select-option>
 | 
			
		||||
                    </ion-select>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
                <!-- The course has no groups. -->
 | 
			
		||||
                <ion-item class="ion-text-wrap" *ngIf="!loadingGroups && courseGroupSet && !groups.length" class="core-danger-item">
 | 
			
		||||
                    <ion-label><p>{{ 'core.coursenogroups' | translate }}</p></ion-label>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
                <!-- Select the group. -->
 | 
			
		||||
                <ion-item class="ion-text-wrap" *ngIf="!loadingGroups && groups.length > 0">
 | 
			
		||||
                    <ion-label id="addon-calendar-group-label">
 | 
			
		||||
                        <h2 [core-mark-required]="true">
 | 
			
		||||
                            {{ 'core.group' | translate }}
 | 
			
		||||
                        </h2>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                    <ion-select formControlName="groupid" aria-labelledby="addon-calendar-group-label" interface="action-sheet"
 | 
			
		||||
                        [placeholder]="'core.noselection' | translate">
 | 
			
		||||
                        <ion-select-option *ngFor="let group of groups" [value]="group.id">{{ group.name }}</ion-select-option>
 | 
			
		||||
                    </ion-select>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
                <!-- Loading groups. -->
 | 
			
		||||
                <ion-item class="ion-text-wrap" *ngIf="loadingGroups">
 | 
			
		||||
                    <ion-label><ion-spinner *ngIf="loadingGroups"></ion-spinner></ion-label>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
            </ng-container>
 | 
			
		||||
 | 
			
		||||
            <!-- Advanced options. -->
 | 
			
		||||
            <ion-item-divider class="ion-text-wrap" (click)="toggleAdvanced()" class="core-expandable">
 | 
			
		||||
                <ion-icon *ngIf="!advanced" name="fas-caret-right" slot="start"></ion-icon>
 | 
			
		||||
                <ion-icon *ngIf="advanced" name="fas-caret-down" slot="start"></ion-icon>
 | 
			
		||||
                <ion-label>
 | 
			
		||||
                    <span *ngIf="!advanced">{{ 'core.showmore' | translate }}</span>
 | 
			
		||||
                    <span *ngIf="advanced">{{ 'core.showless' | translate }}</span>
 | 
			
		||||
                </ion-label>
 | 
			
		||||
            </ion-item-divider>
 | 
			
		||||
 | 
			
		||||
            <div [hidden]="!advanced">
 | 
			
		||||
                <!-- Description. -->
 | 
			
		||||
                <ion-item class="ion-text-wrap">
 | 
			
		||||
                    <ion-label position="stacked">
 | 
			
		||||
                        <h2>{{ 'core.description' | translate }}</h2>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                    <core-rich-text-editor item-content [control]="descriptionControl"
 | 
			
		||||
                        [placeholder]="'core.description' | translate" name="description" [component]="component"
 | 
			
		||||
                        [componentId]="eventId" [autoSave]="false"></core-rich-text-editor>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
 | 
			
		||||
                <!-- Location. -->
 | 
			
		||||
                <ion-item class="ion-text-wrap">
 | 
			
		||||
                    <ion-label position="stacked"><h2>{{ 'core.location' | translate }}</h2></ion-label>
 | 
			
		||||
                    <ion-input type="text" name="location" [placeholder]="'core.location' | translate" formControlName="location">
 | 
			
		||||
                    </ion-input>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
 | 
			
		||||
                <!-- Duration. -->
 | 
			
		||||
                <div class="ion-text-wrap" class="addon-calendar-radio-container">
 | 
			
		||||
                    <ion-radio-group formControlName="duration">
 | 
			
		||||
                        <ion-item class="addon-calendar-radio-title">
 | 
			
		||||
                            <ion-label>
 | 
			
		||||
                                <h2>
 | 
			
		||||
                                    {{ 'addon.calendar.eventduration' | translate }}
 | 
			
		||||
                                </h2>
 | 
			
		||||
                            </ion-label>
 | 
			
		||||
                        </ion-item>
 | 
			
		||||
                        <ion-item>
 | 
			
		||||
                            <ion-radio slot="start" value="0"></ion-radio>
 | 
			
		||||
                            <ion-label>{{ 'addon.calendar.durationnone' | translate }}</ion-label>
 | 
			
		||||
                        </ion-item>
 | 
			
		||||
                        <ion-item  (click)="selectDuration('1')">
 | 
			
		||||
                            <ion-radio slot="start" value="1"></ion-radio>
 | 
			
		||||
                            <ion-label>{{ 'addon.calendar.durationuntil' | translate }}</ion-label>
 | 
			
		||||
                            <ion-datetime formControlName="timedurationuntil"
 | 
			
		||||
                                [placeholder]="'addon.calendar.durationuntil' | translate"
 | 
			
		||||
                                [displayFormat]="dateFormat" [disabled]="form.controls.duration.value != 1"></ion-datetime>
 | 
			
		||||
                        </ion-item>
 | 
			
		||||
                        <ion-item (click)="selectDuration('2')">
 | 
			
		||||
                            <ion-radio slot="start" value="2"></ion-radio>
 | 
			
		||||
                            <ion-label>{{ 'addon.calendar.durationminutes' | translate }}</ion-label>
 | 
			
		||||
                            <ion-input type="number" name="timedurationminutes" slot="end"
 | 
			
		||||
                                [placeholder]="'addon.calendar.durationminutes' | translate"
 | 
			
		||||
                                formControlName="timedurationminutes" [disabled]="form.controls.duration.value != 2"></ion-input>
 | 
			
		||||
                        </ion-item>
 | 
			
		||||
                    </ion-radio-group>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <!-- Repeat (for new events). -->
 | 
			
		||||
                <ng-container *ngIf="!eventId || eventId < 0">
 | 
			
		||||
                    <ion-item class="ion-text-wrap">
 | 
			
		||||
                        <ion-label>
 | 
			
		||||
                            <h2>{{ 'addon.calendar.repeatevent' | translate }}</h2>
 | 
			
		||||
                        </ion-label>
 | 
			
		||||
                        <ion-checkbox slot="end" formControlName="repeat"></ion-checkbox>
 | 
			
		||||
                    </ion-item>
 | 
			
		||||
                    <ion-item class="ion-text-wrap" *ngIf="form.controls.repeat.value">
 | 
			
		||||
                        <ion-label position="stacked"><h2>{{ 'addon.calendar.repeatweeksl' | translate }}</h2></ion-label>
 | 
			
		||||
                        <ion-input type="number" name="repeats" formControlName="repeats"></ion-input>
 | 
			
		||||
                    </ion-item>
 | 
			
		||||
                </ng-container>
 | 
			
		||||
 | 
			
		||||
                <!-- Apply to all events or just this one (editing repeated events). -->
 | 
			
		||||
                <div *ngIf="eventRepeatId" class="ion-text-wrap" class="addon-calendar-radio-container">
 | 
			
		||||
                    <ion-radio-group formControlName="repeateditall">
 | 
			
		||||
                        <ion-item class="addon-calendar-radio-title">
 | 
			
		||||
                            <ion-label>
 | 
			
		||||
                                <h2>
 | 
			
		||||
                                    {{ 'addon.calendar.repeatedevents' | translate }}
 | 
			
		||||
                                </h2>
 | 
			
		||||
                            </ion-label>
 | 
			
		||||
                        </ion-item>
 | 
			
		||||
                        <ion-item>
 | 
			
		||||
                            <ion-label>{{ 'addon.calendar.repeateditall' | translate:{$a: otherEventsCount} }}</ion-label>
 | 
			
		||||
                            <ion-radio slot="start" [value]="1"></ion-radio>
 | 
			
		||||
                        </ion-item>
 | 
			
		||||
                        <ion-item>
 | 
			
		||||
                            <ion-label>{{ 'addon.calendar.repeateditthis' | translate }}</ion-label>
 | 
			
		||||
                            <ion-radio slot="start" [value]="0"></ion-radio>
 | 
			
		||||
                        </ion-item>
 | 
			
		||||
                    </ion-radio-group>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <ion-item>
 | 
			
		||||
                <ion-label>
 | 
			
		||||
                    <ion-row>
 | 
			
		||||
                        <ion-col>
 | 
			
		||||
                            <ion-button expand="block" (click)="submit()" [disabled]="!form.valid">
 | 
			
		||||
                                {{ 'core.save' | translate }}
 | 
			
		||||
                            </ion-button>
 | 
			
		||||
                        </ion-col>
 | 
			
		||||
                        <ion-col *ngIf="hasOffline && eventId && eventId < 0">
 | 
			
		||||
                            <ion-button expand="block" color="light" (click)="discard()">{{ 'core.discard' | translate }}</ion-button>
 | 
			
		||||
                        </ion-col>
 | 
			
		||||
                    </ion-row>
 | 
			
		||||
                </ion-label>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
        </form>
 | 
			
		||||
    </core-loading>
 | 
			
		||||
</ion-content>
 | 
			
		||||
							
								
								
									
										52
									
								
								src/addons/calendar/pages/edit-event/edit-event.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,52 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { NgModule } from '@angular/core';
 | 
			
		||||
import { IonicModule } from '@ionic/angular';
 | 
			
		||||
import { TranslateModule } from '@ngx-translate/core';
 | 
			
		||||
import { RouterModule, Routes } from '@angular/router';
 | 
			
		||||
import { CommonModule } from '@angular/common';
 | 
			
		||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
 | 
			
		||||
 | 
			
		||||
import { CoreComponentsModule } from '@components/components.module';
 | 
			
		||||
import { CoreDirectivesModule } from '@directives/directives.module';
 | 
			
		||||
import { CoreEditorComponentsModule } from '@features/editor/components/components.module';
 | 
			
		||||
 | 
			
		||||
import { AddonCalendarEditEventPage } from './edit-event.page';
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
    {
 | 
			
		||||
        path: '',
 | 
			
		||||
        component: AddonCalendarEditEventPage,
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    imports: [
 | 
			
		||||
        RouterModule.forChild(routes),
 | 
			
		||||
        CommonModule,
 | 
			
		||||
        IonicModule,
 | 
			
		||||
        FormsModule,
 | 
			
		||||
        ReactiveFormsModule,
 | 
			
		||||
        TranslateModule.forChild(),
 | 
			
		||||
        CoreComponentsModule,
 | 
			
		||||
        CoreDirectivesModule,
 | 
			
		||||
        CoreEditorComponentsModule,
 | 
			
		||||
    ],
 | 
			
		||||
    declarations: [
 | 
			
		||||
        AddonCalendarEditEventPage,
 | 
			
		||||
    ],
 | 
			
		||||
    exports: [RouterModule],
 | 
			
		||||
})
 | 
			
		||||
export class AddonCalendarEditEventPageModule {}
 | 
			
		||||
							
								
								
									
										636
									
								
								src/addons/calendar/pages/edit-event/edit-event.page.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,636 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
 | 
			
		||||
import { FormControl, FormGroup, FormBuilder, Validators } from '@angular/forms';
 | 
			
		||||
import { IonRefresher, NavController } from '@ionic/angular';
 | 
			
		||||
import { CoreEvents } from '@singletons/events';
 | 
			
		||||
import { CoreGroup, CoreGroups } from '@services/groups';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreSync } from '@services/sync';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { CoreTimeUtils } from '@services/utils/time';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { CoreCategoryData, CoreCourses, CoreCourseSearchedData, CoreEnrolledCourseData } from '@features/courses/services/courses';
 | 
			
		||||
// @todo import { CoreSplitViewComponent } from '@components/split-view/split-view';
 | 
			
		||||
import { CoreEditorRichTextEditorComponent } from '@features/editor/components/rich-text-editor/rich-text-editor.ts';
 | 
			
		||||
import {
 | 
			
		||||
    AddonCalendarProvider,
 | 
			
		||||
    AddonCalendarGetCalendarAccessInformationWSResponse,
 | 
			
		||||
    AddonCalendarEvent,
 | 
			
		||||
    AddonCalendarEventType,
 | 
			
		||||
    AddonCalendar,
 | 
			
		||||
    AddonCalendarSubmitCreateUpdateFormDataWSParams,
 | 
			
		||||
    AddonCalendarUpdatedEventEvent,
 | 
			
		||||
} from '../../services/calendar';
 | 
			
		||||
import { AddonCalendarOffline } from '../../services/calendar-offline';
 | 
			
		||||
import { AddonCalendarEventTypeOption, AddonCalendarHelper } from '../../services/calendar-helper';
 | 
			
		||||
import { AddonCalendarSync, AddonCalendarSyncProvider } from '../../services/calendar-sync';
 | 
			
		||||
import { CoreSite } from '@classes/site';
 | 
			
		||||
import { Translate } from '@singletons';
 | 
			
		||||
import { CoreFilterHelper } from '@features/filter/services/filter-helper';
 | 
			
		||||
import { ActivatedRoute } from '@angular/router';
 | 
			
		||||
import { AddonCalendarOfflineEventDBRecord } from '../../services/database/calendar-offline';
 | 
			
		||||
import { CoreError } from '@classes/errors/error';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Page that displays a form to create/edit an event.
 | 
			
		||||
 */
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'page-addon-calendar-edit-event',
 | 
			
		||||
    templateUrl: 'edit-event.html',
 | 
			
		||||
    styleUrls: ['edit-event.scss'],
 | 
			
		||||
})
 | 
			
		||||
export class AddonCalendarEditEventPage implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
    @ViewChild(CoreEditorRichTextEditorComponent) descriptionEditor!: CoreEditorRichTextEditorComponent;
 | 
			
		||||
    @ViewChild('editEventForm') formElement!: ElementRef;
 | 
			
		||||
 | 
			
		||||
    title = 'addon.calendar.newevent';
 | 
			
		||||
    dateFormat: string;
 | 
			
		||||
    component = AddonCalendarProvider.COMPONENT;
 | 
			
		||||
    loaded = false;
 | 
			
		||||
    hasOffline = false;
 | 
			
		||||
    eventTypes: AddonCalendarEventTypeOption[] = [];
 | 
			
		||||
    categories: CoreCategoryData[] = [];
 | 
			
		||||
    courses: CoreCourseSearchedData[] | CoreEnrolledCourseData[] = [];
 | 
			
		||||
    groups: CoreGroup[] = [];
 | 
			
		||||
    loadingGroups = false;
 | 
			
		||||
    courseGroupSet = false;
 | 
			
		||||
    advanced = false;
 | 
			
		||||
    errors: Record<string, string>;
 | 
			
		||||
    error = false;
 | 
			
		||||
    eventRepeatId?: number;
 | 
			
		||||
    otherEventsCount = 0;
 | 
			
		||||
    eventId?: number;
 | 
			
		||||
 | 
			
		||||
    // Form variables.
 | 
			
		||||
    form: FormGroup;
 | 
			
		||||
    typeControl: FormControl;
 | 
			
		||||
    groupControl: FormControl;
 | 
			
		||||
    descriptionControl: FormControl;
 | 
			
		||||
 | 
			
		||||
    protected courseId!: number;
 | 
			
		||||
    protected originalData?: AddonCalendarOfflineEventDBRecord;
 | 
			
		||||
    protected currentSite: CoreSite;
 | 
			
		||||
    protected types: { [name: string]: boolean } = {}; // Object with the supported types.
 | 
			
		||||
    protected showAll = false;
 | 
			
		||||
    protected isDestroyed = false;
 | 
			
		||||
    protected gotEventData = false;
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        protected navCtrl: NavController,
 | 
			
		||||
        protected route: ActivatedRoute,
 | 
			
		||||
        protected fb: FormBuilder,
 | 
			
		||||
    ) {
 | 
			
		||||
 | 
			
		||||
        this.currentSite = CoreSites.instance.getCurrentSite()!;
 | 
			
		||||
        this.errors = {
 | 
			
		||||
            required: Translate.instance.instant('core.required'),
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Calculate format to use. ion-datetime doesn't support escaping characters ([]), so we remove them.
 | 
			
		||||
        this.dateFormat = CoreTimeUtils.instance.convertPHPToMoment(Translate.instance.instant('core.strftimedatetimeshort'))
 | 
			
		||||
            .replace(/[[\]]/g, '');
 | 
			
		||||
 | 
			
		||||
        this.form = new FormGroup({});
 | 
			
		||||
 | 
			
		||||
        // Initialize form variables.
 | 
			
		||||
        this.typeControl = this.fb.control('', Validators.required);
 | 
			
		||||
        this.groupControl = this.fb.control('');
 | 
			
		||||
        this.descriptionControl = this.fb.control('');
 | 
			
		||||
        this.form.addControl('name', this.fb.control('', Validators.required));
 | 
			
		||||
        this.form.addControl('eventtype', this.typeControl);
 | 
			
		||||
        this.form.addControl('categoryid', this.fb.control(''));
 | 
			
		||||
        this.form.addControl('groupcourseid', this.fb.control(''));
 | 
			
		||||
        this.form.addControl('groupid', this.groupControl);
 | 
			
		||||
        this.form.addControl('description', this.descriptionControl);
 | 
			
		||||
        this.form.addControl('location', this.fb.control(''));
 | 
			
		||||
        this.form.addControl('duration', this.fb.control(0));
 | 
			
		||||
        this.form.addControl('timedurationminutes', this.fb.control(''));
 | 
			
		||||
        this.form.addControl('repeat', this.fb.control(false));
 | 
			
		||||
        this.form.addControl('repeats', this.fb.control('1'));
 | 
			
		||||
        this.form.addControl('repeateditall', this.fb.control(1));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Component being initialized.
 | 
			
		||||
     */
 | 
			
		||||
    ngOnInit(): void {
 | 
			
		||||
        this.eventId = this.route.snapshot.queryParams['eventId'];
 | 
			
		||||
        this.courseId = parseInt(this.route.snapshot.queryParams['courseId'], 10) || 0;
 | 
			
		||||
        this.title = this.eventId ? 'addon.calendar.editevent' : 'addon.calendar.newevent';
 | 
			
		||||
 | 
			
		||||
        const timestamp = parseInt(this.route.snapshot.queryParams['timestamp'], 10);
 | 
			
		||||
        const currentDate = CoreTimeUtils.instance.toDatetimeFormat(timestamp);
 | 
			
		||||
        this.form.addControl('timestart', this.fb.control(currentDate, Validators.required));
 | 
			
		||||
        this.form.addControl('timedurationuntil', this.fb.control(currentDate));
 | 
			
		||||
        this.form.addControl('courseid', this.fb.control(this.courseId));
 | 
			
		||||
 | 
			
		||||
        this.fetchData().finally(() => {
 | 
			
		||||
            this.originalData = CoreUtils.instance.clone(this.form.value);
 | 
			
		||||
            this.loaded = true;
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Fetch the data needed to render the form.
 | 
			
		||||
     *
 | 
			
		||||
     * @param refresh Whether it's refreshing data.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async fetchData(): Promise<void> {
 | 
			
		||||
        let accessInfo: AddonCalendarGetCalendarAccessInformationWSResponse;
 | 
			
		||||
 | 
			
		||||
        this.error = false;
 | 
			
		||||
 | 
			
		||||
        // Get access info.
 | 
			
		||||
        try {
 | 
			
		||||
            accessInfo = await AddonCalendar.instance.getAccessInformation(this.courseId);
 | 
			
		||||
            this.types = await AddonCalendar.instance.getAllowedEventTypes(this.courseId);
 | 
			
		||||
 | 
			
		||||
            const promises: Promise<void>[] = [];
 | 
			
		||||
            const eventTypes = AddonCalendarHelper.instance.getEventTypeOptions(this.types);
 | 
			
		||||
 | 
			
		||||
            if (!eventTypes.length) {
 | 
			
		||||
                throw new CoreError(Translate.instance.instant('addon.calendar.nopermissiontoupdatecalendar'));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (this.eventId && !this.gotEventData) {
 | 
			
		||||
                // Editing an event, get the event data. Wait for sync first.
 | 
			
		||||
                promises.push(AddonCalendarSync.instance.waitForSync(AddonCalendarSyncProvider.SYNC_ID).then(async () => {
 | 
			
		||||
                    // Do not block if the scope is already destroyed.
 | 
			
		||||
                    if (!this.isDestroyed && this.eventId) {
 | 
			
		||||
                        CoreSync.instance.blockOperation(AddonCalendarProvider.COMPONENT, this.eventId);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    let eventForm: AddonCalendarEvent | AddonCalendarOfflineEventDBRecord | undefined;
 | 
			
		||||
 | 
			
		||||
                    // Get the event offline data if there's any.
 | 
			
		||||
                    try {
 | 
			
		||||
                        eventForm = await AddonCalendarOffline.instance.getEvent(this.eventId!);
 | 
			
		||||
 | 
			
		||||
                        this.hasOffline = true;
 | 
			
		||||
                    } catch {
 | 
			
		||||
                        // No offline data.
 | 
			
		||||
                        this.hasOffline = false;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if (this.eventId! > 0) {
 | 
			
		||||
                        // It's an online event. get its data from server.
 | 
			
		||||
                        const event = await AddonCalendar.instance.getEventById(this.eventId!);
 | 
			
		||||
 | 
			
		||||
                        if (!eventForm) {
 | 
			
		||||
                            eventForm = event; // Use offline data first.
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        this.eventRepeatId = event?.repeatid;
 | 
			
		||||
                        if (this.eventRepeatId) {
 | 
			
		||||
 | 
			
		||||
                            this.otherEventsCount = event.eventcount ? event.eventcount - 1 : 0;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    this.gotEventData = true;
 | 
			
		||||
 | 
			
		||||
                    if (eventForm) {
 | 
			
		||||
                        // Load the data in the form.
 | 
			
		||||
                        return this.loadEventData(eventForm, this.hasOffline);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return;
 | 
			
		||||
                }));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (this.types.category) {
 | 
			
		||||
                // Get the categories.
 | 
			
		||||
                promises.push(this.fetchCategories());
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.showAll = CoreUtils.instance.isTrueOrOne(this.currentSite.getStoredConfig('calendar_adminseesall')) &&
 | 
			
		||||
                accessInfo.canmanageentries;
 | 
			
		||||
 | 
			
		||||
            if (this.types.course || this.types.groups) {
 | 
			
		||||
                promises.push(this.fetchCourses());
 | 
			
		||||
            }
 | 
			
		||||
            await Promise.all(promises);
 | 
			
		||||
 | 
			
		||||
            if (!this.typeControl.value) {
 | 
			
		||||
                // Initialize event type value. If course is allowed, select it first.
 | 
			
		||||
                if (this.types.course) {
 | 
			
		||||
                    this.typeControl.setValue(AddonCalendarEventType.COURSE);
 | 
			
		||||
                } else {
 | 
			
		||||
                    this.typeControl.setValue(eventTypes[0].value);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.eventTypes = eventTypes;
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.instance.showErrorModalDefault(error, 'Error getting data.');
 | 
			
		||||
            this.error = true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected async fetchCategories(): Promise<void> {
 | 
			
		||||
        this.categories = await CoreCourses.instance.getCategories(0, true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected async fetchCourses(): Promise<void> {
 | 
			
		||||
        // Get the courses.
 | 
			
		||||
        let courses = await (this.showAll ? CoreCourses.instance.getCoursesByField() : CoreCourses.instance.getUserCourses());
 | 
			
		||||
 | 
			
		||||
        if (courses.length < 0) {
 | 
			
		||||
            this.courses = [];
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const courseFillterFullname = (course: CoreCourseSearchedData | CoreEnrolledCourseData): Promise<void> =>
 | 
			
		||||
            CoreFilterHelper.instance.getFiltersAndFormatText(course.fullname, 'course', course.id)
 | 
			
		||||
                .then((result) => {
 | 
			
		||||
                    course.fullname = result.text;
 | 
			
		||||
 | 
			
		||||
                    return;
 | 
			
		||||
                }).catch(() => {
 | 
			
		||||
                    // Ignore errors.
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        if (this.showAll) {
 | 
			
		||||
            // Remove site home from the list of courses.
 | 
			
		||||
            const siteHomeId = CoreSites.instance.getCurrentSiteHomeId();
 | 
			
		||||
 | 
			
		||||
            if ('contacts' in courses[0]) {
 | 
			
		||||
                courses = (courses as CoreCourseSearchedData[]).filter((course) => course.id != siteHomeId);
 | 
			
		||||
            } else {
 | 
			
		||||
                courses = (courses as CoreEnrolledCourseData[]).filter((course) => course.id != siteHomeId);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Format the name of the courses.
 | 
			
		||||
        if ('contacts' in courses[0]) {
 | 
			
		||||
            await Promise.all((courses as CoreCourseSearchedData[]).map(courseFillterFullname));
 | 
			
		||||
        } else {
 | 
			
		||||
            await Promise.all((courses as CoreEnrolledCourseData[]).map(courseFillterFullname));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        // Sort courses by name.
 | 
			
		||||
        this.courses = courses.sort((a, b) => {
 | 
			
		||||
            const compareA = a.fullname.toLowerCase();
 | 
			
		||||
            const compareB = b.fullname.toLowerCase();
 | 
			
		||||
 | 
			
		||||
            return compareA.localeCompare(compareB);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Load an event data into the form.
 | 
			
		||||
     *
 | 
			
		||||
     * @param event Event data.
 | 
			
		||||
     * @param isOffline Whether the data is from offline or not.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async loadEventData(
 | 
			
		||||
        event: AddonCalendarEvent | AddonCalendarOfflineEventDBRecord,
 | 
			
		||||
        isOffline: boolean,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        if (!event) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const offlineEvent = (event as AddonCalendarOfflineEventDBRecord);
 | 
			
		||||
        const onlineEvent = (event as AddonCalendarEvent);
 | 
			
		||||
 | 
			
		||||
        const courseId = isOffline ? offlineEvent.courseid : onlineEvent.course?.id;
 | 
			
		||||
 | 
			
		||||
        this.form.controls.name.setValue(event.name);
 | 
			
		||||
        this.form.controls.timestart.setValue(CoreTimeUtils.instance.toDatetimeFormat(event.timestart * 1000));
 | 
			
		||||
        this.form.controls.eventtype.setValue(event.eventtype);
 | 
			
		||||
        this.form.controls.categoryid.setValue(event.categoryid || '');
 | 
			
		||||
        this.form.controls.courseid.setValue(courseId || '');
 | 
			
		||||
        this.form.controls.groupcourseid.setValue(courseId || '');
 | 
			
		||||
        this.form.controls.groupid.setValue(event.groupid || '');
 | 
			
		||||
        this.form.controls.description.setValue(event.description);
 | 
			
		||||
        this.form.controls.location.setValue(event.location);
 | 
			
		||||
 | 
			
		||||
        if (isOffline) {
 | 
			
		||||
            // It's an offline event, use the data as it is.
 | 
			
		||||
            this.form.controls.duration.setValue(offlineEvent.duration);
 | 
			
		||||
            this.form.controls.timedurationuntil.setValue(
 | 
			
		||||
                CoreTimeUtils.instance.toDatetimeFormat(((offlineEvent.timedurationuntil || 0) * 1000) || Date.now()),
 | 
			
		||||
            );
 | 
			
		||||
            this.form.controls.timedurationminutes.setValue(offlineEvent.timedurationminutes || '');
 | 
			
		||||
            this.form.controls.repeat.setValue(!!offlineEvent.repeat);
 | 
			
		||||
            this.form.controls.repeats.setValue(offlineEvent.repeats || '1');
 | 
			
		||||
            this.form.controls.repeateditall.setValue(offlineEvent.repeateditall || 1);
 | 
			
		||||
        } else {
 | 
			
		||||
            // Online event, we'll have to calculate the data.
 | 
			
		||||
 | 
			
		||||
            if (onlineEvent.timeduration > 0) {
 | 
			
		||||
                this.form.controls.duration.setValue(1);
 | 
			
		||||
                this.form.controls.timedurationuntil.setValue(CoreTimeUtils.instance.toDatetimeFormat(
 | 
			
		||||
                    (onlineEvent.timestart + onlineEvent.timeduration) * 1000,
 | 
			
		||||
                ));
 | 
			
		||||
            } else {
 | 
			
		||||
                // No duration.
 | 
			
		||||
                this.form.controls.duration.setValue(0);
 | 
			
		||||
                this.form.controls.timedurationuntil.setValue(CoreTimeUtils.instance.toDatetimeFormat());
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.form.controls.timedurationminutes.setValue('');
 | 
			
		||||
            this.form.controls.repeat.setValue(!!onlineEvent.repeatid);
 | 
			
		||||
            this.form.controls.repeats.setValue(onlineEvent.eventcount || '1');
 | 
			
		||||
            this.form.controls.repeateditall.setValue(1);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (event.eventtype == AddonCalendarEventType.GROUP && courseId) {
 | 
			
		||||
            await this.loadGroups(courseId);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Pull to refresh.
 | 
			
		||||
     *
 | 
			
		||||
     * @param refresher Refresher.
 | 
			
		||||
     */
 | 
			
		||||
    refreshData(refresher?: CustomEvent<IonRefresher>): void {
 | 
			
		||||
        const promises = [
 | 
			
		||||
            AddonCalendar.instance.invalidateAccessInformation(this.courseId),
 | 
			
		||||
            AddonCalendar.instance.invalidateAllowedEventTypes(this.courseId),
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        if (this.types) {
 | 
			
		||||
            if (this.types.category) {
 | 
			
		||||
                promises.push(CoreCourses.instance.invalidateCategories(0, true));
 | 
			
		||||
            }
 | 
			
		||||
            if (this.types.course || this.types.groups) {
 | 
			
		||||
                if (this.showAll) {
 | 
			
		||||
                    promises.push(CoreCourses.instance.invalidateCoursesByField());
 | 
			
		||||
                } else {
 | 
			
		||||
                    promises.push(CoreCourses.instance.invalidateUserCourses());
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Promise.all(promises).finally(() => {
 | 
			
		||||
            this.fetchData().finally(() => {
 | 
			
		||||
                refresher?.detail.complete();
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * A course was selected, get its groups.
 | 
			
		||||
     *
 | 
			
		||||
     * @param courseId Course ID.
 | 
			
		||||
     */
 | 
			
		||||
    async groupCourseSelected(courseId: number): Promise<void> {
 | 
			
		||||
        if (!courseId) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const modal = await CoreDomUtils.instance.showModalLoading();
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await this.loadGroups(courseId);
 | 
			
		||||
 | 
			
		||||
            this.groupControl.setValue('');
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.instance.showErrorModalDefault(error, 'Error getting data.');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        modal.dismiss();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Load groups of a certain course.
 | 
			
		||||
     *
 | 
			
		||||
     * @param courseId Course ID.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async loadGroups(courseId: number): Promise<void> {
 | 
			
		||||
        this.loadingGroups = true;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            this.groups = await CoreGroups.instance.getUserGroupsInCourse(courseId);
 | 
			
		||||
            this.courseGroupSet = true;
 | 
			
		||||
        } finally {
 | 
			
		||||
            this.loadingGroups = false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Show or hide advanced form fields.
 | 
			
		||||
     */
 | 
			
		||||
    toggleAdvanced(): void {
 | 
			
		||||
        this.advanced = !this.advanced;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    selectDuration(duration: string): void {
 | 
			
		||||
        this.form.controls.duration.setValue(duration);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create the event.
 | 
			
		||||
     */
 | 
			
		||||
    async submit(): Promise<void> {
 | 
			
		||||
        // Validate data.
 | 
			
		||||
        const formData = this.form.value;
 | 
			
		||||
        const timeStartDate = CoreTimeUtils.instance.convertToTimestamp(formData.timestart);
 | 
			
		||||
        const timeUntilDate = CoreTimeUtils.instance.convertToTimestamp(formData.timedurationuntil);
 | 
			
		||||
        const timeDurationMinutes = parseInt(formData.timedurationminutes || '', 10);
 | 
			
		||||
        let error: string | undefined;
 | 
			
		||||
 | 
			
		||||
        if (formData.eventtype == AddonCalendarEventType.COURSE && !formData.courseid) {
 | 
			
		||||
            error = 'core.selectacourse';
 | 
			
		||||
        } else if (formData.eventtype == AddonCalendarEventType.GROUP && !formData.groupcourseid) {
 | 
			
		||||
            error = 'core.selectacourse';
 | 
			
		||||
        } else if (formData.eventtype == AddonCalendarEventType.GROUP && !formData.groupid) {
 | 
			
		||||
            error = 'core.selectagroup';
 | 
			
		||||
        } else if (formData.eventtype == AddonCalendarEventType.CATEGORY && !formData.categoryid) {
 | 
			
		||||
            error = 'core.selectacategory';
 | 
			
		||||
        } else if (formData.duration == 1 && timeStartDate > timeUntilDate) {
 | 
			
		||||
            error = 'addon.calendar.invalidtimedurationuntil';
 | 
			
		||||
        } else if (formData.duration == 2 && (isNaN(timeDurationMinutes) || timeDurationMinutes < 1)) {
 | 
			
		||||
            error = 'addon.calendar.invalidtimedurationminutes';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (error) {
 | 
			
		||||
            // Show error and stop.
 | 
			
		||||
            CoreDomUtils.instance.showErrorModal(Translate.instance.instant(error));
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Format the data to send.
 | 
			
		||||
        const data: AddonCalendarSubmitCreateUpdateFormDataWSParams = {
 | 
			
		||||
            name: formData.name,
 | 
			
		||||
            eventtype: formData.eventtype,
 | 
			
		||||
            timestart: timeStartDate,
 | 
			
		||||
            description: {
 | 
			
		||||
                text: formData.description || '',
 | 
			
		||||
                format: 1,
 | 
			
		||||
            },
 | 
			
		||||
            location: formData.location,
 | 
			
		||||
            duration: formData.duration,
 | 
			
		||||
            repeat: formData.repeat,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if (formData.eventtype == AddonCalendarEventType.COURSE) {
 | 
			
		||||
            data.courseid = formData.courseid;
 | 
			
		||||
        } else if (formData.eventtype == AddonCalendarEventType.GROUP) {
 | 
			
		||||
            data.groupcourseid = formData.groupcourseid;
 | 
			
		||||
            data.groupid = formData.groupid;
 | 
			
		||||
        } else if (formData.eventtype == AddonCalendarEventType.CATEGORY) {
 | 
			
		||||
            data.categoryid = formData.categoryid;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (formData.duration == 1) {
 | 
			
		||||
            data.timedurationuntil = timeUntilDate;
 | 
			
		||||
        } else if (formData.duration == 2) {
 | 
			
		||||
            data.timedurationminutes = formData.timedurationminutes;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (formData.repeat) {
 | 
			
		||||
            data.repeats = Number(formData.repeats);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.eventRepeatId) {
 | 
			
		||||
            data.repeatid = this.eventRepeatId;
 | 
			
		||||
            data.repeateditall = formData.repeateditall;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Send the data.
 | 
			
		||||
        const modal = await CoreDomUtils.instance.showModalLoading('core.sending', true);
 | 
			
		||||
        let event: AddonCalendarEvent | AddonCalendarOfflineEventDBRecord;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const result = await AddonCalendar.instance.submitEvent(this.eventId, data);
 | 
			
		||||
            event = result.event;
 | 
			
		||||
 | 
			
		||||
            CoreDomUtils.instance.triggerFormSubmittedEvent(this.formElement, result.sent, this.currentSite.getId());
 | 
			
		||||
 | 
			
		||||
            if (result.sent) {
 | 
			
		||||
                // Event created or edited, invalidate right days & months.
 | 
			
		||||
                const numberOfRepetitions = formData.repeat ? formData.repeats :
 | 
			
		||||
                    (data.repeateditall && this.otherEventsCount ? this.otherEventsCount + 1 : 1);
 | 
			
		||||
 | 
			
		||||
                try {
 | 
			
		||||
                    await AddonCalendarHelper.instance.refreshAfterChangeEvent(result.event, numberOfRepetitions);
 | 
			
		||||
                } catch  {
 | 
			
		||||
                    // Ignore errors.
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.returnToList(event);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.instance.showErrorModalDefault(error, 'Error sending data.');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        modal.dismiss();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Convenience function to update or return to event list depending on device.
 | 
			
		||||
     *
 | 
			
		||||
     * @param event Event.
 | 
			
		||||
     */
 | 
			
		||||
    protected returnToList(event?: AddonCalendarEvent | AddonCalendarOfflineEventDBRecord): void {
 | 
			
		||||
        // Unblock the sync because the view will be destroyed and the sync process could be triggered before ngOnDestroy.
 | 
			
		||||
        this.unblockSync();
 | 
			
		||||
 | 
			
		||||
        if (this.eventId && this.eventId > 0) {
 | 
			
		||||
            // Editing an event.
 | 
			
		||||
            CoreEvents.trigger<AddonCalendarUpdatedEventEvent>(
 | 
			
		||||
                AddonCalendarProvider.EDIT_EVENT_EVENT,
 | 
			
		||||
                { eventId: this.eventId },
 | 
			
		||||
                this.currentSite.getId(),
 | 
			
		||||
            );
 | 
			
		||||
        } else {
 | 
			
		||||
            if (event) {
 | 
			
		||||
                CoreEvents.trigger<AddonCalendarUpdatedEventEvent>(
 | 
			
		||||
                    AddonCalendarProvider.NEW_EVENT_EVENT,
 | 
			
		||||
                    { eventId: event.id! },
 | 
			
		||||
                    this.currentSite.getId(),
 | 
			
		||||
                );
 | 
			
		||||
            } else {
 | 
			
		||||
                CoreEvents.trigger(AddonCalendarProvider.NEW_EVENT_DISCARDED_EVENT, {}, this.currentSite.getId());
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /* if (this.svComponent && this.svComponent.isOn()) {
 | 
			
		||||
            // Empty form.
 | 
			
		||||
            this.hasOffline = false;
 | 
			
		||||
            this.form.reset(this.originalData);
 | 
			
		||||
            this.originalData = CoreUtils.instance.clone(this.form.value);
 | 
			
		||||
        } else {*/
 | 
			
		||||
        this.originalData = undefined; // Avoid asking for confirmation.
 | 
			
		||||
        this.navCtrl.pop();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Discard an offline saved discussion.
 | 
			
		||||
     */
 | 
			
		||||
    async discard(): Promise<void> {
 | 
			
		||||
        try {
 | 
			
		||||
            await CoreDomUtils.instance.showConfirm(Translate.instance.instant('core.areyousure'));
 | 
			
		||||
            try {
 | 
			
		||||
                await AddonCalendarOffline.instance.deleteEvent(this.eventId!);
 | 
			
		||||
 | 
			
		||||
                CoreDomUtils.instance.triggerFormCancelledEvent(this.formElement, this.currentSite.getId());
 | 
			
		||||
 | 
			
		||||
                this.returnToList();
 | 
			
		||||
            } catch {
 | 
			
		||||
                // Shouldn't happen.
 | 
			
		||||
                CoreDomUtils.instance.showErrorModal('Error discarding event.');
 | 
			
		||||
            }
 | 
			
		||||
        } catch {
 | 
			
		||||
            // Ignore errors
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if we can leave the page or not.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Resolved if we can leave it, rejected if not.
 | 
			
		||||
     */
 | 
			
		||||
    async ionViewCanLeave(): Promise<void> {
 | 
			
		||||
        if (AddonCalendarHelper.instance.hasEventDataChanged(this.form.value, this.originalData)) {
 | 
			
		||||
            // Show confirmation if some data has been modified.
 | 
			
		||||
            await CoreDomUtils.instance.showConfirm(Translate.instance.instant('core.confirmcanceledit'));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        CoreDomUtils.instance.triggerFormCancelledEvent(this.formElement, this.currentSite.getId());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Unblock sync.
 | 
			
		||||
     */
 | 
			
		||||
    protected unblockSync(): void {
 | 
			
		||||
        if (this.eventId) {
 | 
			
		||||
            CoreSync.instance.unblockOperation(AddonCalendarProvider.COMPONENT, this.eventId);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Page destroyed.
 | 
			
		||||
     */
 | 
			
		||||
    ngOnDestroy(): void {
 | 
			
		||||
        this.unblockSync();
 | 
			
		||||
        this.isDestroyed = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										11
									
								
								src/addons/calendar/pages/edit-event/edit-event.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,11 @@
 | 
			
		||||
:host {
 | 
			
		||||
    .addon-calendar-eventtype-container.item-select-disabled {
 | 
			
		||||
        ion-label, ion-select {
 | 
			
		||||
            opacity: 1;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ion-select::part(icon) {
 | 
			
		||||
            display: none;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										169
									
								
								src/addons/calendar/pages/event/event.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,169 @@
 | 
			
		||||
<ion-header>
 | 
			
		||||
    <ion-toolbar>
 | 
			
		||||
        <ion-buttons slot="start">
 | 
			
		||||
            <ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
        <ion-title *ngIf="event">
 | 
			
		||||
            <img *ngIf="event.moduleIcon" src="{{event.moduleIcon}}" alt="" role="presentation" class="core-module-icon">
 | 
			
		||||
            <ion-icon *ngIf="event.eventIcon && !event.moduleIcon" [name]="event.eventIcon"></ion-icon>
 | 
			
		||||
            <core-format-text [text]="event.name" [contextLevel]="event.contextLevel"
 | 
			
		||||
                [contextInstanceId]="event.contextInstanceId"></core-format-text>
 | 
			
		||||
        </ion-title>
 | 
			
		||||
        <ion-buttons slot="end">
 | 
			
		||||
            <!-- The context menu will be added in here. -->
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
    </ion-toolbar>
 | 
			
		||||
</ion-header>
 | 
			
		||||
<core-navbar-buttons slot="end">
 | 
			
		||||
    <core-context-menu>
 | 
			
		||||
        <core-context-menu-item [hidden]="isSplitViewOn || !eventLoaded || (!hasOffline && event && !event.deleted) || !isOnline"
 | 
			
		||||
            [priority]="400" [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(undefined, $event, true)"
 | 
			
		||||
            [iconAction]="syncIcon" [closeOnClick]="false">
 | 
			
		||||
        </core-context-menu-item>
 | 
			
		||||
        <core-context-menu-item [hidden]="!canEdit || !event || !event.canedit || event.deleted" [priority]="300"
 | 
			
		||||
            [content]="'core.edit' | translate" (action)="openEdit()" iconAction="fas-edit">
 | 
			
		||||
        </core-context-menu-item>
 | 
			
		||||
        <core-context-menu-item [hidden]="!canDelete || !event || !event.candelete || event.deleted" [priority]="200"
 | 
			
		||||
            [content]="'core.delete' | translate" (action)="deleteEvent()"
 | 
			
		||||
            iconAction="fas-trash"></core-context-menu-item>
 | 
			
		||||
        <core-context-menu-item [hidden]="!event || !event.deleted" [priority]="200" [content]="'core.restore' | translate"
 | 
			
		||||
            (action)="undoDelete()" iconAction="fas-undo-alt"></core-context-menu-item>
 | 
			
		||||
    </core-context-menu>
 | 
			
		||||
</core-navbar-buttons>
 | 
			
		||||
<ion-content>
 | 
			
		||||
    <ion-refresher slot="fixed" [disabled]="!eventLoaded" (ionRefresh)="doRefresh($event)">
 | 
			
		||||
        <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
 | 
			
		||||
    </ion-refresher>
 | 
			
		||||
    <core-loading [hideUntil]="eventLoaded">
 | 
			
		||||
        <!-- There is data to be synchronized -->
 | 
			
		||||
        <ion-card class="core-warning-card" *ngIf="hasOffline || (event && event.deleted)">
 | 
			
		||||
            <ion-item>
 | 
			
		||||
                <ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon>
 | 
			
		||||
                <ion-label>{{ 'core.hasdatatosync' | translate:{$a: 'addon.calendar.calendarevent' | translate} }}</ion-label>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
        </ion-card>
 | 
			
		||||
 | 
			
		||||
        <ion-card>
 | 
			
		||||
            <ion-card-content *ngIf="event">
 | 
			
		||||
                <ion-item class="ion-text-wrap" *ngIf="isSplitViewOn">
 | 
			
		||||
                    <img *ngIf="event.moduleIcon" src="{{event.moduleIcon}}" slot="start" alt="" role="presentation"
 | 
			
		||||
                        class="core-module-icon">
 | 
			
		||||
                    <ion-icon *ngIf="event.eventIcon && !event.moduleIcon" [name]="event.eventIcon" slot="start">
 | 
			
		||||
                    </ion-icon>
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <h2>{{ 'addon.calendar.eventname' | translate }}</h2>
 | 
			
		||||
                        <p>
 | 
			
		||||
                            <core-format-text [text]="event.name" [contextLevel]="event.contextLevel"
 | 
			
		||||
                             [contextInstanceId]="event.contextInstanceId"></core-format-text>
 | 
			
		||||
                        </p>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                    <ion-note slot="end" *ngIf="event.deleted">
 | 
			
		||||
                        <ion-icon name="fas-trash"></ion-icon> {{ 'core.deletedoffline' | translate }}
 | 
			
		||||
                    </ion-note>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
                <ion-item>
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <h2>{{ 'addon.calendar.when' | translate }}</h2>
 | 
			
		||||
                        <p [innerHTML]="event.formattedtime"></p>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                    <ion-note slot="end" *ngIf="!isSplitViewOn && event.deleted">
 | 
			
		||||
                        <ion-icon name="fas-trash"></ion-icon> {{ 'core.deletedoffline' | translate }}
 | 
			
		||||
                    </ion-note>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
                <ion-item>
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <h2>{{ 'addon.calendar.eventtype' | translate }}</h2>
 | 
			
		||||
                        <p>{{ 'addon.calendar.type' + event.formattedType | translate }}</p>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
                <ion-item class="ion-text-wrap" *ngIf="courseName" [href]="courseUrl" core-link capture="true">
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <h2>{{ 'core.course' | translate}}</h2>
 | 
			
		||||
                        <p>
 | 
			
		||||
                            <core-format-text [text]="courseName" contextLevel="course" [contextInstanceId]="courseId">
 | 
			
		||||
                            </core-format-text>
 | 
			
		||||
                        </p>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
                <ion-item class="ion-text-wrap" *ngIf="groupName">
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <h2>{{ 'core.group' | translate}}</h2>
 | 
			
		||||
                        <p>{{ groupName }}</p>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
                <ion-item class="ion-text-wrap" *ngIf="categoryPath">
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <h2>{{ 'core.category' | translate}}</h2>
 | 
			
		||||
                        <p><core-format-text [text]="categoryPath" contextLevel="coursecat"
 | 
			
		||||
                            [contextInstanceId]="event.categoryid"></core-format-text></p>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
                <ion-item class="ion-text-wrap" *ngIf="event.description">
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <h2>{{ 'core.description' | translate}}</h2>
 | 
			
		||||
                        <p>
 | 
			
		||||
                            <core-format-text [text]="event.description" [contextLevel]="event.contextLevel"
 | 
			
		||||
                                [contextInstanceId]="event.contextInstanceId"></core-format-text>
 | 
			
		||||
                        </p>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
                <ion-item class="ion-text-wrap" *ngIf="event.location">
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <h2>{{ 'core.location' | translate}}</h2>
 | 
			
		||||
                        <p>
 | 
			
		||||
                            <a [href]="event.encodedLocation" core-link auto-login="no">
 | 
			
		||||
                                <core-format-text [text]="event.location" [contextLevel]="event.contextLevel"
 | 
			
		||||
                                    [contextInstanceId]="event.contextInstanceId"></core-format-text>
 | 
			
		||||
                            </a>
 | 
			
		||||
                        </p>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
                <ion-item *ngIf="moduleUrl">
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <ion-button expand="block" color="primary" [href]="moduleUrl" core-link capture="true">
 | 
			
		||||
                            {{ 'addon.calendar.gotoactivity' | translate }}
 | 
			
		||||
                        </ion-button>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
            </ion-card-content>
 | 
			
		||||
        </ion-card>
 | 
			
		||||
 | 
			
		||||
        <ion-card list *ngIf="notificationsEnabled && event">
 | 
			
		||||
            <ion-item>
 | 
			
		||||
                <ion-label>
 | 
			
		||||
                    <h2>{{ 'addon.calendar.reminders' | translate }}</h2>
 | 
			
		||||
                </ion-label>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
            <ng-container *ngFor="let reminder of reminders">
 | 
			
		||||
                <ion-item  *ngIf="reminder.time > 0 || defaultTime > 0" class="ion-text-wrap"
 | 
			
		||||
                    [class.item-dimmed]="(reminder.time == -1 ? (event.timestart - defaultTime) : reminder.time) <= currentTime!">
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <p *ngIf="reminder.time == -1">
 | 
			
		||||
                            {{ 'core.defaultvalue' | translate :{$a: ((event.timestart - defaultTime) * 1000) | coreFormatDate } }}
 | 
			
		||||
                        </p>
 | 
			
		||||
                        <p *ngIf="reminder.time > 0">{{ reminder.time * 1000 | coreFormatDate }}</p>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                    <ion-button fill="clear" (click)="cancelNotification(reminder.id, $event)"
 | 
			
		||||
                        [attr.aria-label]=" 'core.delete' | translate" slot="end"
 | 
			
		||||
                        *ngIf="(reminder.time == -1 ? (event.timestart - defaultTime) : reminder.time) > currentTime!">
 | 
			
		||||
                        <ion-icon name="fas-trash" color="danger" slot="icon-only"></ion-icon>
 | 
			
		||||
                    </ion-button>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
            </ng-container>
 | 
			
		||||
 | 
			
		||||
            <ng-container *ngIf="event.timestart + event.timeduration > currentTime!">
 | 
			
		||||
                <ion-item>
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <ion-button expand="block" color="primary" (click)="notificationPicker.open()">
 | 
			
		||||
                            {{ 'addon.calendar.setnewreminder' | translate }}
 | 
			
		||||
                        </ion-button>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
                <ion-datetime #notificationPicker hidden [(ngModel)]="notificationTimeText"
 | 
			
		||||
                    [displayFormat]="notificationFormat" [min]="notificationMin" [max]="notificationMax"
 | 
			
		||||
                    doneText]="'core.add' | translate"(ionChange)="addNotificationTime()">
 | 
			
		||||
                </ion-datetime>
 | 
			
		||||
            </ng-container>
 | 
			
		||||
        </ion-card>
 | 
			
		||||
    </core-loading>
 | 
			
		||||
</ion-content>
 | 
			
		||||
							
								
								
									
										53
									
								
								src/addons/calendar/pages/event/event.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,53 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { NgModule } from '@angular/core';
 | 
			
		||||
import { IonicModule } from '@ionic/angular';
 | 
			
		||||
import { TranslateModule } from '@ngx-translate/core';
 | 
			
		||||
import { RouterModule, Routes } from '@angular/router';
 | 
			
		||||
import { CommonModule } from '@angular/common';
 | 
			
		||||
import { FormsModule } from '@angular/forms';
 | 
			
		||||
 | 
			
		||||
import { CoreComponentsModule } from '@components/components.module';
 | 
			
		||||
import { CoreDirectivesModule } from '@directives/directives.module';
 | 
			
		||||
import { CorePipesModule } from '@pipes/pipes.module';
 | 
			
		||||
import { AddonCalendarComponentsModule } from '../../components/components.module';
 | 
			
		||||
 | 
			
		||||
import { AddonCalendarEventPage } from './event.page';
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
    {
 | 
			
		||||
        path: '',
 | 
			
		||||
        component: AddonCalendarEventPage,
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    imports: [
 | 
			
		||||
        RouterModule.forChild(routes),
 | 
			
		||||
        CommonModule,
 | 
			
		||||
        IonicModule,
 | 
			
		||||
        FormsModule,
 | 
			
		||||
        TranslateModule.forChild(),
 | 
			
		||||
        CoreComponentsModule,
 | 
			
		||||
        CoreDirectivesModule,
 | 
			
		||||
        CorePipesModule,
 | 
			
		||||
        AddonCalendarComponentsModule,
 | 
			
		||||
    ],
 | 
			
		||||
    declarations: [
 | 
			
		||||
        AddonCalendarEventPage,
 | 
			
		||||
    ],
 | 
			
		||||
    exports: [RouterModule],
 | 
			
		||||
})
 | 
			
		||||
export class AddonCalendarEventPageModule {}
 | 
			
		||||
							
								
								
									
										582
									
								
								src/addons/calendar/pages/event/event.page.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,582 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Component, OnDestroy, OnInit } from '@angular/core';
 | 
			
		||||
import { IonRefresher } from '@ionic/angular';
 | 
			
		||||
import { AlertOptions } from '@ionic/core';
 | 
			
		||||
import {
 | 
			
		||||
    AddonCalendar,
 | 
			
		||||
    AddonCalendarEvent,
 | 
			
		||||
    AddonCalendarEventBase,
 | 
			
		||||
    AddonCalendarEventToDisplay,
 | 
			
		||||
    AddonCalendarGetEventsEvent,
 | 
			
		||||
    AddonCalendarProvider,
 | 
			
		||||
    AddonCalendarUpdatedEventEvent,
 | 
			
		||||
} from '../../services/calendar';
 | 
			
		||||
import { AddonCalendarHelper } from '../../services/calendar-helper';
 | 
			
		||||
import { AddonCalendarOffline } from '../../services/calendar-offline';
 | 
			
		||||
import { AddonCalendarSync, AddonCalendarSyncEvents, AddonCalendarSyncProvider } from '../../services/calendar-sync';
 | 
			
		||||
import { CoreCourses } from '@features/courses/services/courses';
 | 
			
		||||
import { CoreApp } from '@services/app';
 | 
			
		||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { CoreTextUtils } from '@services/utils/text';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreLocalNotifications } from '@services/local-notifications';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
import { CoreTimeUtils } from '@services/utils/time';
 | 
			
		||||
import { CoreGroups } from '@services/groups';
 | 
			
		||||
// @todo import { CoreSplitViewComponent } from '@components/split-view/split-view';
 | 
			
		||||
import { Network, NgZone, Translate } from '@singletons';
 | 
			
		||||
import { Subscription } from 'rxjs';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { AddonCalendarReminderDBRecord } from '../../services/database/calendar';
 | 
			
		||||
import { ActivatedRoute } from '@angular/router';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Page that displays a single calendar event.
 | 
			
		||||
 */
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'page-addon-calendar-event',
 | 
			
		||||
    templateUrl: 'event.html',
 | 
			
		||||
    styleUrls: ['event.scss'],
 | 
			
		||||
})
 | 
			
		||||
export class AddonCalendarEventPage implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
    protected eventId!: number;
 | 
			
		||||
    protected siteHomeId: number;
 | 
			
		||||
    protected editEventObserver: CoreEventObserver;
 | 
			
		||||
    protected syncObserver: CoreEventObserver;
 | 
			
		||||
    protected manualSyncObserver: CoreEventObserver;
 | 
			
		||||
    protected onlineObserver: Subscription;
 | 
			
		||||
    protected currentSiteId: string;
 | 
			
		||||
 | 
			
		||||
    eventLoaded = false;
 | 
			
		||||
    notificationFormat?: string;
 | 
			
		||||
    notificationMin?: string;
 | 
			
		||||
    notificationMax?: string;
 | 
			
		||||
    notificationTimeText?: string;
 | 
			
		||||
    event?: AddonCalendarEventToDisplay;
 | 
			
		||||
    courseId?: number;
 | 
			
		||||
    courseName = '';
 | 
			
		||||
    groupName?: string;
 | 
			
		||||
    courseUrl = '';
 | 
			
		||||
    notificationsEnabled = false;
 | 
			
		||||
    moduleUrl = '';
 | 
			
		||||
    categoryPath = '';
 | 
			
		||||
    currentTime?: number;
 | 
			
		||||
    defaultTime = 0;
 | 
			
		||||
    reminders: AddonCalendarReminderDBRecord[] = [];
 | 
			
		||||
    canEdit = false;
 | 
			
		||||
    canDelete = false;
 | 
			
		||||
    hasOffline = false;
 | 
			
		||||
    isOnline = false;
 | 
			
		||||
    syncIcon = 'spinner'; // Sync icon.
 | 
			
		||||
    isSplitViewOn = false;
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        protected route: ActivatedRoute,
 | 
			
		||||
        // @Optional() private svComponent: CoreSplitViewComponent,
 | 
			
		||||
    ) {
 | 
			
		||||
 | 
			
		||||
        this.notificationsEnabled = CoreLocalNotifications.instance.isAvailable();
 | 
			
		||||
        this.siteHomeId = CoreSites.instance.getCurrentSiteHomeId();
 | 
			
		||||
        this.currentSiteId = CoreSites.instance.getCurrentSiteId();
 | 
			
		||||
        // this.isSplitViewOn = this.svComponent && this.svComponent.isOn();
 | 
			
		||||
 | 
			
		||||
        // Check if site supports editing and deleting. No need to check allowed types, event.canedit already does it.
 | 
			
		||||
        this.canEdit = AddonCalendar.instance.canEditEventsInSite();
 | 
			
		||||
        this.canDelete = AddonCalendar.instance.canDeleteEventsInSite();
 | 
			
		||||
 | 
			
		||||
        this.asyncConstructor();
 | 
			
		||||
 | 
			
		||||
        // Listen for event edited. If current event is edited, reload the data.
 | 
			
		||||
        this.editEventObserver = CoreEvents.on(AddonCalendarProvider.EDIT_EVENT_EVENT, (data: AddonCalendarUpdatedEventEvent) => {
 | 
			
		||||
            if (data && data.eventId == this.eventId) {
 | 
			
		||||
                this.eventLoaded = false;
 | 
			
		||||
                this.refreshEvent(true, false);
 | 
			
		||||
            }
 | 
			
		||||
        }, this.currentSiteId);
 | 
			
		||||
 | 
			
		||||
        // Refresh data if this calendar event is synchronized automatically.
 | 
			
		||||
        this.syncObserver = CoreEvents.on(
 | 
			
		||||
            AddonCalendarSyncProvider.AUTO_SYNCED,
 | 
			
		||||
            this.checkSyncResult.bind(this, false),
 | 
			
		||||
            this.currentSiteId,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Refresh data if calendar events are synchronized manually but not by this page.
 | 
			
		||||
        this.manualSyncObserver = CoreEvents.on(
 | 
			
		||||
            AddonCalendarSyncProvider.MANUAL_SYNCED,
 | 
			
		||||
            this.checkSyncResult.bind(this, true),
 | 
			
		||||
            this.currentSiteId,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Refresh online status when changes.
 | 
			
		||||
        this.onlineObserver = Network.instance.onChange().subscribe(() => {
 | 
			
		||||
            // Execute the callback in the Angular zone, so change detection doesn't stop working.
 | 
			
		||||
            NgZone.instance.run(() => {
 | 
			
		||||
                this.isOnline = CoreApp.instance.isOnline();
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected async asyncConstructor(): Promise<void> {
 | 
			
		||||
        if (this.notificationsEnabled) {
 | 
			
		||||
            this.reminders = await AddonCalendar.instance.getEventReminders(this.eventId);
 | 
			
		||||
            this.defaultTime = await AddonCalendar.instance.getDefaultNotificationTime() * 60;
 | 
			
		||||
 | 
			
		||||
            // Calculate format to use.
 | 
			
		||||
            this.notificationFormat =
 | 
			
		||||
                CoreTimeUtils.instance.fixFormatForDatetime(CoreTimeUtils.instance.convertPHPToMoment(
 | 
			
		||||
                    Translate.instance.instant('core.strftimedatetime'),
 | 
			
		||||
                ));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * View loaded.
 | 
			
		||||
     */
 | 
			
		||||
    ngOnInit(): void {
 | 
			
		||||
        this.eventId = this.route.snapshot.queryParams['id'];
 | 
			
		||||
 | 
			
		||||
        this.syncIcon = 'spinner';
 | 
			
		||||
 | 
			
		||||
        this.fetchEvent();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Fetches the event and updates the view.
 | 
			
		||||
     *
 | 
			
		||||
     * @param sync Whether it should try to synchronize offline events.
 | 
			
		||||
     * @param showErrors Whether to show sync errors to the user.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async fetchEvent(sync = false, showErrors = false): Promise<void> {
 | 
			
		||||
        const currentSite = CoreSites.instance.getCurrentSite();
 | 
			
		||||
        const canGetById = AddonCalendar.instance.isGetEventByIdAvailableInSite();
 | 
			
		||||
        let deleted = false;
 | 
			
		||||
 | 
			
		||||
        this.isOnline = CoreApp.instance.isOnline();
 | 
			
		||||
 | 
			
		||||
        if (sync) {
 | 
			
		||||
            // Try to synchronize offline events.
 | 
			
		||||
            try {
 | 
			
		||||
                const result = await AddonCalendarSync.instance.syncEvents();
 | 
			
		||||
                if (result.warnings && result.warnings.length) {
 | 
			
		||||
                    CoreDomUtils.instance.showErrorModal(result.warnings[0]);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (result.deleted && result.deleted.indexOf(this.eventId) != -1) {
 | 
			
		||||
                    // This event was deleted during the sync.
 | 
			
		||||
                    deleted = true;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (result.updated) {
 | 
			
		||||
                    // Trigger a manual sync event.
 | 
			
		||||
                    result.source = 'event';
 | 
			
		||||
 | 
			
		||||
                    CoreEvents.trigger<AddonCalendarSyncEvents>(
 | 
			
		||||
                        AddonCalendarSyncProvider.MANUAL_SYNCED,
 | 
			
		||||
                        result,
 | 
			
		||||
                        this.currentSiteId,
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                if (showErrors) {
 | 
			
		||||
                    CoreDomUtils.instance.showErrorModalDefault(error, 'core.errorsync', true);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (deleted) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            let event: AddonCalendarEvent | AddonCalendarEventBase | AddonCalendarGetEventsEvent;
 | 
			
		||||
            // Get the event data.
 | 
			
		||||
            if (canGetById) {
 | 
			
		||||
                event = await AddonCalendar.instance.getEventById(this.eventId);
 | 
			
		||||
            } else {
 | 
			
		||||
                event = await AddonCalendar.instance.getEvent(this.eventId);
 | 
			
		||||
            }
 | 
			
		||||
            this.event = AddonCalendarHelper.instance.formatEventData(event);
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                const offlineEvent = AddonCalendarHelper.instance.formatOfflineEventData(
 | 
			
		||||
                    await AddonCalendarOffline.instance.getEvent(this.eventId),
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                // There is offline data, apply it.
 | 
			
		||||
                this.hasOffline = true;
 | 
			
		||||
 | 
			
		||||
                this.event = Object.assign(this.event, offlineEvent);
 | 
			
		||||
            } catch {
 | 
			
		||||
                // No offline data.
 | 
			
		||||
                this.hasOffline = false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.currentTime = CoreTimeUtils.instance.timestamp();
 | 
			
		||||
            this.notificationMin = CoreTimeUtils.instance.userDate(this.currentTime * 1000, 'YYYY-MM-DDTHH:mm', false);
 | 
			
		||||
            this.notificationMax = CoreTimeUtils.instance.userDate(
 | 
			
		||||
                (this.event!.timestart + this.event!.timeduration) * 1000,
 | 
			
		||||
                'YYYY-MM-DDTHH:mm',
 | 
			
		||||
                false,
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            // Reset some of the calculated data.
 | 
			
		||||
            this.categoryPath = '';
 | 
			
		||||
            this.courseName = '';
 | 
			
		||||
            this.courseUrl = '';
 | 
			
		||||
            this.moduleUrl = '';
 | 
			
		||||
 | 
			
		||||
            if (this.event!.moduleIcon) {
 | 
			
		||||
                // It's a module event, translate the module name to the current language.
 | 
			
		||||
                const name = CoreCourse.instance.translateModuleName(this.event!.modulename || '');
 | 
			
		||||
                if (name.indexOf('core.mod_') === -1) {
 | 
			
		||||
                    this.event!.modulename = name;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Get the module URL.
 | 
			
		||||
                if (canGetById) {
 | 
			
		||||
                    this.moduleUrl = this.event!.url || '';
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const promises: Promise<void>[] = [];
 | 
			
		||||
 | 
			
		||||
            const courseId = this.event.courseid;
 | 
			
		||||
            if (courseId != this.siteHomeId) {
 | 
			
		||||
                // If the event belongs to a course, get the course name and the URL to view it.
 | 
			
		||||
                if (canGetById && this.event.course) {
 | 
			
		||||
                    this.courseId = this.event.course.id;
 | 
			
		||||
                    this.courseName = this.event.course.fullname;
 | 
			
		||||
                    this.courseUrl = this.event.course.viewurl;
 | 
			
		||||
                } else if (!canGetById && this.event.courseid ) {
 | 
			
		||||
                    // Retrieve the course.
 | 
			
		||||
                    promises.push(CoreCourses.instance.getUserCourse(this.event.courseid, true).then((course) => {
 | 
			
		||||
                        this.courseId = course.id;
 | 
			
		||||
                        this.courseName = course.fullname;
 | 
			
		||||
                        this.courseUrl = currentSite ? CoreTextUtils.instance.concatenatePaths(
 | 
			
		||||
                            currentSite.siteUrl,
 | 
			
		||||
                            '/course/view.php?id=' + this.courseId,
 | 
			
		||||
                        ) : '';
 | 
			
		||||
 | 
			
		||||
                        return;
 | 
			
		||||
                    }).catch(() => {
 | 
			
		||||
                        // Error getting course, just don't show the course name.
 | 
			
		||||
                    }));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // If it's a group event, get the name of the group.
 | 
			
		||||
            if (courseId && this.event.groupid) {
 | 
			
		||||
                promises.push(CoreGroups.instance.getUserGroupsInCourse(courseId).then((groups) => {
 | 
			
		||||
                    const group = groups.find((group) => group.id == this.event!.groupid);
 | 
			
		||||
 | 
			
		||||
                    this.groupName = group ? group.name : '';
 | 
			
		||||
 | 
			
		||||
                    return;
 | 
			
		||||
                }).catch(() => {
 | 
			
		||||
                    // Error getting groups, just don't show the group name.
 | 
			
		||||
                    this.groupName = '';
 | 
			
		||||
                }));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (canGetById && this.event.iscategoryevent && this.event.category) {
 | 
			
		||||
                this.categoryPath = this.event.category.nestedname;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (this.event.location) {
 | 
			
		||||
                // Build a link to open the address in maps.
 | 
			
		||||
                this.event.location = CoreTextUtils.instance.decodeHTML(this.event.location);
 | 
			
		||||
                this.event.encodedLocation = CoreTextUtils.instance.buildAddressURL(this.event.location);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Check if event was deleted in offine.
 | 
			
		||||
            promises.push(AddonCalendarOffline.instance.isEventDeleted(this.eventId).then((deleted) => {
 | 
			
		||||
                this.event!.deleted = deleted;
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }));
 | 
			
		||||
 | 
			
		||||
            // Re-calculate the formatted time so it uses the device date.
 | 
			
		||||
            promises.push(AddonCalendar.instance.getCalendarTimeFormat().then(async (timeFormat) => {
 | 
			
		||||
                this.event!.formattedtime = await AddonCalendar.instance.formatEventTime(this.event!, timeFormat);
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }));
 | 
			
		||||
 | 
			
		||||
            await Promise.all(promises);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.instance.showErrorModalDefault(error, 'addon.calendar.errorloadevent', true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.eventLoaded = true;
 | 
			
		||||
        this.syncIcon = 'fas-sync-alt';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Add a reminder for this event.
 | 
			
		||||
     */
 | 
			
		||||
    async addNotificationTime(): Promise<void> {
 | 
			
		||||
        if (this.notificationTimeText && this.event && this.event.id) {
 | 
			
		||||
            let notificationTime = CoreTimeUtils.instance.convertToTimestamp(this.notificationTimeText);
 | 
			
		||||
 | 
			
		||||
            const currentTime = CoreTimeUtils.instance.timestamp();
 | 
			
		||||
            const minute = Math.floor(currentTime / 60) * 60;
 | 
			
		||||
 | 
			
		||||
            // Check if the notification time is in the same minute as we are, so the notification is triggered.
 | 
			
		||||
            if (notificationTime >=  minute && notificationTime < minute + 60) {
 | 
			
		||||
                notificationTime  = currentTime + 1;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await AddonCalendar.instance.addEventReminder(this.event, notificationTime);
 | 
			
		||||
            this.reminders = await AddonCalendar.instance.getEventReminders(this.eventId);
 | 
			
		||||
            this.notificationTimeText = undefined;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Cancel the selected notification.
 | 
			
		||||
     *
 | 
			
		||||
     * @param id Reminder ID.
 | 
			
		||||
     * @param e Click event.
 | 
			
		||||
     */
 | 
			
		||||
    async cancelNotification(id: number, e: Event): Promise<void> {
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        e.stopPropagation();
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await CoreDomUtils.instance.showDeleteConfirm();
 | 
			
		||||
 | 
			
		||||
            const modal = await CoreDomUtils.instance.showModalLoading('core.deleting', true);
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                await AddonCalendar.instance.deleteEventReminder(id);
 | 
			
		||||
                this.reminders = await AddonCalendar.instance.getEventReminders(this.eventId);
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                CoreDomUtils.instance.showErrorModalDefault(error, 'Error deleting reminder');
 | 
			
		||||
            } finally {
 | 
			
		||||
                modal.dismiss();
 | 
			
		||||
            }
 | 
			
		||||
        } catch {
 | 
			
		||||
            // Ignore errors.
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Refresh the data.
 | 
			
		||||
     *
 | 
			
		||||
     * @param refresher Refresher.
 | 
			
		||||
     * @param done Function to call when done.
 | 
			
		||||
     * @param showErrors Whether to show sync errors to the user.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async doRefresh(refresher?: CustomEvent<IonRefresher>, done?: () => void, showErrors= false): Promise<void> {
 | 
			
		||||
        if (!this.eventLoaded) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await this.refreshEvent(true, showErrors).finally(() => {
 | 
			
		||||
            refresher?.detail.complete();
 | 
			
		||||
            done && done();
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Refresh the event.
 | 
			
		||||
     *
 | 
			
		||||
     * @param sync Whether it should try to synchronize offline events.
 | 
			
		||||
     * @param showErrors Whether to show sync errors to the user.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async refreshEvent(sync = false, showErrors = false): Promise<void> {
 | 
			
		||||
        this.syncIcon = 'spinner';
 | 
			
		||||
 | 
			
		||||
        const promises: Promise<void>[] = [];
 | 
			
		||||
 | 
			
		||||
        promises.push(AddonCalendar.instance.invalidateEvent(this.eventId));
 | 
			
		||||
        promises.push(AddonCalendar.instance.invalidateTimeFormat());
 | 
			
		||||
 | 
			
		||||
        await CoreUtils.instance.allPromisesIgnoringErrors(promises);
 | 
			
		||||
 | 
			
		||||
        await this.fetchEvent(sync, showErrors);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Open the page to edit the event.
 | 
			
		||||
     */
 | 
			
		||||
    openEdit(): void {
 | 
			
		||||
        // Decide which navCtrl to use. If this page is inside a split view, use the split view's master nav.
 | 
			
		||||
        // @todo const navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl;
 | 
			
		||||
        CoreNavigator.instance.navigateToSitePath('/calendar/edit', { params: { eventId: this.eventId } });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Delete the event.
 | 
			
		||||
     */
 | 
			
		||||
    async deleteEvent(): Promise<void> {
 | 
			
		||||
        if (!this.event) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const title = Translate.instance.instant('addon.calendar.deleteevent');
 | 
			
		||||
        const options: AlertOptions = {};
 | 
			
		||||
        let message: string;
 | 
			
		||||
 | 
			
		||||
        if (this.event.eventcount > 1) {
 | 
			
		||||
            // It's a repeated event.
 | 
			
		||||
            message = Translate.instance.instant(
 | 
			
		||||
                'addon.calendar.confirmeventseriesdelete',
 | 
			
		||||
                { $a: { name: this.event.name, count: this.event.eventcount } },
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            options.inputs = [
 | 
			
		||||
                {
 | 
			
		||||
                    type: 'radio',
 | 
			
		||||
                    name: 'deleteall',
 | 
			
		||||
                    checked: true,
 | 
			
		||||
                    value: false,
 | 
			
		||||
                    label: Translate.instance.instant('addon.calendar.deleteoneevent'),
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    type: 'radio',
 | 
			
		||||
                    name: 'deleteall',
 | 
			
		||||
                    checked: false,
 | 
			
		||||
                    value: true,
 | 
			
		||||
                    label: Translate.instance.instant('addon.calendar.deleteallevents'),
 | 
			
		||||
                },
 | 
			
		||||
            ];
 | 
			
		||||
        } else {
 | 
			
		||||
            // Not repeated, display a simple confirm.
 | 
			
		||||
            message = Translate.instance.instant('addon.calendar.confirmeventdelete', { $a: this.event.name });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let deleteAll = false;
 | 
			
		||||
        try {
 | 
			
		||||
            deleteAll = await CoreDomUtils.instance.showConfirm(message, title, undefined, undefined, options);
 | 
			
		||||
        } catch {
 | 
			
		||||
 | 
			
		||||
            // User canceled.
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const modal = await CoreDomUtils.instance.showModalLoading('core.sending', true);
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const sent = await AddonCalendar.instance.deleteEvent(this.event.id, this.event.name, deleteAll);
 | 
			
		||||
 | 
			
		||||
            if (sent) {
 | 
			
		||||
                // Event deleted, invalidate right days & months.
 | 
			
		||||
                try {
 | 
			
		||||
                    await AddonCalendarHelper.instance.refreshAfterChangeEvent(this.event, deleteAll ? this.event.eventcount : 1);
 | 
			
		||||
                } catch {
 | 
			
		||||
                    // Ignore errors.
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Trigger an event.
 | 
			
		||||
            CoreEvents.trigger<AddonCalendarUpdatedEventEvent>(AddonCalendarProvider.DELETED_EVENT_EVENT, {
 | 
			
		||||
                eventId: this.eventId,
 | 
			
		||||
                sent: sent,
 | 
			
		||||
            }, CoreSites.instance.getCurrentSiteId());
 | 
			
		||||
 | 
			
		||||
            if (sent) {
 | 
			
		||||
                CoreDomUtils.instance.showToast('addon.calendar.eventcalendareventdeleted', true, 3000);
 | 
			
		||||
 | 
			
		||||
                // Event deleted, close the view.
 | 
			
		||||
                /* if (!this.svComponent || !this.svComponent.isOn()) {
 | 
			
		||||
                    this.navCtrl.pop();
 | 
			
		||||
                }*/
 | 
			
		||||
            } else {
 | 
			
		||||
                // Event deleted in offline, just mark it as deleted.
 | 
			
		||||
                this.event.deleted = true;
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.instance.showErrorModalDefault(error, 'Error deleting event.');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        modal.dismiss();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Undo delete the event.
 | 
			
		||||
     */
 | 
			
		||||
    async undoDelete(): Promise<void> {
 | 
			
		||||
        if (!this.event) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const modal = await CoreDomUtils.instance.showModalLoading('core.sending', true);
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
 | 
			
		||||
            await AddonCalendarOffline.instance.unmarkDeleted(this.event.id);
 | 
			
		||||
 | 
			
		||||
            // Trigger an event.
 | 
			
		||||
            CoreEvents.trigger<AddonCalendarUpdatedEventEvent>(AddonCalendarProvider.UNDELETED_EVENT_EVENT, {
 | 
			
		||||
                eventId: this.eventId,
 | 
			
		||||
            }, CoreSites.instance.getCurrentSiteId());
 | 
			
		||||
 | 
			
		||||
            this.event.deleted = false;
 | 
			
		||||
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.instance.showErrorModalDefault(error, 'Error undeleting event.');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        modal.dismiss();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check the result of an automatic sync or a manual sync not done by this page.
 | 
			
		||||
     *
 | 
			
		||||
     * @param isManual Whether it's a manual sync.
 | 
			
		||||
     * @param data Sync result.
 | 
			
		||||
     */
 | 
			
		||||
    protected checkSyncResult(isManual: boolean, data: AddonCalendarSyncEvents): void {
 | 
			
		||||
        if (!data) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (data.deleted && data.deleted.indexOf(this.eventId) != -1) {
 | 
			
		||||
            CoreDomUtils.instance.showToast('addon.calendar.eventcalendareventdeleted', true, 3000);
 | 
			
		||||
 | 
			
		||||
            // Event was deleted, close the view.
 | 
			
		||||
            /* if (!this.svComponent || !this.svComponent.isOn()) {
 | 
			
		||||
                this.navCtrl.pop();
 | 
			
		||||
            }*/
 | 
			
		||||
        } else if (data.events && (!isManual || data.source != 'event')) {
 | 
			
		||||
            const event = data.events.find((ev) => ev.id == this.eventId);
 | 
			
		||||
 | 
			
		||||
            if (event) {
 | 
			
		||||
                this.eventLoaded = false;
 | 
			
		||||
                this.refreshEvent();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Page destroyed.
 | 
			
		||||
     */
 | 
			
		||||
    ngOnDestroy(): void {
 | 
			
		||||
        this.editEventObserver?.off();
 | 
			
		||||
        this.syncObserver?.off();
 | 
			
		||||
        this.manualSyncObserver?.off();
 | 
			
		||||
        this.onlineObserver?.unsubscribe();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										9
									
								
								src/addons/calendar/pages/event/event.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,9 @@
 | 
			
		||||
:host {
 | 
			
		||||
    ion-card ion-note {
 | 
			
		||||
        font-size: 1.6rem;
 | 
			
		||||
    }
 | 
			
		||||
    ion-title ion-icon, ion-title img {
 | 
			
		||||
        margin-left: 10px;
 | 
			
		||||
        margin-right: 10px;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										55
									
								
								src/addons/calendar/pages/index/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,55 @@
 | 
			
		||||
<ion-header>
 | 
			
		||||
    <ion-toolbar>
 | 
			
		||||
        <ion-buttons slot="start">
 | 
			
		||||
            <ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
        <ion-title>{{ (showCalendar ? 'addon.calendar.calendarevents' : 'addon.calendar.upcomingevents') | translate }}</ion-title>
 | 
			
		||||
        <ion-buttons slot="end">
 | 
			
		||||
            <ion-button (click)="openFilter($event)" [attr.aria-label]="'core.filter' | translate">
 | 
			
		||||
                <ion-icon slot="icon-only" name="fas-filter"></ion-icon>
 | 
			
		||||
            </ion-button>
 | 
			
		||||
            <core-context-menu>
 | 
			
		||||
                <core-context-menu-item *ngIf="showCalendar" [priority]="800"
 | 
			
		||||
                [content]="'addon.calendar.upcomingevents' | translate" iconAction="fas-th-list"
 | 
			
		||||
                (action)="toggleDisplay()"></core-context-menu-item>
 | 
			
		||||
                <core-context-menu-item *ngIf="!showCalendar" [priority]="800"
 | 
			
		||||
                [content]="'addon.calendar.monthlyview' | translate" iconAction="fas-calendar-alt"
 | 
			
		||||
                (action)="toggleDisplay()"></core-context-menu-item>
 | 
			
		||||
                <core-context-menu-item [hidden]="!notificationsEnabled" [priority]="600"
 | 
			
		||||
                [content]="'core.settings.settings' | translate" (action)="openSettings()" iconAction="fas-cogs">
 | 
			
		||||
                </core-context-menu-item>
 | 
			
		||||
                <core-context-menu-item [hidden]="!loaded || !hasOffline || !isOnline"  [priority]="400"
 | 
			
		||||
                [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(undefined, $event, true)"
 | 
			
		||||
                [iconAction]="syncIcon" [closeOnClick]="false"></core-context-menu-item>
 | 
			
		||||
            </core-context-menu>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
    </ion-toolbar>
 | 
			
		||||
</ion-header>
 | 
			
		||||
<ion-content>
 | 
			
		||||
    <ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event)">
 | 
			
		||||
        <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
 | 
			
		||||
    </ion-refresher>
 | 
			
		||||
 | 
			
		||||
    <!-- There is data to be synchronized -->
 | 
			
		||||
    <ion-card class="core-warning-card" *ngIf="hasOffline">
 | 
			
		||||
        <ion-item>
 | 
			
		||||
            <ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon>
 | 
			
		||||
            <ion-label>{{ 'core.hasdatatosync' | translate:{$a: 'addon.calendar.calendar' | translate} }}</ion-label>
 | 
			
		||||
        </ion-item>
 | 
			
		||||
    </ion-card>
 | 
			
		||||
 | 
			
		||||
    <addon-calendar-calendar [hidden]="!showCalendar" [initialYear]="year" [initialMonth]="month" [filter]="filter"
 | 
			
		||||
        [displayNavButtons]="showCalendar" (onEventClicked)="gotoEvent($event)" (onDayClicked)="gotoDay($event)">
 | 
			
		||||
    </addon-calendar-calendar>
 | 
			
		||||
 | 
			
		||||
    <addon-calendar-upcoming-events *ngIf="loadUpcoming" [hidden]="showCalendar" [filter]="filter"
 | 
			
		||||
        (onEventClicked)="gotoEvent($event)">
 | 
			
		||||
    </addon-calendar-upcoming-events>
 | 
			
		||||
 | 
			
		||||
    <!-- Create a calendar event. -->
 | 
			
		||||
    <ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="canCreate">
 | 
			
		||||
        <ion-fab-button (click)="openEdit()" [attr.aria-label]="'addon.calendar.newevent' | translate">
 | 
			
		||||
            <ion-icon name="fas-plus"></ion-icon>
 | 
			
		||||
        </ion-fab-button>
 | 
			
		||||
    </ion-fab>
 | 
			
		||||
</ion-content>
 | 
			
		||||
							
								
								
									
										51
									
								
								src/addons/calendar/pages/index/index.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,51 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { NgModule } from '@angular/core';
 | 
			
		||||
import { IonicModule } from '@ionic/angular';
 | 
			
		||||
import { TranslateModule } from '@ngx-translate/core';
 | 
			
		||||
import { RouterModule, Routes } from '@angular/router';
 | 
			
		||||
import { CommonModule } from '@angular/common';
 | 
			
		||||
 | 
			
		||||
import { CoreComponentsModule } from '@components/components.module';
 | 
			
		||||
import { CoreDirectivesModule } from '@directives/directives.module';
 | 
			
		||||
import { CorePipesModule } from '@pipes/pipes.module';
 | 
			
		||||
import { AddonCalendarComponentsModule } from '../../components/components.module';
 | 
			
		||||
 | 
			
		||||
import { AddonCalendarIndexPage } from './index.page';
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
    {
 | 
			
		||||
        path: '',
 | 
			
		||||
        component: AddonCalendarIndexPage,
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    imports: [
 | 
			
		||||
        RouterModule.forChild(routes),
 | 
			
		||||
        CommonModule,
 | 
			
		||||
        IonicModule,
 | 
			
		||||
        TranslateModule.forChild(),
 | 
			
		||||
        CoreComponentsModule,
 | 
			
		||||
        CoreDirectivesModule,
 | 
			
		||||
        CorePipesModule,
 | 
			
		||||
        AddonCalendarComponentsModule,
 | 
			
		||||
    ],
 | 
			
		||||
    declarations: [
 | 
			
		||||
        AddonCalendarIndexPage,
 | 
			
		||||
    ],
 | 
			
		||||
    exports: [RouterModule],
 | 
			
		||||
})
 | 
			
		||||
export class AddonCalendarIndexPageModule {}
 | 
			
		||||
							
								
								
									
										406
									
								
								src/addons/calendar/pages/index/index.page.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,406 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
 | 
			
		||||
import { PopoverController, IonRefresher } from '@ionic/angular';
 | 
			
		||||
import { CoreApp } from '@services/app';
 | 
			
		||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { CoreCoursesHelper } from '@features/courses/services/courses-helper';
 | 
			
		||||
import { AddonCalendar, AddonCalendarProvider, AddonCalendarUpdatedEventEvent } from '../../services/calendar';
 | 
			
		||||
import { AddonCalendarOffline } from '../../services/calendar-offline';
 | 
			
		||||
import { AddonCalendarSync, AddonCalendarSyncEvents, AddonCalendarSyncProvider } from '../../services/calendar-sync';
 | 
			
		||||
import { AddonCalendarFilter, AddonCalendarHelper } from '../../services/calendar-helper';
 | 
			
		||||
import { Network, NgZone } from '@singletons';
 | 
			
		||||
import { Subscription } from 'rxjs';
 | 
			
		||||
import { CoreEnrolledCourseData } from '@features/courses/services/courses';
 | 
			
		||||
import { ActivatedRoute, Params } from '@angular/router';
 | 
			
		||||
import { AddonCalendarCalendarComponent } from '../../components/calendar/calendar';
 | 
			
		||||
import { AddonCalendarUpcomingEventsComponent } from '../../components/upcoming-events/upcoming-events';
 | 
			
		||||
import { AddonCalendarFilterPopoverComponent } from '../../components/filter/filter';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { CoreLocalNotifications } from '@services/local-notifications';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Page that displays the calendar events.
 | 
			
		||||
 */
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'page-addon-calendar-index',
 | 
			
		||||
    templateUrl: 'index.html',
 | 
			
		||||
})
 | 
			
		||||
export class AddonCalendarIndexPage implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
    @ViewChild(AddonCalendarCalendarComponent) calendarComponent?: AddonCalendarCalendarComponent;
 | 
			
		||||
    @ViewChild(AddonCalendarUpcomingEventsComponent) upcomingEventsComponent?: AddonCalendarUpcomingEventsComponent;
 | 
			
		||||
 | 
			
		||||
    protected eventId?: number;
 | 
			
		||||
    protected currentSiteId: string;
 | 
			
		||||
 | 
			
		||||
    // Observers.
 | 
			
		||||
    protected newEventObserver?: CoreEventObserver;
 | 
			
		||||
    protected discardedObserver?: CoreEventObserver;
 | 
			
		||||
    protected editEventObserver?: CoreEventObserver;
 | 
			
		||||
    protected deleteEventObserver?: CoreEventObserver;
 | 
			
		||||
    protected undeleteEventObserver?: CoreEventObserver;
 | 
			
		||||
    protected syncObserver?: CoreEventObserver;
 | 
			
		||||
    protected manualSyncObserver?: CoreEventObserver;
 | 
			
		||||
    protected onlineObserver?: Subscription;
 | 
			
		||||
    protected filterChangedObserver?: CoreEventObserver;
 | 
			
		||||
 | 
			
		||||
    year?: number;
 | 
			
		||||
    month?: number;
 | 
			
		||||
    canCreate = false;
 | 
			
		||||
    courses: Partial<CoreEnrolledCourseData>[] = [];
 | 
			
		||||
    notificationsEnabled = false;
 | 
			
		||||
    loaded = false;
 | 
			
		||||
    hasOffline = false;
 | 
			
		||||
    isOnline = false;
 | 
			
		||||
    syncIcon = 'spinner';
 | 
			
		||||
    showCalendar = true;
 | 
			
		||||
    loadUpcoming = false;
 | 
			
		||||
    filter: AddonCalendarFilter = {
 | 
			
		||||
        filtered: false,
 | 
			
		||||
        courseId: -1,
 | 
			
		||||
        categoryId: undefined,
 | 
			
		||||
        course: true,
 | 
			
		||||
        group: true,
 | 
			
		||||
        site: true,
 | 
			
		||||
        user: true,
 | 
			
		||||
        category: true,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        protected popoverCtrl: PopoverController,
 | 
			
		||||
        protected route: ActivatedRoute,
 | 
			
		||||
    ) {
 | 
			
		||||
        this.currentSiteId = CoreSites.instance.getCurrentSiteId();
 | 
			
		||||
 | 
			
		||||
        // Listen for events added. When an event is added, reload the data.
 | 
			
		||||
        this.newEventObserver = CoreEvents.on(
 | 
			
		||||
            AddonCalendarProvider.NEW_EVENT_EVENT,
 | 
			
		||||
            (data: AddonCalendarUpdatedEventEvent) => {
 | 
			
		||||
                if (data && data.eventId) {
 | 
			
		||||
                    this.loaded = false;
 | 
			
		||||
                    this.refreshData(true, false);
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            this.currentSiteId,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Listen for new event discarded event. When it does, reload the data.
 | 
			
		||||
        this.discardedObserver = CoreEvents.on(AddonCalendarProvider.NEW_EVENT_DISCARDED_EVENT, () => {
 | 
			
		||||
            this.loaded = false;
 | 
			
		||||
            this.refreshData(true, false);
 | 
			
		||||
        }, this.currentSiteId);
 | 
			
		||||
 | 
			
		||||
        // Listen for events edited. When an event is edited, reload the data.
 | 
			
		||||
        this.editEventObserver = CoreEvents.on(
 | 
			
		||||
            AddonCalendarProvider.EDIT_EVENT_EVENT,
 | 
			
		||||
            (data: AddonCalendarUpdatedEventEvent) => {
 | 
			
		||||
                if (data && data.eventId) {
 | 
			
		||||
                    this.loaded = false;
 | 
			
		||||
                    this.refreshData(true, false);
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            this.currentSiteId,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Refresh data if calendar events are synchronized automatically.
 | 
			
		||||
        this.syncObserver = CoreEvents.on(AddonCalendarSyncProvider.AUTO_SYNCED, () => {
 | 
			
		||||
            this.loaded = false;
 | 
			
		||||
            this.refreshData(false, false);
 | 
			
		||||
        }, this.currentSiteId);
 | 
			
		||||
 | 
			
		||||
        // Refresh data if calendar events are synchronized manually but not by this page.
 | 
			
		||||
        this.manualSyncObserver = CoreEvents.on(AddonCalendarSyncProvider.MANUAL_SYNCED, (data: AddonCalendarSyncEvents) => {
 | 
			
		||||
            if (data && data.source != 'index') {
 | 
			
		||||
                this.loaded = false;
 | 
			
		||||
                this.refreshData(false, false);
 | 
			
		||||
            }
 | 
			
		||||
        }, this.currentSiteId);
 | 
			
		||||
 | 
			
		||||
        // Update the events when an event is deleted.
 | 
			
		||||
        this.deleteEventObserver = CoreEvents.on(AddonCalendarProvider.DELETED_EVENT_EVENT, () => {
 | 
			
		||||
            this.loaded = false;
 | 
			
		||||
            this.refreshData(false, false);
 | 
			
		||||
        }, this.currentSiteId);
 | 
			
		||||
 | 
			
		||||
        // Update the "hasOffline" property if an event deleted in offline is restored.
 | 
			
		||||
        this.undeleteEventObserver = CoreEvents.on(AddonCalendarProvider.UNDELETED_EVENT_EVENT, async () => {
 | 
			
		||||
            this.hasOffline = await AddonCalendarOffline.instance.hasOfflineData();
 | 
			
		||||
        }, this.currentSiteId);
 | 
			
		||||
 | 
			
		||||
        this.filterChangedObserver = CoreEvents.on(
 | 
			
		||||
            AddonCalendarProvider.FILTER_CHANGED_EVENT,
 | 
			
		||||
            async (filterData: AddonCalendarFilter) => {
 | 
			
		||||
                this.filter = filterData;
 | 
			
		||||
 | 
			
		||||
                // Course viewed has changed, check if the user can create events for this course calendar.
 | 
			
		||||
                this.canCreate = await AddonCalendarHelper.instance.canEditEvents(this.filter['courseId']);
 | 
			
		||||
            },
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Refresh online status when changes.
 | 
			
		||||
        this.onlineObserver = Network.instance.onChange().subscribe(() => {
 | 
			
		||||
            // Execute the callback in the Angular zone, so change detection doesn't stop working.
 | 
			
		||||
            NgZone.instance.run(() => {
 | 
			
		||||
                this.isOnline = CoreApp.instance.isOnline();
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * View loaded.
 | 
			
		||||
     */
 | 
			
		||||
    ngOnInit(): void {
 | 
			
		||||
        this.notificationsEnabled = CoreLocalNotifications.instance.isAvailable();
 | 
			
		||||
 | 
			
		||||
        this.route.queryParams.subscribe(params => {
 | 
			
		||||
            this.eventId = parseInt(params['eventId'], 10) || undefined;
 | 
			
		||||
            this.filter.courseId = parseInt(params['courseId'], 10) || -1;
 | 
			
		||||
            this.year = parseInt(params['year'], 10) || undefined;
 | 
			
		||||
            this.month = parseInt(params['month'], 10) || undefined;
 | 
			
		||||
            this.loadUpcoming = !!params['upcoming'];
 | 
			
		||||
            this.showCalendar = !this.loadUpcoming;
 | 
			
		||||
            this.filter.filtered = this.filter.courseId > 0;
 | 
			
		||||
 | 
			
		||||
            if (this.eventId) {
 | 
			
		||||
                // There is an event to load, open the event in a new state.
 | 
			
		||||
                this.gotoEvent(this.eventId);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.fetchData(true, false);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Fetch all the data required for the view.
 | 
			
		||||
     *
 | 
			
		||||
     * @param sync Whether it should try to synchronize offline events.
 | 
			
		||||
     * @param showErrors Whether to show sync errors to the user.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async fetchData(sync?: boolean, showErrors?: boolean): Promise<void> {
 | 
			
		||||
 | 
			
		||||
        this.syncIcon = 'spinner';
 | 
			
		||||
        this.isOnline = CoreApp.instance.isOnline();
 | 
			
		||||
 | 
			
		||||
        if (sync) {
 | 
			
		||||
            // Try to synchronize offline events.
 | 
			
		||||
            try {
 | 
			
		||||
                const result = await AddonCalendarSync.instance.syncEvents();
 | 
			
		||||
                if (result.warnings && result.warnings.length) {
 | 
			
		||||
                    CoreDomUtils.instance.showErrorModal(result.warnings[0]);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (result.updated) {
 | 
			
		||||
                    // Trigger a manual sync event.
 | 
			
		||||
                    result.source = 'index';
 | 
			
		||||
 | 
			
		||||
                    CoreEvents.trigger<AddonCalendarSyncEvents>(
 | 
			
		||||
                        AddonCalendarSyncProvider.MANUAL_SYNCED,
 | 
			
		||||
                        result,
 | 
			
		||||
                        this.currentSiteId,
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                if (showErrors) {
 | 
			
		||||
                    CoreDomUtils.instance.showErrorModalDefault(error, 'core.errorsync', true);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const promises: Promise<void>[] = [];
 | 
			
		||||
 | 
			
		||||
            this.hasOffline = false;
 | 
			
		||||
 | 
			
		||||
            // Load courses for the popover.
 | 
			
		||||
            promises.push(CoreCoursesHelper.instance.getCoursesForPopover(this.filter.courseId).then((data) => {
 | 
			
		||||
                this.courses = data.courses;
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }));
 | 
			
		||||
 | 
			
		||||
            // Check if user can create events.
 | 
			
		||||
            promises.push(AddonCalendarHelper.instance.canEditEvents(this.filter.courseId).then((canEdit) => {
 | 
			
		||||
                this.canCreate = canEdit;
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }));
 | 
			
		||||
 | 
			
		||||
            // Check if there is offline data.
 | 
			
		||||
            promises.push(AddonCalendarOffline.instance.hasOfflineData().then((hasOffline) => {
 | 
			
		||||
                this.hasOffline = hasOffline;
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }));
 | 
			
		||||
 | 
			
		||||
            await Promise.all(promises);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.instance.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.loaded = true;
 | 
			
		||||
        this.syncIcon = 'fas-sync-alt';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Refresh the data.
 | 
			
		||||
     *
 | 
			
		||||
     * @param refresher Refresher.
 | 
			
		||||
     * @param done Function to call when done.
 | 
			
		||||
     * @param showErrors Whether to show sync errors to the user.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async doRefresh(refresher?: CustomEvent<IonRefresher>, done?: () => void, showErrors?: boolean): Promise<void> {
 | 
			
		||||
        if (!this.loaded) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await this.refreshData(true, showErrors).finally(() => {
 | 
			
		||||
            refresher?.detail.complete();
 | 
			
		||||
            done && done();
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Refresh the data.
 | 
			
		||||
     *
 | 
			
		||||
     * @param sync Whether it should try to synchronize offline events.
 | 
			
		||||
     * @param showErrors Whether to show sync errors to the user.
 | 
			
		||||
     * @param afterChange Whether the refresh is done after an event has changed or has been synced.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async refreshData(sync = false, showErrors = false): Promise<void> {
 | 
			
		||||
        this.syncIcon = 'spinner';
 | 
			
		||||
 | 
			
		||||
        const promises: Promise<void>[] = [];
 | 
			
		||||
 | 
			
		||||
        promises.push(AddonCalendar.instance.invalidateAllowedEventTypes());
 | 
			
		||||
 | 
			
		||||
        // Refresh the sub-component.
 | 
			
		||||
        if (this.showCalendar && this.calendarComponent) {
 | 
			
		||||
            promises.push(this.calendarComponent.refreshData());
 | 
			
		||||
        } else if (!this.showCalendar && this.upcomingEventsComponent) {
 | 
			
		||||
            promises.push(this.upcomingEventsComponent.refreshData());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await Promise.all(promises).finally(() => this.fetchData(sync, showErrors));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Navigate to a particular event.
 | 
			
		||||
     *
 | 
			
		||||
     * @param eventId Event to load.
 | 
			
		||||
     */
 | 
			
		||||
    gotoEvent(eventId: number): void {
 | 
			
		||||
        if (eventId < 0) {
 | 
			
		||||
            // It's an offline event, go to the edit page.
 | 
			
		||||
            this.openEdit(eventId);
 | 
			
		||||
        } else {
 | 
			
		||||
            CoreNavigator.instance.navigateToSitePath('/calendar/event', { params: { id: eventId } });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * View a certain day.
 | 
			
		||||
     *
 | 
			
		||||
     * @param data Data with the year, month and day.
 | 
			
		||||
     */
 | 
			
		||||
    gotoDay(data: {day: number; month: number; year: number}): void {
 | 
			
		||||
        const params: Params = {
 | 
			
		||||
            day: data.day,
 | 
			
		||||
            month: data.month,
 | 
			
		||||
            year: data.year,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        Object.keys(this.filter).forEach((key) => {
 | 
			
		||||
            params[key] = this.filter[key];
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        CoreNavigator.instance.navigateToSitePath('/calendar/day', { params });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Show the context menu.
 | 
			
		||||
     *
 | 
			
		||||
     * @param event Event.
 | 
			
		||||
     */
 | 
			
		||||
    async openFilter(event: MouseEvent): Promise<void> {
 | 
			
		||||
        const popover = await this.popoverCtrl.create({
 | 
			
		||||
            component: AddonCalendarFilterPopoverComponent,
 | 
			
		||||
            componentProps: {
 | 
			
		||||
                courses: this.courses,
 | 
			
		||||
                filter: this.filter,
 | 
			
		||||
            },
 | 
			
		||||
            event,
 | 
			
		||||
        });
 | 
			
		||||
        await popover.present();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Open page to create/edit an event.
 | 
			
		||||
     *
 | 
			
		||||
     * @param eventId Event ID to edit.
 | 
			
		||||
     */
 | 
			
		||||
    openEdit(eventId?: number): void {
 | 
			
		||||
        const params: Params = {};
 | 
			
		||||
 | 
			
		||||
        if (eventId) {
 | 
			
		||||
            params.eventId = eventId;
 | 
			
		||||
        }
 | 
			
		||||
        if (this.filter.courseId) {
 | 
			
		||||
            params.courseId = this.filter.courseId;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        CoreNavigator.instance.navigateToSitePath('/calendar/edit', { params });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Open calendar events settings.
 | 
			
		||||
     */
 | 
			
		||||
    openSettings(): void {
 | 
			
		||||
        CoreNavigator.instance.navigateToSitePath('/calendar/settings');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Toogle display: monthly view or upcoming events.
 | 
			
		||||
     */
 | 
			
		||||
    toggleDisplay(): void {
 | 
			
		||||
        this.showCalendar = !this.showCalendar;
 | 
			
		||||
 | 
			
		||||
        if (!this.showCalendar) {
 | 
			
		||||
            this.loadUpcoming = true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Page destroyed.
 | 
			
		||||
     */
 | 
			
		||||
    ngOnDestroy(): void {
 | 
			
		||||
        this.newEventObserver?.off();
 | 
			
		||||
        this.discardedObserver?.off();
 | 
			
		||||
        this.editEventObserver?.off();
 | 
			
		||||
        this.deleteEventObserver?.off();
 | 
			
		||||
        this.undeleteEventObserver?.off();
 | 
			
		||||
        this.syncObserver?.off();
 | 
			
		||||
        this.manualSyncObserver?.off();
 | 
			
		||||
        this.filterChangedObserver?.off();
 | 
			
		||||
        this.onlineObserver?.unsubscribe();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										90
									
								
								src/addons/calendar/pages/list/list.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,90 @@
 | 
			
		||||
<ion-header>
 | 
			
		||||
    <ion-toolbar>
 | 
			
		||||
        <ion-buttons slot="start">
 | 
			
		||||
            <ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
        <ion-title>{{ 'addon.calendar.calendarevents' | translate }}</ion-title>
 | 
			
		||||
        <ion-buttons slot="end">
 | 
			
		||||
            <ion-button (click)="openFilter($event)" [attr.aria-label]="'core.filter' | translate">
 | 
			
		||||
                <ion-icon slot="icon-only" name="fas-filter"></ion-icon>
 | 
			
		||||
            </ion-button>
 | 
			
		||||
            <core-context-menu>
 | 
			
		||||
                <core-context-menu-item [hidden]="!notificationsEnabled" [priority]="600"
 | 
			
		||||
                [content]="'core.settings.settings' | translate" (action)="openSettings()" iconAction="fas-cogs">
 | 
			
		||||
            </core-context-menu-item>
 | 
			
		||||
                <core-context-menu-item [hidden]="!eventsLoaded || !hasOffline || !isOnline"  [priority]="400"
 | 
			
		||||
                [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(undefined, $event, true)"
 | 
			
		||||
                [iconAction]="syncIcon" [closeOnClick]="false"></core-context-menu-item>
 | 
			
		||||
            </core-context-menu>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
    </ion-toolbar>
 | 
			
		||||
</ion-header>
 | 
			
		||||
<!--<core-split-view>-->
 | 
			
		||||
    <ion-content>
 | 
			
		||||
        <ion-refresher slot="fixed" [disabled]="!eventsLoaded" (ionRefresh)="doRefresh($event)">
 | 
			
		||||
            <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
 | 
			
		||||
        </ion-refresher>
 | 
			
		||||
        <core-loading [hideUntil]="eventsLoaded">
 | 
			
		||||
            <!-- There is data to be synchronized -->
 | 
			
		||||
            <ion-card class="core-warning-card" *ngIf="hasOffline">
 | 
			
		||||
                <ion-item>
 | 
			
		||||
                    <ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon>
 | 
			
		||||
                    <ion-label>{{ 'core.hasdatatosync' | translate:{$a: 'addon.calendar.calendar' | translate} }}</ion-label>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
            </ion-card>
 | 
			
		||||
 | 
			
		||||
            <core-empty-box *ngIf="!filteredEvents || !filteredEvents.length" icon="fas-calendar"
 | 
			
		||||
                [message]="'addon.calendar.noevents' | translate">
 | 
			
		||||
            </core-empty-box>
 | 
			
		||||
 | 
			
		||||
            <ion-list *ngIf="filteredEvents && filteredEvents.length"  class="ion-no-margin">
 | 
			
		||||
                <ng-container *ngFor="let event of filteredEvents">
 | 
			
		||||
                    <ion-item-divider *ngIf="event.showDate">
 | 
			
		||||
                        <ion-label>{{ event.timestart * 1000 | coreFormatDate: "strftimedayshort" }}</ion-label>
 | 
			
		||||
                    </ion-item-divider>
 | 
			
		||||
                    <ion-item class="ion-text-wrap" [title]="event.name" (click)="gotoEvent(event.id)"
 | 
			
		||||
                    [class.core-split-item-selected]="event.id == eventId" class="addon-calendar-event"
 | 
			
		||||
                    [ngClass]="['addon-calendar-eventtype-'+event.eventtype]">
 | 
			
		||||
                        <img *ngIf="event.moduleIcon" src="{{event.moduleIcon}}" slot="start" class="core-module-icon">
 | 
			
		||||
                        <ion-icon *ngIf="event.eventIcon && !event.moduleIcon" [name]="event.eventIcon" slot="start">
 | 
			
		||||
                        </ion-icon>
 | 
			
		||||
                        <ion-label>
 | 
			
		||||
                            <h2>
 | 
			
		||||
                                <core-format-text [text]="event.name" [contextLevel]="event.contextLevel"
 | 
			
		||||
                                    [contextInstanceId]="event.contextInstanceId">
 | 
			
		||||
                                </core-format-text>
 | 
			
		||||
                            </h2>
 | 
			
		||||
                            <p>
 | 
			
		||||
                                {{ event.timestart * 1000 | coreFormatDate: "strftimetime" }}
 | 
			
		||||
                                <span *ngIf="event.timeduration && event.endsSameDay">
 | 
			
		||||
                                     - {{ (event.timestart + event.timeduration) * 1000 | coreFormatDate: "strftimetime" }}
 | 
			
		||||
                                </span>
 | 
			
		||||
                                <span *ngIf="event.timeduration && !event.endsSameDay">
 | 
			
		||||
                                     - {{ (event.timestart + event.timeduration) * 1000 | coreFormatDate: "strftimedatetimeshort" }}
 | 
			
		||||
                                </span>
 | 
			
		||||
                            </p>
 | 
			
		||||
                        </ion-label>
 | 
			
		||||
                        <ion-note *ngIf="event.offline && !event.deleted" slot="end">
 | 
			
		||||
                            <ion-icon name="far-clock"></ion-icon>
 | 
			
		||||
                            <span class="ion-text-wrap">{{ 'core.notsent' | translate }}</span>
 | 
			
		||||
                        </ion-note>
 | 
			
		||||
                        <ion-note *ngIf="event.deleted" slot="end">
 | 
			
		||||
                            <ion-icon name="fas-trash"></ion-icon>
 | 
			
		||||
                            <span class="ion-text-wrap">{{ 'core.deletedoffline' | translate }}</span>
 | 
			
		||||
                        </ion-note>
 | 
			
		||||
                    </ion-item>
 | 
			
		||||
                </ng-container>
 | 
			
		||||
            </ion-list>
 | 
			
		||||
 | 
			
		||||
            <core-infinite-loading [enabled]="canLoadMore" (action)="loadMoreEvents($event)" [error]="loadMoreError">
 | 
			
		||||
            </core-infinite-loading>
 | 
			
		||||
        </core-loading>
 | 
			
		||||
 | 
			
		||||
        <!-- Create a calendar event. -->
 | 
			
		||||
        <ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="canCreate">
 | 
			
		||||
            <ion-fab-button (click)="openEdit()" [attr.aria-label]="'addon.calendar.newevent' | translate">
 | 
			
		||||
                <ion-icon name="fas-plus"></ion-icon>
 | 
			
		||||
            </ion-fab-button>
 | 
			
		||||
        </ion-fab>
 | 
			
		||||
    </ion-content>
 | 
			
		||||
<!--</core-split-view>-->
 | 
			
		||||
							
								
								
									
										49
									
								
								src/addons/calendar/pages/list/list.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,49 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { NgModule } from '@angular/core';
 | 
			
		||||
import { IonicModule } from '@ionic/angular';
 | 
			
		||||
import { TranslateModule } from '@ngx-translate/core';
 | 
			
		||||
import { RouterModule, Routes } from '@angular/router';
 | 
			
		||||
import { CommonModule } from '@angular/common';
 | 
			
		||||
 | 
			
		||||
import { CoreComponentsModule } from '@components/components.module';
 | 
			
		||||
import { CoreDirectivesModule } from '@directives/directives.module';
 | 
			
		||||
import { CorePipesModule } from '@pipes/pipes.module';
 | 
			
		||||
 | 
			
		||||
import { AddonCalendarListPage } from './list.page';
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
    {
 | 
			
		||||
        path: '',
 | 
			
		||||
        component: AddonCalendarListPage,
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    imports: [
 | 
			
		||||
        RouterModule.forChild(routes),
 | 
			
		||||
        CommonModule,
 | 
			
		||||
        IonicModule,
 | 
			
		||||
        TranslateModule.forChild(),
 | 
			
		||||
        CoreComponentsModule,
 | 
			
		||||
        CoreDirectivesModule,
 | 
			
		||||
        CorePipesModule,
 | 
			
		||||
    ],
 | 
			
		||||
    declarations: [
 | 
			
		||||
        AddonCalendarListPage,
 | 
			
		||||
    ],
 | 
			
		||||
    exports: [RouterModule],
 | 
			
		||||
})
 | 
			
		||||
export class AddonCalendarListPageModule {}
 | 
			
		||||
							
								
								
									
										704
									
								
								src/addons/calendar/pages/list/list.page.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,704 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Component, ViewChild, OnDestroy, OnInit } from '@angular/core';
 | 
			
		||||
import { PopoverController, IonContent, IonRefresher } from '@ionic/angular';
 | 
			
		||||
import {
 | 
			
		||||
    AddonCalendarProvider,
 | 
			
		||||
    AddonCalendar,
 | 
			
		||||
    AddonCalendarEventToDisplay,
 | 
			
		||||
    AddonCalendarUpdatedEventEvent,
 | 
			
		||||
} from '../../services/calendar';
 | 
			
		||||
import { AddonCalendarOffline } from '../../services/calendar-offline';
 | 
			
		||||
import { AddonCalendarFilter, AddonCalendarHelper } from '../../services/calendar-helper';
 | 
			
		||||
import { AddonCalendarSync, AddonCalendarSyncEvents, AddonCalendarSyncProvider } from '../../services/calendar-sync';
 | 
			
		||||
import { CoreCategoryData, CoreCourses, CoreEnrolledCourseData } from '@features/courses/services/courses';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { CoreTimeUtils } from '@services/utils/time';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreLocalNotifications } from '@services/local-notifications';
 | 
			
		||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
 | 
			
		||||
import { CoreApp } from '@services/app';
 | 
			
		||||
// @todo import { CoreSplitViewComponent } from '@components/split-view/split-view';
 | 
			
		||||
import moment from 'moment';
 | 
			
		||||
import { CoreConstants } from '@/core/constants';
 | 
			
		||||
import { AddonCalendarFilterPopoverComponent } from '../../components/filter/filter';
 | 
			
		||||
import { ActivatedRoute, Params } from '@angular/router';
 | 
			
		||||
import { Subscription } from 'rxjs';
 | 
			
		||||
import { Network, NgZone } from '@singletons';
 | 
			
		||||
import { CoreCoursesHelper } from '@features/courses/services/courses-helper';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Page that displays the list of calendar events.
 | 
			
		||||
 */
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'page-addon-calendar-list',
 | 
			
		||||
    templateUrl: 'list.html',
 | 
			
		||||
    styleUrls: ['../../calendar-common.scss', 'list.scss'],
 | 
			
		||||
})
 | 
			
		||||
export class AddonCalendarListPage implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
    @ViewChild(IonContent) content?: IonContent;
 | 
			
		||||
    // @ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent;
 | 
			
		||||
 | 
			
		||||
    protected initialTime = 0;
 | 
			
		||||
    protected daysLoaded = 0;
 | 
			
		||||
    protected emptyEventsTimes = 0; // Variable to identify consecutive calls returning 0 events.
 | 
			
		||||
    protected categoriesRetrieved = false;
 | 
			
		||||
    protected getCategories = false;
 | 
			
		||||
    protected categories: { [id: number]: CoreCategoryData } = {};
 | 
			
		||||
    protected siteHomeId: number;
 | 
			
		||||
    protected currentSiteId: string;
 | 
			
		||||
    protected onlineEvents: AddonCalendarEventToDisplay[] = [];
 | 
			
		||||
    protected offlineEvents: AddonCalendarEventToDisplay[] = [];
 | 
			
		||||
    protected deletedEvents: number [] = [];
 | 
			
		||||
 | 
			
		||||
    // Observers.
 | 
			
		||||
    protected obsDefaultTimeChange?: CoreEventObserver;
 | 
			
		||||
    protected newEventObserver: CoreEventObserver;
 | 
			
		||||
    protected discardedObserver: CoreEventObserver;
 | 
			
		||||
    protected editEventObserver: CoreEventObserver;
 | 
			
		||||
    protected deleteEventObserver: CoreEventObserver;
 | 
			
		||||
    protected undeleteEventObserver: CoreEventObserver;
 | 
			
		||||
    protected syncObserver: CoreEventObserver;
 | 
			
		||||
    protected manualSyncObserver: CoreEventObserver;
 | 
			
		||||
    protected filterChangedObserver: CoreEventObserver;
 | 
			
		||||
    protected onlineObserver: Subscription;
 | 
			
		||||
 | 
			
		||||
    eventId?: number; // Selected EventId on list
 | 
			
		||||
    courses: Partial<CoreEnrolledCourseData>[] = [];
 | 
			
		||||
    eventsLoaded = false;
 | 
			
		||||
    events: AddonCalendarEventToDisplay[] = []; // Events (both online and offline).
 | 
			
		||||
    notificationsEnabled = false;
 | 
			
		||||
    filteredEvents: AddonCalendarEventToDisplay[] = [];
 | 
			
		||||
    canLoadMore = false;
 | 
			
		||||
    loadMoreError = false;
 | 
			
		||||
    canCreate = false;
 | 
			
		||||
    hasOffline = false;
 | 
			
		||||
    isOnline = false;
 | 
			
		||||
    syncIcon = 'spinner';
 | 
			
		||||
    filter: AddonCalendarFilter = {
 | 
			
		||||
        filtered: false,
 | 
			
		||||
        courseId: -1,
 | 
			
		||||
        categoryId: undefined,
 | 
			
		||||
        course: true,
 | 
			
		||||
        group: true,
 | 
			
		||||
        site: true,
 | 
			
		||||
        user: true,
 | 
			
		||||
        category: true,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        protected route: ActivatedRoute,
 | 
			
		||||
        private popoverCtrl: PopoverController,
 | 
			
		||||
    ) {
 | 
			
		||||
 | 
			
		||||
        this.siteHomeId = CoreSites.instance.getCurrentSiteHomeId();
 | 
			
		||||
        this.notificationsEnabled = CoreLocalNotifications.instance.isAvailable();
 | 
			
		||||
        this.currentSiteId = CoreSites.instance.getCurrentSiteId();
 | 
			
		||||
 | 
			
		||||
        if (this.notificationsEnabled) {
 | 
			
		||||
            // Re-schedule events if default time changes.
 | 
			
		||||
            this.obsDefaultTimeChange = CoreEvents.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, () => {
 | 
			
		||||
                AddonCalendar.instance.scheduleEventsNotifications(this.onlineEvents);
 | 
			
		||||
            }, this.currentSiteId);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Listen for events added. When an event is added, reload the data.
 | 
			
		||||
        this.newEventObserver = CoreEvents.on(AddonCalendarProvider.NEW_EVENT_EVENT, (data: AddonCalendarUpdatedEventEvent) => {
 | 
			
		||||
            if (data && data.eventId) {
 | 
			
		||||
                /* if (this.splitviewCtrl.isOn()) {
 | 
			
		||||
                    // Discussion added, clear details page.
 | 
			
		||||
                    this.splitviewCtrl.emptyDetails();
 | 
			
		||||
                }*/
 | 
			
		||||
 | 
			
		||||
                this.eventsLoaded = false;
 | 
			
		||||
                this.refreshEvents(true, false).finally(() => {
 | 
			
		||||
 | 
			
		||||
                    // In tablet mode try to open the event (only if it's an online event).
 | 
			
		||||
                    /* if (this.splitviewCtrl.isOn() && data.event.id > 0) {
 | 
			
		||||
                        this.gotoEvent(data.event.id);
 | 
			
		||||
                    }*/
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        }, this.currentSiteId);
 | 
			
		||||
 | 
			
		||||
        // Listen for new event discarded event. When it does, reload the data.
 | 
			
		||||
        this.discardedObserver = CoreEvents.on(AddonCalendarProvider.NEW_EVENT_DISCARDED_EVENT, () => {
 | 
			
		||||
            /* if (this.splitviewCtrl.isOn()) {
 | 
			
		||||
                // Discussion added, clear details page.
 | 
			
		||||
                this.splitviewCtrl.emptyDetails();
 | 
			
		||||
            }*/
 | 
			
		||||
 | 
			
		||||
            this.eventsLoaded = false;
 | 
			
		||||
            this.refreshEvents(true, false);
 | 
			
		||||
        }, this.currentSiteId);
 | 
			
		||||
 | 
			
		||||
        // Listen for events edited. When an event is edited, reload the data.
 | 
			
		||||
        this.editEventObserver = CoreEvents.on(AddonCalendarProvider.EDIT_EVENT_EVENT, (data: AddonCalendarUpdatedEventEvent) => {
 | 
			
		||||
            if (data && data.eventId) {
 | 
			
		||||
                this.eventsLoaded = false;
 | 
			
		||||
                this.refreshEvents(true, false);
 | 
			
		||||
            }
 | 
			
		||||
        }, this.currentSiteId);
 | 
			
		||||
 | 
			
		||||
        // Refresh data if calendar events are synchronized automatically.
 | 
			
		||||
        this.syncObserver = CoreEvents.on(AddonCalendarSyncProvider.AUTO_SYNCED, () => {
 | 
			
		||||
            this.eventsLoaded = false;
 | 
			
		||||
            this.refreshEvents();
 | 
			
		||||
 | 
			
		||||
            /* if (this.splitviewCtrl.isOn() && this.eventId && data && data.deleted && data.deleted.indexOf(this.eventId) != -1) {
 | 
			
		||||
                // Current selected event was deleted. Clear details.
 | 
			
		||||
                this.splitviewCtrl.emptyDetails();
 | 
			
		||||
            } */
 | 
			
		||||
        }, this.currentSiteId);
 | 
			
		||||
 | 
			
		||||
        // Refresh data if calendar events are synchronized manually but not by this page.
 | 
			
		||||
        this.manualSyncObserver = CoreEvents.on(AddonCalendarSyncProvider.MANUAL_SYNCED, (data: AddonCalendarSyncEvents) => {
 | 
			
		||||
            if (data && data.source != 'list') {
 | 
			
		||||
                this.eventsLoaded = false;
 | 
			
		||||
                this.refreshEvents();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            /* if (this.splitviewCtrl.isOn() && this.eventId && data && data.deleted && data.deleted.indexOf(this.eventId) != -1) {
 | 
			
		||||
                // Current selected event was deleted. Clear details.
 | 
			
		||||
                this.splitviewCtrl.emptyDetails();
 | 
			
		||||
            }*/
 | 
			
		||||
        }, this.currentSiteId);
 | 
			
		||||
 | 
			
		||||
        // Update the list when an event is deleted.
 | 
			
		||||
        this.deleteEventObserver = CoreEvents.on(
 | 
			
		||||
            AddonCalendarProvider.DELETED_EVENT_EVENT,
 | 
			
		||||
            (data: AddonCalendarUpdatedEventEvent) => {
 | 
			
		||||
                if (data && !data.sent) {
 | 
			
		||||
                // Event was deleted in offline. Just mark it as deleted, no need to refresh.
 | 
			
		||||
                    this.markAsDeleted(data.eventId, true);
 | 
			
		||||
                    this.deletedEvents.push(data.eventId);
 | 
			
		||||
                    this.hasOffline = true;
 | 
			
		||||
                } else {
 | 
			
		||||
                // Event deleted, clear the details if needed and refresh the view.
 | 
			
		||||
                /* if (this.splitviewCtrl.isOn()) {
 | 
			
		||||
                    this.splitviewCtrl.emptyDetails();
 | 
			
		||||
                } */
 | 
			
		||||
 | 
			
		||||
                    this.eventsLoaded = false;
 | 
			
		||||
                    this.refreshEvents();
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            this.currentSiteId,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Listen for events "undeleted" (offline).
 | 
			
		||||
        this.undeleteEventObserver = CoreEvents.on(
 | 
			
		||||
            AddonCalendarProvider.UNDELETED_EVENT_EVENT,
 | 
			
		||||
            (data: AddonCalendarUpdatedEventEvent) => {
 | 
			
		||||
                if (!data || !data.eventId) {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Mark it as undeleted, no need to refresh.
 | 
			
		||||
                this.markAsDeleted(data.eventId, false);
 | 
			
		||||
 | 
			
		||||
                // Remove it from the list of deleted events if it's there.
 | 
			
		||||
                const index = this.deletedEvents.indexOf(data.eventId);
 | 
			
		||||
                if (index != -1) {
 | 
			
		||||
                    this.deletedEvents.splice(index, 1);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                this.hasOffline = !!this.offlineEvents.length || !!this.deletedEvents.length;
 | 
			
		||||
            },
 | 
			
		||||
            this.currentSiteId,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        this.filterChangedObserver =
 | 
			
		||||
            CoreEvents.on(AddonCalendarProvider.FILTER_CHANGED_EVENT, async (data: AddonCalendarFilter) => {
 | 
			
		||||
                this.filter = data;
 | 
			
		||||
 | 
			
		||||
                // Course viewed has changed, check if the user can create events for this course calendar.
 | 
			
		||||
                this.canCreate = await AddonCalendarHelper.instance.canEditEvents(this.filter.courseId);
 | 
			
		||||
 | 
			
		||||
                this.filterEvents();
 | 
			
		||||
 | 
			
		||||
                this.content?.scrollToTop();
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        // Refresh online status when changes.
 | 
			
		||||
        this.onlineObserver = Network.instance.onChange().subscribe(() => {
 | 
			
		||||
            // Execute the callback in the Angular zone, so change detection doesn't stop working.
 | 
			
		||||
            NgZone.instance.run(() => {
 | 
			
		||||
                this.isOnline = CoreApp.instance.isOnline();
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * View loaded.
 | 
			
		||||
     */
 | 
			
		||||
    async ngOnInit(): Promise<void> {
 | 
			
		||||
        this.eventId = this.route.snapshot.queryParams['eventId'] || undefined;
 | 
			
		||||
        this.filter.courseId = this.route.snapshot.queryParams['courseId'];
 | 
			
		||||
 | 
			
		||||
        if (this.eventId) {
 | 
			
		||||
            // There is an event to load, open the event in a new state.
 | 
			
		||||
            this.gotoEvent(this.eventId);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.syncIcon = 'spinner';
 | 
			
		||||
 | 
			
		||||
        await this.fetchData(false, true, false);
 | 
			
		||||
 | 
			
		||||
        /* if (!this.eventId && this.splitviewCtrl.isOn() && this.events.length > 0) {
 | 
			
		||||
            // Take first online event and load it. If no online event, load the first offline.
 | 
			
		||||
            if (this.onlineEvents[0]) {
 | 
			
		||||
                this.gotoEvent(this.onlineEvents[0].id);
 | 
			
		||||
            } else {
 | 
			
		||||
                this.gotoEvent(this.offlineEvents[0].id);
 | 
			
		||||
            }
 | 
			
		||||
        }*/
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Fetch all the data required for the view.
 | 
			
		||||
     *
 | 
			
		||||
     * @param refresh Empty events array first.
 | 
			
		||||
     * @param sync Whether it should try to synchronize offline events.
 | 
			
		||||
     * @param showErrors Whether to show sync errors to the user.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async fetchData(refresh = false, sync = false, showErrors = false): Promise<void> {
 | 
			
		||||
        this.initialTime = CoreTimeUtils.instance.timestamp();
 | 
			
		||||
        this.daysLoaded = 0;
 | 
			
		||||
        this.emptyEventsTimes = 0;
 | 
			
		||||
        this.isOnline = CoreApp.instance.isOnline();
 | 
			
		||||
 | 
			
		||||
        if (sync) {
 | 
			
		||||
            // Try to synchronize offline events.
 | 
			
		||||
            try {
 | 
			
		||||
                const result = await AddonCalendarSync.instance.syncEvents();
 | 
			
		||||
                if (result.warnings && result.warnings.length) {
 | 
			
		||||
                    CoreDomUtils.instance.showErrorModal(result.warnings[0]);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (result.updated) {
 | 
			
		||||
                    // Trigger a manual sync event.
 | 
			
		||||
                    result.source = 'list';
 | 
			
		||||
 | 
			
		||||
                    CoreEvents.trigger<AddonCalendarSyncEvents>(
 | 
			
		||||
                        AddonCalendarSyncProvider.MANUAL_SYNCED,
 | 
			
		||||
                        result,
 | 
			
		||||
                        this.currentSiteId,
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                if (showErrors) {
 | 
			
		||||
                    CoreDomUtils.instance.showErrorModalDefault(error, 'core.errorsync', true);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const promises: Promise<void>[] = [];
 | 
			
		||||
 | 
			
		||||
            this.hasOffline = false;
 | 
			
		||||
 | 
			
		||||
            promises.push(AddonCalendarHelper.instance.canEditEvents(this.filter.courseId).then((canEdit) => {
 | 
			
		||||
                this.canCreate = canEdit;
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }));
 | 
			
		||||
 | 
			
		||||
            // Load courses for the popover.
 | 
			
		||||
            promises.push(CoreCoursesHelper.instance.getCoursesForPopover(this.filter.courseId).then((result) => {
 | 
			
		||||
                this.courses = result.courses;
 | 
			
		||||
 | 
			
		||||
                return this.fetchEvents(refresh);
 | 
			
		||||
            }));
 | 
			
		||||
 | 
			
		||||
            // Get offline events.
 | 
			
		||||
            promises.push(AddonCalendarOffline.instance.getAllEditedEvents().then((offlineEvents) => {
 | 
			
		||||
                this.hasOffline = this.hasOffline || !!offlineEvents.length;
 | 
			
		||||
 | 
			
		||||
                // Format data and sort by timestart.
 | 
			
		||||
                const events: AddonCalendarEventToDisplay[] = offlineEvents.map((event) =>
 | 
			
		||||
                    AddonCalendarHelper.instance.formatOfflineEventData(event));
 | 
			
		||||
 | 
			
		||||
                this.offlineEvents = AddonCalendarHelper.instance.sortEvents(events);
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }));
 | 
			
		||||
 | 
			
		||||
            // Get events deleted in offline.
 | 
			
		||||
            promises.push(AddonCalendarOffline.instance.getAllDeletedEventsIds().then((ids) => {
 | 
			
		||||
                this.hasOffline = this.hasOffline || !!ids.length;
 | 
			
		||||
                this.deletedEvents = ids;
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }));
 | 
			
		||||
 | 
			
		||||
            await Promise.all(promises);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.instance.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.eventsLoaded = true;
 | 
			
		||||
        this.syncIcon = 'fas-sync-alt';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Fetches the events and updates the view.
 | 
			
		||||
     *
 | 
			
		||||
     * @param refresh Empty events array first.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async fetchEvents(refresh = false): Promise<void> {
 | 
			
		||||
        this.loadMoreError = false;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const onlineEventsTemp =
 | 
			
		||||
                await AddonCalendar.instance.getEventsList(this.initialTime, this.daysLoaded, AddonCalendarProvider.DAYS_INTERVAL);
 | 
			
		||||
 | 
			
		||||
            if (onlineEventsTemp.length === 0) {
 | 
			
		||||
                this.emptyEventsTimes++;
 | 
			
		||||
                if (this.emptyEventsTimes > 5) { // Stop execution if we retrieve empty list 6 consecutive times.
 | 
			
		||||
                    this.canLoadMore = false;
 | 
			
		||||
                    if (refresh) {
 | 
			
		||||
                        this.onlineEvents = [];
 | 
			
		||||
                        this.filteredEvents = [];
 | 
			
		||||
                        this.events = this.offlineEvents;
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    // No events returned, load next events.
 | 
			
		||||
                    this.daysLoaded += AddonCalendarProvider.DAYS_INTERVAL;
 | 
			
		||||
 | 
			
		||||
                    return this.fetchEvents();
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                const onlineEvents = onlineEventsTemp.map((event) => AddonCalendarHelper.instance.formatEventData(event));
 | 
			
		||||
 | 
			
		||||
                // Get the merged events of this period.
 | 
			
		||||
                const events = this.mergeEvents(onlineEvents);
 | 
			
		||||
 | 
			
		||||
                this.getCategories = this.shouldLoadCategories(onlineEvents);
 | 
			
		||||
 | 
			
		||||
                if (refresh) {
 | 
			
		||||
                    this.onlineEvents = onlineEvents;
 | 
			
		||||
                    this.events = events;
 | 
			
		||||
                } else {
 | 
			
		||||
                    // Filter events with same ID. Repeated events are returned once per WS call, show them only once.
 | 
			
		||||
                    this.onlineEvents = CoreUtils.instance.mergeArraysWithoutDuplicates(this.onlineEvents, onlineEvents, 'id');
 | 
			
		||||
                    this.events = CoreUtils.instance.mergeArraysWithoutDuplicates(this.events, events, 'id');
 | 
			
		||||
                }
 | 
			
		||||
                this.filterEvents();
 | 
			
		||||
 | 
			
		||||
                // Calculate which evemts need to display the date.
 | 
			
		||||
                this.filteredEvents.forEach((event, index) => {
 | 
			
		||||
                    event.showDate = this.showDate(event, this.filteredEvents[index - 1]);
 | 
			
		||||
                    event.endsSameDay = this.endsSameDay(event);
 | 
			
		||||
                });
 | 
			
		||||
                this.canLoadMore = true;
 | 
			
		||||
 | 
			
		||||
                // Schedule notifications for the events retrieved (might have new events).
 | 
			
		||||
                AddonCalendar.instance.scheduleEventsNotifications(this.onlineEvents);
 | 
			
		||||
 | 
			
		||||
                this.daysLoaded += AddonCalendarProvider.DAYS_INTERVAL;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // 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) {
 | 
			
		||||
            CoreDomUtils.instance.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
 | 
			
		||||
            this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading.
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Success retrieving events. Get categories if needed.
 | 
			
		||||
        if (this.getCategories) {
 | 
			
		||||
            this.getCategories = false;
 | 
			
		||||
 | 
			
		||||
            return this.loadCategories();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Function to load more events.
 | 
			
		||||
     *
 | 
			
		||||
     * @param infiniteComplete Infinite scroll complete function. Only used from core-infinite-loading.
 | 
			
		||||
     * @return Resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    loadMoreEvents(infiniteComplete?: () => void ): void {
 | 
			
		||||
        this.fetchEvents().finally(() => {
 | 
			
		||||
            infiniteComplete && infiniteComplete();
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected filterEvents(): void {
 | 
			
		||||
        this.filteredEvents = AddonCalendarHelper.instance.getFilteredEvents(this.events, this.filter, this.categories);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns if the current state should load categories or not.
 | 
			
		||||
     *
 | 
			
		||||
     * @param events Events to parse.
 | 
			
		||||
     * @return True if categories should be loaded.
 | 
			
		||||
     */
 | 
			
		||||
    protected shouldLoadCategories(events: AddonCalendarEventToDisplay[]): 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.
 | 
			
		||||
        const found = events.some((event) => typeof event.categoryid != 'undefined' && event.categoryid > 0);
 | 
			
		||||
 | 
			
		||||
        return found || this.getCategories;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Load categories to be able to filter events.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async loadCategories(): Promise<void> {
 | 
			
		||||
        try {
 | 
			
		||||
            const cats = await CoreCourses.instance.getCategories(0, true);
 | 
			
		||||
            this.categoriesRetrieved = true;
 | 
			
		||||
            this.categories = {};
 | 
			
		||||
            // Index categories by ID.
 | 
			
		||||
            cats.forEach((category) => {
 | 
			
		||||
                this.categories[category.id] = category;
 | 
			
		||||
            });
 | 
			
		||||
        } catch {
 | 
			
		||||
            // Ignore errors.
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Merge a period of online events with the offline events of that period.
 | 
			
		||||
     *
 | 
			
		||||
     * @param onlineEvents Online events.
 | 
			
		||||
     * @return Merged events.
 | 
			
		||||
     */
 | 
			
		||||
    protected mergeEvents(onlineEvents: AddonCalendarEventToDisplay[]): AddonCalendarEventToDisplay[] {
 | 
			
		||||
        if (!this.offlineEvents.length && !this.deletedEvents.length) {
 | 
			
		||||
            // No offline events, nothing to merge.
 | 
			
		||||
            return onlineEvents;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const start = this.initialTime + (CoreConstants.SECONDS_DAY * this.daysLoaded);
 | 
			
		||||
        const end = start + (CoreConstants.SECONDS_DAY * AddonCalendarProvider.DAYS_INTERVAL) - 1;
 | 
			
		||||
        let result = onlineEvents;
 | 
			
		||||
 | 
			
		||||
        if (this.deletedEvents.length) {
 | 
			
		||||
            // Mark as deleted the events that were deleted in offline.
 | 
			
		||||
            result.forEach((event) => {
 | 
			
		||||
                event.deleted = this.deletedEvents.indexOf(event.id) != -1;
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.offlineEvents.length) {
 | 
			
		||||
            // Remove the online events that were modified in offline.
 | 
			
		||||
            result = result.filter((event) => {
 | 
			
		||||
                const offlineEvent = this.offlineEvents.find((ev) => ev.id == event.id);
 | 
			
		||||
 | 
			
		||||
                return !offlineEvent;
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Now get the offline events that belong to this period.
 | 
			
		||||
        const periodOfflineEvents = this.offlineEvents.filter((event) => {
 | 
			
		||||
            if (this.daysLoaded == 0 && event.timestart < start) {
 | 
			
		||||
                // Display offline events that are previous to current time to allow editing them.
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return (event.timestart >= start || event.timestart + event.timeduration >= start) && event.timestart <= end;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Merge both arrays and sort them.
 | 
			
		||||
        result = result.concat(periodOfflineEvents);
 | 
			
		||||
 | 
			
		||||
        return AddonCalendarHelper.instance.sortEvents(result);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Refresh the data.
 | 
			
		||||
     *
 | 
			
		||||
     * @param refresher Refresher.
 | 
			
		||||
     * @param done Function to call when done.
 | 
			
		||||
     * @param showErrors Whether to show sync errors to the user.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async doRefresh(refresher?: CustomEvent<IonRefresher>, done?: () => void, showErrors?: boolean): Promise<void> {
 | 
			
		||||
        if (!this.eventsLoaded) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await this.refreshEvents(true, showErrors).finally(() => {
 | 
			
		||||
            refresher?.detail.complete();
 | 
			
		||||
            done && done();
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Refresh the events.
 | 
			
		||||
     *
 | 
			
		||||
     * @param sync Whether it should try to synchronize offline events.
 | 
			
		||||
     * @param showErrors Whether to show sync errors to the user.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async refreshEvents(sync?: boolean, showErrors?: boolean): Promise<void> {
 | 
			
		||||
        this.syncIcon = 'spinner';
 | 
			
		||||
 | 
			
		||||
        const promises: Promise<void>[] = [];
 | 
			
		||||
 | 
			
		||||
        promises.push(AddonCalendar.instance.invalidateEventsList());
 | 
			
		||||
        promises.push(AddonCalendar.instance.invalidateAllowedEventTypes());
 | 
			
		||||
 | 
			
		||||
        if (this.categoriesRetrieved) {
 | 
			
		||||
            promises.push(CoreCourses.instance.invalidateCategories(0, true));
 | 
			
		||||
            this.categoriesRetrieved = false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await Promise.all(promises).finally(() => this.fetchData(true, sync, showErrors));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check date should be shown on event list for the current event.
 | 
			
		||||
     * If date has changed from previous to current event it should be shown.
 | 
			
		||||
     *
 | 
			
		||||
     * @param event Current event where to show the date.
 | 
			
		||||
     * @param prevEvent Previous event where to compare the date with.
 | 
			
		||||
     * @return If date has changed and should be shown.
 | 
			
		||||
     */
 | 
			
		||||
    protected showDate(event: AddonCalendarEventToDisplay, prevEvent?: AddonCalendarEventToDisplay): boolean {
 | 
			
		||||
        if (!prevEvent) {
 | 
			
		||||
            // First event, show it.
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check if day has changed.
 | 
			
		||||
        return !moment(event.timestart * 1000).isSame(prevEvent.timestart * 1000, 'day');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if event ends the same date or not.
 | 
			
		||||
     *
 | 
			
		||||
     * @param event Event info.
 | 
			
		||||
     * @return If date has changed and should be shown.
 | 
			
		||||
     */
 | 
			
		||||
    protected endsSameDay(event: AddonCalendarEventToDisplay): boolean {
 | 
			
		||||
        if (!event.timeduration) {
 | 
			
		||||
            // No duration.
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check if day has changed.
 | 
			
		||||
        return moment(event.timestart * 1000).isSame((event.timestart + event.timeduration) * 1000, 'day');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Show the context menu.
 | 
			
		||||
     *
 | 
			
		||||
     * @param event Event.
 | 
			
		||||
     */
 | 
			
		||||
    async openFilter(event: MouseEvent): Promise<void> {
 | 
			
		||||
        const popover = await this.popoverCtrl.create({
 | 
			
		||||
            component: AddonCalendarFilterPopoverComponent,
 | 
			
		||||
            componentProps: {
 | 
			
		||||
                courses: this.courses,
 | 
			
		||||
                filter: this.filter,
 | 
			
		||||
            },
 | 
			
		||||
            event,
 | 
			
		||||
        });
 | 
			
		||||
        await popover.present();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Open page to create/edit an event.
 | 
			
		||||
     *
 | 
			
		||||
     * @param eventId Event ID to edit.
 | 
			
		||||
     */
 | 
			
		||||
    openEdit(eventId?: number): void {
 | 
			
		||||
        this.eventId = undefined;
 | 
			
		||||
 | 
			
		||||
        const params: Params = {};
 | 
			
		||||
 | 
			
		||||
        if (eventId) {
 | 
			
		||||
            params.eventId = eventId;
 | 
			
		||||
        }
 | 
			
		||||
        if (this.filter.courseId) {
 | 
			
		||||
            params.courseId = this.filter.courseId;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        CoreNavigator.instance.navigateToSitePath('/calendar/edit', { params }); // @todo , this.splitviewCtrl);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Open calendar events settings.
 | 
			
		||||
     */
 | 
			
		||||
    openSettings(): void {
 | 
			
		||||
        CoreNavigator.instance.navigateToSitePath('/calendar/settings');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Navigate to a particular event.
 | 
			
		||||
     *
 | 
			
		||||
     * @param eventId Event to load.
 | 
			
		||||
     */
 | 
			
		||||
    gotoEvent(eventId: number): void {
 | 
			
		||||
        this.eventId = eventId;
 | 
			
		||||
 | 
			
		||||
        if (eventId < 0) {
 | 
			
		||||
            // It's an offline event, go to the edit page.
 | 
			
		||||
            this.openEdit(eventId);
 | 
			
		||||
        } else {
 | 
			
		||||
            /* this.splitviewCtrl.push('/calendar/event', {
 | 
			
		||||
                id: eventId,
 | 
			
		||||
            });*/
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Find an event and mark it as deleted.
 | 
			
		||||
     *
 | 
			
		||||
     * @param eventId Event ID.
 | 
			
		||||
     * @param deleted Whether to mark it as deleted or not.
 | 
			
		||||
     */
 | 
			
		||||
    protected markAsDeleted(eventId: number, deleted: boolean): void {
 | 
			
		||||
        const event = this.onlineEvents.find((event) => event.id == eventId);
 | 
			
		||||
 | 
			
		||||
        if (event) {
 | 
			
		||||
            event.deleted = deleted;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Page destroyed.
 | 
			
		||||
     */
 | 
			
		||||
    ngOnDestroy(): void {
 | 
			
		||||
        this.obsDefaultTimeChange?.off();
 | 
			
		||||
        this.newEventObserver?.off();
 | 
			
		||||
        this.discardedObserver?.off();
 | 
			
		||||
        this.editEventObserver?.off();
 | 
			
		||||
        this.deleteEventObserver?.off();
 | 
			
		||||
        this.undeleteEventObserver?.off();
 | 
			
		||||
        this.syncObserver?.off();
 | 
			
		||||
        this.manualSyncObserver?.off();
 | 
			
		||||
        this.filterChangedObserver?.off();
 | 
			
		||||
        this.onlineObserver?.unsubscribe();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										5
									
								
								src/addons/calendar/pages/list/list.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,5 @@
 | 
			
		||||
:host {
 | 
			
		||||
    ion-note {
 | 
			
		||||
        max-width: 30%;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										25
									
								
								src/addons/calendar/pages/settings/settings.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,25 @@
 | 
			
		||||
<ion-header>
 | 
			
		||||
    <ion-toolbar>
 | 
			
		||||
        <ion-buttons slot="start">
 | 
			
		||||
            <ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
        <ion-title>{{ 'core.settings.settings' | translate }}</ion-title>
 | 
			
		||||
    </ion-toolbar>
 | 
			
		||||
</ion-header>
 | 
			
		||||
<ion-content>
 | 
			
		||||
    <ion-list>
 | 
			
		||||
        <ion-item>
 | 
			
		||||
            <ion-label>{{ 'addon.calendar.defaultnotificationtime' | translate }}</ion-label>
 | 
			
		||||
            <ion-select [(ngModel)]="defaultTime" (ionChange)="updateDefaultTime($event)" interface="action-sheet">
 | 
			
		||||
                <ion-select-option value="0">{{ 'core.settings.disabled' | translate }}</ion-select-option>
 | 
			
		||||
                <ion-select-option value="10">{{ 600 | coreDuration }}</ion-select-option>
 | 
			
		||||
                <ion-select-option value="30">{{ 1800 | coreDuration }}</ion-select-option>
 | 
			
		||||
                <ion-select-option value="60">{{ 3600 | coreDuration }}</ion-select-option>
 | 
			
		||||
                <ion-select-option value="120">{{ 7200 | coreDuration }}</ion-select-option>
 | 
			
		||||
                <ion-select-option value="360">{{ 21600 | coreDuration }}</ion-select-option>
 | 
			
		||||
                <ion-select-option value="720">{{ 43200 | coreDuration }}</ion-select-option>
 | 
			
		||||
                <ion-select-option value="1440">{{ 86400 | coreDuration }}</ion-select-option>
 | 
			
		||||
            </ion-select>
 | 
			
		||||
        </ion-item>
 | 
			
		||||
    </ion-list>
 | 
			
		||||
</ion-content>
 | 
			
		||||
							
								
								
									
										50
									
								
								src/addons/calendar/pages/settings/settings.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,50 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
import { NgModule } from '@angular/core';
 | 
			
		||||
import { IonicModule } from '@ionic/angular';
 | 
			
		||||
import { TranslateModule } from '@ngx-translate/core';
 | 
			
		||||
import { RouterModule, Routes } from '@angular/router';
 | 
			
		||||
import { CommonModule } from '@angular/common';
 | 
			
		||||
import { FormsModule } from '@angular/forms';
 | 
			
		||||
 | 
			
		||||
import { CoreDirectivesModule } from '@directives/directives.module';
 | 
			
		||||
import { CorePipesModule } from '@pipes/pipes.module';
 | 
			
		||||
 | 
			
		||||
import { AddonCalendarSettingsPage } from './settings';
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
    {
 | 
			
		||||
        path: '',
 | 
			
		||||
        component: AddonCalendarSettingsPage,
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    imports: [
 | 
			
		||||
        RouterModule.forChild(routes),
 | 
			
		||||
        CommonModule,
 | 
			
		||||
        IonicModule,
 | 
			
		||||
        FormsModule,
 | 
			
		||||
        TranslateModule.forChild(),
 | 
			
		||||
        CoreDirectivesModule,
 | 
			
		||||
        CorePipesModule,
 | 
			
		||||
    ],
 | 
			
		||||
    declarations: [
 | 
			
		||||
        AddonCalendarSettingsPage,
 | 
			
		||||
    ],
 | 
			
		||||
    exports: [RouterModule],
 | 
			
		||||
})
 | 
			
		||||
export class AddonCalendarSettingsPageModule {}
 | 
			
		||||
							
								
								
									
										53
									
								
								src/addons/calendar/pages/settings/settings.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,53 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Component, OnInit } from '@angular/core';
 | 
			
		||||
import { AddonCalendar, AddonCalendarProvider } from '../../services/calendar';
 | 
			
		||||
import { CoreEvents } from '@singletons/events';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Page that displays the calendar settings.
 | 
			
		||||
 */
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'page-addon-calendar-settings',
 | 
			
		||||
    templateUrl: 'settings.html',
 | 
			
		||||
})
 | 
			
		||||
export class AddonCalendarSettingsPage implements OnInit {
 | 
			
		||||
 | 
			
		||||
    defaultTime = 0;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * View loaded.
 | 
			
		||||
     */
 | 
			
		||||
    async ngOnInit(): Promise<void> {
 | 
			
		||||
        this.defaultTime = await AddonCalendar.instance.getDefaultNotificationTime();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update default time.
 | 
			
		||||
     *
 | 
			
		||||
     * @param newTime New time.
 | 
			
		||||
     */
 | 
			
		||||
    updateDefaultTime(newTime: number): void {
 | 
			
		||||
        AddonCalendar.instance.setDefaultNotificationTime(newTime);
 | 
			
		||||
 | 
			
		||||
        CoreEvents.trigger(
 | 
			
		||||
            AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED,
 | 
			
		||||
            { time: newTime },
 | 
			
		||||
            CoreSites.instance.getCurrentSiteId(),
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										736
									
								
								src/addons/calendar/services/calendar-helper.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,736 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Injectable } from '@angular/core';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import {
 | 
			
		||||
    AddonCalendar,
 | 
			
		||||
    AddonCalendarDayName,
 | 
			
		||||
    AddonCalendarEvent,
 | 
			
		||||
    AddonCalendarEventBase,
 | 
			
		||||
    AddonCalendarEventToDisplay,
 | 
			
		||||
    AddonCalendarEventType,
 | 
			
		||||
    AddonCalendarGetEventsEvent,
 | 
			
		||||
    AddonCalendarProvider,
 | 
			
		||||
    AddonCalendarWeek,
 | 
			
		||||
    AddonCalendarWeekDay,
 | 
			
		||||
} from './calendar';
 | 
			
		||||
import { CoreConfig } from '@services/config';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
import { ContextLevel, CoreConstants } from '@/core/constants';
 | 
			
		||||
import moment from 'moment';
 | 
			
		||||
import { makeSingleton } from '@singletons';
 | 
			
		||||
import { AddonCalendarSyncInvalidateEvent } from './calendar-sync';
 | 
			
		||||
import { AddonCalendarOfflineEventDBRecord } from './database/calendar-offline';
 | 
			
		||||
import { CoreCategoryData } from '@features/courses/services/courses';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Context levels enumeration.
 | 
			
		||||
 */
 | 
			
		||||
export enum AddonCalendarEventIcons {
 | 
			
		||||
    SITE = 'fas-globe',
 | 
			
		||||
    CATEGORY = 'fas-cubes',
 | 
			
		||||
    COURSE = 'fas-graduation-cap',
 | 
			
		||||
    GROUP = 'fas-users',
 | 
			
		||||
    USER = 'fas-user',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Service that provides some features regarding lists of courses and categories.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable({ providedIn: 'root' })
 | 
			
		||||
export class AddonCalendarHelperProvider {
 | 
			
		||||
 | 
			
		||||
    protected eventTypeIcons: string[] = [];
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns event icon based on event type.
 | 
			
		||||
     *
 | 
			
		||||
     * @param eventType Type of the event.
 | 
			
		||||
     * @return Event icon.
 | 
			
		||||
     */
 | 
			
		||||
    getEventIcon(eventType: AddonCalendarEventType): string {
 | 
			
		||||
        if (this.eventTypeIcons.length == 0) {
 | 
			
		||||
            CoreUtils.instance.enumKeys(AddonCalendarEventType).forEach((name) => {
 | 
			
		||||
                const value = AddonCalendarEventType[name];
 | 
			
		||||
                this.eventTypeIcons[value] = AddonCalendarEventIcons[name];
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return this.eventTypeIcons[eventType] || '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Calculate some day data based on a list of events for that day.
 | 
			
		||||
     *
 | 
			
		||||
     * @param day Day.
 | 
			
		||||
     * @param events Events.
 | 
			
		||||
     */
 | 
			
		||||
    calculateDayData(day: AddonCalendarWeekDay, events: AddonCalendarEventToDisplay[]): void {
 | 
			
		||||
        day.hasevents = events.length > 0;
 | 
			
		||||
        day.haslastdayofevent = false;
 | 
			
		||||
 | 
			
		||||
        const types = {};
 | 
			
		||||
        events.forEach((event) => {
 | 
			
		||||
            types[event.formattedType || event.eventtype] = true;
 | 
			
		||||
 | 
			
		||||
            if (event.islastday) {
 | 
			
		||||
                day.haslastdayofevent = true;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        day.calendareventtypes = Object.keys(types) as AddonCalendarEventType[];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if current user can create/edit events.
 | 
			
		||||
     *
 | 
			
		||||
     * @param courseId Course ID. If not defined, site calendar.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with boolean: whether the user can create events.
 | 
			
		||||
     */
 | 
			
		||||
    async canEditEvents(courseId?: number, siteId?: string): Promise<boolean> {
 | 
			
		||||
        try {
 | 
			
		||||
            const canEdit = await AddonCalendar.instance.canEditEvents(siteId);
 | 
			
		||||
            if (!canEdit) {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const types = await AddonCalendar.instance.getAllowedEventTypes(courseId, siteId);
 | 
			
		||||
 | 
			
		||||
            return Object.keys(types).length > 0;
 | 
			
		||||
        } catch {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Classify events into their respective months and days. If an event duration covers more than one day,
 | 
			
		||||
     * it will be included in all the days it lasts.
 | 
			
		||||
     *
 | 
			
		||||
     * @param events Events to classify.
 | 
			
		||||
     * @return Object with the classified events.
 | 
			
		||||
     */
 | 
			
		||||
    classifyIntoMonths(
 | 
			
		||||
        offlineEvents: AddonCalendarOfflineEventDBRecord[],
 | 
			
		||||
    ): { [monthId: string]: { [day: number]: AddonCalendarEventToDisplay[] } } {
 | 
			
		||||
        // Format data.
 | 
			
		||||
        const events: AddonCalendarEventToDisplay[] = offlineEvents.map((event) =>
 | 
			
		||||
            AddonCalendarHelper.instance.formatOfflineEventData(event));
 | 
			
		||||
 | 
			
		||||
        const result = {};
 | 
			
		||||
 | 
			
		||||
        events.forEach((event) => {
 | 
			
		||||
            const treatedDay = moment(new Date(event.timestart * 1000));
 | 
			
		||||
            const endDay = moment(new Date((event.timestart + event.timeduration) * 1000));
 | 
			
		||||
 | 
			
		||||
            // Add the event to all the days it lasts.
 | 
			
		||||
            while (!treatedDay.isAfter(endDay, 'day')) {
 | 
			
		||||
                const monthId = this.getMonthId(treatedDay.year(), treatedDay.month() + 1);
 | 
			
		||||
                const day = treatedDay.date();
 | 
			
		||||
 | 
			
		||||
                if (!result[monthId]) {
 | 
			
		||||
                    result[monthId] = {};
 | 
			
		||||
                }
 | 
			
		||||
                if (!result[monthId][day]) {
 | 
			
		||||
                    result[monthId][day] = [];
 | 
			
		||||
                }
 | 
			
		||||
                result[monthId][day].push(event);
 | 
			
		||||
 | 
			
		||||
                treatedDay.add(1, 'day'); // Treat next day.
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Convenience function to format some event data to be rendered.
 | 
			
		||||
     *
 | 
			
		||||
     * @param event Event to format.
 | 
			
		||||
     */
 | 
			
		||||
    formatEventData(event: AddonCalendarEvent | AddonCalendarEventBase | AddonCalendarGetEventsEvent): AddonCalendarEventToDisplay {
 | 
			
		||||
 | 
			
		||||
        const eventFormatted: AddonCalendarEventToDisplay = {
 | 
			
		||||
            id: event.id!,
 | 
			
		||||
            name: event.name,
 | 
			
		||||
            eventtype: event.eventtype,
 | 
			
		||||
            categoryid: event.categoryid,
 | 
			
		||||
            groupid: event.groupid,
 | 
			
		||||
            description: event.description,
 | 
			
		||||
            location: 'location' in event? event.location : undefined,
 | 
			
		||||
            timestart: event.timestart,
 | 
			
		||||
            timeduration: event.timeduration,
 | 
			
		||||
            eventcount: 'eventcount' in event? event.eventcount || 0 : 0,
 | 
			
		||||
            repeatid: event.repeatid || 0,
 | 
			
		||||
            // repeateditall: event.repeateditall,
 | 
			
		||||
            userid: event.userid,
 | 
			
		||||
            timemodified: event.timemodified,
 | 
			
		||||
            eventIcon: this.getEventIcon(event.eventtype),
 | 
			
		||||
            formattedType: AddonCalendar.instance.getEventType(event),
 | 
			
		||||
            modulename: event.modulename,
 | 
			
		||||
            format: 1,
 | 
			
		||||
            visible: 1,
 | 
			
		||||
            offline: false,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if (event.modulename) {
 | 
			
		||||
            eventFormatted.eventIcon = CoreCourse.instance.getModuleIconSrc(event.modulename);
 | 
			
		||||
            eventFormatted.moduleIcon = eventFormatted.eventIcon;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        eventFormatted.formattedType = AddonCalendar.instance.getEventType(event);
 | 
			
		||||
 | 
			
		||||
        // Calculate context.
 | 
			
		||||
        if ('course' in event) {
 | 
			
		||||
            eventFormatted.courseid = event.course?.id;
 | 
			
		||||
        } else if ('courseid' in event) {
 | 
			
		||||
            eventFormatted.courseid = event.courseid;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Calculate context.
 | 
			
		||||
        if ('category' in event) {
 | 
			
		||||
            eventFormatted.categoryid = event.category?.id;
 | 
			
		||||
        } else if ('categoryid' in event) {
 | 
			
		||||
            eventFormatted.categoryid = event.categoryid;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ('canedit' in event) {
 | 
			
		||||
            eventFormatted.canedit = event.canedit;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ('candelete' in event) {
 | 
			
		||||
            eventFormatted.candelete = event.candelete;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.formatEventContext(eventFormatted, eventFormatted.courseid, eventFormatted.categoryid);
 | 
			
		||||
 | 
			
		||||
        return eventFormatted;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Convenience function to format some event data to be rendered.
 | 
			
		||||
     *
 | 
			
		||||
     * @param e Event to format.
 | 
			
		||||
     */
 | 
			
		||||
    formatOfflineEventData(event: AddonCalendarOfflineEventDBRecord): AddonCalendarEventToDisplay {
 | 
			
		||||
 | 
			
		||||
        const eventFormatted: AddonCalendarEventToDisplay = {
 | 
			
		||||
            id: event.id!,
 | 
			
		||||
            name: event.name,
 | 
			
		||||
            timestart: event.timestart,
 | 
			
		||||
            eventtype: event.eventtype,
 | 
			
		||||
            categoryid: event.categoryid,
 | 
			
		||||
            courseid: event.courseid || event.groupcourseid,
 | 
			
		||||
            groupid: event.groupid,
 | 
			
		||||
            description: event.description,
 | 
			
		||||
            location: event.location,
 | 
			
		||||
            duration: event.duration,
 | 
			
		||||
            timedurationuntil: event.timedurationuntil,
 | 
			
		||||
            timedurationminutes: event.timedurationminutes,
 | 
			
		||||
            // repeat: event.repeat,
 | 
			
		||||
            eventcount: event.repeats || 0,
 | 
			
		||||
            repeatid: event.repeatid || 0,
 | 
			
		||||
            // repeateditall: event.repeateditall,
 | 
			
		||||
            userid: event.userid,
 | 
			
		||||
            timemodified: event.timecreated || 0,
 | 
			
		||||
            eventIcon: this.getEventIcon(event.eventtype),
 | 
			
		||||
            formattedType: event.eventtype,
 | 
			
		||||
            format: 1,
 | 
			
		||||
            visible: 1,
 | 
			
		||||
            offline: true,
 | 
			
		||||
            timeduration: 0,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Calculate context.
 | 
			
		||||
        const categoryId = event.categoryid;
 | 
			
		||||
        const courseId = event.courseid || event.groupcourseid;
 | 
			
		||||
        this.formatEventContext(eventFormatted, courseId, categoryId);
 | 
			
		||||
 | 
			
		||||
        if (eventFormatted.duration == 1) {
 | 
			
		||||
            eventFormatted.timeduration = (event.timedurationuntil || 0) - event.timestart;
 | 
			
		||||
        } else if (eventFormatted.duration == 2) {
 | 
			
		||||
            eventFormatted.timeduration = (event.timedurationminutes || 0) * CoreConstants.SECONDS_MINUTE;
 | 
			
		||||
        } else {
 | 
			
		||||
            eventFormatted.timeduration = 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return eventFormatted;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Modifies event data with the context information.
 | 
			
		||||
     *
 | 
			
		||||
     * @param eventFormatted Event formatted to be displayed.
 | 
			
		||||
     * @param courseId Course Id if any.
 | 
			
		||||
     * @param categoryId Category Id if any.
 | 
			
		||||
     */
 | 
			
		||||
    protected formatEventContext(eventFormatted: AddonCalendarEventToDisplay, courseId?: number, categoryId?: number): void {
 | 
			
		||||
        if (categoryId && categoryId > 0) {
 | 
			
		||||
            eventFormatted.contextLevel = ContextLevel.COURSECAT;
 | 
			
		||||
            eventFormatted.contextInstanceId = categoryId;
 | 
			
		||||
        } else if (courseId && courseId > 0) {
 | 
			
		||||
            eventFormatted.contextLevel = ContextLevel.COURSE;
 | 
			
		||||
            eventFormatted.contextInstanceId = courseId;
 | 
			
		||||
        } else {
 | 
			
		||||
            eventFormatted.contextLevel = ContextLevel.USER;
 | 
			
		||||
            eventFormatted.contextInstanceId = eventFormatted.userid;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get options (name & value) for each allowed event type.
 | 
			
		||||
     *
 | 
			
		||||
     * @param eventTypes Result of getAllowedEventTypes.
 | 
			
		||||
     * @return Options.
 | 
			
		||||
     */
 | 
			
		||||
    getEventTypeOptions(eventTypes: {[name: string]: boolean}): AddonCalendarEventTypeOption[] {
 | 
			
		||||
        const options: AddonCalendarEventTypeOption[] = [];
 | 
			
		||||
 | 
			
		||||
        if (eventTypes.user) {
 | 
			
		||||
            options.push({ name: 'core.user', value: AddonCalendarEventType.USER });
 | 
			
		||||
        }
 | 
			
		||||
        if (eventTypes.group) {
 | 
			
		||||
            options.push({ name: 'core.group', value: AddonCalendarEventType.GROUP });
 | 
			
		||||
        }
 | 
			
		||||
        if (eventTypes.course) {
 | 
			
		||||
            options.push({ name: 'core.course', value: AddonCalendarEventType.COURSE });
 | 
			
		||||
        }
 | 
			
		||||
        if (eventTypes.category) {
 | 
			
		||||
            options.push({ name: 'core.category', value: AddonCalendarEventType.CATEGORY });
 | 
			
		||||
        }
 | 
			
		||||
        if (eventTypes.site) {
 | 
			
		||||
            options.push({ name: 'core.site', value: AddonCalendarEventType.SITE });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return options;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the month "id" (year + month).
 | 
			
		||||
     *
 | 
			
		||||
     * @param year Year.
 | 
			
		||||
     * @param month Month.
 | 
			
		||||
     * @return The "id".
 | 
			
		||||
     */
 | 
			
		||||
    getMonthId(year: number, month: number): string {
 | 
			
		||||
        return year + '#' + month;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get weeks of a month in offline (with no events).
 | 
			
		||||
     *
 | 
			
		||||
     * The result has the same structure than getMonthlyEvents, but it only contains fields that are actually used by the app.
 | 
			
		||||
     *
 | 
			
		||||
     * @param year Year to get.
 | 
			
		||||
     * @param month Month to get.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with the response.
 | 
			
		||||
     */
 | 
			
		||||
    async getOfflineMonthWeeks(
 | 
			
		||||
        year: number,
 | 
			
		||||
        month: number,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<{ daynames: Partial<AddonCalendarDayName>[]; weeks: Partial<AddonCalendarWeek>[] }> {
 | 
			
		||||
        const site = await CoreSites.instance.getSite(siteId);
 | 
			
		||||
        // Get starting week day user preference, fallback to site configuration.
 | 
			
		||||
        let startWeekDayStr = site.getStoredConfig('calendar_startwday');
 | 
			
		||||
        startWeekDayStr = await CoreConfig.instance.get(AddonCalendarProvider.STARTING_WEEK_DAY, startWeekDayStr);
 | 
			
		||||
        const startWeekDay = parseInt(startWeekDayStr, 10);
 | 
			
		||||
 | 
			
		||||
        const today = moment();
 | 
			
		||||
        const isCurrentMonth = today.year() == year && today.month() == month - 1;
 | 
			
		||||
        const weeks: Partial<AddonCalendarWeek>[] = [];
 | 
			
		||||
 | 
			
		||||
        let date = moment({ year, month: month - 1, date: 1 });
 | 
			
		||||
        for (let mday = 1; mday <= date.daysInMonth(); mday++) {
 | 
			
		||||
            date = moment({ year, month: month - 1, date: mday });
 | 
			
		||||
 | 
			
		||||
            // Add new week and calculate prepadding.
 | 
			
		||||
            if (!weeks.length || date.day() == startWeekDay) {
 | 
			
		||||
                const prepaddingLength = (date.day() - startWeekDay + 7) % 7;
 | 
			
		||||
                const prepadding: number[] = [];
 | 
			
		||||
                for (let i = 0; i < prepaddingLength; i++) {
 | 
			
		||||
                    prepadding.push(i);
 | 
			
		||||
                }
 | 
			
		||||
                weeks.push({ prepadding, postpadding: [], days: [] });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Calculate postpadding of last week.
 | 
			
		||||
            if (mday == date.daysInMonth()) {
 | 
			
		||||
                const postpaddingLength = (startWeekDay - date.day() + 6) % 7;
 | 
			
		||||
                const postpadding: number[] = [];
 | 
			
		||||
                for (let i = 0; i < postpaddingLength; i++) {
 | 
			
		||||
                    postpadding.push(i);
 | 
			
		||||
                }
 | 
			
		||||
                weeks[weeks.length - 1].postpadding = postpadding;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Add day to current week.
 | 
			
		||||
            weeks[weeks.length - 1].days!.push({
 | 
			
		||||
                events: [],
 | 
			
		||||
                hasevents: false,
 | 
			
		||||
                mday: date.date(),
 | 
			
		||||
                isweekend: date.day() == 0 || date.day() == 6,
 | 
			
		||||
                istoday: isCurrentMonth && today.date() == date.date(),
 | 
			
		||||
                calendareventtypes: [],
 | 
			
		||||
                // Added to match the type. And possibly unused.
 | 
			
		||||
                popovertitle: '',
 | 
			
		||||
                ispast: today.date() > date.date(),
 | 
			
		||||
                seconds: date.seconds(),
 | 
			
		||||
                minutes: date.minutes(),
 | 
			
		||||
                hours: date.hours(),
 | 
			
		||||
                wday: date.weekday(),
 | 
			
		||||
                year: year,
 | 
			
		||||
                yday: date.dayOfYear(),
 | 
			
		||||
                timestamp: date.date(),
 | 
			
		||||
                haslastdayofevent: false,
 | 
			
		||||
                neweventtimestamp: 0,
 | 
			
		||||
                previousperiod: 0, // Previousperiod.
 | 
			
		||||
                nextperiod: 0, // Nextperiod.
 | 
			
		||||
                navigation: '', // Navigation.
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return { weeks, daynames: [{ dayno: startWeekDay }] };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if the data of an event has changed.
 | 
			
		||||
     *
 | 
			
		||||
     * @param data Current data.
 | 
			
		||||
     * @param original Original data.
 | 
			
		||||
     * @return True if data has changed, false otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    hasEventDataChanged(data: AddonCalendarOfflineEventDBRecord, original?: AddonCalendarOfflineEventDBRecord): boolean {
 | 
			
		||||
        if (!original) {
 | 
			
		||||
            // There is no original data, assume it hasn't changed.
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check the fields that don't depend on any other.
 | 
			
		||||
        if (data.name != original.name || data.timestart != original.timestart || data.eventtype != original.eventtype ||
 | 
			
		||||
                data.description != original.description || data.location != original.location ||
 | 
			
		||||
                data.duration != original.duration || data.repeat != original.repeat) {
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check data that depends on eventtype.
 | 
			
		||||
        if ((data.eventtype == AddonCalendarEventType.CATEGORY && data.categoryid != original.categoryid) ||
 | 
			
		||||
                (data.eventtype == AddonCalendarEventType.COURSE && data.courseid != original.courseid) ||
 | 
			
		||||
                (data.eventtype == AddonCalendarEventType.GROUP && data.groupcourseid != original.groupcourseid &&
 | 
			
		||||
                    data.groupid != original.groupid)) {
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check data that depends on duration.
 | 
			
		||||
        if ((data.duration == 1 && data.timedurationuntil != original.timedurationuntil) ||
 | 
			
		||||
                (data.duration == 2 && data.timedurationminutes != original.timedurationminutes)) {
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (data.repeat && data.repeats != original.repeats) {
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Filter events to be shown on the events list.
 | 
			
		||||
     *
 | 
			
		||||
     * @param events Events without filtering.
 | 
			
		||||
     * @param filter Filter from popover.
 | 
			
		||||
     * @param categories Categories indexed by ID.
 | 
			
		||||
     * @return Filtered events.
 | 
			
		||||
     */
 | 
			
		||||
    getFilteredEvents(
 | 
			
		||||
        events: AddonCalendarEventToDisplay[],
 | 
			
		||||
        filter: AddonCalendarFilter,
 | 
			
		||||
        categories: { [id: number]: CoreCategoryData },
 | 
			
		||||
    ): AddonCalendarEventToDisplay[] {
 | 
			
		||||
        // Do not filter.
 | 
			
		||||
        if (!filter.filtered) {
 | 
			
		||||
            return events;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const courseId = filter.courseId ? Number(filter.courseId) : undefined;
 | 
			
		||||
 | 
			
		||||
        if (!courseId || courseId < 0) {
 | 
			
		||||
            // Filter only by type.
 | 
			
		||||
            return events.filter((event) => filter[event.formattedType]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const categoryId = filter.categoryId ? Number(filter.categoryId) : undefined;
 | 
			
		||||
 | 
			
		||||
        return  events.filter((event) => filter[event.formattedType] &&
 | 
			
		||||
                this.shouldDisplayEvent(event, categories, courseId, categoryId));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if an event should be displayed based on the filter.
 | 
			
		||||
     *
 | 
			
		||||
     * @param event Event object.
 | 
			
		||||
     * @param courseId Course ID to filter.
 | 
			
		||||
     * @param categoryId Category ID the course belongs to.
 | 
			
		||||
     * @param categories Categories indexed by ID.
 | 
			
		||||
     * @return Whether it should be displayed.
 | 
			
		||||
     */
 | 
			
		||||
    protected shouldDisplayEvent(
 | 
			
		||||
        event: AddonCalendarEventToDisplay,
 | 
			
		||||
        categories: { [id: number]: CoreCategoryData },
 | 
			
		||||
        courseId: number,
 | 
			
		||||
        categoryId?: number,
 | 
			
		||||
    ): boolean {
 | 
			
		||||
        if (event.eventtype == 'user' || event.eventtype == 'site') {
 | 
			
		||||
            // User or site event, display it.
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (event.eventtype == 'category' && categories) {
 | 
			
		||||
            if (!event.categoryid || !Object.keys(categories).length) {
 | 
			
		||||
                // We can't tell if the course belongs to the category, display them all.
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (event.categoryid == categoryId) {
 | 
			
		||||
                // The event is in the same category as the course, display it.
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Check parent categories.
 | 
			
		||||
            let category = categories[categoryId!];
 | 
			
		||||
            while (category) {
 | 
			
		||||
                if (!category.parent) {
 | 
			
		||||
                    // Category doesn't have parent, stop.
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (event.categoryid == category.parent) {
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
                category = categories[category.parent];
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const eventCourse = (event.course && event.course.id) || event.courseid;
 | 
			
		||||
 | 
			
		||||
        // Show the event if it is from site home or if it matches the selected course.
 | 
			
		||||
        return !!eventCourse && (eventCourse == CoreSites.instance.getCurrentSiteHomeId() || eventCourse == courseId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Refresh the month & day for several created/edited/deleted events, and invalidate the months & days
 | 
			
		||||
     * for their repeated events if needed.
 | 
			
		||||
     *
 | 
			
		||||
     * @param events Events that have been touched and number of times each event is repeated.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async refreshAfterChangeEvents(events: AddonCalendarSyncInvalidateEvent[], siteId?: string): Promise<void> {
 | 
			
		||||
        const site = await CoreSites.instance.getSite(siteId);
 | 
			
		||||
        const fetchTimestarts: number[] = [];
 | 
			
		||||
        const invalidateTimestarts: number[] = [];
 | 
			
		||||
        const promises: Promise<unknown>[] = [];
 | 
			
		||||
 | 
			
		||||
        // Always fetch upcoming events.
 | 
			
		||||
        promises.push(AddonCalendar.instance.getUpcomingEvents(undefined, undefined, true, site.id));
 | 
			
		||||
 | 
			
		||||
        promises.concat(events.map(async (eventData) => {
 | 
			
		||||
 | 
			
		||||
            if (eventData.repeated <= 1) {
 | 
			
		||||
                // Not repeated.
 | 
			
		||||
                fetchTimestarts.push(eventData.timestart);
 | 
			
		||||
 | 
			
		||||
                return AddonCalendar.instance.invalidateEvent(eventData.id);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (eventData.repeatid) {
 | 
			
		||||
                // Being edited or deleted.
 | 
			
		||||
                // We need to calculate the days to invalidate because the event date could have changed.
 | 
			
		||||
                // We don't know if the repeated events are before or after this one, invalidate them all.
 | 
			
		||||
                fetchTimestarts.push(eventData.timestart);
 | 
			
		||||
 | 
			
		||||
                for (let i = 1; i < eventData.repeated; i++) {
 | 
			
		||||
                    invalidateTimestarts.push(eventData.timestart + CoreConstants.SECONDS_DAY * 7 * i);
 | 
			
		||||
                    invalidateTimestarts.push(eventData.timestart - CoreConstants.SECONDS_DAY * 7 * i);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Get the repeated events to invalidate them.
 | 
			
		||||
                const repeatedEvents =
 | 
			
		||||
                    await AddonCalendar.instance.getLocalEventsByRepeatIdFromLocalDb(eventData.repeatid, site.id);
 | 
			
		||||
 | 
			
		||||
                await CoreUtils.instance.allPromises(repeatedEvents.map((event) =>
 | 
			
		||||
                    AddonCalendar.instance.invalidateEvent(event.id!)));
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Being added.
 | 
			
		||||
            let time = eventData.timestart;
 | 
			
		||||
            fetchTimestarts.push(time);
 | 
			
		||||
 | 
			
		||||
            while (eventData.repeated > 1) {
 | 
			
		||||
                time += CoreConstants.SECONDS_DAY * 7;
 | 
			
		||||
                eventData.repeated--;
 | 
			
		||||
                invalidateTimestarts.push(time);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await CoreUtils.instance.allPromisesIgnoringErrors(promises);
 | 
			
		||||
        } finally {
 | 
			
		||||
            const treatedMonths = {};
 | 
			
		||||
            const treatedDays = {};
 | 
			
		||||
            const finalPromises: Promise<unknown>[] =[AddonCalendar.instance.invalidateAllUpcomingEvents()];
 | 
			
		||||
 | 
			
		||||
            // Fetch months and days.
 | 
			
		||||
            fetchTimestarts.map((fetchTime) => {
 | 
			
		||||
                const day = moment(new Date(fetchTime * 1000));
 | 
			
		||||
 | 
			
		||||
                const monthId = this.getMonthId(day.year(), day.month() + 1);
 | 
			
		||||
                if (!treatedMonths[monthId]) {
 | 
			
		||||
                    // Month not refetch or invalidated already, do it now.
 | 
			
		||||
                    treatedMonths[monthId] = true;
 | 
			
		||||
 | 
			
		||||
                    finalPromises.push(AddonCalendar.instance.getMonthlyEvents(
 | 
			
		||||
                        day.year(),
 | 
			
		||||
                        day.month() + 1,
 | 
			
		||||
                        undefined,
 | 
			
		||||
                        undefined,
 | 
			
		||||
                        true,
 | 
			
		||||
                        site.id,
 | 
			
		||||
                    ));
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                const dayId = monthId + '#' + day.date();
 | 
			
		||||
                if (!treatedDays[dayId]) {
 | 
			
		||||
                    // Dat not refetch or invalidated already, do it now.
 | 
			
		||||
                    treatedDays[dayId] = true;
 | 
			
		||||
 | 
			
		||||
                    finalPromises.push(AddonCalendar.instance.getDayEvents(
 | 
			
		||||
                        day.year(),
 | 
			
		||||
                        day.month() + 1,
 | 
			
		||||
                        day.date(),
 | 
			
		||||
                        undefined,
 | 
			
		||||
                        undefined,
 | 
			
		||||
                        true,
 | 
			
		||||
                        site.id,
 | 
			
		||||
                    ));
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            // Invalidate months and days.
 | 
			
		||||
            invalidateTimestarts.map((fetchTime) => {
 | 
			
		||||
                const day = moment(new Date(fetchTime * 1000));
 | 
			
		||||
 | 
			
		||||
                const monthId = this.getMonthId(day.year(), day.month() + 1);
 | 
			
		||||
                if (!treatedMonths[monthId]) {
 | 
			
		||||
                    // Month not refetch or invalidated already, do it now.
 | 
			
		||||
                    treatedMonths[monthId] = true;
 | 
			
		||||
 | 
			
		||||
                    finalPromises.push(AddonCalendar.instance.invalidateMonthlyEvents(day.year(), day.month() + 1, site.id));
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                const dayId = monthId + '#' + day.date();
 | 
			
		||||
                if (!treatedDays[dayId]) {
 | 
			
		||||
                    // Dat not refetch or invalidated already, do it now.
 | 
			
		||||
                    treatedDays[dayId] = true;
 | 
			
		||||
 | 
			
		||||
                    finalPromises.push(AddonCalendar.instance.invalidateDayEvents(
 | 
			
		||||
                        day.year(),
 | 
			
		||||
                        day.month() + 1,
 | 
			
		||||
                        day.date(),
 | 
			
		||||
                        site.id,
 | 
			
		||||
                    ));
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            await CoreUtils.instance.allPromisesIgnoringErrors(finalPromises);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Refresh the month & day for a created/edited/deleted event, and invalidate the months & days
 | 
			
		||||
     * for their repeated events if needed.
 | 
			
		||||
     *
 | 
			
		||||
     * @param event Event that has been touched.
 | 
			
		||||
     * @param repeated Number of times the event is repeated.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    refreshAfterChangeEvent(
 | 
			
		||||
        event: {
 | 
			
		||||
            id?: number;
 | 
			
		||||
            repeatid?: number;
 | 
			
		||||
            timestart: number;
 | 
			
		||||
        },
 | 
			
		||||
        repeated: number,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        return this.refreshAfterChangeEvents(
 | 
			
		||||
            [{
 | 
			
		||||
                id: event.id!,
 | 
			
		||||
                repeatid: event.repeatid,
 | 
			
		||||
                timestart: event.timestart,
 | 
			
		||||
                repeated: repeated,
 | 
			
		||||
            }],
 | 
			
		||||
            siteId,
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sort events by timestart.
 | 
			
		||||
     *
 | 
			
		||||
     * @param events List to sort.
 | 
			
		||||
     */
 | 
			
		||||
    sortEvents(events: (AddonCalendarEventToDisplay)[]): (AddonCalendarEventToDisplay)[] {
 | 
			
		||||
        return events.sort((a, b) => {
 | 
			
		||||
            if (a.timestart == b.timestart) {
 | 
			
		||||
                return a.timeduration - b.timeduration;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return a.timestart - b.timestart;
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class AddonCalendarHelper extends makeSingleton(AddonCalendarHelperProvider) {}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Calculated data for Calendar filtering.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonCalendarFilter = {
 | 
			
		||||
    filtered: boolean; // If filter enabled (some filters applied).
 | 
			
		||||
    courseId: number; // Course Id to filter.
 | 
			
		||||
    categoryId?: number; // Category Id to filter.
 | 
			
		||||
    course: boolean; // Filter to show course events.
 | 
			
		||||
    group: boolean; // Filter to show group events.
 | 
			
		||||
    site: boolean; // Filter to show show site events.
 | 
			
		||||
    user: boolean; // Filter to show user events.
 | 
			
		||||
    category: boolean; // Filter to show category events.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type AddonCalendarEventTypeOption = {
 | 
			
		||||
    name: string;
 | 
			
		||||
    value: AddonCalendarEventType;
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										276
									
								
								src/addons/calendar/services/calendar-offline.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,276 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Injectable } from '@angular/core';
 | 
			
		||||
import { SQLiteDBRecordValues } from '@classes/sqlitedb';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { makeSingleton } from '@singletons';
 | 
			
		||||
import { AddonCalendarSubmitCreateUpdateFormDataWSParams } from './calendar';
 | 
			
		||||
import {
 | 
			
		||||
    AddonCalendarOfflineDeletedEventDBRecord,
 | 
			
		||||
    AddonCalendarOfflineEventDBRecord,
 | 
			
		||||
    DELETED_EVENTS_TABLE,
 | 
			
		||||
    EVENTS_TABLE,
 | 
			
		||||
} from './database/calendar-offline';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Service to handle offline calendar events.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable({ providedIn: 'root' })
 | 
			
		||||
export class AddonCalendarOfflineProvider {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Delete an offline event.
 | 
			
		||||
     *
 | 
			
		||||
     * @param eventId Event ID.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved if deleted, rejected if failure.
 | 
			
		||||
     */
 | 
			
		||||
    async deleteEvent(eventId: number, siteId?: string): Promise<void> {
 | 
			
		||||
        const site = await CoreSites.instance.getSite(siteId);
 | 
			
		||||
 | 
			
		||||
        const conditions: SQLiteDBRecordValues = {
 | 
			
		||||
            id: eventId,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        await site.getDb().deleteRecords(EVENTS_TABLE, conditions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the IDs of all the events created/edited/deleted in offline.
 | 
			
		||||
     *
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with the IDs.
 | 
			
		||||
     */
 | 
			
		||||
    async getAllEventsIds(siteId?: string): Promise<number[]> {
 | 
			
		||||
        const promises: Promise<number[]>[] = [];
 | 
			
		||||
 | 
			
		||||
        promises.push(this.getAllDeletedEventsIds(siteId));
 | 
			
		||||
        promises.push(this.getAllEditedEventsIds(siteId));
 | 
			
		||||
 | 
			
		||||
        const result = await Promise.all(promises);
 | 
			
		||||
 | 
			
		||||
        return CoreUtils.instance.mergeArraysWithoutDuplicates(result[0], result[1]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get all the events deleted in offline.
 | 
			
		||||
     *
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with all the events deleted in offline.
 | 
			
		||||
     */
 | 
			
		||||
    async getAllDeletedEvents(siteId?: string): Promise<AddonCalendarOfflineDeletedEventDBRecord[]> {
 | 
			
		||||
        const site = await CoreSites.instance.getSite(siteId);
 | 
			
		||||
 | 
			
		||||
        return await site.getDb().getRecords(DELETED_EVENTS_TABLE);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the IDs of all the events deleted in offline.
 | 
			
		||||
     *
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with the IDs of all the events deleted in offline.
 | 
			
		||||
     */
 | 
			
		||||
    async getAllDeletedEventsIds(siteId?: string): Promise<number[]> {
 | 
			
		||||
        const events = await this.getAllDeletedEvents(siteId);
 | 
			
		||||
 | 
			
		||||
        return events.map((event) => event.id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get all the events created/edited in offline.
 | 
			
		||||
     *
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with events.
 | 
			
		||||
     */
 | 
			
		||||
    async getAllEditedEvents(siteId?: string): Promise<AddonCalendarOfflineEventDBRecord[]> {
 | 
			
		||||
        const site = await CoreSites.instance.getSite(siteId);
 | 
			
		||||
 | 
			
		||||
        return await site.getDb().getRecords(EVENTS_TABLE);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the IDs of all the events created/edited in offline.
 | 
			
		||||
     *
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with events IDs.
 | 
			
		||||
     */
 | 
			
		||||
    async getAllEditedEventsIds(siteId?: string): Promise<number[]> {
 | 
			
		||||
        const events = await this.getAllEditedEvents(siteId);
 | 
			
		||||
 | 
			
		||||
        return events.map((event) => event.id!);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get an event deleted in offline.
 | 
			
		||||
     *
 | 
			
		||||
     * @param eventId Event ID.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with the deleted event.
 | 
			
		||||
     */
 | 
			
		||||
    async getDeletedEvent(eventId: number, siteId?: string): Promise<AddonCalendarOfflineDeletedEventDBRecord> {
 | 
			
		||||
        const site = await CoreSites.instance.getSite(siteId);
 | 
			
		||||
        const conditions: SQLiteDBRecordValues = {
 | 
			
		||||
            id: eventId,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return await site.getDb().getRecord(DELETED_EVENTS_TABLE, conditions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get an offline event.
 | 
			
		||||
     *
 | 
			
		||||
     * @param eventId Event ID.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with the event.
 | 
			
		||||
     */
 | 
			
		||||
    async getEvent(eventId: number, siteId?: string): Promise<AddonCalendarOfflineEventDBRecord> {
 | 
			
		||||
        const site = await CoreSites.instance.getSite(siteId);
 | 
			
		||||
        const conditions: SQLiteDBRecordValues = {
 | 
			
		||||
            id: eventId,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return await site.getDb().getRecord(EVENTS_TABLE, conditions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if there are offline events to send.
 | 
			
		||||
     *
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with boolean: true if has offline events, false otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    async hasEditedEvents(siteId?: string): Promise<boolean> {
 | 
			
		||||
        try {
 | 
			
		||||
            const events = await this.getAllEditedEvents(siteId);
 | 
			
		||||
 | 
			
		||||
            return !!events.length;
 | 
			
		||||
        } catch {
 | 
			
		||||
            // No offline data found, return false.
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check whether there's offline data for a site.
 | 
			
		||||
     *
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with boolean: true if has offline data, false otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    async hasOfflineData(siteId?: string): Promise<boolean> {
 | 
			
		||||
        const ids = await this.getAllEventsIds(siteId);
 | 
			
		||||
 | 
			
		||||
        return ids.length > 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if an event is deleted.
 | 
			
		||||
     *
 | 
			
		||||
     * @param eventId Event ID.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with boolean: whether the event is deleted.
 | 
			
		||||
     */
 | 
			
		||||
    async isEventDeleted(eventId: number, siteId?: string): Promise<boolean> {
 | 
			
		||||
        try {
 | 
			
		||||
            const event = await this.getDeletedEvent(eventId, siteId);
 | 
			
		||||
 | 
			
		||||
            return !!event;
 | 
			
		||||
        } catch {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Mark an event as deleted.
 | 
			
		||||
     *
 | 
			
		||||
     * @param eventId Event ID to delete.
 | 
			
		||||
     * @param name Name of the event to delete.
 | 
			
		||||
     * @param deleteAll If it's a repeated event. whether to delete all events of the series.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async markDeleted(eventId: number, name: string, deleteAll?: boolean, siteId?: string): Promise<number> {
 | 
			
		||||
        const site = await CoreSites.instance.getSite(siteId);
 | 
			
		||||
        const event: AddonCalendarOfflineDeletedEventDBRecord = {
 | 
			
		||||
            id: eventId,
 | 
			
		||||
            name: name || '',
 | 
			
		||||
            repeat: deleteAll ? 1 : 0,
 | 
			
		||||
            timemodified: Date.now(),
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return await site.getDb().insertRecord(DELETED_EVENTS_TABLE, event);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Offline version for adding a new discussion to a forum.
 | 
			
		||||
     *
 | 
			
		||||
     * @param eventId Event ID. If it's a new event, set it to undefined/null.
 | 
			
		||||
     * @param data Event data.
 | 
			
		||||
     * @param timeCreated The time the event was created. If not defined, current time.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with the stored event.
 | 
			
		||||
     */
 | 
			
		||||
    async saveEvent(
 | 
			
		||||
        eventId: number | undefined,
 | 
			
		||||
        data: AddonCalendarSubmitCreateUpdateFormDataWSParams,
 | 
			
		||||
        timeCreated?: number,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<AddonCalendarOfflineEventDBRecord> {
 | 
			
		||||
        const site = await CoreSites.instance.getSite(siteId);
 | 
			
		||||
        timeCreated = timeCreated || Date.now();
 | 
			
		||||
        const event: AddonCalendarOfflineEventDBRecord = {
 | 
			
		||||
            id: eventId || -timeCreated,
 | 
			
		||||
            name: data.name,
 | 
			
		||||
            timestart: data.timestart,
 | 
			
		||||
            eventtype: data.eventtype,
 | 
			
		||||
            categoryid: data.categoryid,
 | 
			
		||||
            courseid: data.courseid,
 | 
			
		||||
            groupcourseid: data.groupcourseid,
 | 
			
		||||
            groupid: data.groupid,
 | 
			
		||||
            description: data.description && data.description.text,
 | 
			
		||||
            location: data.location,
 | 
			
		||||
            duration: data.duration,
 | 
			
		||||
            timedurationuntil: data.timedurationuntil,
 | 
			
		||||
            timedurationminutes: data.timedurationminutes,
 | 
			
		||||
            repeat: data.repeat ? 1 : 0,
 | 
			
		||||
            repeats: data.repeats,
 | 
			
		||||
            repeatid: data.repeatid,
 | 
			
		||||
            repeateditall: data.repeateditall ? 1 : 0,
 | 
			
		||||
            timecreated: timeCreated,
 | 
			
		||||
            userid: site.getUserId(),
 | 
			
		||||
        };
 | 
			
		||||
        await site.getDb().insertRecord(EVENTS_TABLE, event);
 | 
			
		||||
 | 
			
		||||
        return event;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Unmark an event as deleted.
 | 
			
		||||
     *
 | 
			
		||||
     * @param eventId Event ID.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved if deleted, rejected if failure.
 | 
			
		||||
     */
 | 
			
		||||
    async unmarkDeleted(eventId: number, siteId?: string): Promise<void> {
 | 
			
		||||
        const site = await CoreSites.instance.getSite(siteId);
 | 
			
		||||
        const conditions: SQLiteDBRecordValues = {
 | 
			
		||||
            id: eventId,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        await site.getDb().deleteRecords(DELETED_EVENTS_TABLE, conditions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
export class AddonCalendarOffline extends makeSingleton(AddonCalendarOfflineProvider) {}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										322
									
								
								src/addons/calendar/services/calendar-sync.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,322 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Injectable } from '@angular/core';
 | 
			
		||||
import { CoreSyncBaseProvider } from '@classes/base-sync';
 | 
			
		||||
import { CoreApp } from '@services/app';
 | 
			
		||||
import { CoreEvents } from '@singletons/events';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import {
 | 
			
		||||
    AddonCalendar,
 | 
			
		||||
    AddonCalendarEvent,
 | 
			
		||||
    AddonCalendarProvider,
 | 
			
		||||
    AddonCalendarSubmitCreateUpdateFormDataWSParams,
 | 
			
		||||
} from './calendar';
 | 
			
		||||
import { AddonCalendarOffline } from './calendar-offline';
 | 
			
		||||
import { AddonCalendarHelper } from './calendar-helper';
 | 
			
		||||
import { makeSingleton, Translate } from '@singletons';
 | 
			
		||||
import { CoreError } from '@classes/errors/error';
 | 
			
		||||
import { CoreSync } from '@services/sync';
 | 
			
		||||
import { CoreTextUtils } from '@services/utils/text';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Service to sync calendar.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable({ providedIn: 'root' })
 | 
			
		||||
export class AddonCalendarSyncProvider extends CoreSyncBaseProvider<AddonCalendarSyncEvents> {
 | 
			
		||||
 | 
			
		||||
    static readonly AUTO_SYNCED = 'addon_calendar_autom_synced';
 | 
			
		||||
    static readonly MANUAL_SYNCED = 'addon_calendar_manual_synced';
 | 
			
		||||
    static readonly SYNC_ID = 'calendar';
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super('AddonCalendarSync');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Try to synchronize all events in a certain site or in all sites.
 | 
			
		||||
     *
 | 
			
		||||
     * @param siteId Site ID to sync. If not defined, sync all sites.
 | 
			
		||||
     * @param force Wether to force sync not depending on last execution.
 | 
			
		||||
     * @return Promise resolved if sync is successful, rejected if sync fails.
 | 
			
		||||
     */
 | 
			
		||||
    async syncAllEvents(siteId?: string, force?: boolean): Promise<void> {
 | 
			
		||||
        await this.syncOnSites('all calendar events', this.syncAllEventsFunc.bind(this, [force]), siteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sync all events on a site.
 | 
			
		||||
     *
 | 
			
		||||
     * @param siteId Site ID to sync.
 | 
			
		||||
     * @param force Wether to force sync not depending on last execution.
 | 
			
		||||
     * @return Promise resolved if sync is successful, rejected if sync fails.
 | 
			
		||||
     */
 | 
			
		||||
    protected async syncAllEventsFunc(siteId: string, force?: boolean): Promise<void> {
 | 
			
		||||
        const result = await (force ? this.syncEvents(siteId) : this.syncEventsIfNeeded(siteId));
 | 
			
		||||
 | 
			
		||||
        if (result && result.updated) {
 | 
			
		||||
            // Sync successful, send event.
 | 
			
		||||
            CoreEvents.trigger<AddonCalendarSyncEvents>(AddonCalendarSyncProvider.AUTO_SYNCED, result, siteId);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sync a site events only if a certain time has passed since the last time.
 | 
			
		||||
     *
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved when the events are synced or if it doesn't need to be synced.
 | 
			
		||||
     */
 | 
			
		||||
    async syncEventsIfNeeded(siteId?: string): Promise<void> {
 | 
			
		||||
        siteId = siteId || CoreSites.instance.getCurrentSiteId();
 | 
			
		||||
 | 
			
		||||
        const needed = await this.isSyncNeeded(AddonCalendarSyncProvider.SYNC_ID, siteId);
 | 
			
		||||
 | 
			
		||||
        if (needed) {
 | 
			
		||||
            await this.syncEvents(siteId);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Synchronize all offline events of a certain site.
 | 
			
		||||
     *
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved if sync is successful, rejected otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    async syncEvents(siteId?: string): Promise<AddonCalendarSyncEvents> {
 | 
			
		||||
        siteId = siteId || CoreSites.instance.getCurrentSiteId();
 | 
			
		||||
 | 
			
		||||
        if (this.isSyncing(AddonCalendarSyncProvider.SYNC_ID, siteId)) {
 | 
			
		||||
            // There's already a sync ongoing for this site, return the promise.
 | 
			
		||||
            return this.getOngoingSync(AddonCalendarSyncProvider.SYNC_ID, siteId)!;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.logger.debug('Try to sync calendar events for site ' + siteId);
 | 
			
		||||
 | 
			
		||||
        // Get offline events.
 | 
			
		||||
        const syncPromise = this.performSyncEvents(siteId);
 | 
			
		||||
 | 
			
		||||
        return this.addOngoingSync(AddonCalendarSyncProvider.SYNC_ID, syncPromise, siteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sync user preferences of a site.
 | 
			
		||||
     *
 | 
			
		||||
     * @param siteId Site ID to sync.
 | 
			
		||||
     * @param Promise resolved if sync is successful, rejected if sync fails.
 | 
			
		||||
     */
 | 
			
		||||
    protected async performSyncEvents(siteId: string): Promise<AddonCalendarSyncEvents> {
 | 
			
		||||
        const result: AddonCalendarSyncEvents = {
 | 
			
		||||
            warnings: [],
 | 
			
		||||
            events: [],
 | 
			
		||||
            deleted: [],
 | 
			
		||||
            toinvalidate: [],
 | 
			
		||||
            updated: false,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let eventIds: number[] = [];
 | 
			
		||||
        try {
 | 
			
		||||
            eventIds = await AddonCalendarOffline.instance.getAllEventsIds(siteId);
 | 
			
		||||
        } catch {
 | 
			
		||||
            // No offline data found.
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (eventIds.length > 0) {
 | 
			
		||||
            if (!CoreApp.instance.isOnline()) {
 | 
			
		||||
                // Cannot sync in offline.
 | 
			
		||||
                throw new CoreError('Cannot sync while offline');
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const promises = eventIds.map((eventId) => this.syncOfflineEvent(eventId, result, siteId));
 | 
			
		||||
 | 
			
		||||
            await CoreUtils.instance.allPromises(promises);
 | 
			
		||||
 | 
			
		||||
            if (result.updated) {
 | 
			
		||||
 | 
			
		||||
                // Data has been sent to server. Now invalidate the WS calls.
 | 
			
		||||
                const promises = [
 | 
			
		||||
                    AddonCalendar.instance.invalidateEventsList(siteId),
 | 
			
		||||
                    AddonCalendarHelper.instance.refreshAfterChangeEvents(result.toinvalidate, siteId),
 | 
			
		||||
                ];
 | 
			
		||||
 | 
			
		||||
                await CoreUtils.instance.ignoreErrors(Promise.all(promises));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Sync finished, set sync time.
 | 
			
		||||
        await CoreUtils.instance.ignoreErrors(this.setSyncTime(AddonCalendarSyncProvider.SYNC_ID, siteId));
 | 
			
		||||
 | 
			
		||||
        // All done, return the result.
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Synchronize an offline event.
 | 
			
		||||
     *
 | 
			
		||||
     * @param eventId The event ID to sync.
 | 
			
		||||
     * @param result Object where to store the result of the sync.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved if sync is successful, rejected otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    protected async syncOfflineEvent(eventId: number, result: AddonCalendarSyncEvents, siteId?: string): Promise<void> {
 | 
			
		||||
 | 
			
		||||
        // Verify that event isn't blocked.
 | 
			
		||||
        if (CoreSync.instance.isBlocked(AddonCalendarProvider.COMPONENT, eventId, siteId)) {
 | 
			
		||||
            this.logger.debug('Cannot sync event ' + eventId + ' because it is blocked.');
 | 
			
		||||
 | 
			
		||||
            throw Translate.instance.instant(
 | 
			
		||||
                'core.errorsyncblocked',
 | 
			
		||||
                { $a: Translate.instance.instant('addon.calendar.calendarevent') },
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // First of all, check if the event has been deleted.
 | 
			
		||||
        try {
 | 
			
		||||
            const data = await AddonCalendarOffline.instance.getDeletedEvent(eventId, siteId);
 | 
			
		||||
            // Delete the event.
 | 
			
		||||
            try {
 | 
			
		||||
                await AddonCalendar.instance.deleteEventOnline(data.id, !!data.repeat, siteId);
 | 
			
		||||
 | 
			
		||||
                result.updated = true;
 | 
			
		||||
                result.deleted.push(eventId);
 | 
			
		||||
 | 
			
		||||
                // Event sent, delete the offline data.
 | 
			
		||||
                const promises: Promise<void>[] = [];
 | 
			
		||||
 | 
			
		||||
                promises.push(AddonCalendarOffline.instance.unmarkDeleted(eventId, siteId));
 | 
			
		||||
                promises.push(AddonCalendarOffline.instance.deleteEvent(eventId, siteId).catch(() => {
 | 
			
		||||
                    // Ignore errors, maybe there was no edit data.
 | 
			
		||||
                }));
 | 
			
		||||
 | 
			
		||||
                // We need the event data to invalidate it. Get it from local DB.
 | 
			
		||||
                promises.push(AddonCalendar.instance.getEventFromLocalDb(eventId, siteId).then((event) => {
 | 
			
		||||
                    result.toinvalidate.push({
 | 
			
		||||
                        id: event.id,
 | 
			
		||||
                        repeatid: event.repeatid,
 | 
			
		||||
                        timestart: event.timestart,
 | 
			
		||||
                        repeated:  data?.repeat ? (event as AddonCalendarEvent).eventcount || 1 : 1,
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    return;
 | 
			
		||||
                }).catch(() => {
 | 
			
		||||
                    // Ignore errors.
 | 
			
		||||
                }));
 | 
			
		||||
 | 
			
		||||
                await Promise.all(promises);
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
 | 
			
		||||
                if (!CoreUtils.instance.isWebServiceError(error)) {
 | 
			
		||||
                    // Local error, reject.
 | 
			
		||||
                    throw error;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // The WebService has thrown an error, this means that the event cannot be created. Delete it.
 | 
			
		||||
                result.updated = true;
 | 
			
		||||
 | 
			
		||||
                const promises: Promise<void>[] = [];
 | 
			
		||||
 | 
			
		||||
                promises.push(AddonCalendarOffline.instance.unmarkDeleted(eventId, siteId));
 | 
			
		||||
                promises.push(AddonCalendarOffline.instance.deleteEvent(eventId, siteId).catch(() => {
 | 
			
		||||
                    // Ignore errors, maybe there was no edit data.
 | 
			
		||||
                }));
 | 
			
		||||
 | 
			
		||||
                await Promise.all(promises);
 | 
			
		||||
                // Event deleted, add a warning.
 | 
			
		||||
                result.warnings.push(Translate.instance.instant('core.warningofflinedatadeleted', {
 | 
			
		||||
                    component: Translate.instance.instant('addon.calendar.calendarevent'),
 | 
			
		||||
                    name: data.name,
 | 
			
		||||
                    error: CoreTextUtils.instance.getErrorMessageFromError(error),
 | 
			
		||||
                }));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        } catch {
 | 
			
		||||
            // Not deleted.
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Not deleted. Now get the event data.
 | 
			
		||||
        const event = await AddonCalendarOffline.instance.getEvent(eventId, siteId);
 | 
			
		||||
 | 
			
		||||
        // Try to send the data.
 | 
			
		||||
        const data: AddonCalendarSubmitCreateUpdateFormDataWSParams = Object.assign(
 | 
			
		||||
            CoreUtils.instance.clone(event),
 | 
			
		||||
            {
 | 
			
		||||
                description: {
 | 
			
		||||
                    text: event.description || '',
 | 
			
		||||
                    format: 1,
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
        ); // Clone the object because it will be modified in the submit function.
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const newEvent = await AddonCalendar.instance.submitEventOnline(eventId > 0 ? eventId : 0, data, siteId);
 | 
			
		||||
 | 
			
		||||
            result.updated = true;
 | 
			
		||||
            result.events.push(newEvent);
 | 
			
		||||
 | 
			
		||||
            // Add data to invalidate.
 | 
			
		||||
            const numberOfRepetitions = data.repeat ? data.repeats :
 | 
			
		||||
                (data.repeateditall && newEvent.repeatid ? newEvent.eventcount : 1);
 | 
			
		||||
 | 
			
		||||
            result.toinvalidate.push({
 | 
			
		||||
                id: newEvent.id,
 | 
			
		||||
                repeatid: newEvent.repeatid,
 | 
			
		||||
                timestart: newEvent.timestart,
 | 
			
		||||
                repeated: numberOfRepetitions || 1,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            // Event sent, delete the offline data.
 | 
			
		||||
            return AddonCalendarOffline.instance.deleteEvent(event.id!, siteId);
 | 
			
		||||
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            if (!CoreUtils.instance.isWebServiceError(error)) {
 | 
			
		||||
                // Local error, reject.
 | 
			
		||||
                throw error;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // The WebService has thrown an error, this means that the event cannot be created. Delete it.
 | 
			
		||||
            result.updated = true;
 | 
			
		||||
 | 
			
		||||
            await AddonCalendarOffline.instance.deleteEvent(event.id!, siteId);
 | 
			
		||||
            // Event deleted, add a warning.
 | 
			
		||||
            result.warnings.push(Translate.instance.instant('core.warningofflinedatadeleted', {
 | 
			
		||||
                component: Translate.instance.instant('addon.calendar.calendarevent'),
 | 
			
		||||
                name: event.name,
 | 
			
		||||
                error: CoreTextUtils.instance.getErrorMessageFromError(error),
 | 
			
		||||
            }));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class AddonCalendarSync extends makeSingleton(AddonCalendarSyncProvider) {}
 | 
			
		||||
 | 
			
		||||
export type AddonCalendarSyncEvents = {
 | 
			
		||||
    warnings: string[];
 | 
			
		||||
    events: AddonCalendarEvent[];
 | 
			
		||||
    deleted: number[];
 | 
			
		||||
    toinvalidate: AddonCalendarSyncInvalidateEvent[];
 | 
			
		||||
    updated: boolean;
 | 
			
		||||
    source?: string; // Added on pages.
 | 
			
		||||
    day?: number; // Added on day page.
 | 
			
		||||
    month?: number; // Added on day page.
 | 
			
		||||
    year?: number; // Added on day page.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type AddonCalendarSyncInvalidateEvent = {
 | 
			
		||||
    id: number;
 | 
			
		||||
    repeatid?: number;
 | 
			
		||||
    timestart: number;
 | 
			
		||||
    repeated: number;
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										2230
									
								
								src/addons/calendar/services/calendar.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										164
									
								
								src/addons/calendar/services/database/calendar-offline.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,164 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { CoreSiteSchema } from '@services/sites';
 | 
			
		||||
import { AddonCalendarEventType } from '../calendar';
 | 
			
		||||
/**
 | 
			
		||||
 * Database variables for AddonDatabaseOffline service.
 | 
			
		||||
 */
 | 
			
		||||
export const EVENTS_TABLE = 'addon_calendar_offline_events';
 | 
			
		||||
export const DELETED_EVENTS_TABLE = 'addon_calendar_deleted_events';
 | 
			
		||||
export const CALENDAR_OFFLINE_SITE_SCHEMA: CoreSiteSchema = {
 | 
			
		||||
    name: 'AddonCalendarOfflineProvider',
 | 
			
		||||
    version: 1,
 | 
			
		||||
    tables: [
 | 
			
		||||
        {
 | 
			
		||||
            name: EVENTS_TABLE,
 | 
			
		||||
            columns: [
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'id',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                    primaryKey: true,
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'name',
 | 
			
		||||
                    type: 'TEXT',
 | 
			
		||||
                    notNull: true,
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'timestart',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                    notNull: true,
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'eventtype',
 | 
			
		||||
                    type: 'TEXT',
 | 
			
		||||
                    notNull: true,
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'categoryid',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'courseid',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'groupcourseid',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'groupid',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'description',
 | 
			
		||||
                    type: 'TEXT',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'location',
 | 
			
		||||
                    type: 'TEXT',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'duration',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'timedurationuntil',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'timedurationminutes',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'repeat',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'repeats',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'repeatid',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'repeateditall',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'userid',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'timecreated',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            name: DELETED_EVENTS_TABLE,
 | 
			
		||||
            columns: [
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'id',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                    primaryKey: true,
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'name',
 | 
			
		||||
                    type: 'TEXT',
 | 
			
		||||
                    notNull: true,
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'repeat',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'timemodified',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type AddonCalendarOfflineEventDBRecord = {
 | 
			
		||||
    id?: number; // Negative for offline entries.
 | 
			
		||||
    name: string;
 | 
			
		||||
    timestart: number;
 | 
			
		||||
    eventtype: AddonCalendarEventType;
 | 
			
		||||
    categoryid?: number;
 | 
			
		||||
    courseid?: number;
 | 
			
		||||
    groupcourseid?: number;
 | 
			
		||||
    groupid?: number;
 | 
			
		||||
    description?: string;
 | 
			
		||||
    location?: string;
 | 
			
		||||
    duration?: number;
 | 
			
		||||
    timedurationuntil?: number;
 | 
			
		||||
    timedurationminutes?: number;
 | 
			
		||||
    repeat?: number;
 | 
			
		||||
    repeats?: number;
 | 
			
		||||
    repeatid?: number;
 | 
			
		||||
    repeateditall?: number;
 | 
			
		||||
    userid?: number;
 | 
			
		||||
    timecreated?: number;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type AddonCalendarOfflineDeletedEventDBRecord = {
 | 
			
		||||
    id: number;
 | 
			
		||||
    name: string; // Save the name to be able to notify the user.
 | 
			
		||||
    repeat?: number;
 | 
			
		||||
    timemodified?: number;
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										276
									
								
								src/addons/calendar/services/database/calendar.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,276 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { SQLiteDB } from '@classes/sqlitedb';
 | 
			
		||||
import { CoreSiteSchema } from '@services/sites';
 | 
			
		||||
import { AddonCalendarEventType } from '../calendar';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Database variables for AddonDatabase service.
 | 
			
		||||
 */
 | 
			
		||||
export const EVENTS_TABLE = 'addon_calendar_events_3';
 | 
			
		||||
export const REMINDERS_TABLE = 'addon_calendar_reminders';
 | 
			
		||||
export const CALENDAR_SITE_SCHEMA: CoreSiteSchema = {
 | 
			
		||||
    name: 'AddonCalendarProvider',
 | 
			
		||||
    version: 3,
 | 
			
		||||
    canBeCleared: [EVENTS_TABLE],
 | 
			
		||||
    tables: [
 | 
			
		||||
        {
 | 
			
		||||
            name: EVENTS_TABLE,
 | 
			
		||||
            columns: [
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'id',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                    primaryKey: true,
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    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: 'userid',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'instance',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'modulename',
 | 
			
		||||
                    type: 'TEXT',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'timemodified',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'repeatid',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'visible',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'uuid',
 | 
			
		||||
                    type: 'TEXT',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'sequence',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'subscriptionid',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'location',
 | 
			
		||||
                    type: 'TEXT',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'eventcount',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'timesort',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'category',
 | 
			
		||||
                    type: 'TEXT',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'course',
 | 
			
		||||
                    type: 'TEXT',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'subscription',
 | 
			
		||||
                    type: 'TEXT',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'canedit',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'candelete',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'deleteurl',
 | 
			
		||||
                    type: 'TEXT',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'editurl',
 | 
			
		||||
                    type: 'TEXT',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'viewurl',
 | 
			
		||||
                    type: 'TEXT',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'isactionevent',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'url',
 | 
			
		||||
                    type: 'TEXT',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'islastday',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'popupname',
 | 
			
		||||
                    type: 'TEXT',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'mindaytimestamp',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'maxdaytimestamp',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'draggable',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            name: REMINDERS_TABLE,
 | 
			
		||||
            columns: [
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'id',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                    primaryKey: true,
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'eventid',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'time',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
            uniqueKeys: [
 | 
			
		||||
                ['eventid', 'time'],
 | 
			
		||||
            ],
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
    async migrate(db: SQLiteDB, oldVersion: number): Promise<void> {
 | 
			
		||||
        if (oldVersion < 3) {
 | 
			
		||||
            const newTable = EVENTS_TABLE;
 | 
			
		||||
            let oldTable = 'addon_calendar_events_2';
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                await db.tableExists(oldTable);
 | 
			
		||||
            } catch {
 | 
			
		||||
                // The v2 table doesn't exist, try with v1.
 | 
			
		||||
                oldTable = 'addon_calendar_events';
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await db.tableExists(oldTable);
 | 
			
		||||
 | 
			
		||||
            // Move the records from the old table.
 | 
			
		||||
            const events = await db.getAllRecords<AddonCalendarEventDBRecord>(oldTable);
 | 
			
		||||
            const promises = events.map((event) => db.insertRecord(newTable, event));
 | 
			
		||||
 | 
			
		||||
            await Promise.all(promises);
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                db.dropTable(oldTable);
 | 
			
		||||
            } catch {
 | 
			
		||||
                // Old table does not exist, ignore.
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type AddonCalendarEventDBRecord = {
 | 
			
		||||
    id?: number;
 | 
			
		||||
    name: string;
 | 
			
		||||
    description: string;
 | 
			
		||||
    eventtype: AddonCalendarEventType;
 | 
			
		||||
    timestart: number;
 | 
			
		||||
    timeduration: number;
 | 
			
		||||
    categoryid?: number;
 | 
			
		||||
    groupid?: number;
 | 
			
		||||
    userid?: number;
 | 
			
		||||
    instance?: number;
 | 
			
		||||
    modulename?: string;
 | 
			
		||||
    timemodified: number;
 | 
			
		||||
    repeatid?: number;
 | 
			
		||||
    visible: number;
 | 
			
		||||
    // Following properties are only available on AddonCalendarGetEventsEvent
 | 
			
		||||
    courseid?: number;
 | 
			
		||||
    uuid?: string;
 | 
			
		||||
    sequence?: number;
 | 
			
		||||
    subscriptionid?: number;
 | 
			
		||||
    // Following properties are only available on AddonCalendarCalendarEvent
 | 
			
		||||
    location?: string;
 | 
			
		||||
    eventcount?: number;
 | 
			
		||||
    timesort?: number;
 | 
			
		||||
    category?: string;
 | 
			
		||||
    course?: string;
 | 
			
		||||
    subscription?: string;
 | 
			
		||||
    canedit?: number;
 | 
			
		||||
    candelete?: number;
 | 
			
		||||
    deleteurl?: string;
 | 
			
		||||
    editurl?: string;
 | 
			
		||||
    viewurl?: string;
 | 
			
		||||
    isactionevent?: number;
 | 
			
		||||
    url?: string;
 | 
			
		||||
    islastday?: number;
 | 
			
		||||
    popupname?: string;
 | 
			
		||||
    mindaytimestamp?: number;
 | 
			
		||||
    maxdaytimestamp?: number;
 | 
			
		||||
    draggable?: number;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type AddonCalendarReminderDBRecord = {
 | 
			
		||||
    id?: number;
 | 
			
		||||
    eventid: number;
 | 
			
		||||
    time: number;
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										57
									
								
								src/addons/calendar/services/handlers/mainmenu.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,57 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Injectable } from '@angular/core';
 | 
			
		||||
import { AddonCalendar } from '../calendar';
 | 
			
		||||
import { makeSingleton } from '@singletons';
 | 
			
		||||
import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '@features/mainmenu/services/mainmenu-delegate';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Handler to inject an option into main menu.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable({ providedIn: 'root' })
 | 
			
		||||
export class AddonCalendarMainMenuHandlerService implements CoreMainMenuHandler {
 | 
			
		||||
 | 
			
		||||
    static readonly PAGE_NAME = 'calendar';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    name = 'AddonCalendar';
 | 
			
		||||
    priority = 900;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if the handler is enabled on a site level.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Whether or not the handler is enabled on a site level.
 | 
			
		||||
     */
 | 
			
		||||
    async isEnabled(): Promise<boolean> {
 | 
			
		||||
        return !AddonCalendar.instance.isCalendarDisabledInSite();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the data needed to render the handler.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Data needed to render the handler.
 | 
			
		||||
     */
 | 
			
		||||
    getDisplayData(): CoreMainMenuHandlerData {
 | 
			
		||||
        return {
 | 
			
		||||
            icon: 'far-calendar',
 | 
			
		||||
            title: 'addon.calendar.calendar',
 | 
			
		||||
            page: AddonCalendar.instance.getMainCalendarPagePath(),
 | 
			
		||||
            class: 'addon-calendar-handler',
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class AddonCalendarMainMenuHandler extends makeSingleton(AddonCalendarMainMenuHandlerService) {}
 | 
			
		||||
							
								
								
									
										51
									
								
								src/addons/calendar/services/handlers/sync-cron.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,51 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Injectable } from '@angular/core';
 | 
			
		||||
import { CoreCronHandler } from '@services/cron';
 | 
			
		||||
import { makeSingleton } from '@singletons';
 | 
			
		||||
import { AddonCalendarSync } from '../calendar-sync';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Synchronization cron handler.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable({ providedIn: 'root' })
 | 
			
		||||
export class AddonCalendarSyncCronHandlerService implements CoreCronHandler {
 | 
			
		||||
 | 
			
		||||
    name = 'AddonCalendarSyncCronHandler';
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Execute the process.
 | 
			
		||||
     * Receives the ID of the site affected, undefined for all sites.
 | 
			
		||||
     *
 | 
			
		||||
     * @param siteId ID of the site affected, undefined for all sites.
 | 
			
		||||
     * @param force Wether the execution is forced (manual sync).
 | 
			
		||||
     * @return Promise resolved when done, rejected if failure.
 | 
			
		||||
     */
 | 
			
		||||
    async execute(siteId?: string, force?: boolean): Promise<void> {
 | 
			
		||||
        await AddonCalendarSync.instance.syncAllEvents(siteId, force);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the time between consecutive executions.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Time between consecutive executions (in ms).
 | 
			
		||||
     */
 | 
			
		||||
    getInterval(): number {
 | 
			
		||||
        return AddonCalendarSync.instance.syncInterval;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class AddonCalendarSyncCronHandler extends makeSingleton(AddonCalendarSyncCronHandlerService) {}
 | 
			
		||||
							
								
								
									
										114
									
								
								src/addons/calendar/services/handlers/view-link.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,114 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Injectable } from '@angular/core';
 | 
			
		||||
import { Params } from '@angular/router';
 | 
			
		||||
import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler';
 | 
			
		||||
import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { makeSingleton } from '@singletons';
 | 
			
		||||
import { AddonCalendar } from '../calendar';
 | 
			
		||||
 | 
			
		||||
const SUPPORTED_VIEWS = ['month', 'mini', 'minithree', 'day', 'upcoming', 'upcoming_mini'];
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Content links handler for calendar view page.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable({ providedIn: 'root' })
 | 
			
		||||
export class AddonCalendarViewLinkHandlerService extends CoreContentLinksHandlerBase {
 | 
			
		||||
 | 
			
		||||
    name = 'AddonCalendarViewLinkHandler';
 | 
			
		||||
    pattern = /\/calendar\/view\.php/;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the list of actions for a link (url).
 | 
			
		||||
     *
 | 
			
		||||
     * @param siteIds List of sites the URL belongs to.
 | 
			
		||||
     * @param url The URL to treat.
 | 
			
		||||
     * @param params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
 | 
			
		||||
     * @return List of (or promise resolved with list of) actions.
 | 
			
		||||
     */
 | 
			
		||||
    getActions(siteIds: string[], url: string, params: Params): CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
 | 
			
		||||
        return [{
 | 
			
		||||
            action: (siteId?: string): void => {
 | 
			
		||||
                if (!params.view || params.view == 'month' || params.view == 'mini' || params.view == 'minithree') {
 | 
			
		||||
                    // Monthly view, open the calendar tab.
 | 
			
		||||
                    const stateParams: Params = {
 | 
			
		||||
                        courseId: params.course,
 | 
			
		||||
                    };
 | 
			
		||||
                    const timestamp = params.time ? params.time * 1000 : Date.now();
 | 
			
		||||
 | 
			
		||||
                    const date = new Date(timestamp);
 | 
			
		||||
                    stateParams.year = date.getFullYear();
 | 
			
		||||
                    stateParams.month = date.getMonth() + 1;
 | 
			
		||||
 | 
			
		||||
                    // @todo: Add checkMenu param.
 | 
			
		||||
                    CoreNavigator.instance.navigateToSitePath('/calendar/index', { params: stateParams, siteId });
 | 
			
		||||
 | 
			
		||||
                } else if (params.view == 'day') {
 | 
			
		||||
                    // Daily view, open the page.
 | 
			
		||||
                    const stateParams: Params = {
 | 
			
		||||
                        courseId: params.course,
 | 
			
		||||
                    };
 | 
			
		||||
                    const timestamp = params.time ? params.time * 1000 : Date.now();
 | 
			
		||||
 | 
			
		||||
                    const date = new Date(timestamp);
 | 
			
		||||
                    stateParams.year = date.getFullYear();
 | 
			
		||||
                    stateParams.month = date.getMonth() + 1;
 | 
			
		||||
                    stateParams.day = date.getDate();
 | 
			
		||||
 | 
			
		||||
                    CoreNavigator.instance.navigateToSitePath('/calendar/day', { params: stateParams, siteId });
 | 
			
		||||
 | 
			
		||||
                } else if (params.view == 'upcoming' || params.view == 'upcoming_mini') {
 | 
			
		||||
                    // Upcoming view, open the calendar tab.
 | 
			
		||||
                    const stateParams: Params = {
 | 
			
		||||
                        courseId: params.course,
 | 
			
		||||
                        upcoming: true,
 | 
			
		||||
                    };
 | 
			
		||||
 | 
			
		||||
                    // @todo: Add checkMenu param.
 | 
			
		||||
                    CoreNavigator.instance.navigateToSitePath('/calendar/index', { params: stateParams, siteId });
 | 
			
		||||
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
        }];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if the handler is enabled for a certain site (site + user) and a URL.
 | 
			
		||||
     * If not defined, defaults to true.
 | 
			
		||||
     *
 | 
			
		||||
     * @param siteId The site ID.
 | 
			
		||||
     * @param url The URL to treat.
 | 
			
		||||
     * @param params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
 | 
			
		||||
     * @return Whether the handler is enabled for the URL and site.
 | 
			
		||||
     */
 | 
			
		||||
    isEnabled(siteId: string, url: string, params: Params): boolean | Promise<boolean> {
 | 
			
		||||
        if (params.view && SUPPORTED_VIEWS.indexOf(params.view) == -1) {
 | 
			
		||||
            // This type of view isn't supported in the app.
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return AddonCalendar.instance.isDisabled(siteId).then((disabled) => {
 | 
			
		||||
            if (disabled) {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return AddonCalendar.instance.canViewMonth(siteId);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class AddonCalendarViewLinkHandler extends makeSingleton(AddonCalendarViewLinkHandlerService) {}
 | 
			
		||||
							
								
								
									
										89
									
								
								src/assets/img/mod/assign.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,89 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In  -->
 | 
			
		||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
 | 
			
		||||
	<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
 | 
			
		||||
]>
 | 
			
		||||
<svg version="1.1"
 | 
			
		||||
	 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
 | 
			
		||||
	 x="0px" y="0px" width="24px" height="24px" viewBox="0 0 24 24" style="overflow:visible;enable-background:new 0 0 24 24;"
 | 
			
		||||
	 xml:space="preserve" preserveAspectRatio="xMinYMid meet">
 | 
			
		||||
<defs>
 | 
			
		||||
</defs>
 | 
			
		||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="14.0054" y1="0" x2="14.0054" y2="20.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<stop  offset="0.5569" style="stop-color:#84ADEF"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<polygon style="fill:url(#SVGID_1_);" points="16.3,0 15.4,0 6.7,0 6,0 6,20 6.7,20 21.7,20 22,20 22,6.6 "/>
 | 
			
		||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="14.0054" y1="1" x2="14.0054" y2="19.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#DEEFFC"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#DEEFFC"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<polygon style="fill:url(#SVGID_2_);" points="7,19 7,1 15.8,1 21,6.9 21,19 "/>
 | 
			
		||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="14.0054" y1="2" x2="14.0054" y2="18.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#BBDFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#BBDFF8"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<polygon style="fill:url(#SVGID_3_);" points="8,18 8,2 15.4,2 20,7.3 20,18 "/>
 | 
			
		||||
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="18.3101" y1="0" x2="18.3101" y2="7.7852">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<stop  offset="0.5569" style="stop-color:#84ADEF"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_4_);" d="M14.8,7.5c0,0,5.2-1.3,7.2,0.3c0-0.1,0-1.2,0-1.2L16.2,0c0,0-1.5,0-1.6,0
 | 
			
		||||
	C16.8,3,14.8,7.5,14.8,7.5z"/>
 | 
			
		||||
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="16.3003" y1="6.1616" x2="18.5911" y2="3.8708">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<stop  offset="0.5181" style="stop-color:#E5F3FC"/>
 | 
			
		||||
	<stop  offset="0.7045" style="stop-color:#DEF0FB"/>
 | 
			
		||||
	<stop  offset="0.8371" style="stop-color:#D3EBFA"/>
 | 
			
		||||
	<stop  offset="0.872" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#BDD8F0"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<a:midPointStop  offset="0.87" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<a:midPointStop  offset="0.872" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#BDD8F0"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_5_);" d="M16.3,6.2c0.3-1.2,0.5-2.9,0.1-4.4l4,4.4C20,6.1,19.4,6,18.8,6C17.9,6,17,6.1,16.3,6.2z"/>
 | 
			
		||||
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="11.9673" y1="12.167" x2="11.9673" y2="23.8853">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#DDA976"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#9F6B37"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#DDA976"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#DDA976"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#9F6B37"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_6_);" d="M10,18.9c-0.3-2.2,9.4,0.5,9.3-1.4c0-0.8-8.4-3.4-11.4-4.1c-0.9-0.2-6.5-1.2-6.5-1.2
 | 
			
		||||
	c-0.2,0.1-1.3,3.3-1.4,5.2c0.5,0.3,7.3,6.7,10,6.5c2.7-0.2,14.2-3.6,13.9-4.7C23.3,17.1,10.3,21.3,10,18.9z"/>
 | 
			
		||||
<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="11.4893" y1="13.2803" x2="11.4893" y2="22.9336">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#FFDDAA"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#E3B17E"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#FFDDAA"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#FFDDAA"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#E3B17E"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_7_);" d="M1.7,17.4C1.4,17.2,1.2,17,1,16.9c0.1-1.2,0.6-2.8,0.9-3.6c1.8,0.3,5.1,0.9,5.7,1
 | 
			
		||||
	c2.6,0.6,9.4,2.3,9.7,3c0.5,1.1-8.3-1.9-8.1,1.7c0.2,3.5,12.4-0.6,12.7,0.5c0.2,0.7-10.1,3.6-12,3.5C7.9,22.8,3.3,18.8,1.7,17.4z"/>
 | 
			
		||||
<linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="7.5928" y1="14.4141" x2="7.5928" y2="21.9336">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#F1C592"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#E1AF7C"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#F1C592"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#F1C592"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#E1AF7C"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_8_);" d="M10.1,21.9c-0.8-0.1-2.8-1-7.6-5.2l-0.3-0.3c0.1-0.7,0.3-1.4,0.5-2.1c2.1,0.4,4.3,0.8,4.7,0.9
 | 
			
		||||
	c1.5,0.4,2.7,0.7,3.8,0.9c-1,0.1-1.8,0.4-2.3,1c-0.3,0.3-0.7,0.9-0.6,1.9c0.1,1.1,0.8,2.4,4,2.4c0.3,0,0.6,0,0.9,0
 | 
			
		||||
	C11.8,21.7,10.7,21.9,10.1,21.9L10.1,21.9z"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 5.5 KiB  | 
							
								
								
									
										89
									
								
								src/assets/img/mod/assignment.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,89 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In  -->
 | 
			
		||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
 | 
			
		||||
	<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
 | 
			
		||||
]>
 | 
			
		||||
<svg version="1.1"
 | 
			
		||||
	 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
 | 
			
		||||
	 x="0px" y="0px" width="24px" height="24px" viewBox="0 0 24 24" style="overflow:visible;enable-background:new 0 0 24 24;"
 | 
			
		||||
	 xml:space="preserve" preserveAspectRatio="xMinYMid meet">
 | 
			
		||||
<defs>
 | 
			
		||||
</defs>
 | 
			
		||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="14.0054" y1="0" x2="14.0054" y2="20.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<stop  offset="0.5569" style="stop-color:#84ADEF"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<polygon style="fill:url(#SVGID_1_);" points="16.3,0 15.4,0 6.7,0 6,0 6,20 6.7,20 21.7,20 22,20 22,6.6 "/>
 | 
			
		||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="14.0054" y1="1" x2="14.0054" y2="19.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#DEEFFC"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#DEEFFC"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<polygon style="fill:url(#SVGID_2_);" points="7,19 7,1 15.8,1 21,6.9 21,19 "/>
 | 
			
		||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="14.0054" y1="2" x2="14.0054" y2="18.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#BBDFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#BBDFF8"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<polygon style="fill:url(#SVGID_3_);" points="8,18 8,2 15.4,2 20,7.3 20,18 "/>
 | 
			
		||||
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="18.3101" y1="0" x2="18.3101" y2="7.7852">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<stop  offset="0.5569" style="stop-color:#84ADEF"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_4_);" d="M14.8,7.5c0,0,5.2-1.3,7.2,0.3c0-0.1,0-1.2,0-1.2L16.2,0c0,0-1.5,0-1.6,0
 | 
			
		||||
	C16.8,3,14.8,7.5,14.8,7.5z"/>
 | 
			
		||||
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="16.3003" y1="6.1616" x2="18.5911" y2="3.8708">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<stop  offset="0.5181" style="stop-color:#E5F3FC"/>
 | 
			
		||||
	<stop  offset="0.7045" style="stop-color:#DEF0FB"/>
 | 
			
		||||
	<stop  offset="0.8371" style="stop-color:#D3EBFA"/>
 | 
			
		||||
	<stop  offset="0.872" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#BDD8F0"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<a:midPointStop  offset="0.87" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<a:midPointStop  offset="0.872" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#BDD8F0"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_5_);" d="M16.3,6.2c0.3-1.2,0.5-2.9,0.1-4.4l4,4.4C20,6.1,19.4,6,18.8,6C17.9,6,17,6.1,16.3,6.2z"/>
 | 
			
		||||
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="11.9673" y1="12.167" x2="11.9673" y2="23.8853">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#DDA976"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#9F6B37"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#DDA976"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#DDA976"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#9F6B37"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_6_);" d="M10,18.9c-0.3-2.2,9.4,0.5,9.3-1.4c0-0.8-8.4-3.4-11.4-4.1c-0.9-0.2-6.5-1.2-6.5-1.2
 | 
			
		||||
	c-0.2,0.1-1.3,3.3-1.4,5.2c0.5,0.3,7.3,6.7,10,6.5c2.7-0.2,14.2-3.6,13.9-4.7C23.3,17.1,10.3,21.3,10,18.9z"/>
 | 
			
		||||
<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="11.4893" y1="13.2803" x2="11.4893" y2="22.9336">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#FFDDAA"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#E3B17E"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#FFDDAA"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#FFDDAA"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#E3B17E"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_7_);" d="M1.7,17.4C1.4,17.2,1.2,17,1,16.9c0.1-1.2,0.6-2.8,0.9-3.6c1.8,0.3,5.1,0.9,5.7,1
 | 
			
		||||
	c2.6,0.6,9.4,2.3,9.7,3c0.5,1.1-8.3-1.9-8.1,1.7c0.2,3.5,12.4-0.6,12.7,0.5c0.2,0.7-10.1,3.6-12,3.5C7.9,22.8,3.3,18.8,1.7,17.4z"/>
 | 
			
		||||
<linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="7.5928" y1="14.4141" x2="7.5928" y2="21.9336">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#F1C592"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#E1AF7C"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#F1C592"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#F1C592"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#E1AF7C"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_8_);" d="M10.1,21.9c-0.8-0.1-2.8-1-7.6-5.2l-0.3-0.3c0.1-0.7,0.3-1.4,0.5-2.1c2.1,0.4,4.3,0.8,4.7,0.9
 | 
			
		||||
	c1.5,0.4,2.7,0.7,3.8,0.9c-1,0.1-1.8,0.4-2.3,1c-0.3,0.3-0.7,0.9-0.6,1.9c0.1,1.1,0.8,2.4,4,2.4c0.3,0,0.6,0,0.9,0
 | 
			
		||||
	C11.8,21.7,10.7,21.9,10.1,21.9L10.1,21.9z"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 5.5 KiB  | 
							
								
								
									
										80
									
								
								src/assets/img/mod/book.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,80 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In  -->
 | 
			
		||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
 | 
			
		||||
	<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
 | 
			
		||||
]>
 | 
			
		||||
<svg version="1.1"
 | 
			
		||||
	 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
 | 
			
		||||
	 x="0px" y="0px" width="24px" height="24px" viewBox="-2 0 24 24" style="overflow:visible;enable-background:new -2 0 24 24;"
 | 
			
		||||
	 xml:space="preserve" preserveAspectRatio="xMinYMid meet">
 | 
			
		||||
<defs>
 | 
			
		||||
</defs>
 | 
			
		||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="12.0005" y1="0" x2="12.0005" y2="24.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#90C50E"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#70A034"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#90C50E"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#90C50E"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#70A034"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="4" style="fill:url(#SVGID_1_);" width="16" height="24"/>
 | 
			
		||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="12.0005" y1="1" x2="12.0005" y2="23.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#D9F991"/>
 | 
			
		||||
	<stop  offset="0.2388" style="stop-color:#D7F88D"/>
 | 
			
		||||
	<stop  offset="0.4501" style="stop-color:#D1F383"/>
 | 
			
		||||
	<stop  offset="0.6509" style="stop-color:#C6EC71"/>
 | 
			
		||||
	<stop  offset="0.844" style="stop-color:#B7E257"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#A8D73D"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#D9F991"/>
 | 
			
		||||
	<a:midPointStop  offset="0.7317" style="stop-color:#D9F991"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#A8D73D"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="5" y="1" style="fill:url(#SVGID_2_);" width="14" height="22"/>
 | 
			
		||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="12.0005" y1="2" x2="12.0005" y2="22.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#B3E810"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#90C60D"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#B3E810"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#B3E810"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#90C60D"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="6" y="2" style="fill:url(#SVGID_3_);" width="12" height="20"/>
 | 
			
		||||
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="12.0005" y1="4" x2="12.0005" y2="9">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#90C50E"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#70A034"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#90C50E"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#90C50E"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#70A034"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_4_);" d="M7,4v5h10V4H7z M16,8H8V5h8V8z"/>
 | 
			
		||||
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="2.5" y1="0" x2="2.5" y2="24.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#656565"/>
 | 
			
		||||
	<stop  offset="1.342887e-02" style="stop-color:#646464"/>
 | 
			
		||||
	<stop  offset="0.4453" style="stop-color:#3C3C3C"/>
 | 
			
		||||
	<stop  offset="0.7891" style="stop-color:#242424"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#1B1B1B"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#656565"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#656565"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#1B1B1B"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_5_);" d="M4,0H2.6H1C0.5,0,0,0.5,0,1v22c0,0.5,0.5,1,1,1h1.6H4h1v-1V1V0H4z"/>
 | 
			
		||||
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="2.5" y1="1" x2="2.5" y2="23.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#8E8E8E"/>
 | 
			
		||||
	<stop  offset="4.191053e-02" style="stop-color:#8A8A8A"/>
 | 
			
		||||
	<stop  offset="0.4613" style="stop-color:#626262"/>
 | 
			
		||||
	<stop  offset="0.7952" style="stop-color:#4A4A4A"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#414141"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#8E8E8E"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#8E8E8E"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#414141"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="1" y="1" style="fill:url(#SVGID_6_);" width="3" height="22"/>
 | 
			
		||||
<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="2.5" y1="2" x2="2.5" y2="22.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#7C7C7C"/>
 | 
			
		||||
	<stop  offset="0.3898" style="stop-color:#5C5C5C"/>
 | 
			
		||||
	<stop  offset="0.768" style="stop-color:#444444"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#3B3B3B"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#7C7C7C"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#7C7C7C"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#3B3B3B"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="2" y="2" style="fill:url(#SVGID_7_);" width="1" height="20"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 4.5 KiB  | 
							
								
								
									
										77
									
								
								src/assets/img/mod/chat.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,77 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In  -->
 | 
			
		||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
 | 
			
		||||
	<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
 | 
			
		||||
]>
 | 
			
		||||
<svg version="1.1"
 | 
			
		||||
	 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
 | 
			
		||||
	 x="0px" y="0px" width="24px" height="24px" viewBox="-0.1 -0.1 24 24"
 | 
			
		||||
	 style="overflow:visible;enable-background:new -0.1 -0.1 24 24;" xml:space="preserve" preserveAspectRatio="xMinYMid meet">
 | 
			
		||||
<defs>
 | 
			
		||||
</defs>
 | 
			
		||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="10.9429" y1="0" x2="10.9429" y2="19.7881">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_1_);" d="M10.8,0C4.7,0.1-0.1,4.3,0,9.3c0,2.6,1.5,5,3.7,6.6c0,0.1,0,0.2,0.1,0.3
 | 
			
		||||
	c0.3,2-0.9,3.6-0.9,3.6s2.2-0.5,3.5-1.5c0.2-0.2,0.5-0.4,0.7-0.7c1.3,0.4,2.6,0.6,4.1,0.6c6-0.1,10.9-4.3,10.8-9.3
 | 
			
		||||
	C21.8,3.9,16.8-0.1,10.8,0z"/>
 | 
			
		||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="10.9434" y1="1" x2="10.9434" y2="18.1289">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#82B4FB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#82B4FB"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_2_);" d="M4.6,18.1c0.1-0.6,0.2-1.3,0.1-2.1c0-0.1,0-0.2-0.1-0.3l-0.1-0.4l-0.3-0.3
 | 
			
		||||
	C2.2,13.6,1,11.5,1,9.3C0.9,4.8,5.3,1.1,10.8,1L11,1c5.4,0,9.8,3.5,9.9,7.9c0.1,4.5-4.3,8.2-9.8,8.3l-0.2,0c-1.2,0-2.4-0.2-3.5-0.5
 | 
			
		||||
	l-0.6-0.2l-0.4,0.5c-0.2,0.2-0.4,0.4-0.5,0.6C5.4,17.7,5,17.9,4.6,18.1z"/>
 | 
			
		||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="10.9434" y1="2" x2="10.9434" y2="16.1631">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<stop  offset="0.5569" style="stop-color:#84ADEF"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_3_);" d="M10.8,16.2c-1.1,0-2.2-0.2-3.2-0.5l-1.3-0.4L5.7,16c0,0,0-0.1,0-0.1c0-0.1,0-0.3-0.1-0.4
 | 
			
		||||
	l-0.1-0.7l-0.6-0.6l0,0C3,13,2,11.2,2,9.3C1.9,5.3,5.9,2.1,10.8,2L11,2c4.8,0,8.8,3.1,8.8,6.9c0.1,3.9-3.9,7.2-8.8,7.2L10.8,16.2z"
 | 
			
		||||
	/>
 | 
			
		||||
<path style="fill:#FFFFFF;" d="M16.6,9.6c0,0.7-0.6,1.3-1.2,1.3c-0.7,0-1.3-0.6-1.3-1.3c0-0.7,0.6-1.2,1.3-1.2
 | 
			
		||||
	C16,8.4,16.6,8.9,16.6,9.6z M11.5,8.4c-0.7,0-1.2,0.6-1.2,1.2c0,0.7,0.6,1.3,1.2,1.3c0.7,0,1.3-0.6,1.3-1.3
 | 
			
		||||
	C12.7,8.9,12.2,8.4,11.5,8.4z M7.5,8.4c-0.7,0-1.3,0.6-1.3,1.2c0,0.7,0.6,1.3,1.3,1.3s1.3-0.6,1.3-1.3C8.8,8.9,8.2,8.4,7.5,8.4z"/>
 | 
			
		||||
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="14.0254" y1="6.667" x2="14.0254" y2="23.8398">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#F0A829"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#C7671A"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#F0A829"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#F0A829"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#C7671A"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_4_);" d="M20.7,20.5c1.9-1.4,3.2-3.4,3.2-5.7c0.1-4.4-4.3-8-9.8-8.1C8.7,6.6,4.2,10,4.1,14.4
 | 
			
		||||
	c-0.1,4.4,4.3,8,9.8,8.1c1.4,0,2.7-0.2,3.9-0.5c1,1.3,3.7,1.9,3.7,1.9S20.3,22.2,20.7,20.5z"/>
 | 
			
		||||
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="14.1436" y1="7.667" x2="14.1436" y2="22.1943">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#FFEBA8"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#F8BE27"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#FFEBA8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#FFEBA8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#F8BE27"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_5_);" d="M19.9,22.2c-0.5-0.2-1-0.5-1.7-1.4c-0.4,0.1-1.7,0.7-4.2,0.7c-5,0-8.9-3.3-8.8-7.1
 | 
			
		||||
	c0.1-3.7,4-6.8,9-6.8c2.3,0,4.7,0.8,6.3,2.2c1.6,1.3,2.5,3.1,2.4,4.9c0,1.9-1,3.7-3.1,5.1C19.7,20.7,19.7,21.6,19.9,22.2z"/>
 | 
			
		||||
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="14.0234" y1="8.6689" x2="14.0234" y2="20.5176">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#F5AE0D"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#F5AE0D"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_6_);" d="M13.9,20.5c-6.1-0.2-7.9-4.2-7.8-6.1c0.2-3.2,3.3-5.8,8-5.8c2.1,0,4.3,0.8,5.7,2
 | 
			
		||||
	c1.4,1.1,2.1,2.6,2.1,4.1c-0.1,2.9-2.9,4.5-2.9,4.5s-0.2,0.7-0.2,0.9l-0.4-0.5C18.4,19.6,16.5,20.6,13.9,20.5z"/>
 | 
			
		||||
<path style="fill:#FFFFFF;" d="M19.4,14.9c0,0.7-0.6,1.3-1.2,1.3c-0.7,0-1.3-0.6-1.3-1.3c0-0.7,0.6-1.2,1.3-1.2
 | 
			
		||||
	C18.8,13.7,19.4,14.2,19.4,14.9z M14.3,13.7c-0.7,0-1.2,0.6-1.2,1.2c0,0.7,0.6,1.3,1.2,1.3c0.7,0,1.3-0.6,1.3-1.3
 | 
			
		||||
	C15.5,14.2,15,13.7,14.3,13.7z M10.3,13.7c-0.7,0-1.3,0.6-1.3,1.2c0,0.7,0.6,1.3,1.3,1.3s1.3-0.6,1.3-1.3
 | 
			
		||||
	C11.6,14.2,11,13.7,10.3,13.7z"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 5.2 KiB  | 
							
								
								
									
										46
									
								
								src/assets/img/mod/choice.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,46 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In  -->
 | 
			
		||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
 | 
			
		||||
	<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
 | 
			
		||||
]>
 | 
			
		||||
<svg version="1.1"
 | 
			
		||||
	 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
 | 
			
		||||
	 x="0px" y="0px" width="24px" height="24px" viewBox="-4.2 -0.3 24 24"
 | 
			
		||||
	 style="overflow:visible;enable-background:new -4.2 -0.3 24 24;" xml:space="preserve" preserveAspectRatio="xMinYMid meet">
 | 
			
		||||
<defs>
 | 
			
		||||
</defs>
 | 
			
		||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="7.8237" y1="0" x2="7.8237" y2="23.3262">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_1_);" d="M2.4,8.4C1.1,8.4,0,7.5,0,6.1C0,3.3,2.8,0,7.7,0c3.3,0,7.9,2.2,7.9,6c0,2-1.2,3.5-3.6,4.4
 | 
			
		||||
	c-3.3,1.2-1.4,3.7-4.5,3.7c-1.3,0-2.1-0.8-2.1-2.1c0-2.7,2.8-4.2,2.8-6.9c0-0.7-0.3-1.6-1.1-1.6c-0.9,0-1,0.9-1,1.5
 | 
			
		||||
	C5.8,7.1,4.5,8.4,2.4,8.4z M7.3,23.3c-2,0-3.6-1.6-3.6-3.6c0-2,1.6-3.6,3.6-3.6c2,0,3.6,1.6,3.6,3.6C10.8,21.7,9.2,23.3,7.3,23.3z"
 | 
			
		||||
	/>
 | 
			
		||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="7.8237" y1="1" x2="7.8237" y2="22.3262">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#82B4FB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#82B4FB"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_2_);" d="M7.3,22.3c-1.4,0-2.6-1.1-2.6-2.6c0-1.4,1.1-2.6,2.6-2.6s2.6,1.1,2.6,2.6
 | 
			
		||||
	C9.8,21.2,8.7,22.3,7.3,22.3z M7.6,13.1c-0.8,0-1.1-0.4-1.1-1.1c0-1.1,0.6-1.9,1.2-2.9c0.7-1.1,1.5-2.3,1.5-4c0-1.3-0.7-2.6-2.1-2.6
 | 
			
		||||
	c-1.8,0-2,1.7-2,2.4C4.9,6,4.3,7.4,2.4,7.4C1.6,7.4,1,6.9,1,6.1C1,4,3.1,1,7.7,1c2.9,0,6.9,1.9,6.9,5c0,1.6-1,2.7-2.9,3.4
 | 
			
		||||
	c-2,0.8-2.5,2-2.9,2.8C8.5,13,8.5,13.1,7.6,13.1z"/>
 | 
			
		||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="7.8237" y1="2.0986" x2="7.8237" y2="21.3262">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<stop  offset="0.5569" style="stop-color:#84ADEF"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_3_);" d="M7.3,21.3c-0.9,0-1.6-0.7-1.6-1.6s0.7-1.6,1.6-1.6c0.9,0,1.6,0.7,1.6,1.6S8.2,21.3,7.3,21.3z
 | 
			
		||||
	 M7.6,12.1c-0.1,0-0.1,0-0.1,0c0,0,0-0.1,0-0.1c0-0.8,0.5-1.5,1-2.3c0.8-1.2,1.7-2.6,1.7-4.6c0-1.3-0.5-2.3-1.3-3
 | 
			
		||||
	c2.2,0.4,4.8,1.8,4.8,3.9c0,0.4,0,1.6-2.3,2.5C9,9.4,8.3,11,7.9,11.9c0,0.1-0.1,0.2-0.1,0.2C7.8,12.1,7.7,12.1,7.6,12.1z M2.4,6.4
 | 
			
		||||
	C2,6.4,2,6.2,2,6.1c0-1.1,0.9-2.7,2.7-3.5C4.4,3.1,4.1,3.8,4,4.8C3.9,6.2,3.1,6.4,2.4,6.4z"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 3.1 KiB  | 
							
								
								
									
										87
									
								
								src/assets/img/mod/data.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,87 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In  -->
 | 
			
		||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
 | 
			
		||||
	<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
 | 
			
		||||
]>
 | 
			
		||||
<svg version="1.1"
 | 
			
		||||
	 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
 | 
			
		||||
	 x="0px" y="0px" width="24px" height="24px" viewBox="-2 -1 24 24" style="overflow:visible;enable-background:new -2 -1 24 24;"
 | 
			
		||||
	 xml:space="preserve" preserveAspectRatio="xMinYMid meet">
 | 
			
		||||
<defs>
 | 
			
		||||
</defs>
 | 
			
		||||
<radialGradient id="SVGID_1_" cx="10" cy="19.5" r="7.4917" gradientUnits="userSpaceOnUse">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
</radialGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_1_);" d="M20,21c0,1.1-0.9,2-2,2H2c-1.1,0-2-0.9-2-2v-3c0-1.1,0.9-2,2-2h16c1.1,0,2,0.9,2,2V21z"/>
 | 
			
		||||
<radialGradient id="SVGID_2_" cx="10" cy="19.5" r="6.6049" gradientUnits="userSpaceOnUse">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#82B4FB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#82B4FB"/>
 | 
			
		||||
</radialGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_2_);" d="M2,22c-0.6,0-1-0.4-1-1v-3c0-0.6,0.4-1,1-1h16c0.6,0,1,0.4,1,1v3c0,0.6-0.4,1-1,1H2z"/>
 | 
			
		||||
<radialGradient id="SVGID_3_" cx="10" cy="19.5" r="5.7554" gradientUnits="userSpaceOnUse">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<stop  offset="0.5569" style="stop-color:#84ADEF"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
</radialGradient>
 | 
			
		||||
<rect x="2" y="18" style="fill:url(#SVGID_3_);" width="16" height="3"/>
 | 
			
		||||
<radialGradient id="SVGID_4_" cx="10" cy="11.5" r="7.4917" gradientUnits="userSpaceOnUse">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
</radialGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_4_);" d="M20,13c0,1.1-0.9,2-2,2H2c-1.1,0-2-0.9-2-2v-3c0-1.1,0.9-2,2-2h16c1.1,0,2,0.9,2,2V13z"/>
 | 
			
		||||
<radialGradient id="SVGID_5_" cx="10" cy="11.5" r="6.6049" gradientUnits="userSpaceOnUse">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#82B4FB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#82B4FB"/>
 | 
			
		||||
</radialGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_5_);" d="M2,14c-0.6,0-1-0.4-1-1v-3c0-0.6,0.4-1,1-1h16c0.6,0,1,0.4,1,1v3c0,0.6-0.4,1-1,1H2z"/>
 | 
			
		||||
<radialGradient id="SVGID_6_" cx="10" cy="11.5" r="5.7554" gradientUnits="userSpaceOnUse">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<stop  offset="0.5569" style="stop-color:#84ADEF"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
</radialGradient>
 | 
			
		||||
<rect x="2" y="10" style="fill:url(#SVGID_6_);" width="16" height="3"/>
 | 
			
		||||
<radialGradient id="SVGID_7_" cx="10" cy="3.5" r="7.4917" gradientUnits="userSpaceOnUse">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
</radialGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_7_);" d="M20,5c0,1.1-0.9,2-2,2H2C0.9,7,0,6.1,0,5V2c0-1.1,0.9-2,2-2h16c1.1,0,2,0.9,2,2V5z"/>
 | 
			
		||||
<radialGradient id="SVGID_8_" cx="10" cy="3.5" r="6.6049" gradientUnits="userSpaceOnUse">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#82B4FB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#82B4FB"/>
 | 
			
		||||
</radialGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_8_);" d="M2,6C1.4,6,1,5.6,1,5V2c0-0.6,0.4-1,1-1h16c0.6,0,1,0.4,1,1v3c0,0.6-0.4,1-1,1H2z"/>
 | 
			
		||||
<radialGradient id="SVGID_9_" cx="10" cy="3.5" r="5.7554" gradientUnits="userSpaceOnUse">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<stop  offset="0.5569" style="stop-color:#84ADEF"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
</radialGradient>
 | 
			
		||||
<rect x="2" y="2" style="fill:url(#SVGID_9_);" width="16" height="3"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 5.1 KiB  | 
							
								
								
									
										87
									
								
								src/assets/img/mod/database.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,87 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In  -->
 | 
			
		||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
 | 
			
		||||
	<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
 | 
			
		||||
]>
 | 
			
		||||
<svg version="1.1"
 | 
			
		||||
	 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
 | 
			
		||||
	 x="0px" y="0px" width="24px" height="24px" viewBox="-2 -1 24 24" style="overflow:visible;enable-background:new -2 -1 24 24;"
 | 
			
		||||
	 xml:space="preserve" preserveAspectRatio="xMinYMid meet">
 | 
			
		||||
<defs>
 | 
			
		||||
</defs>
 | 
			
		||||
<radialGradient id="SVGID_1_" cx="10" cy="19.5" r="7.4917" gradientUnits="userSpaceOnUse">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
</radialGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_1_);" d="M20,21c0,1.1-0.9,2-2,2H2c-1.1,0-2-0.9-2-2v-3c0-1.1,0.9-2,2-2h16c1.1,0,2,0.9,2,2V21z"/>
 | 
			
		||||
<radialGradient id="SVGID_2_" cx="10" cy="19.5" r="6.6049" gradientUnits="userSpaceOnUse">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#82B4FB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#82B4FB"/>
 | 
			
		||||
</radialGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_2_);" d="M2,22c-0.6,0-1-0.4-1-1v-3c0-0.6,0.4-1,1-1h16c0.6,0,1,0.4,1,1v3c0,0.6-0.4,1-1,1H2z"/>
 | 
			
		||||
<radialGradient id="SVGID_3_" cx="10" cy="19.5" r="5.7554" gradientUnits="userSpaceOnUse">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<stop  offset="0.5569" style="stop-color:#84ADEF"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
</radialGradient>
 | 
			
		||||
<rect x="2" y="18" style="fill:url(#SVGID_3_);" width="16" height="3"/>
 | 
			
		||||
<radialGradient id="SVGID_4_" cx="10" cy="11.5" r="7.4917" gradientUnits="userSpaceOnUse">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
</radialGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_4_);" d="M20,13c0,1.1-0.9,2-2,2H2c-1.1,0-2-0.9-2-2v-3c0-1.1,0.9-2,2-2h16c1.1,0,2,0.9,2,2V13z"/>
 | 
			
		||||
<radialGradient id="SVGID_5_" cx="10" cy="11.5" r="6.6049" gradientUnits="userSpaceOnUse">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#82B4FB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#82B4FB"/>
 | 
			
		||||
</radialGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_5_);" d="M2,14c-0.6,0-1-0.4-1-1v-3c0-0.6,0.4-1,1-1h16c0.6,0,1,0.4,1,1v3c0,0.6-0.4,1-1,1H2z"/>
 | 
			
		||||
<radialGradient id="SVGID_6_" cx="10" cy="11.5" r="5.7554" gradientUnits="userSpaceOnUse">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<stop  offset="0.5569" style="stop-color:#84ADEF"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
</radialGradient>
 | 
			
		||||
<rect x="2" y="10" style="fill:url(#SVGID_6_);" width="16" height="3"/>
 | 
			
		||||
<radialGradient id="SVGID_7_" cx="10" cy="3.5" r="7.4917" gradientUnits="userSpaceOnUse">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
</radialGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_7_);" d="M20,5c0,1.1-0.9,2-2,2H2C0.9,7,0,6.1,0,5V2c0-1.1,0.9-2,2-2h16c1.1,0,2,0.9,2,2V5z"/>
 | 
			
		||||
<radialGradient id="SVGID_8_" cx="10" cy="3.5" r="6.6049" gradientUnits="userSpaceOnUse">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#82B4FB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#82B4FB"/>
 | 
			
		||||
</radialGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_8_);" d="M2,6C1.4,6,1,5.6,1,5V2c0-0.6,0.4-1,1-1h16c0.6,0,1,0.4,1,1v3c0,0.6-0.4,1-1,1H2z"/>
 | 
			
		||||
<radialGradient id="SVGID_9_" cx="10" cy="3.5" r="5.7554" gradientUnits="userSpaceOnUse">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<stop  offset="0.5569" style="stop-color:#84ADEF"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
</radialGradient>
 | 
			
		||||
<rect x="2" y="2" style="fill:url(#SVGID_9_);" width="16" height="3"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 5.1 KiB  | 
							
								
								
									
										55
									
								
								src/assets/img/mod/external-tool.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,55 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In  -->
 | 
			
		||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
 | 
			
		||||
	<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
 | 
			
		||||
]>
 | 
			
		||||
<svg version="1.1"
 | 
			
		||||
	 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
 | 
			
		||||
	 x="0px" y="0px" width="24px" height="24px" viewBox="0 0 24 24" style="overflow:visible;enable-background:new 0 0 24 24;"
 | 
			
		||||
	 xml:space="preserve" preserveAspectRatio="xMinYMid meet">
 | 
			
		||||
<defs>
 | 
			
		||||
</defs>
 | 
			
		||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="11.9995" y1="0" x2="11.9995" y2="24.001">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#90C50E"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#70A034"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#90C50E"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#90C50E"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#70A034"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_1_);" d="M21.1,12.3c1,0,1.7,0.9,1.7,0.9c0.3,0.4,0.7,0.8,0.9,0.8s0.3-0.5,0.3-1V8c0-0.5-0.5-1-1-1h-3
 | 
			
		||||
	c-0.5,0-1-0.1-1-0.3c0-0.2,0.3-0.7,0.7-1.1c0,0,0.8-0.9,0.8-2.1C20.5,1.6,18.9,0,17,0s-3.5,1.6-3.5,3.5c0,1.2,0.8,2.1,0.8,2.1
 | 
			
		||||
	C14.7,6,15,6.5,15,6.7C15,6.9,14.5,7,14,7H8C7.5,7,7,7.5,7,8v6c0,0.5-0.1,1-0.3,1S6,14.7,5.6,14.3c0,0-0.9-0.8-2.1-0.8
 | 
			
		||||
	C1.6,13.5,0,15.1,0,17s1.6,3.5,3.5,3.5c1.2,0,2.1-0.8,2.1-0.8C6,19.3,6.5,19,6.7,19S7,19.5,7,20v3c0,0.5,0.5,1,1,1h4
 | 
			
		||||
	c0.5,0,1-0.1,1-0.3s-0.3-0.6-0.7-1c0,0-0.6-0.6-0.6-1.6c0-1.4,1.3-2.5,2.7-2.5c1.4,0,2.4,1.1,2.4,2.5c0,1-0.9,1.7-0.9,1.7
 | 
			
		||||
	c-0.4,0.3-0.8,0.7-0.8,0.9s0.5,0.3,1,0.3h7c0.5,0,1-0.5,1-1v-6c0-0.5-0.1-1-0.3-1s-0.6,0.3-1,0.7c0,0-0.6,0.6-1.6,0.6
 | 
			
		||||
	c-1.4,0-2.5-1.3-2.5-2.7S19.7,12.3,21.1,12.3z"/>
 | 
			
		||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="11.9995" y1="1" x2="11.9995" y2="23.001">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#D9F991"/>
 | 
			
		||||
	<stop  offset="0.2388" style="stop-color:#D7F88D"/>
 | 
			
		||||
	<stop  offset="0.4501" style="stop-color:#D1F383"/>
 | 
			
		||||
	<stop  offset="0.6509" style="stop-color:#C6EC71"/>
 | 
			
		||||
	<stop  offset="0.844" style="stop-color:#B7E257"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#A8D73D"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#D9F991"/>
 | 
			
		||||
	<a:midPointStop  offset="0.7317" style="stop-color:#D9F991"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#A8D73D"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_2_);" d="M17,23c0.4-0.4,0.7-1.1,0.7-1.9c0-2-1.5-3.5-3.4-3.5c-2,0-3.7,1.6-3.7,3.5
 | 
			
		||||
	c0,0.9,0.3,1.5,0.6,1.9H8v-3c0-1.5-0.7-2-1.3-2c-0.6,0-1.3,0.6-1.7,0.9c0,0-0.7,0.6-1.5,0.6C2.1,19.5,1,18.4,1,17s1.1-2.5,2.5-2.5
 | 
			
		||||
	c0.8,0,1.4,0.6,1.5,0.6C5.3,15.4,6.1,16,6.7,16C7.3,16,8,15.5,8,14V8h6c1.5,0,2-0.7,2-1.3c0-0.6-0.5-1.3-0.9-1.7
 | 
			
		||||
	c0,0-0.6-0.7-0.6-1.5C14.5,2.1,15.6,1,17,1s2.5,1.1,2.5,2.5c0,0.8-0.6,1.5-0.6,1.5C18.5,5.4,18,6.1,18,6.7C18,7.3,18.5,8,20,8h3v4
 | 
			
		||||
	c-0.4-0.4-1.1-0.7-1.9-0.7c-2,0-3.5,1.5-3.5,3.4c0,2,1.6,3.7,3.5,3.7c0.9,0,1.5-0.3,1.9-0.6V23H17z"/>
 | 
			
		||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="11.9995" y1="2" x2="11.9995" y2="22.001">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#B3E810"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#90C60D"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#B3E810"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#B3E810"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#90C60D"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_3_);" d="M18.6,22c0.1-0.3,0.1-0.6,0.1-0.9c0-2.5-1.9-4.5-4.4-4.5c-2.5,0-4.7,2.1-4.7,4.5
 | 
			
		||||
	c0,0.3,0,0.6,0.1,0.9H9v-2c0-2.1-1.2-3-2.3-3C7.8,17,9,16.1,9,14V9h5c2.1,0,3-1.2,3-2.3c0-0.7-0.4-1.5-1.2-2.4
 | 
			
		||||
	c-0.1-0.1-0.3-0.5-0.3-0.8C15.5,2.7,16.2,2,17,2s1.5,0.7,1.5,1.5c0,0.3-0.3,0.7-0.3,0.8C17.4,5.2,17,6,17,6.7C17,7.8,17.9,9,20,9h2
 | 
			
		||||
	v1.4c-0.3-0.1-0.6-0.1-0.9-0.1c-2.5,0-4.5,1.9-4.5,4.4c0,2.5,2.1,4.7,4.5,4.7c0.3,0,0.6,0,0.9-0.1V22H18.6z M3.5,18.5
 | 
			
		||||
	C2.7,18.5,2,17.8,2,17s0.7-1.5,1.5-1.5c0.3,0,0.7,0.3,0.8,0.3C5.2,16.6,6,17,6.7,17c-0.7,0-1.5,0.4-2.4,1.2
 | 
			
		||||
	C4.2,18.3,3.8,18.5,3.5,18.5z"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 3.9 KiB  | 
							
								
								
									
										133
									
								
								src/assets/img/mod/feedback.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,133 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In  -->
 | 
			
		||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
 | 
			
		||||
	<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
 | 
			
		||||
]>
 | 
			
		||||
<svg version="1.1"
 | 
			
		||||
	 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
 | 
			
		||||
	 x="0px" y="0px" width="24px" height="24px" viewBox="0 0 24 24" style="overflow:visible;enable-background:new 0 0 24 24;"
 | 
			
		||||
	 xml:space="preserve" preserveAspectRatio="xMinYMid meet">
 | 
			
		||||
<defs>
 | 
			
		||||
</defs>
 | 
			
		||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="7.7681" y1="10.2231" x2="7.7681" y2="23.9805">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#DB6D17"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#BF3B08"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#DB6D17"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#DB6D17"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#BF3B08"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_1_);" d="M10.9,19.1c-1.1-0.8-1.7-1.1-2-2.4c-0.2-1.1-1.3-6.4-1.3-6.4l-4.9,1c0,0,0.9,4.6,1.8,9
 | 
			
		||||
	c0.9,4.4,1,4.3,6.8,3.1C13.7,22.8,12.8,20.5,10.9,19.1z"/>
 | 
			
		||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="7.8569" y1="11.3994" x2="7.8569" y2="22.9805">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#F6A55E"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#EA5B03"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#F6A55E"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#F6A55E"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#EA5B03"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_2_);" d="M7,23c-0.8,0-0.9,0-1.5-3l-1.6-8l2.9-0.6L8,16.9c0.3,1.6,1,2.1,1.9,2.7l0.4,0.3
 | 
			
		||||
	c1.1,0.8,1.5,1.7,1.4,2.1c0,0.2-0.5,0.3-0.6,0.3l-0.5,0.1C9,22.7,7.8,23,7,23L7,23z"/>
 | 
			
		||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="7.7373" y1="12.5757" x2="7.7373" y2="21.8926">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#F17219"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#EA5B03"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#F17219"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#F17219"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#EA5B03"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_3_);" d="M7.7,21.9c-0.7,0-0.7,0-1.2-2.1l-1.4-7l1-0.2L7,17.1c0.4,1.9,1.4,2.7,2.3,3.3l0.4,0.3
 | 
			
		||||
	C10.8,21.6,10.6,21.4,7.7,21.9z"/>
 | 
			
		||||
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="11.0005" y1="0" x2="11.0005" y2="19.0039">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#C3C3C3"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#ACACAC"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#C3C3C3"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#C3C3C3"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#ACACAC"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_4_);" d="M12.5,5H4.6C2.1,5,0,7.2,0,9.5C0,11.8,2.1,14,4.6,14h7.6c0,0,5.7,0,9.8,5c0-4.9,0-9.5,0-9.5
 | 
			
		||||
	s0-4.3,0-9.5C18.7,4.8,12.5,5,12.5,5z"/>
 | 
			
		||||
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="11.0005" y1="2.6079" x2="11.0005" y2="16.5342">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#E9E9E9"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#C4C4C4"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#E9E9E9"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#E9E9E9"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#C4C4C4"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_5_);" d="M21,16.5C17,13,12.5,13,12.2,13H4.6C2.7,13,1,11.4,1,9.5S2.7,6,4.6,6h7.8
 | 
			
		||||
	c0.3,0,4.9-0.2,8.5-3.4V16.5z"/>
 | 
			
		||||
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="11.0005" y1="4.5991" x2="11.0005" y2="14.5273">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#F7F7F7"/>
 | 
			
		||||
	<stop  offset="0.1044" style="stop-color:#FCFCFC"/>
 | 
			
		||||
	<stop  offset="0.3293" style="stop-color:#FFFFFF"/>
 | 
			
		||||
	<stop  offset="0.5692" style="stop-color:#E8E8E8"/>
 | 
			
		||||
	<stop  offset="0.8153" style="stop-color:#D7D7D7"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#D1D1D1"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#F7F7F7"/>
 | 
			
		||||
	<a:midPointStop  offset="0.2222" style="stop-color:#F7F7F7"/>
 | 
			
		||||
	<a:midPointStop  offset="0.3293" style="stop-color:#FFFFFF"/>
 | 
			
		||||
	<a:midPointStop  offset="0.3545" style="stop-color:#FFFFFF"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#D1D1D1"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_6_);" d="M20,14.5C16.2,12,12.4,12,12.2,12H4.6C3.3,12,2,10.8,2,9.5S3.3,7,4.6,7h7.8
 | 
			
		||||
	c0.2,0,4-0.2,7.5-2.4V14.5z"/>
 | 
			
		||||
<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="5.501" y1="5.0015" x2="5.501" y2="14.002">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#DB6D17"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#BF3B08"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#DB6D17"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#DB6D17"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#BF3B08"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_7_);" d="M11,5H4.6C2.1,5,0,7.2,0,9.5C0,11.8,2.1,14,4.6,14H11V5z"/>
 | 
			
		||||
<linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="6.001" y1="6.0015" x2="6.001" y2="13.0015">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#F6A55E"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#EA5B03"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#F6A55E"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#F6A55E"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#EA5B03"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_8_);" d="M11,6H4.6C2.7,6,1,7.6,1,9.5S2.7,13,4.6,13H11V6z"/>
 | 
			
		||||
<linearGradient id="SVGID_9_" gradientUnits="userSpaceOnUse" x1="6.501" y1="7.0015" x2="6.501" y2="12.0015">
 | 
			
		||||
	<stop  offset="0.2195" style="stop-color:#F6A55E"/>
 | 
			
		||||
	<stop  offset="0.5076" style="stop-color:#F28C3F"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#EA5B03"/>
 | 
			
		||||
	<a:midPointStop  offset="0.2195" style="stop-color:#F6A55E"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5304" style="stop-color:#F6A55E"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#EA5B03"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_9_);" d="M11,7H4.6C3.3,7,2,8.2,2,9.5S3.3,12,4.6,12H11V7z"/>
 | 
			
		||||
<linearGradient id="SVGID_10_" gradientUnits="userSpaceOnUse" x1="22.002" y1="1.464844e-03" x2="22.002" y2="19.002">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#DB6D17"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#BF3B08"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#DB6D17"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#DB6D17"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#BF3B08"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_10_);" d="M20,17c0,1.1,0.9,2,2,2l0,0c1.1,0,2-0.9,2-2V2c0-1.1-0.9-2-2-2l0,0c-1.1,0-2,0.9-2,2V17z"/>
 | 
			
		||||
<linearGradient id="SVGID_11_" gradientUnits="userSpaceOnUse" x1="22.002" y1="1.0015" x2="22.002" y2="18.002">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#F6A55E"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#EA5B03"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#F6A55E"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#F6A55E"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#EA5B03"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_11_);" d="M22,18c-0.6,0-1-0.4-1-1V2c0-0.6,0.4-1,1-1s1,0.4,1,1v15C23,17.6,22.6,18,22,18z"/>
 | 
			
		||||
<linearGradient id="SVGID_12_" gradientUnits="userSpaceOnUse" x1="22.002" y1="1.0015" x2="22.002" y2="18.002">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#F17219"/>
 | 
			
		||||
	<stop  offset="9.009037e-02" style="stop-color:#F38A39"/>
 | 
			
		||||
	<stop  offset="0.183" style="stop-color:#F59E54"/>
 | 
			
		||||
	<stop  offset="0.2378" style="stop-color:#F6A55E"/>
 | 
			
		||||
	<stop  offset="0.2464" style="stop-color:#F5A35C"/>
 | 
			
		||||
	<stop  offset="0.3809" style="stop-color:#EC8740"/>
 | 
			
		||||
	<stop  offset="0.5155" style="stop-color:#E5722C"/>
 | 
			
		||||
	<stop  offset="0.649" style="stop-color:#E06620"/>
 | 
			
		||||
	<stop  offset="0.7805" style="stop-color:#DF621C"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#D64701"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#F17219"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4103" style="stop-color:#F17219"/>
 | 
			
		||||
	<a:midPointStop  offset="0.2378" style="stop-color:#F6A55E"/>
 | 
			
		||||
	<a:midPointStop  offset="0.296" style="stop-color:#F6A55E"/>
 | 
			
		||||
	<a:midPointStop  offset="0.7805" style="stop-color:#DF621C"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#DF621C"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#D64701"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_12_);" d="M22,18c-0.6,0-1-0.4-1-1V2c0-0.6,0.4-1,1-1s1,0.4,1,1v15C23,17.6,22.6,18,22,18z"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 8.1 KiB  | 
							
								
								
									
										60
									
								
								src/assets/img/mod/file.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,60 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In  -->
 | 
			
		||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
 | 
			
		||||
	<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
 | 
			
		||||
]>
 | 
			
		||||
<svg version="1.1"
 | 
			
		||||
	 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
 | 
			
		||||
	 x="0px" y="0px" width="24px" height="24px" viewBox="-3 0 24 24" style="overflow:visible;enable-background:new -3 0 24 24;"
 | 
			
		||||
	 xml:space="preserve" preserveAspectRatio="xMinYMid meet">
 | 
			
		||||
<defs>
 | 
			
		||||
</defs>
 | 
			
		||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="9.4995" y1="0" x2="9.4995" y2="24.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<stop  offset="0.5569" style="stop-color:#84ADEF"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<polygon style="fill:url(#SVGID_1_);" points="11.5,0 0,0 0,24 19,24 19,7.9 "/>
 | 
			
		||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="9.4995" y1="1" x2="9.4995" y2="23.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#DEEFFC"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#DEEFFC"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<polygon style="fill:url(#SVGID_2_);" points="1,23 1,1 11.1,1 18,8.3 18,23 "/>
 | 
			
		||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="9.4995" y1="2" x2="9.4995" y2="22.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#BBDFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#BBDFF8"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<polygon style="fill:url(#SVGID_3_);" points="2,22 2,2 10.6,2 17,8.7 17,22 "/>
 | 
			
		||||
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="14.2451" y1="0" x2="14.2451" y2="9.3594">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<stop  offset="0.5569" style="stop-color:#84ADEF"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_4_);" d="M10,9c0,0,5.2-1.5,9,0.4c0-0.1,0-1.5,0-1.5L11.5,0c0,0-1.8,0-2,0C12.1,3.7,10,9,10,9z"/>
 | 
			
		||||
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="11.3223" y1="7.5449" x2="14.4504" y2="4.4168">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<stop  offset="0.5181" style="stop-color:#E5F3FC"/>
 | 
			
		||||
	<stop  offset="0.7045" style="stop-color:#DEF0FB"/>
 | 
			
		||||
	<stop  offset="0.8371" style="stop-color:#D3EBFA"/>
 | 
			
		||||
	<stop  offset="0.872" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#BDD8F0"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<a:midPointStop  offset="0.87" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<a:midPointStop  offset="0.872" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#BDD8F0"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_5_);" d="M17.5,7.8c-0.9-0.2-2-0.3-3.1-0.3c-1.1,0-2.1,0.1-3,0.3c0.4-1.6,0.7-4.1-0.2-6.4L17.5,7.8z"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 3.5 KiB  | 
							
								
								
									
										65
									
								
								src/assets/img/mod/folder.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,65 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In  -->
 | 
			
		||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
 | 
			
		||||
	<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
 | 
			
		||||
]>
 | 
			
		||||
<svg version="1.1"
 | 
			
		||||
	 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
 | 
			
		||||
	 x="0px" y="0px" width="24px" height="24px" viewBox="-0.1 -2 24 24"
 | 
			
		||||
	 style="overflow:visible;enable-background:new -0.1 -2 24 24;" xml:space="preserve" preserveAspectRatio="xMinYMid meet">
 | 
			
		||||
<defs>
 | 
			
		||||
</defs>
 | 
			
		||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="11.9351" y1="0" x2="11.9351" y2="20.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_1_);" d="M21.9,19c0,0.5-0.5,1-1,1h-18c-0.5,0-1-0.5-1-1V1.1c0-0.5,0.5-1.1,1-1.1h4
 | 
			
		||||
	c0.5,0,1.4,0.3,1.8,0.6l0.9,0.7C10.1,1.7,10.9,2,11.4,2l9.5,0c0.5,0,1,0.5,1,1V19z"/>
 | 
			
		||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="11.9351" y1="0.9888" x2="11.9351" y2="19.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#82B4FB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#82B4FB"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_2_);" d="M2.9,19V1.1C2.9,1.1,3,1,3,1l3.9,0c0.3,0,0.9,0.2,1.2,0.4L9,2.2C9.7,2.6,10.7,3,11.4,3l9.5,0
 | 
			
		||||
	v16H2.9z"/>
 | 
			
		||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="11.9351" y1="1.9917" x2="11.9351" y2="18.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<stop  offset="0.5569" style="stop-color:#84ADEF"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_3_);" d="M3.9,18V2l3,0C7,2,7.4,2.1,7.5,2.2L8.4,3c0.8,0.6,2.1,1,3,1l8.5,0v14H3.9z"/>
 | 
			
		||||
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="11.936" y1="5" x2="11.936" y2="20.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_4_);" d="M23,19c0,0.5-0.5,1-1.1,1h-20c-0.5,0-1-0.5-1.1-1L0,6c0-0.5,0.4-1,0.9-1h22c0.5,0,1,0.5,0.9,1
 | 
			
		||||
	L23,19z"/>
 | 
			
		||||
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="11.9673" y1="5.9541" x2="11.9673" y2="19.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#82B4FB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#82B4FB"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_5_);" d="M1.9,19c0,0-0.1-0.1-0.1-0.1L1,6l21.9,0L22,18.9c0,0,0,0.1-0.1,0.1H1.9z"/>
 | 
			
		||||
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="11.9663" y1="6.9565" x2="11.9663" y2="18.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<stop  offset="0.5569" style="stop-color:#84ADEF"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<polygon style="fill:url(#SVGID_6_);" points="2.8,18 2.1,7 21.9,7 21.1,18 "/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 3.9 KiB  | 
							
								
								
									
										71
									
								
								src/assets/img/mod/forum.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,71 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In  -->
 | 
			
		||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
 | 
			
		||||
	<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
 | 
			
		||||
]>
 | 
			
		||||
<svg version="1.1"
 | 
			
		||||
	 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
 | 
			
		||||
	 x="0px" y="0px" width="24px" height="24px" viewBox="0 0 24 24" style="overflow:visible;enable-background:new 0 0 24 24;"
 | 
			
		||||
	 xml:space="preserve" preserveAspectRatio="xMinYMid meet">
 | 
			
		||||
<defs>
 | 
			
		||||
</defs>
 | 
			
		||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="10.4995" y1="0" x2="10.4995" y2="19.4312">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_1_);" d="M19,0H2C0.9,0,0,0.9,0,2v11.3C0,14.4,0.9,15,2,15h0v4.4L6.1,15H19c1.1,0,2-0.5,2-1.6V2
 | 
			
		||||
	C21,0.9,20,0,19,0z"/>
 | 
			
		||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="10.4995" y1="0.9531" x2="10.4995" y2="16.8384">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#82B4FB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#82B4FB"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_2_);" d="M3,14H2c-0.4,0-1-0.1-1-0.6V2c0-0.6,0.5-1,1-1H19c0.5,0,1,0.5,1,1v11.3c0,0.6-0.8,0.6-1,0.6
 | 
			
		||||
	H5.7L3,16.8V14z"/>
 | 
			
		||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="10.4985" y1="1.9775" x2="10.4985" y2="14.314">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<stop  offset="0.5569" style="stop-color:#84ADEF"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_3_);" d="M4,13c0,0-2,0-2,0V2l17,0l0,11c0,0-13.8,0-13.8,0L4,14.3V13z"/>
 | 
			
		||||
<path style="fill:#FFFFFF;" d="M17,11H4v-1h13V11z M17,8H4v1h13V8z M17,6H4v1h13V6z M17,4H4v1h13V4z"/>
 | 
			
		||||
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="15" y1="7.8955" x2="15" y2="23.897">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#90C50E"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#70A034"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#90C50E"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#90C50E"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#70A034"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_4_);" d="M22,7.9H8c-1.1,0-2,0.7-2,1.8v9c0,1.1,0.9,2,2,2v3.2l3.1-3H22c1.1,0,2-1.1,2-2.2v-9
 | 
			
		||||
	C24,8.6,23.1,7.9,22,7.9z"/>
 | 
			
		||||
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="15" y1="8.8955" x2="15" y2="21.3911">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#D9F991"/>
 | 
			
		||||
	<stop  offset="0.2388" style="stop-color:#D7F88D"/>
 | 
			
		||||
	<stop  offset="0.4501" style="stop-color:#D1F383"/>
 | 
			
		||||
	<stop  offset="0.6509" style="stop-color:#C6EC71"/>
 | 
			
		||||
	<stop  offset="0.844" style="stop-color:#B7E257"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#A8D73D"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#D9F991"/>
 | 
			
		||||
	<a:midPointStop  offset="0.7317" style="stop-color:#D9F991"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#A8D73D"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_5_);" d="M9,19.9H8c-0.6,0-1-0.7-1-1.2v-9c0-0.6,0.4-0.8,1-0.8h14c0.6,0,1,0.2,1,0.8v9
 | 
			
		||||
	c0,0.6-0.4,1.2-1,1.2H10.6L9,21.4V19.9z"/>
 | 
			
		||||
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="15" y1="9.8955" x2="15" y2="18.896">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#B3E810"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#90C60D"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#B3E810"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#B3E810"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#90C60D"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<polygon style="fill:url(#SVGID_6_);" points="10,18.9 8,18.9 8,9.9 22,9.9 22,18.9 10.2,18.9 10,18.9 "/>
 | 
			
		||||
<path style="fill:#FFFFFF;" d="M20,16.9H10v-1h10V16.9z M20,13.9H10v1h10V13.9z M20,11.9H10v1h10V11.9z"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 4.2 KiB  | 
							
								
								
									
										146
									
								
								src/assets/img/mod/glossary.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,146 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In  -->
 | 
			
		||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
 | 
			
		||||
	<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
 | 
			
		||||
]>
 | 
			
		||||
<svg version="1.1"
 | 
			
		||||
	 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
 | 
			
		||||
	 x="0px" y="0px" width="24px" height="24px" viewBox="-2 0 24 24" style="overflow:visible;enable-background:new -2 0 24 24;"
 | 
			
		||||
	 xml:space="preserve" preserveAspectRatio="xMinYMid meet">
 | 
			
		||||
<defs>
 | 
			
		||||
</defs>
 | 
			
		||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="17.5" y1="16" x2="17.5" y2="22">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#DB6D17"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#BF3B08"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#DB6D17"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#DB6D17"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#BF3B08"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="15" y="16" style="fill:url(#SVGID_1_);" width="5" height="6"/>
 | 
			
		||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="17.5" y1="17" x2="17.5" y2="21">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#F6A55E"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#EA5B03"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#F6A55E"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#F6A55E"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#EA5B03"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="16" y="17" style="fill:url(#SVGID_2_);" width="3" height="4"/>
 | 
			
		||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="17.5" y1="18" x2="17.5" y2="20">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#F17219"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#EA5B03"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#F17219"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#F17219"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#EA5B03"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="17" y="18" style="fill:url(#SVGID_3_);" width="1" height="2"/>
 | 
			
		||||
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="17.5" y1="9" x2="17.5" y2="15.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#90C50E"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#70A034"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#90C50E"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#90C50E"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#70A034"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="15" y="9" style="fill:url(#SVGID_4_);" width="5" height="6"/>
 | 
			
		||||
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="17.5" y1="10" x2="17.5" y2="14.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#D9F991"/>
 | 
			
		||||
	<stop  offset="0.2388" style="stop-color:#D7F88D"/>
 | 
			
		||||
	<stop  offset="0.4501" style="stop-color:#D1F383"/>
 | 
			
		||||
	<stop  offset="0.6509" style="stop-color:#C6EC71"/>
 | 
			
		||||
	<stop  offset="0.844" style="stop-color:#B7E257"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#A8D73D"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#D9F991"/>
 | 
			
		||||
	<a:midPointStop  offset="0.7317" style="stop-color:#D9F991"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#A8D73D"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="16" y="10" style="fill:url(#SVGID_5_);" width="3" height="4"/>
 | 
			
		||||
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="17.5" y1="11" x2="17.5" y2="13">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#B3E810"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#90C60D"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#B3E810"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#B3E810"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#90C60D"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="17" y="11" style="fill:url(#SVGID_6_);" width="1" height="2"/>
 | 
			
		||||
<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="8.4995" y1="0" x2="8.4995" y2="24.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<stop  offset="0.5569" style="stop-color:#84ADEF"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect style="fill:url(#SVGID_7_);" width="17" height="24"/>
 | 
			
		||||
<linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="8.4995" y1="1" x2="8.4995" y2="23.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#DEEFFC"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#DEEFFC"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="1" y="1" style="fill:url(#SVGID_8_);" width="15" height="22"/>
 | 
			
		||||
<linearGradient id="SVGID_9_" gradientUnits="userSpaceOnUse" x1="8.4995" y1="2" x2="8.4995" y2="22.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#BBDFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#BBDFF8"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="2" y="2" style="fill:url(#SVGID_9_);" width="13" height="20"/>
 | 
			
		||||
<linearGradient id="SVGID_10_" gradientUnits="userSpaceOnUse" x1="17.5" y1="2" x2="17.5" y2="8">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="15" y="2" style="fill:url(#SVGID_10_);" width="5" height="6"/>
 | 
			
		||||
<linearGradient id="SVGID_11_" gradientUnits="userSpaceOnUse" x1="17.5" y1="3" x2="17.5" y2="7">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#82B4FB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#82B4FB"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="16" y="3" style="fill:url(#SVGID_11_);" width="3" height="4"/>
 | 
			
		||||
<linearGradient id="SVGID_12_" gradientUnits="userSpaceOnUse" x1="17.5" y1="4" x2="17.5" y2="6">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<stop  offset="0.5569" style="stop-color:#84ADEF"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="17" y="4" style="fill:url(#SVGID_12_);" width="1" height="2"/>
 | 
			
		||||
<linearGradient id="SVGID_13_" gradientUnits="userSpaceOnUse" x1="12.4248" y1="10.7285" x2="12.4248" y2="15.9702">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<stop  offset="5.472010e-02" style="stop-color:#739DE9"/>
 | 
			
		||||
	<stop  offset="0.2045" style="stop-color:#6F95DE"/>
 | 
			
		||||
	<stop  offset="0.4149" style="stop-color:#6C91D7"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="0.13" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_13_);" d="M12.7,15.2C12,15.7,11.5,16,11,16c-0.3,0-0.5-0.1-0.7-0.3C10.1,15.5,10,15.3,10,15
 | 
			
		||||
	c0-0.4,0.2-0.7,0.5-1c0.3-0.3,1-0.7,2.2-1.2v-0.5c0-0.4,0-0.6-0.1-0.7c0-0.1-0.1-0.2-0.2-0.3c-0.1-0.1-0.2-0.1-0.4-0.1
 | 
			
		||||
	c-0.2,0-0.4,0.1-0.6,0.2c-0.1,0.1-0.1,0.1-0.1,0.2c0,0.1,0,0.2,0.2,0.3c0.1,0.2,0.2,0.3,0.2,0.4c0,0.2-0.1,0.3-0.2,0.4
 | 
			
		||||
	c-0.1,0.1-0.3,0.2-0.5,0.2c-0.2,0-0.4-0.1-0.6-0.2s-0.2-0.3-0.2-0.5c0-0.3,0.1-0.5,0.3-0.7c0.2-0.2,0.5-0.4,0.9-0.5s0.7-0.2,1.1-0.2
 | 
			
		||||
	c0.5,0,0.9,0.1,1.1,0.3c0.3,0.2,0.5,0.4,0.5,0.7c0.1,0.2,0.1,0.5,0.1,1v1.9c0,0.2,0,0.4,0,0.4c0,0.1,0,0.1,0.1,0.1c0,0,0.1,0,0.1,0
 | 
			
		||||
	c0.1,0,0.2-0.1,0.3-0.2l0.2,0.1c-0.2,0.3-0.4,0.5-0.6,0.6c-0.2,0.1-0.4,0.2-0.7,0.2c-0.3,0-0.5-0.1-0.7-0.2
 | 
			
		||||
	C12.8,15.6,12.7,15.4,12.7,15.2z M12.7,14.8v-1.7c-0.4,0.3-0.8,0.5-1,0.8c-0.1,0.2-0.2,0.4-0.2,0.6c0,0.2,0.1,0.3,0.2,0.4
 | 
			
		||||
	c0.1,0.1,0.2,0.1,0.4,0.1C12.2,15.1,12.4,15,12.7,14.8z"/>
 | 
			
		||||
<linearGradient id="SVGID_14_" gradientUnits="userSpaceOnUse" x1="6.3535" y1="8.2671" x2="6.3535" y2="15.7007">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<stop  offset="5.472010e-02" style="stop-color:#739DE9"/>
 | 
			
		||||
	<stop  offset="0.2045" style="stop-color:#6F95DE"/>
 | 
			
		||||
	<stop  offset="0.4149" style="stop-color:#6C91D7"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="0.13" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_14_);" d="M7,13.6H4.4l-0.3,0.7c-0.1,0.2-0.2,0.4-0.2,0.6c0,0.2,0.1,0.4,0.2,0.5
 | 
			
		||||
	c0.1,0.1,0.3,0.1,0.7,0.1v0.2H2.5v-0.2c0.3,0,0.5-0.1,0.6-0.3c0.2-0.2,0.4-0.5,0.6-1.1l2.6-5.8h0.1l2.6,6c0.2,0.6,0.5,0.9,0.6,1.1
 | 
			
		||||
	c0.1,0.1,0.3,0.2,0.5,0.2v0.2H6.7v-0.2h0.1c0.3,0,0.5,0,0.6-0.1c0.1-0.1,0.1-0.1,0.1-0.2c0-0.1,0-0.1,0-0.2c0,0-0.1-0.2-0.2-0.4
 | 
			
		||||
	L7,13.6z M6.8,13.2l-1.1-2.5l-1.1,2.5H6.8z"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 8.9 KiB  | 
							
								
								
									
										1
									
								
								src/assets/img/mod/h5pactivity.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1 @@
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" preserveAspectRatio="xMinYMid meet"><title>h5p finalArtboard 1</title><rect width="24" height="24" rx="3" ry="3" fill="#0882c8"/><path d="M22.1,8a3.37,3.37,0,0,0-2.42-.77H16.05v2H11.71l-.36,1.46a6.32,6.32,0,0,1,1-.35,3.49,3.49,0,0,1,.86-.06,3.24,3.24,0,0,1,2.35.88,2.93,2.93,0,0,1,.9,2.2A3.72,3.72,0,0,1,16,15.19a3.16,3.16,0,0,1-1.31,1.32,3.41,3.41,0,0,1-.67.27H17.7V13.28h1.65A3.8,3.8,0,0,0,22,12.46a3,3,0,0,0,.88-2.28A2.9,2.9,0,0,0,22.1,8Zm-2.44,3a1.88,1.88,0,0,1-1.21.29H17.7V9.2h.87a1.56,1.56,0,0,1,1.13.31,1,1,0,0,1,.3.76A.94.94,0,0,1,19.66,11Z" fill="#fff"/><path d="M12.27,12.05a1.33,1.33,0,0,0-1.19.74l-2.6-.37,1.17-5.2H7.29v4.08H4V7.23H1.1v9.55H4V13.28H7.29v3.49h3.57a3.61,3.61,0,0,1-1.13-.53A3.2,3.2,0,0,1,9,15.43a4,4,0,0,1-.48-1.09L11.09,14a1.32,1.32,0,1,0,1.18-1.92Z" fill="#fff"/></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 859 B  | 
							
								
								
									
										156
									
								
								src/assets/img/mod/ims.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,156 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In  -->
 | 
			
		||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
 | 
			
		||||
	<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
 | 
			
		||||
]>
 | 
			
		||||
<svg version="1.1"
 | 
			
		||||
	 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
 | 
			
		||||
	 x="0px" y="0px" width="24px" height="24px" viewBox="0 -2 24 24" style="overflow:visible;enable-background:new 0 -2 24 24;"
 | 
			
		||||
	 xml:space="preserve" preserveAspectRatio="xMinYMid meet">
 | 
			
		||||
<defs>
 | 
			
		||||
</defs>
 | 
			
		||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="18" y1="10" x2="18" y2="20.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#F0A829"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#C7671A"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#F0A829"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#F0A829"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#C7671A"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="13" y="10" style="fill:url(#SVGID_1_);" width="10" height="10"/>
 | 
			
		||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="18" y1="11" x2="18" y2="19.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#FFEBA8"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#F8BE27"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#FFEBA8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#FFEBA8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#F8BE27"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="14" y="11" style="fill:url(#SVGID_2_);" width="8" height="8"/>
 | 
			
		||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="18" y1="12" x2="18" y2="18">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#F5AE0D"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#F5AE0D"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="15" y="12" style="fill:url(#SVGID_3_);" width="6" height="6"/>
 | 
			
		||||
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="18" y1="10" x2="18" y2="14.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#F0A829"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#C7671A"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#F0A829"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#F0A829"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#C7671A"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="12" y="10" style="fill:url(#SVGID_4_);" width="12" height="4"/>
 | 
			
		||||
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="18" y1="11" x2="18" y2="13.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#F5AE0D"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#F5AE0D"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="13" y="11" style="fill:url(#SVGID_5_);" width="10" height="2"/>
 | 
			
		||||
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="18" y1="10" x2="18" y2="16.4233">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#8D470D"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#7C3D09"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#8D470D"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#8D470D"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#7C3D09"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<polygon style="fill:url(#SVGID_6_);" points="17,10 17,11.7 17,15 17,16.4 18,15.5 19,16.4 19,15 19,11.7 19,10 "/>
 | 
			
		||||
<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="6" y1="10" x2="6" y2="20.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#F0A829"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#C7671A"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#F0A829"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#F0A829"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#C7671A"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="1" y="10" style="fill:url(#SVGID_7_);" width="10" height="10"/>
 | 
			
		||||
<linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="6" y1="11" x2="6" y2="19.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#FFEBA8"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#F8BE27"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#FFEBA8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#FFEBA8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#F8BE27"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="2" y="11" style="fill:url(#SVGID_8_);" width="8" height="8"/>
 | 
			
		||||
<linearGradient id="SVGID_9_" gradientUnits="userSpaceOnUse" x1="6" y1="12" x2="6" y2="18">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#F5AE0D"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#F5AE0D"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="3" y="12" style="fill:url(#SVGID_9_);" width="6" height="6"/>
 | 
			
		||||
<linearGradient id="SVGID_10_" gradientUnits="userSpaceOnUse" x1="6" y1="10" x2="6" y2="14.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#F0A829"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#C7671A"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#F0A829"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#F0A829"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#C7671A"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect y="10" style="fill:url(#SVGID_10_);" width="12" height="4"/>
 | 
			
		||||
<linearGradient id="SVGID_11_" gradientUnits="userSpaceOnUse" x1="6" y1="11" x2="6" y2="13.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#F5AE0D"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#F5AE0D"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="1" y="11" style="fill:url(#SVGID_11_);" width="10" height="2"/>
 | 
			
		||||
<linearGradient id="SVGID_12_" gradientUnits="userSpaceOnUse" x1="6" y1="10" x2="6" y2="16.4233">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#8D470D"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#7C3D09"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#8D470D"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#8D470D"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#7C3D09"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<polygon style="fill:url(#SVGID_12_);" points="5,10 5,11.7 5,15 5,16.4 6,15.5 7,16.4 7,15 7,11.7 7,10 "/>
 | 
			
		||||
<linearGradient id="SVGID_13_" gradientUnits="userSpaceOnUse" x1="11.9995" y1="0" x2="11.9995" y2="10">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#F0A829"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#C7671A"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#F0A829"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#F0A829"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#C7671A"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="7" style="fill:url(#SVGID_13_);" width="10" height="10"/>
 | 
			
		||||
<linearGradient id="SVGID_14_" gradientUnits="userSpaceOnUse" x1="11.9995" y1="1" x2="11.9995" y2="9">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#FFEBA8"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#F8BE27"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#FFEBA8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#FFEBA8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#F8BE27"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="8" y="1" style="fill:url(#SVGID_14_);" width="8" height="8"/>
 | 
			
		||||
<linearGradient id="SVGID_15_" gradientUnits="userSpaceOnUse" x1="11.9995" y1="2" x2="11.9995" y2="8">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#F5AE0D"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#F5AE0D"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="9" y="2" style="fill:url(#SVGID_15_);" width="6" height="6"/>
 | 
			
		||||
<linearGradient id="SVGID_16_" gradientUnits="userSpaceOnUse" x1="11.9995" y1="0" x2="11.9995" y2="4">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#F0A829"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#C7671A"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#F0A829"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#F0A829"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#C7671A"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="6" style="fill:url(#SVGID_16_);" width="12" height="4"/>
 | 
			
		||||
<linearGradient id="SVGID_17_" gradientUnits="userSpaceOnUse" x1="11.9995" y1="1" x2="11.9995" y2="3">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#F5AE0D"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#F5AE0D"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="7" y="1" style="fill:url(#SVGID_17_);" width="10" height="2"/>
 | 
			
		||||
<linearGradient id="SVGID_18_" gradientUnits="userSpaceOnUse" x1="11.9995" y1="0" x2="11.9995" y2="6.4229">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#8D470D"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#7C3D09"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#8D470D"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#8D470D"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#7C3D09"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<polygon style="fill:url(#SVGID_18_);" points="11,0 11,1.7 11,5 11,6.4 12,5.5 13,6.4 13,5 13,1.7 13,0 "/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 9.0 KiB  | 
							
								
								
									
										156
									
								
								src/assets/img/mod/imscp.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,156 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In  -->
 | 
			
		||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
 | 
			
		||||
	<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
 | 
			
		||||
]>
 | 
			
		||||
<svg version="1.1"
 | 
			
		||||
	 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
 | 
			
		||||
	 x="0px" y="0px" width="24px" height="24px" viewBox="0 -2 24 24" style="overflow:visible;enable-background:new 0 -2 24 24;"
 | 
			
		||||
	 xml:space="preserve" preserveAspectRatio="xMinYMid meet">
 | 
			
		||||
<defs>
 | 
			
		||||
</defs>
 | 
			
		||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="18" y1="10" x2="18" y2="20.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#F0A829"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#C7671A"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#F0A829"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#F0A829"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#C7671A"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="13" y="10" style="fill:url(#SVGID_1_);" width="10" height="10"/>
 | 
			
		||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="18" y1="11" x2="18" y2="19.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#FFEBA8"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#F8BE27"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#FFEBA8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#FFEBA8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#F8BE27"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="14" y="11" style="fill:url(#SVGID_2_);" width="8" height="8"/>
 | 
			
		||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="18" y1="12" x2="18" y2="18">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#F5AE0D"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#F5AE0D"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="15" y="12" style="fill:url(#SVGID_3_);" width="6" height="6"/>
 | 
			
		||||
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="18" y1="10" x2="18" y2="14.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#F0A829"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#C7671A"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#F0A829"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#F0A829"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#C7671A"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="12" y="10" style="fill:url(#SVGID_4_);" width="12" height="4"/>
 | 
			
		||||
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="18" y1="11" x2="18" y2="13.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#F5AE0D"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#F5AE0D"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="13" y="11" style="fill:url(#SVGID_5_);" width="10" height="2"/>
 | 
			
		||||
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="18" y1="10" x2="18" y2="16.4233">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#8D470D"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#7C3D09"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#8D470D"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#8D470D"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#7C3D09"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<polygon style="fill:url(#SVGID_6_);" points="17,10 17,11.7 17,15 17,16.4 18,15.5 19,16.4 19,15 19,11.7 19,10 "/>
 | 
			
		||||
<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="6" y1="10" x2="6" y2="20.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#F0A829"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#C7671A"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#F0A829"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#F0A829"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#C7671A"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="1" y="10" style="fill:url(#SVGID_7_);" width="10" height="10"/>
 | 
			
		||||
<linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="6" y1="11" x2="6" y2="19.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#FFEBA8"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#F8BE27"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#FFEBA8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#FFEBA8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#F8BE27"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="2" y="11" style="fill:url(#SVGID_8_);" width="8" height="8"/>
 | 
			
		||||
<linearGradient id="SVGID_9_" gradientUnits="userSpaceOnUse" x1="6" y1="12" x2="6" y2="18">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#F5AE0D"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#F5AE0D"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="3" y="12" style="fill:url(#SVGID_9_);" width="6" height="6"/>
 | 
			
		||||
<linearGradient id="SVGID_10_" gradientUnits="userSpaceOnUse" x1="6" y1="10" x2="6" y2="14.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#F0A829"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#C7671A"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#F0A829"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#F0A829"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#C7671A"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect y="10" style="fill:url(#SVGID_10_);" width="12" height="4"/>
 | 
			
		||||
<linearGradient id="SVGID_11_" gradientUnits="userSpaceOnUse" x1="6" y1="11" x2="6" y2="13.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#F5AE0D"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#F5AE0D"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="1" y="11" style="fill:url(#SVGID_11_);" width="10" height="2"/>
 | 
			
		||||
<linearGradient id="SVGID_12_" gradientUnits="userSpaceOnUse" x1="6" y1="10" x2="6" y2="16.4233">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#8D470D"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#7C3D09"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#8D470D"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#8D470D"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#7C3D09"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<polygon style="fill:url(#SVGID_12_);" points="5,10 5,11.7 5,15 5,16.4 6,15.5 7,16.4 7,15 7,11.7 7,10 "/>
 | 
			
		||||
<linearGradient id="SVGID_13_" gradientUnits="userSpaceOnUse" x1="11.9995" y1="0" x2="11.9995" y2="10">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#F0A829"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#C7671A"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#F0A829"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#F0A829"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#C7671A"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="7" style="fill:url(#SVGID_13_);" width="10" height="10"/>
 | 
			
		||||
<linearGradient id="SVGID_14_" gradientUnits="userSpaceOnUse" x1="11.9995" y1="1" x2="11.9995" y2="9">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#FFEBA8"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#F8BE27"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#FFEBA8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#FFEBA8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#F8BE27"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="8" y="1" style="fill:url(#SVGID_14_);" width="8" height="8"/>
 | 
			
		||||
<linearGradient id="SVGID_15_" gradientUnits="userSpaceOnUse" x1="11.9995" y1="2" x2="11.9995" y2="8">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#F5AE0D"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#F5AE0D"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="9" y="2" style="fill:url(#SVGID_15_);" width="6" height="6"/>
 | 
			
		||||
<linearGradient id="SVGID_16_" gradientUnits="userSpaceOnUse" x1="11.9995" y1="0" x2="11.9995" y2="4">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#F0A829"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#C7671A"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#F0A829"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#F0A829"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#C7671A"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="6" style="fill:url(#SVGID_16_);" width="12" height="4"/>
 | 
			
		||||
<linearGradient id="SVGID_17_" gradientUnits="userSpaceOnUse" x1="11.9995" y1="1" x2="11.9995" y2="3">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#F5AE0D"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#F5AE0D"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="7" y="1" style="fill:url(#SVGID_17_);" width="10" height="2"/>
 | 
			
		||||
<linearGradient id="SVGID_18_" gradientUnits="userSpaceOnUse" x1="11.9995" y1="0" x2="11.9995" y2="6.4229">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#8D470D"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#7C3D09"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#8D470D"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#8D470D"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#7C3D09"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<polygon style="fill:url(#SVGID_18_);" points="11,0 11,1.7 11,5 11,6.4 12,5.5 13,6.4 13,5 13,1.7 13,0 "/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 9.0 KiB  | 
							
								
								
									
										94
									
								
								src/assets/img/mod/label.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,94 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In  -->
 | 
			
		||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
 | 
			
		||||
	<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
 | 
			
		||||
]>
 | 
			
		||||
<svg version="1.1"
 | 
			
		||||
	 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
 | 
			
		||||
	 x="0px" y="0px" width="24px" height="24px" viewBox="0 0 24 24" style="overflow:visible;enable-background:new 0 0 24 24;"
 | 
			
		||||
	 xml:space="preserve" preserveAspectRatio="xMinYMid meet">
 | 
			
		||||
<defs>
 | 
			
		||||
</defs>
 | 
			
		||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="11.9854" y1="0" x2="11.9854" y2="24.0161">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#F0A829"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#C7671A"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#F0A829"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#F0A829"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#C7671A"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_1_);" d="M14.4,0L0.6,13.9c-0.8,0.8-0.8,2.1,0,2.8l6.7,6.7c0.8,0.8,2,0.8,2.8,0L24,9.6V0H14.4z"/>
 | 
			
		||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="11.9854" y1="1" x2="11.9854" y2="23.0161">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#FFEBA8"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#F8BE27"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#FFEBA8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#FFEBA8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#F8BE27"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_2_);" d="M8.7,23c-0.3,0-0.5-0.1-0.7-0.3L1.3,16C1.1,15.8,1,15.6,1,15.3c0-0.3,0.1-0.5,0.3-0.7L14.9,1
 | 
			
		||||
	H23v8.2L9.4,22.7C9.2,22.9,9,23,8.7,23C8.7,23,8.7,23,8.7,23z"/>
 | 
			
		||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="11.9844" y1="2" x2="11.9844" y2="22.0142">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#F5AE0D"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#F5AE0D"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<polygon style="fill:url(#SVGID_3_);" points="2,15.3 15.3,2 22,2 22,8.8 8.7,22 "/>
 | 
			
		||||
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="8.7197" y1="9.9688" x2="8.7197" y2="20.6313">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<polygon style="fill:url(#SVGID_4_);" points="3.4,15.3 8.7,10 14.1,15.3 8.7,20.6 "/>
 | 
			
		||||
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="11.1465" y1="22.0312" x2="11.1465" y2="23.8711" gradientTransform="matrix(0.7071 -0.7071 0.7071 0.7071 -14.0909 8.2514)">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#57C3F6"/>
 | 
			
		||||
	<stop  offset="0.1648" style="stop-color:#83D3F8"/>
 | 
			
		||||
	<stop  offset="0.3554" style="stop-color:#AFE3FB"/>
 | 
			
		||||
	<stop  offset="0.5396" style="stop-color:#D2EFFD"/>
 | 
			
		||||
	<stop  offset="0.7128" style="stop-color:#EBF8FE"/>
 | 
			
		||||
	<stop  offset="0.8709" style="stop-color:#FAFDFF"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#FFFFFF"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#57C3F6"/>
 | 
			
		||||
	<a:midPointStop  offset="0.3354" style="stop-color:#57C3F6"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#FFFFFF"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<polygon style="fill:url(#SVGID_5_);" points="7.4,17.9 8.7,19.2 12.6,15.3 11.3,14 "/>
 | 
			
		||||
<path style="fill:#F2EFD5;" d="M12.6,15.3L12.3,15c-0.3,0.2-0.8,0.5-1.4,1.1c-1.1,0.9-1,0.5-2,0.9c-0.4,0.2-0.9,0.6-1.4,1l1.2,1.2
 | 
			
		||||
	L12.6,15.3z"/>
 | 
			
		||||
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="8.3882" y1="23.1035" x2="13.9038" y2="23.1035" gradientTransform="matrix(0.7071 -0.7071 0.7071 0.7071 -14.0909 8.2514)">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#F8E5B5"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#F9E5B6"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#F8E5B5"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4451" style="stop-color:#F8E5B5"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#F9E5B6"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_6_);" d="M12.6,15.3l-0.2-0.2c-0.4,0.2-0.6,0.6-1.3,1.2C10,17.2,10,16.6,9,17c-0.4,0.2-0.9,0.6-1.3,1.1
 | 
			
		||||
	l1.1,1.1L12.6,15.3z"/>
 | 
			
		||||
<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="12.1709" y1="17.6572" x2="10.1204" y2="22.7323" gradientTransform="matrix(0.7071 -0.7071 0.7071 0.7071 -14.0909 8.2514)">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#57C3F6"/>
 | 
			
		||||
	<stop  offset="4.801393e-03" style="stop-color:#59C4F6"/>
 | 
			
		||||
	<stop  offset="0.1237" style="stop-color:#85D3F8"/>
 | 
			
		||||
	<stop  offset="0.2474" style="stop-color:#AAE1FA"/>
 | 
			
		||||
	<stop  offset="0.376" style="stop-color:#C9ECFC"/>
 | 
			
		||||
	<stop  offset="0.51" style="stop-color:#E1F4FD"/>
 | 
			
		||||
	<stop  offset="0.6519" style="stop-color:#F2FAFE"/>
 | 
			
		||||
	<stop  offset="0.807" style="stop-color:#FCFEFF"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#FFFFFF"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#57C3F6"/>
 | 
			
		||||
	<a:midPointStop  offset="0.25" style="stop-color:#57C3F6"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#FFFFFF"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<polygon style="fill:url(#SVGID_7_);" points="4.8,15.3 7.4,17.9 11.3,14 8.7,11.4 "/>
 | 
			
		||||
<path style="fill:#FFFFFF;" d="M9,11.6C8.7,11.8,8.7,12,8.7,12s-0.4-0.1-0.6,0.4c-0.1,0.4,0.3,0.5,0.3,0.5s0,0-0.1,0.1
 | 
			
		||||
	c0,0.1,0.1,0.1,0.1,0.1s-0.2,0.1-0.1,0.4c0.1,0.3,0.3,0.3,0.3,0.3s-0.1,0.5,0.4,0.6c0.4,0.1,0.5-0.3,0.5-0.3s0.3,0,0.6-0.2
 | 
			
		||||
	c0.3-0.2,0.4-0.5,0.4-0.5s0.1,0,0.2,0L9,11.6z"/>
 | 
			
		||||
<path style="fill:#FFFFFF;" d="M7.9,14.5c0,0-0.1,0-0.2,0.1c0,0.1,0.1,0.2,0.1,0.2s0,0,0,0c0,0,0,0,0,0s-0.1,0,0,0.1
 | 
			
		||||
	C7.8,15,7.9,15,7.9,15s0,0.1,0.1,0.2c0.1,0,0.1-0.1,0.1-0.1s0.1,0,0.2,0c0.1-0.1,0.1-0.1,0.1-0.1s0.1,0,0.1-0.1c0-0.1,0-0.2,0-0.2
 | 
			
		||||
	s0.2,0,0.1-0.2c-0.1-0.1-0.2,0-0.2,0s-0.1-0.1-0.1-0.1c-0.1,0-0.1,0.1-0.1,0.1s0,0,0,0c0,0-0.1,0-0.1,0S8,14.3,7.9,14.3
 | 
			
		||||
	C7.8,14.4,7.9,14.5,7.9,14.5z"/>
 | 
			
		||||
<path style="fill:#FFFFFF;" d="M19.6,8.4l-5.5,5.5l-0.7-0.7l5.5-5.5L19.6,8.4z M17.5,6.3L12,11.8l0.7,0.7L18.2,7L17.5,6.3z
 | 
			
		||||
	 M16.1,4.9l-5.5,5.5l0.7,0.7l5.5-5.5L16.1,4.9z"/>
 | 
			
		||||
<ellipse style="fill:#FFFFFF;" cx="19.7" cy="4.3" rx="1.3" ry="1.3"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 6.1 KiB  | 
							
								
								
									
										126
									
								
								src/assets/img/mod/lesson.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,126 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In  -->
 | 
			
		||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
 | 
			
		||||
	<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
 | 
			
		||||
]>
 | 
			
		||||
<svg version="1.1"
 | 
			
		||||
	 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
 | 
			
		||||
	 x="0px" y="0px" width="24px" height="24px" viewBox="0 0 24 24" style="overflow:visible;enable-background:new 0 0 24 24;"
 | 
			
		||||
	 xml:space="preserve" preserveAspectRatio="xMinYMid meet">
 | 
			
		||||
<defs>
 | 
			
		||||
</defs>
 | 
			
		||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="5.5" y1="10" x2="5.5" y2="14.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<stop  offset="0.5569" style="stop-color:#84ADEF"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="5" y="10" style="fill:url(#SVGID_1_);" width="1" height="4"/>
 | 
			
		||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="15" y1="5" x2="15" y2="9">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<stop  offset="0.5569" style="stop-color:#84ADEF"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<polygon style="fill:url(#SVGID_2_);" points="18,5 11,5 11,6 18,6 18,9 19,9 19,6 19,5 "/>
 | 
			
		||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="5.5" y1="14" x2="5.5" y2="24">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_3_);" d="M10,14H1c-0.6,0-1,0.4-1,1v1v7v1h1h9h1v-1v-7v-1C11,14.4,10.6,14,10,14z"/>
 | 
			
		||||
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="5.5" y1="17" x2="5.5" y2="23">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#DEEFFC"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#DEEFFC"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="1" y="17" style="fill:url(#SVGID_4_);" width="9" height="6"/>
 | 
			
		||||
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="5.5" y1="18" x2="5.5" y2="22">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#BBDFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#BBDFF8"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="2" y="18" style="fill:url(#SVGID_5_);" width="7" height="4"/>
 | 
			
		||||
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="5.5" y1="15" x2="5.5" y2="16">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#82B4FB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#82B4FB"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="1" y="15" style="fill:url(#SVGID_6_);" width="9" height="1"/>
 | 
			
		||||
<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="18.5" y1="9" x2="18.5" y2="19.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_7_);" d="M23,9h-9c-0.6,0-1,0.4-1,1v1v7v1h1h9h1v-1v-7v-1C24,9.4,23.6,9,23,9z"/>
 | 
			
		||||
<linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="18.5" y1="12" x2="18.5" y2="18.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#DEEFFC"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#DEEFFC"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="14" y="12" style="fill:url(#SVGID_8_);" width="9" height="6"/>
 | 
			
		||||
<linearGradient id="SVGID_9_" gradientUnits="userSpaceOnUse" x1="18.5" y1="13" x2="18.5" y2="17.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#BBDFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#BBDFF8"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="15" y="13" style="fill:url(#SVGID_9_);" width="7" height="4"/>
 | 
			
		||||
<linearGradient id="SVGID_10_" gradientUnits="userSpaceOnUse" x1="18.5" y1="10" x2="18.5" y2="11">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#82B4FB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#82B4FB"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="14" y="10" style="fill:url(#SVGID_10_);" width="9" height="1"/>
 | 
			
		||||
<linearGradient id="SVGID_11_" gradientUnits="userSpaceOnUse" x1="5.5" y1="0" x2="5.5" y2="10">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_11_);" d="M10,0H1C0.4,0,0,0.4,0,1v1v7v1h1h9h1V9V2V1C11,0.4,10.6,0,10,0z"/>
 | 
			
		||||
<linearGradient id="SVGID_12_" gradientUnits="userSpaceOnUse" x1="5.5" y1="3" x2="5.5" y2="9">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#DEEFFC"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#DEEFFC"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="1" y="3" style="fill:url(#SVGID_12_);" width="9" height="6"/>
 | 
			
		||||
<linearGradient id="SVGID_13_" gradientUnits="userSpaceOnUse" x1="5.5" y1="4" x2="5.5" y2="8">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#BBDFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#BBDFF8"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="2" y="4" style="fill:url(#SVGID_13_);" width="7" height="4"/>
 | 
			
		||||
<linearGradient id="SVGID_14_" gradientUnits="userSpaceOnUse" x1="5.5" y1="1" x2="5.5" y2="2">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#82B4FB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#82B4FB"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="1" y="1" style="fill:url(#SVGID_14_);" width="9" height="1"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 7.2 KiB  | 
							
								
								
									
										55
									
								
								src/assets/img/mod/lti.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,55 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In  -->
 | 
			
		||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
 | 
			
		||||
	<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
 | 
			
		||||
]>
 | 
			
		||||
<svg version="1.1"
 | 
			
		||||
	 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
 | 
			
		||||
	 x="0px" y="0px" width="24px" height="24px" viewBox="0 0 24 24" style="overflow:visible;enable-background:new 0 0 24 24;"
 | 
			
		||||
	 xml:space="preserve" preserveAspectRatio="xMinYMid meet">
 | 
			
		||||
<defs>
 | 
			
		||||
</defs>
 | 
			
		||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="11.9995" y1="0" x2="11.9995" y2="24.001">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#90C50E"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#70A034"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#90C50E"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#90C50E"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#70A034"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_1_);" d="M21.1,12.3c1,0,1.7,0.9,1.7,0.9c0.3,0.4,0.7,0.8,0.9,0.8s0.3-0.5,0.3-1V8c0-0.5-0.5-1-1-1h-3
 | 
			
		||||
	c-0.5,0-1-0.1-1-0.3c0-0.2,0.3-0.7,0.7-1.1c0,0,0.8-0.9,0.8-2.1C20.5,1.6,18.9,0,17,0s-3.5,1.6-3.5,3.5c0,1.2,0.8,2.1,0.8,2.1
 | 
			
		||||
	C14.7,6,15,6.5,15,6.7C15,6.9,14.5,7,14,7H8C7.5,7,7,7.5,7,8v6c0,0.5-0.1,1-0.3,1S6,14.7,5.6,14.3c0,0-0.9-0.8-2.1-0.8
 | 
			
		||||
	C1.6,13.5,0,15.1,0,17s1.6,3.5,3.5,3.5c1.2,0,2.1-0.8,2.1-0.8C6,19.3,6.5,19,6.7,19S7,19.5,7,20v3c0,0.5,0.5,1,1,1h4
 | 
			
		||||
	c0.5,0,1-0.1,1-0.3s-0.3-0.6-0.7-1c0,0-0.6-0.6-0.6-1.6c0-1.4,1.3-2.5,2.7-2.5c1.4,0,2.4,1.1,2.4,2.5c0,1-0.9,1.7-0.9,1.7
 | 
			
		||||
	c-0.4,0.3-0.8,0.7-0.8,0.9s0.5,0.3,1,0.3h7c0.5,0,1-0.5,1-1v-6c0-0.5-0.1-1-0.3-1s-0.6,0.3-1,0.7c0,0-0.6,0.6-1.6,0.6
 | 
			
		||||
	c-1.4,0-2.5-1.3-2.5-2.7S19.7,12.3,21.1,12.3z"/>
 | 
			
		||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="11.9995" y1="1" x2="11.9995" y2="23.001">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#D9F991"/>
 | 
			
		||||
	<stop  offset="0.2388" style="stop-color:#D7F88D"/>
 | 
			
		||||
	<stop  offset="0.4501" style="stop-color:#D1F383"/>
 | 
			
		||||
	<stop  offset="0.6509" style="stop-color:#C6EC71"/>
 | 
			
		||||
	<stop  offset="0.844" style="stop-color:#B7E257"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#A8D73D"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#D9F991"/>
 | 
			
		||||
	<a:midPointStop  offset="0.7317" style="stop-color:#D9F991"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#A8D73D"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_2_);" d="M17,23c0.4-0.4,0.7-1.1,0.7-1.9c0-2-1.5-3.5-3.4-3.5c-2,0-3.7,1.6-3.7,3.5
 | 
			
		||||
	c0,0.9,0.3,1.5,0.6,1.9H8v-3c0-1.5-0.7-2-1.3-2c-0.6,0-1.3,0.6-1.7,0.9c0,0-0.7,0.6-1.5,0.6C2.1,19.5,1,18.4,1,17s1.1-2.5,2.5-2.5
 | 
			
		||||
	c0.8,0,1.4,0.6,1.5,0.6C5.3,15.4,6.1,16,6.7,16C7.3,16,8,15.5,8,14V8h6c1.5,0,2-0.7,2-1.3c0-0.6-0.5-1.3-0.9-1.7
 | 
			
		||||
	c0,0-0.6-0.7-0.6-1.5C14.5,2.1,15.6,1,17,1s2.5,1.1,2.5,2.5c0,0.8-0.6,1.5-0.6,1.5C18.5,5.4,18,6.1,18,6.7C18,7.3,18.5,8,20,8h3v4
 | 
			
		||||
	c-0.4-0.4-1.1-0.7-1.9-0.7c-2,0-3.5,1.5-3.5,3.4c0,2,1.6,3.7,3.5,3.7c0.9,0,1.5-0.3,1.9-0.6V23H17z"/>
 | 
			
		||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="11.9995" y1="2" x2="11.9995" y2="22.001">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#B3E810"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#90C60D"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#B3E810"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#B3E810"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#90C60D"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_3_);" d="M18.6,22c0.1-0.3,0.1-0.6,0.1-0.9c0-2.5-1.9-4.5-4.4-4.5c-2.5,0-4.7,2.1-4.7,4.5
 | 
			
		||||
	c0,0.3,0,0.6,0.1,0.9H9v-2c0-2.1-1.2-3-2.3-3C7.8,17,9,16.1,9,14V9h5c2.1,0,3-1.2,3-2.3c0-0.7-0.4-1.5-1.2-2.4
 | 
			
		||||
	c-0.1-0.1-0.3-0.5-0.3-0.8C15.5,2.7,16.2,2,17,2s1.5,0.7,1.5,1.5c0,0.3-0.3,0.7-0.3,0.8C17.4,5.2,17,6,17,6.7C17,7.8,17.9,9,20,9h2
 | 
			
		||||
	v1.4c-0.3-0.1-0.6-0.1-0.9-0.1c-2.5,0-4.5,1.9-4.5,4.4c0,2.5,2.1,4.7,4.5,4.7c0.3,0,0.6,0,0.9-0.1V22H18.6z M3.5,18.5
 | 
			
		||||
	C2.7,18.5,2,17.8,2,17s0.7-1.5,1.5-1.5c0.3,0,0.7,0.3,0.8,0.3C5.2,16.6,6,17,6.7,17c-0.7,0-1.5,0.4-2.4,1.2
 | 
			
		||||
	C4.2,18.3,3.8,18.5,3.5,18.5z"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 3.9 KiB  | 
							
								
								
									
										112
									
								
								src/assets/img/mod/page.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,112 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In  -->
 | 
			
		||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
 | 
			
		||||
	<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
 | 
			
		||||
]>
 | 
			
		||||
<svg version="1.1"
 | 
			
		||||
	 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
 | 
			
		||||
	 x="0px" y="0px" width="24px" height="24px" viewBox="-2 0 24 24" style="overflow:visible;enable-background:new -2 0 24 24;"
 | 
			
		||||
	 xml:space="preserve" preserveAspectRatio="xMinYMid meet">
 | 
			
		||||
<defs>
 | 
			
		||||
</defs>
 | 
			
		||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="9.9995" y1="0" x2="9.9995" y2="24.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<stop  offset="0.5569" style="stop-color:#84ADEF"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<polygon style="fill:url(#SVGID_1_);" points="12.5,0 11.5,0 1,0 0,0 0,24 1,24 19,24 20,24 20,7.9 "/>
 | 
			
		||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="9.9995" y1="1" x2="9.9995" y2="23.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#DEEFFC"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#DEEFFC"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<polygon style="fill:url(#SVGID_2_);" points="12.1,1 11.1,1 2,1 1,1 1,23 2,23 18,23 19,23 19,8.3 "/>
 | 
			
		||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="9.9995" y1="2" x2="9.9995" y2="22.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#BBDFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#BBDFF8"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<polygon style="fill:url(#SVGID_3_);" points="11.6,2 10.6,2 3,2 2,2 2,22 3,22 17,22 18,22 18,8.7 "/>
 | 
			
		||||
<path style="fill:#FFFFFF;" d="M16,21H4v-1h12V21z M16,18H4v1h12V18z M16,16H4v1h12V16z M16,14H4v1h12V14z"/>
 | 
			
		||||
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="9.9995" y1="4" x2="9.9995" y2="12">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<polygon style="fill:url(#SVGID_4_);" points="15.3,4 14.3,4 4,4 3,4 3,12 4,12 16,12 17,12 17,5.8 "/>
 | 
			
		||||
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="9.9995" y1="9" x2="9.9995" y2="11">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#57C3F6"/>
 | 
			
		||||
	<stop  offset="0.1648" style="stop-color:#83D3F8"/>
 | 
			
		||||
	<stop  offset="0.3554" style="stop-color:#AFE3FB"/>
 | 
			
		||||
	<stop  offset="0.5396" style="stop-color:#D2EFFD"/>
 | 
			
		||||
	<stop  offset="0.7128" style="stop-color:#EBF8FE"/>
 | 
			
		||||
	<stop  offset="0.8709" style="stop-color:#FAFDFF"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#FFFFFF"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#57C3F6"/>
 | 
			
		||||
	<a:midPointStop  offset="0.3354" style="stop-color:#57C3F6"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#FFFFFF"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="4" y="9" style="fill:url(#SVGID_5_);" width="12" height="2"/>
 | 
			
		||||
<path style="fill:#F2EFD5;" d="M12.4,10.9c-0.5-0.2-1.1-0.3-2.7-0.4c-1.5-0.1-0.9-0.4-2-0.8C6.7,9.2,4,9.4,4,9.4V11h8.6
 | 
			
		||||
	C12.6,11,12.5,10.9,12.4,10.9z"/>
 | 
			
		||||
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="4" y1="10.2476" x2="12.2959" y2="10.2476">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#F8E5B5"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#F9E5B6"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#F8E5B5"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4451" style="stop-color:#F8E5B5"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#F9E5B6"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_6_);" d="M9.7,10.7c-1.5-0.1-0.9-0.6-2-1c-1-0.4-3.7,0-3.7,0V11h8.3C11.5,11,11.2,10.8,9.7,10.7z"/>
 | 
			
		||||
<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="12.5342" y1="1.5674" x2="8.163" y2="10.9413">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#57C3F6"/>
 | 
			
		||||
	<stop  offset="0.2292" style="stop-color:#8AD5F9"/>
 | 
			
		||||
	<stop  offset="0.4827" style="stop-color:#BCE7FB"/>
 | 
			
		||||
	<stop  offset="0.705" style="stop-color:#E1F4FD"/>
 | 
			
		||||
	<stop  offset="0.885" style="stop-color:#F7FCFF"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#FFFFFF"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#57C3F6"/>
 | 
			
		||||
	<a:midPointStop  offset="0.3902" style="stop-color:#57C3F6"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#FFFFFF"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="4" y="5" style="fill:url(#SVGID_7_);" width="12" height="4"/>
 | 
			
		||||
<path style="fill:#FFFFFF;" d="M11.2,5.6c0,0-0.2-0.4-0.7-0.2c-0.4,0.3-0.1,0.7-0.1,0.7s-0.1,0-0.1,0c-0.1,0,0,0.2,0,0.2
 | 
			
		||||
	s-0.3,0-0.4,0.2C9.6,6.9,9.8,7,9.8,7S9.3,7.4,9.6,7.8c0.3,0.4,0.6,0.1,0.6,0.1s0.2,0.3,0.6,0.3c0.4,0.1,0.6-0.1,0.6-0.1
 | 
			
		||||
	s0.3,0.2,0.5,0.1C12.4,8.2,12.5,8,12.5,8s0.6,0.3,0.8-0.3C13.4,7.3,13,7.1,13,7.1s0.1-0.4-0.2-0.6s-0.6,0-0.6,0s0.1,0,0-0.1
 | 
			
		||||
	c0-0.1-0.3-0.1-0.3-0.1s0.1-0.6-0.2-0.8C11.4,5.3,11.2,5.6,11.2,5.6z"/>
 | 
			
		||||
<path style="fill:#FFFFFF;" d="M8.6,6.9c0,0-0.1-0.1-0.2,0C8.3,6.9,8.4,7,8.4,7s0,0,0,0c0,0,0,0,0,0s-0.1,0-0.1,0.1
 | 
			
		||||
	c0,0.1,0,0.1,0,0.1s-0.1,0.1,0,0.2c0.1,0.1,0.2,0,0.2,0s0.1,0.1,0.2,0.1c0.1,0,0.2,0,0.2,0s0.1,0.1,0.2,0C9,7.6,9,7.6,9,7.6
 | 
			
		||||
	s0.2,0.1,0.2-0.1c0-0.1-0.1-0.2-0.1-0.2s0-0.1-0.1-0.2c-0.1,0-0.2,0-0.2,0s0,0,0,0s-0.1,0-0.1,0s0-0.2-0.1-0.2
 | 
			
		||||
	C8.7,6.8,8.6,6.9,8.6,6.9z"/>
 | 
			
		||||
<linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="15.2451" y1="0" x2="15.2451" y2="9.3594">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<stop  offset="0.5569" style="stop-color:#84ADEF"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_8_);" d="M11,9c0,0,5.2-1.5,9,0.4c0-0.1,0-1.5,0-1.5L12.5,0c0,0-1.8,0-2,0C13.1,3.7,11,9,11,9z"/>
 | 
			
		||||
<linearGradient id="SVGID_9_" gradientUnits="userSpaceOnUse" x1="12.3223" y1="7.5449" x2="15.4504" y2="4.4168">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<stop  offset="0.5181" style="stop-color:#E5F3FC"/>
 | 
			
		||||
	<stop  offset="0.7045" style="stop-color:#DEF0FB"/>
 | 
			
		||||
	<stop  offset="0.8371" style="stop-color:#D3EBFA"/>
 | 
			
		||||
	<stop  offset="0.872" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#BDD8F0"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<a:midPointStop  offset="0.87" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<a:midPointStop  offset="0.872" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#BDD8F0"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_9_);" d="M18.5,7.8c-0.9-0.2-2-0.3-3.1-0.3c-1.1,0-2.1,0.1-3,0.3c0.4-1.6,0.7-4.1-0.2-6.4L18.5,7.8z"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 7.0 KiB  | 
							
								
								
									
										90
									
								
								src/assets/img/mod/quiz.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,90 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In  -->
 | 
			
		||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
 | 
			
		||||
	<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
 | 
			
		||||
]>
 | 
			
		||||
<svg version="1.1"
 | 
			
		||||
	 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
 | 
			
		||||
	 x="0px" y="0px" width="24px" height="24px" viewBox="0 0 24 24" style="overflow:visible;enable-background:new 0 0 24 24;"
 | 
			
		||||
	 xml:space="preserve" preserveAspectRatio="xMinYMid meet">
 | 
			
		||||
<defs>
 | 
			
		||||
</defs>
 | 
			
		||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="11.9565" y1="0" x2="11.9565" y2="24.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<stop  offset="0.5569" style="stop-color:#84ADEF"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<polygon style="fill:url(#SVGID_1_);" points="14.4,0 13.4,0 3,0 2,0 2,24 3,24 21,24 22,24 22,7.9 "/>
 | 
			
		||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="11.9565" y1="1" x2="11.9565" y2="23.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#DEEFFC"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#DEEFFC"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<polygon style="fill:url(#SVGID_2_);" points="14,1 13,1 4,1 3,1 3,23 4,23 20,23 21,23 21,8.3 "/>
 | 
			
		||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="11.9565" y1="2" x2="11.9565" y2="22.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#BBDFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#BBDFF8"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<polygon style="fill:url(#SVGID_3_);" points="13.6,2 12.6,2 5,2 4,2 4,22 5,22 19,22 20,22 20,8.7 "/>
 | 
			
		||||
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="17.2021" y1="0" x2="17.2021" y2="9.3594">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<stop  offset="0.5569" style="stop-color:#84ADEF"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_4_);" d="M13,9c0,0,5.2-1.5,9,0.4c0-0.1,0-1.5,0-1.5L14.4,0c0,0-1.8,0-2,0C15.1,3.7,13,9,13,9z"/>
 | 
			
		||||
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="14.2793" y1="7.5449" x2="17.4074" y2="4.4168">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<stop  offset="0.5181" style="stop-color:#E5F3FC"/>
 | 
			
		||||
	<stop  offset="0.7045" style="stop-color:#DEF0FB"/>
 | 
			
		||||
	<stop  offset="0.8371" style="stop-color:#D3EBFA"/>
 | 
			
		||||
	<stop  offset="0.872" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#BDD8F0"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<a:midPointStop  offset="0.87" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<a:midPointStop  offset="0.872" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#BDD8F0"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_5_);" d="M20.4,7.8c-0.9-0.2-2-0.3-3.1-0.3c-1.1,0-2.1,0.1-3,0.3c0.4-1.6,0.7-4.1-0.2-6.4L20.4,7.8z"/>
 | 
			
		||||
<g>
 | 
			
		||||
	<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="11.8569" y1="1.8521" x2="11.8569" y2="23.9487">
 | 
			
		||||
		<stop  offset="0" style="stop-color:#DB6D17"/>
 | 
			
		||||
		<stop  offset="1" style="stop-color:#BF3B08"/>
 | 
			
		||||
		<a:midPointStop  offset="0" style="stop-color:#DB6D17"/>
 | 
			
		||||
		<a:midPointStop  offset="0.5" style="stop-color:#DB6D17"/>
 | 
			
		||||
		<a:midPointStop  offset="1" style="stop-color:#BF3B08"/>
 | 
			
		||||
	</linearGradient>
 | 
			
		||||
	<polygon style="fill:url(#SVGID_6_);" points="8,16.9 3.2,11.9 0,15.1 8.8,23.9 23.7,1.9 	"/>
 | 
			
		||||
</g>
 | 
			
		||||
<g>
 | 
			
		||||
	<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="9.5522" y1="9.0078" x2="9.5522" y2="22.3833">
 | 
			
		||||
		<stop  offset="0" style="stop-color:#F6A55E"/>
 | 
			
		||||
		<stop  offset="1" style="stop-color:#EA5B03"/>
 | 
			
		||||
		<a:midPointStop  offset="0" style="stop-color:#F6A55E"/>
 | 
			
		||||
		<a:midPointStop  offset="0.5" style="stop-color:#F6A55E"/>
 | 
			
		||||
		<a:midPointStop  offset="1" style="stop-color:#EA5B03"/>
 | 
			
		||||
	</linearGradient>
 | 
			
		||||
	<polygon style="fill:url(#SVGID_7_);" points="1.4,15.1 3.2,13.4 7.9,18.4 17.7,9 8.7,22.4 	"/>
 | 
			
		||||
</g>
 | 
			
		||||
<g>
 | 
			
		||||
	<linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="7.2485" y1="14.7998" x2="7.2485" y2="20.8174">
 | 
			
		||||
		<stop  offset="0" style="stop-color:#F17219"/>
 | 
			
		||||
		<stop  offset="1" style="stop-color:#EA5B03"/>
 | 
			
		||||
		<a:midPointStop  offset="0" style="stop-color:#F17219"/>
 | 
			
		||||
		<a:midPointStop  offset="0.5" style="stop-color:#F17219"/>
 | 
			
		||||
		<a:midPointStop  offset="1" style="stop-color:#EA5B03"/>
 | 
			
		||||
	</linearGradient>
 | 
			
		||||
	<polygon style="fill:url(#SVGID_8_);" points="2.8,15.1 3.1,14.8 7.9,19.8 11.7,16.2 8.5,20.8 	"/>
 | 
			
		||||
</g>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 5.1 KiB  | 
							
								
								
									
										60
									
								
								src/assets/img/mod/resource.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,60 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In  -->
 | 
			
		||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
 | 
			
		||||
	<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
 | 
			
		||||
]>
 | 
			
		||||
<svg version="1.1"
 | 
			
		||||
	 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
 | 
			
		||||
	 x="0px" y="0px" width="24px" height="24px" viewBox="-3 0 24 24" style="overflow:visible;enable-background:new -3 0 24 24;"
 | 
			
		||||
	 xml:space="preserve" preserveAspectRatio="xMinYMid meet">
 | 
			
		||||
<defs>
 | 
			
		||||
</defs>
 | 
			
		||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="9.4995" y1="0" x2="9.4995" y2="24.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<stop  offset="0.5569" style="stop-color:#84ADEF"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<polygon style="fill:url(#SVGID_1_);" points="11.5,0 0,0 0,24 19,24 19,7.9 "/>
 | 
			
		||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="9.4995" y1="1" x2="9.4995" y2="23.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#DEEFFC"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#DEEFFC"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<polygon style="fill:url(#SVGID_2_);" points="1,23 1,1 11.1,1 18,8.3 18,23 "/>
 | 
			
		||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="9.4995" y1="2" x2="9.4995" y2="22.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#BBDFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#BBDFF8"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<polygon style="fill:url(#SVGID_3_);" points="2,22 2,2 10.6,2 17,8.7 17,22 "/>
 | 
			
		||||
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="14.2451" y1="0" x2="14.2451" y2="9.3594">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<stop  offset="0.5569" style="stop-color:#84ADEF"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_4_);" d="M10,9c0,0,5.2-1.5,9,0.4c0-0.1,0-1.5,0-1.5L11.5,0c0,0-1.8,0-2,0C12.1,3.7,10,9,10,9z"/>
 | 
			
		||||
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="11.3223" y1="7.5449" x2="14.4504" y2="4.4168">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<stop  offset="0.5181" style="stop-color:#E5F3FC"/>
 | 
			
		||||
	<stop  offset="0.7045" style="stop-color:#DEF0FB"/>
 | 
			
		||||
	<stop  offset="0.8371" style="stop-color:#D3EBFA"/>
 | 
			
		||||
	<stop  offset="0.872" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#BDD8F0"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<a:midPointStop  offset="0.87" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<a:midPointStop  offset="0.872" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#BDD8F0"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_5_);" d="M17.5,7.8c-0.9-0.2-2-0.3-3.1-0.3c-1.1,0-2.1,0.1-3,0.3c0.4-1.6,0.7-4.1-0.2-6.4L17.5,7.8z"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 3.5 KiB  | 
							
								
								
									
										84
									
								
								src/assets/img/mod/scorm.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,84 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In  -->
 | 
			
		||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
 | 
			
		||||
	<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
 | 
			
		||||
]>
 | 
			
		||||
<svg version="1.1"
 | 
			
		||||
	 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
 | 
			
		||||
	 x="0px" y="0px" width="24px" height="24px" viewBox="0 -2 24 24" style="overflow:visible;enable-background:new 0 -2 24 24;"
 | 
			
		||||
	 xml:space="preserve" preserveAspectRatio="xMinYMid meet">
 | 
			
		||||
<defs>
 | 
			
		||||
</defs>
 | 
			
		||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="11.9995" y1="0" x2="11.9995" y2="20.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#F0A829"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#C7671A"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#F0A829"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#F0A829"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#C7671A"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="1" style="fill:url(#SVGID_1_);" width="22" height="20"/>
 | 
			
		||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="11.9995" y1="1" x2="11.9995" y2="19.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#FFEBA8"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#F8BE27"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#FFEBA8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#FFEBA8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#F8BE27"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="2" y="1" style="fill:url(#SVGID_2_);" width="20" height="18"/>
 | 
			
		||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="11.9995" y1="2" x2="11.9995" y2="18.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#F5AE0D"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#F5AE0D"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="3" y="2" style="fill:url(#SVGID_3_);" width="18" height="16"/>
 | 
			
		||||
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="11.9995" y1="0" x2="11.9995" y2="7">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#F0A829"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#C7671A"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#F0A829"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#F0A829"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#C7671A"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect style="fill:url(#SVGID_4_);" width="24" height="7"/>
 | 
			
		||||
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="11.9995" y1="1" x2="11.9995" y2="6">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#FFEBA8"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#F8BE27"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#FFEBA8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#FFEBA8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#F8BE27"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="1" y="1" style="fill:url(#SVGID_5_);" width="22" height="5"/>
 | 
			
		||||
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="11.9995" y1="2" x2="11.9995" y2="5">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#F5AE0D"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#F5AE0D"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="2" y="2" style="fill:url(#SVGID_6_);" width="20" height="3"/>
 | 
			
		||||
<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="11.4995" y1="0" x2="11.4995" y2="13.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#8D470D"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#7C3D09"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#8D470D"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#8D470D"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#7C3D09"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<polygon style="fill:url(#SVGID_7_);" points="9,0 9,3 9,10 9,13 11.5,11 14,13 14,10 14,3 14,0 "/>
 | 
			
		||||
<linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="11.4995" y1="1" x2="11.4995" y2="10.9355">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#D58738"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#AB551F"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#D58738"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#D58738"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#AB551F"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<polygon style="fill:url(#SVGID_8_);" points="10,1 10,4 10,7.9 10,10.9 11.5,9.7 13,10.9 13,7.9 13,4 13,1 "/>
 | 
			
		||||
<linearGradient id="SVGID_9_" gradientUnits="userSpaceOnUse" x1="11.5" y1="2" x2="11.5" y2="8.8711">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#D0813A"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#AF551D"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#D0813A"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#D0813A"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#AF551D"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<polygon style="fill:url(#SVGID_9_);" points="11,2 11,5 11,5.8 11,8.8 11.5,8.4 12,8.9 12,5.9 12,5 12,2 "/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 4.9 KiB  | 
							
								
								
									
										89
									
								
								src/assets/img/mod/survey.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,89 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In  -->
 | 
			
		||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
 | 
			
		||||
	<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
 | 
			
		||||
]>
 | 
			
		||||
<svg version="1.1"
 | 
			
		||||
	 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
 | 
			
		||||
	 x="0px" y="0px" width="24px" height="24px" viewBox="0 0 24 24" style="overflow:visible;enable-background:new 0 0 24 24;"
 | 
			
		||||
	 xml:space="preserve" preserveAspectRatio="xMinYMid meet">
 | 
			
		||||
<defs>
 | 
			
		||||
</defs>
 | 
			
		||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="20" y1="8" x2="20" y2="24.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="16" y="8" style="fill:url(#SVGID_1_);" width="8" height="16"/>
 | 
			
		||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="20" y1="9" x2="20" y2="23.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#82B4FB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#82B4FB"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="17" y="9" style="fill:url(#SVGID_2_);" width="6" height="14"/>
 | 
			
		||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="20" y1="10" x2="20" y2="22.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<stop  offset="0.5569" style="stop-color:#84ADEF"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="18" y="10" style="fill:url(#SVGID_3_);" width="4" height="12"/>
 | 
			
		||||
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="11.9995" y1="0" x2="11.9995" y2="24.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#90C50E"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#70A034"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#90C50E"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#90C50E"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#70A034"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="8" style="fill:url(#SVGID_4_);" width="8" height="24"/>
 | 
			
		||||
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="11.9995" y1="1" x2="11.9995" y2="23.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#D9F991"/>
 | 
			
		||||
	<stop  offset="0.2388" style="stop-color:#D7F88D"/>
 | 
			
		||||
	<stop  offset="0.4501" style="stop-color:#D1F383"/>
 | 
			
		||||
	<stop  offset="0.6509" style="stop-color:#C6EC71"/>
 | 
			
		||||
	<stop  offset="0.844" style="stop-color:#B7E257"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#A8D73D"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#D9F991"/>
 | 
			
		||||
	<a:midPointStop  offset="0.7317" style="stop-color:#D9F991"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#A8D73D"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="9" y="1" style="fill:url(#SVGID_5_);" width="6" height="22"/>
 | 
			
		||||
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="11.9995" y1="2" x2="11.9995" y2="22.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#B3E810"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#90C60D"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#B3E810"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#B3E810"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#90C60D"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="10" y="2" style="fill:url(#SVGID_6_);" width="4" height="20"/>
 | 
			
		||||
<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="4" y1="12" x2="4" y2="24.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#F0A829"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#C7671A"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#F0A829"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#F0A829"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#C7671A"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect y="12" style="fill:url(#SVGID_7_);" width="8" height="12"/>
 | 
			
		||||
<linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="4" y1="13" x2="4" y2="23.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#FFEBA8"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#F8BE27"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#FFEBA8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#FFEBA8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#F8BE27"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="1" y="13" style="fill:url(#SVGID_8_);" width="6" height="10"/>
 | 
			
		||||
<linearGradient id="SVGID_9_" gradientUnits="userSpaceOnUse" x1="4" y1="14" x2="4" y2="22">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#F5AE0D"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#F5AE0D"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="2" y="14" style="fill:url(#SVGID_9_);" width="4" height="8"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 5.0 KiB  | 
							
								
								
									
										485
									
								
								src/assets/img/mod/url.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,485 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In  -->
 | 
			
		||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
 | 
			
		||||
	<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
 | 
			
		||||
]>
 | 
			
		||||
<svg version="1.1"
 | 
			
		||||
	 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
 | 
			
		||||
	 x="0px" y="0px" width="24px" height="24px" viewBox="0 0 24 24" style="overflow:visible;enable-background:new 0 0 24 24;"
 | 
			
		||||
	 xml:space="preserve" preserveAspectRatio="xMinYMid meet">
 | 
			
		||||
<defs>
 | 
			
		||||
</defs>
 | 
			
		||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="11.9653" y1="0" x2="11.9653" y2="24.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<stop  offset="0.5569" style="stop-color:#84ADEF"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<polygon style="fill:url(#SVGID_1_);" points="14.4,0 13.4,0 3,0 2,0 2,24 3,24 21,24 22,24 22,7.9 "/>
 | 
			
		||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="11.9653" y1="1" x2="11.9653" y2="23.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#DEEFFC"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#DEEFFC"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<polygon style="fill:url(#SVGID_2_);" points="14,1 13,1 4,1 3,1 3,23 4,23 20,23 21,23 21,8.3 "/>
 | 
			
		||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="11.9653" y1="2" x2="11.9653" y2="22.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#BBDFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#BBDFF8"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<polygon style="fill:url(#SVGID_3_);" points="13.6,2 12.6,2 5,2 4,2 4,22 5,22 19,22 20,22 20,8.7 "/>
 | 
			
		||||
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="17.2109" y1="0" x2="17.2109" y2="9.3594">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<stop  offset="0.5569" style="stop-color:#84ADEF"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_4_);" d="M13,9c0,0,5.2-1.5,9,0.4c0-0.1,0-1.5,0-1.5L14.4,0c0,0-1.8,0-2,0C15.1,3.7,13,9,13,9z"/>
 | 
			
		||||
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="14.2881" y1="7.5449" x2="17.4162" y2="4.4168">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<stop  offset="0.5181" style="stop-color:#E5F3FC"/>
 | 
			
		||||
	<stop  offset="0.7045" style="stop-color:#DEF0FB"/>
 | 
			
		||||
	<stop  offset="0.8371" style="stop-color:#D3EBFA"/>
 | 
			
		||||
	<stop  offset="0.872" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#BDD8F0"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<a:midPointStop  offset="0.87" style="stop-color:#E7F4FC"/>
 | 
			
		||||
	<a:midPointStop  offset="0.872" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#CEE9F9"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#BDD8F0"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_5_);" d="M20.5,7.8c-0.9-0.2-2-0.3-3.1-0.3c-1.1,0-2.1,0.1-3,0.3c0.4-1.6,0.7-4.1-0.2-6.4L20.5,7.8z"/>
 | 
			
		||||
<radialGradient id="SVGID_6_" cx="7.5312" cy="16.5" r="7.0005" gradientUnits="userSpaceOnUse">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#8BB4F0"/>
 | 
			
		||||
	<stop  offset="7.721406e-02" style="stop-color:#88B1EF"/>
 | 
			
		||||
	<stop  offset="0.488" style="stop-color:#7FA7EC"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#8BB4F0"/>
 | 
			
		||||
	<a:midPointStop  offset="0.25" style="stop-color:#8BB4F0"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
</radialGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_6_);" d="M7.5,23.5C0.4,23.2-1.6,15.1,3,11.2c1.8-1.6,3.8-1.7,4.6-1.7c0.4,0,0.8,0,1.1,0.1
 | 
			
		||||
	C9,9.6,9.4,9.7,9.7,9.8c0.3,0.1,0.7,0.3,1,0.4c0.3,0.2,0.6,0.4,0.9,0.6c0.3,0.2,0.6,0.4,0.8,0.7c0.3,0.3,0.5,0.5,0.7,0.8
 | 
			
		||||
	c0.2,0.3,0.4,0.6,0.6,0.9c0.2,0.3,0.3,0.7,0.4,1c0.1,0.3,0.2,0.7,0.3,1.1c0.1,0.4,0.1,0.8,0.1,1.1s0,0.8-0.1,1.1
 | 
			
		||||
	c-0.1,0.4-0.1,0.7-0.3,1.1c-0.1,0.3-0.3,0.7-0.4,1c-0.2,0.3-0.4,0.6-0.6,0.9c-0.2,0.3-0.4,0.6-0.7,0.8c-0.3,0.3-0.5,0.5-0.8,0.7
 | 
			
		||||
	c-0.3,0.2-0.6,0.4-0.9,0.6c-0.3,0.2-0.7,0.3-1,0.4c-0.3,0.1-0.7,0.2-1.1,0.3C8.3,23.5,7.9,23.5,7.5,23.5"/>
 | 
			
		||||
<radialGradient id="SVGID_7_" cx="7.5308" cy="16.5" r="6.4856" gradientUnits="userSpaceOnUse">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#8BB4F0"/>
 | 
			
		||||
	<stop  offset="7.721406e-02" style="stop-color:#88B1EF"/>
 | 
			
		||||
	<stop  offset="0.488" style="stop-color:#7FA7EC"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#8BB4F0"/>
 | 
			
		||||
	<a:midPointStop  offset="0.25" style="stop-color:#8BB4F0"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
</radialGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_7_);" d="M7.5,23C1,22.7-0.9,15.2,3.3,11.6C5,10.1,6.8,10,7.5,10c0.4,0,0.7,0,1.1,0.1
 | 
			
		||||
	c0.3,0.1,0.7,0.1,1,0.2s0.6,0.2,0.9,0.4c0.3,0.2,0.6,0.3,0.9,0.5c0.3,0.2,0.5,0.4,0.8,0.6c0.2,0.2,0.5,0.5,0.6,0.8
 | 
			
		||||
	c0.2,0.3,0.4,0.6,0.5,0.9s0.3,0.6,0.4,0.9s0.2,0.7,0.2,1c0.1,0.3,0.1,0.7,0.1,1.1c0,0.4,0,0.7-0.1,1.1c-0.1,0.3-0.1,0.7-0.2,1
 | 
			
		||||
	s-0.2,0.6-0.4,0.9s-0.3,0.6-0.5,0.9s-0.4,0.5-0.6,0.8s-0.5,0.5-0.8,0.6c-0.3,0.2-0.6,0.4-0.9,0.5c-0.3,0.2-0.6,0.3-0.9,0.4
 | 
			
		||||
	s-0.7,0.2-1,0.2C8.2,23,7.9,23,7.5,23"/>
 | 
			
		||||
<path style="fill:#B3E710;" d="M6.1,10.8L6.1,10.8c0-0.1,0-0.1,0-0.1c0,0,0,0,0,0c0,0,0,0,0,0C6.1,10.7,6,10.7,6.1,10.8
 | 
			
		||||
	c-0.1-0.1-0.1,0-0.2,0C5.9,10.7,6,10.8,6.1,10.8C6,10.8,6.1,10.8,6.1,10.8 M3,13c0,0,2,0,3.6,0c0,0,0.4-0.1,0.4-0.1l0,0v0l0,0v0v0
 | 
			
		||||
	l0,0v0l0,0c0-0.1-0.3-0.1-0.4-0.1c0-0.1-0.2-0.2-0.1-0.4c0,0-0.1,0-0.1,0c-0.1-0.1-0.3-0.1-0.4-0.1c-0.1,0-0.2-0.1-0.3-0.2
 | 
			
		||||
	c-0.1,0-0.1,0-0.2,0c0,0,0,0,0,0C5.6,12,5.6,12,5.4,11.9c0-0.2,0.2-0.3,0.4-0.5c0,0,0,0,0,0c0,0,0,0,0,0l0,0c0,0,0.1,0,0.1,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0-0.1-0.1c0,0,0,0,0,0v0c0,0,0.1,0.1,0.2,0c0,0,0,0,0-0.1c0.1,0,0.1,0,0.2-0.1c0,0,0,0,0,0
 | 
			
		||||
	c-0.1,0-0.1-0.1-0.2-0.1c0.1,0,0.1,0,0.2,0.1c0,0,0.1,0,0.2-0.1c0,0,0,0,0,0c0,0,0,0,0,0l0,0c0,0,0.1,0,0.1,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0.1,0,0.1,0c0,0,0,0,0-0.1l0,0c0,0,0,0.1,0.1,0.1c0.1-0.1,0.1-0.1,0.2-0.1c0-0.1,0-0.1,0-0.1c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0-0.1c-0.1,0-0.1-0.1-0.2,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0-0.2,0.2-0.2,0.2c-0.1,0,0-0.1,0-0.1c0.1,0,0.1,0,0-0.1
 | 
			
		||||
	c-0.1,0-0.1,0.1-0.1,0.1c0,0,0-0.1,0-0.1c0,0,0,0,0,0c0,0,0,0-0.1,0c0,0,0,0,0.1-0.1c0,0,0-0.1,0-0.1c0,0,0,0,0,0
 | 
			
		||||
	c-0.1,0-0.2,0.1-0.2,0.1c0,0,0,0,0,0c-0.1,0-0.1,0-0.1,0.1c0,0,0,0,0.1,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0s0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0-0.1,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0l-0.3,0.2c0,0,0,0,0,0c0,0,0,0,0.1-0.1c0,0-0.1,0-0.1,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0h0l0,0l0,0h0l0,0l0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c-0.1,0-0.1,0-0.3,0h0l0,0c0,0,0,0,0,0h0l0,0c0,0,0,0,0,0h0l0,0c-0.2,0.1-0.2,0.1-0.3,0.2
 | 
			
		||||
	c0-0.1,0-0.1,0.1-0.1c0,0,0,0,0,0c0,0,0,0,0,0c-0.1,0-0.2,0-0.2,0c0.1,0,0.1,0,0.2-0.1c-0.1,0-0.1,0-0.1-0.1c-0.1,0-0.1,0-0.1,0
 | 
			
		||||
	c0,0,0,0,0.1,0c0,0-0.1,0-0.1,0c0,0,0,0,0.1-0.1c0,0-0.3,0.1-0.5,0.2c0,0,0,0,0.1,0c0.1,0,0.2,0.1,0.3,0c0,0-0.1,0.2-0.1,0.2
 | 
			
		||||
	c0,0,0,0,0,0c-0.1,0-0.1,0-0.1,0c-0.1,0-0.1,0-0.1,0c0,0,0.4,0,0.4,0v-0.1c0-0.1-0.1-0.2,0-0.2c0,0-0.1,0-0.1,0c0,0,0,0,0-0.1
 | 
			
		||||
	c-0.1,0-0.2,0.1-0.3,0.1c-0.3,0.1-0.6,0.4-0.9,0.5c-0.1,0.1-0.3,0.2-0.4,0.3c0,0,0,0-0.1,0l0,0L3,11.8l0,0c0.1-0.1,0.1-0.1,0.2-0.2
 | 
			
		||||
	c0,0,0,0,0,0c0,0-0.1,0.1-0.1,0.1c0,0,0,0,0,0c0,0,0,0,0,0.1c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0.1-0.1,0.1-0.1,0.2c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0.1c0,0,0,0,0,0c0,0,0.1,0,0.1,0
 | 
			
		||||
	c0,0,0,0.1,0,0.1c0,0,0,0,0,0c-0.1,0-0.1,0-0.1,0.1l0,0C3,12.2,3,12.2,2.9,12.3c0,0,0,0,0,0v0c0,0.1-0.1,0.1-0.1,0.1c0,0,0,0,0.1,0
 | 
			
		||||
	l0,0c0,0.1-0.1,0.1-0.1,0.2c0,0,0,0.2,0,0.2c0,0,0,0.3-0.1,0.3c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0h0c0,0,0-0.1,0-0.1c0,0,0-0.1,0-0.1c0,0,0,0-0.1,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0-0.1,0.1c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0 M8.3,10.4C8.3,10.4,8.3,10.4,8.3,10.4L8.3,10.4C8.3,10.4,8.3,10.4,8.3,10.4C8.3,10.4,8.3,10.4,8.3,10.4 M8.3,10.4
 | 
			
		||||
	c0,0-0.1-0.1-0.1-0.1c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0C8.2,10.4,8.2,10.4,8.3,10.4
 | 
			
		||||
	C8.2,10.4,8.2,10.4,8.3,10.4L8.3,10.4L8.3,10.4L8.3,10.4C8.2,10.5,8.2,10.5,8.3,10.4C8.2,10.5,8.2,10.5,8.3,10.4
 | 
			
		||||
	C8.2,10.5,8.3,10.5,8.3,10.4C8.3,10.4,8.3,10.4,8.3,10.4 M5.9,10.7L5.9,10.7C6,10.6,6,10.6,5.9,10.7c0-0.1,0-0.1,0-0.1
 | 
			
		||||
	c0,0,0,0,0.1-0.1c0,0,0.1-0.1,0.1-0.1c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c-0.1,0-0.1,0.1-0.2,0.1c0,0,0,0,0.1-0.1c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c-0.1,0.1-0.1,0.1-0.1,0.1c0.1,0,0.1,0,0.1,0c0,0-0.1,0-0.1,0c0,0,0.1,0,0.1-0.1c0,0-0.3,0.1-0.4,0.1
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0.1,0c0,0,0,0,0,0l0,0c0,0,0,0,0,0c0,0-0.1,0-0.1,0c0,0,0,0,0.1,0c0.1,0,0.1,0,0.1,0
 | 
			
		||||
	c-0.1,0-0.1,0-0.2,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0.1c0,0,0,0,0,0c0,0,0,0,0,0c0,0-0.1,0-0.1,0.1c0,0,0.1,0,0.2,0c0,0,0,0,0,0c0,0,0.1,0,0.1,0c0,0,0,0,0.1,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0.1,0.1,0.1,0.1,0.1C5.8,10.7,5.8,10.7,5.9,10.7c-0.1,0-0.1,0-0.1,0c0,0,0,0,0,0C5.8,10.7,5.9,10.7,5.9,10.7
 | 
			
		||||
	C5.9,10.7,5.9,10.7,5.9,10.7 M8.2,11L8.2,11C8.2,11,8.2,11,8.2,11c-0.1,0-0.1,0-0.1,0c0,0,0,0,0,0c0,0-0.1,0-0.1,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0s0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0-0.1-0.1-0.1-0.1
 | 
			
		||||
	c0,0-0.1,0-0.1,0c0-0.1-0.1,0-0.1-0.1c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0v0l0,0c0,0,0.1,0,0.1,0c0,0-0.1,0-0.1-0.1c0,0,0,0,0.1,0
 | 
			
		||||
	c0,0,0,0-0.1,0c0,0,0,0-0.1,0c0,0,0,0,0,0l0,0c0,0,0,0,0.1,0c0,0-0.1,0-0.1,0c0,0,0,0-0.1,0c0,0,0,0,0,0v0c0,0,0,0-0.1,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0-0.1,0l0,0c0,0,0,0,0.1,0c0,0,0,0,0,0c-0.1-0.1-0.1-0.1-0.1-0.1c0,0,0,0.1-0.1,0.1c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0-0.1,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0s0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0.1,0c0,0,0-0.1-0.2,0c0,0,0,0,0,0c-0.1,0-0.1,0-0.1,0c0,0,0,0,0,0c0,0,0,0-0.1,0c0,0,0,0,0,0c0,0,0,0-0.1,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0-0.1,0-0.1,0-0.1c0,0,0,0,0,0c0,0,0.1-0.1,0.1-0.1c0,0,0,0-0.1,0c-0.1,0-0.2,0.1-0.2,0.1c0,0,0,0,0,0
 | 
			
		||||
	c0,0-0.1,0-0.1,0.1c0.1,0,0.1,0,0.1,0c0,0-0.1,0-0.1,0c0,0.1,0.1,0.1,0.1,0.1c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0s0,0,0,0c0,0,0,0,0,0s0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0s0,0,0,0c0,0,0,0,0,0c0,0,0.1,0,0.1,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0l0,0c0,0,0,0,0,0c0.1,0,0.1,0,0.1,0c0,0-0.1,0-0.1,0l0,0c0,0,0,0,0.1,0c0,0,0.1,0,0.1,0.1c0,0,0,0,0,0c0,0,0,0,0.1,0.1
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0-0.1,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0.1,0c0,0,0,0,0,0c0,0,0,0,0,0s0,0,0,0s0,0,0,0s0,0,0,0s0,0,0,0
 | 
			
		||||
	s0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0.1,0,0.1,0c0,0,0,0.1,0,0.1c0,0-0.1,0.1-0.1,0.1c0,0,0,0-0.1,0
 | 
			
		||||
	c0,0,0.1,0,0.1,0.1c0,0,0,0,0,0c0,0-0.1,0-0.1,0c0,0,0,0,0,0h0c-0.1-0.1-0.1,0-0.1,0c0,0,0,0,0,0c-0.1,0-0.1,0-0.1,0.1
 | 
			
		||||
	c0.1,0,0.2,0,0.2,0c0,0,0.1,0,0.1,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0.1,0,0.1,0c0,0,0,0,0,0h0c0,0,0,0,0,0l0,0h0h0h0l0,0
 | 
			
		||||
	c0,0,0,0,0.1,0.1c0,0,0,0,0,0c0,0.1,0.1,0,0.1,0.1c0,0,0.1,0,0.1,0.1c0.1,0,0.1,0,0.2,0c0,0,0,0,0,0c0-0.1-0.1-0.1-0.2-0.1
 | 
			
		||||
	c-0.1,0-0.1,0-0.1-0.1c0,0,0.1,0,0.1,0c0,0,0,0,0,0c0,0,0.1,0.1,0.1,0.1c0,0,0,0,0,0c0,0,0,0,0.1,0c0,0,0.1,0,0.1,0.1c0,0,0,0,0-0.1
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0-0.1-0.1-0.1-0.1c0,0,0,0.1,0.1,0.1c0,0,0,0-0.1-0.1c0,0,0,0,0,0l0,0c0,0,0,0,0,0c0,0-0.1,0-0.1-0.1
 | 
			
		||||
	c0,0,0,0,0,0l0,0c0,0,0-0.1-0.1-0.1c0,0,0,0,0,0c0,0,0,0,0-0.1c0.1,0,0.1,0,0.1,0c0,0,0,0,0,0c0,0,0,0,0,0C7.9,11,7.9,11,8,11.1
 | 
			
		||||
	c0,0,0,0.1,0.1,0c0,0.1,0,0.1,0.1,0.1c0,0,0,0,0,0c0,0,0,0,0-0.1c0,0,0,0,0,0c0,0,0,0,0,0C8.2,11.1,8.2,11.1,8.2,11
 | 
			
		||||
	C8.2,11,8.2,11,8.2,11C8.2,11,8.2,11,8.2,11C8.2,11,8.2,11,8.2,11 M6.4,10.4L6.4,10.4C6.5,10.4,6.4,10.4,6.4,10.4
 | 
			
		||||
	C6.4,10.4,6.4,10.4,6.4,10.4c0-0.1,0.1-0.1,0.1-0.1c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0-0.1,0c0,0,0,0,0,0c0,0-0.1,0-0.1,0
 | 
			
		||||
	l0,0L6.4,10.4C6.4,10.4,6.4,10.4,6.4,10.4c-0.1-0.1-0.1,0-0.1,0c0,0,0,0,0,0c-0.1,0-0.1,0.1-0.1,0.1c0,0,0,0,0,0.1c0,0,0,0,0.1,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0C6.3,10.5,6.3,10.5,6.4,10.4C6.3,10.5,6.4,10.5,6.4,10.4 M8.1,10.3C8.1,10.3,8.1,10.3,8.1,10.3
 | 
			
		||||
	C8.1,10.3,8.1,10.3,8.1,10.3C8.1,10.3,8.1,10.3,8.1,10.3C8.1,10.3,8.1,10.3,8.1,10.3 M6.8,10.3C6.8,10.3,6.8,10.3,6.8,10.3
 | 
			
		||||
	C6.8,10.3,6.8,10.3,6.8,10.3C6.8,10.3,6.8,10.3,6.8,10.3C6.8,10.3,6.7,10.3,6.8,10.3C6.7,10.3,6.7,10.3,6.8,10.3c-0.1,0-0.1,0-0.2,0
 | 
			
		||||
	c0,0,0,0-0.1,0c0,0,0,0.1-0.1,0.1c0,0,0,0,0.1,0c0,0,0,0,0,0c0,0,0.1,0,0.1,0C6.6,10.4,6.8,10.3,6.8,10.3 M8.1,10.3
 | 
			
		||||
	C8.1,10.3,8.1,10.3,8.1,10.3L8.1,10.3C8.1,10.3,8.1,10.3,8.1,10.3L8.1,10.3 M5.9,10.3L5.9,10.3C6,10.3,6,10.3,5.9,10.3
 | 
			
		||||
	C6,10.3,6,10.3,5.9,10.3C6,10.3,6,10.3,5.9,10.3C6,10.3,6,10.3,5.9,10.3C6,10.3,6,10.3,5.9,10.3C6,10.3,5.9,10.3,5.9,10.3
 | 
			
		||||
	c-0.1,0-0.1,0-0.2,0c-0.1,0-0.1,0-0.2,0.1l0,0l0,0l0,0h0l0,0l0,0l0,0l0,0c0,0-0.1,0-0.1,0.1c0,0,0,0-0.1,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0-0.1,0-0.1,0.1c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0.1,0,0.1,0c0.1,0,0.1-0.1,0.2-0.1c0,0,0.1,0,0.1,0
 | 
			
		||||
	C5.8,10.4,5.9,10.4,5.9,10.3 M7.9,10.2C7.9,10.2,7.9,10.2,7.9,10.2C7.9,10.2,7.9,10.2,7.9,10.2C7.9,10.2,7.9,10.2,7.9,10.2
 | 
			
		||||
	C7.9,10.2,7.9,10.2,7.9,10.2 M7.2,10.2L7.2,10.2C7.2,10.2,7.2,10.2,7.2,10.2C7.2,10.2,7.2,10.2,7.2,10.2C7.2,10.2,7.2,10.2,7.2,10.2
 | 
			
		||||
	c-0.1,0-0.1,0-0.2,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0.1,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0s0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0-0.1,0.1-0.1,0.1c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0.1,0,0.1,0,0.1,0c0,0,0,0,0.1,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0C7.1,10.3,7.1,10.3,7.2,10.2C7.2,10.2,7.2,10.2,7.2,10.2 M6.8,10.2C6.8,10.1,6.8,10.1,6.8,10.2
 | 
			
		||||
	C6.8,10.1,6.8,10.1,6.8,10.2C6.8,10.1,6.8,10.1,6.8,10.2C6.8,10.2,6.8,10.2,6.8,10.2C6.8,10.1,6.8,10.1,6.8,10.2
 | 
			
		||||
	C6.8,10.1,6.8,10.1,6.8,10.2C6.8,10.1,6.8,10.1,6.8,10.2C6.8,10.2,6.7,10.2,6.8,10.2L6.8,10.2C6.7,10.2,6.7,10.2,6.8,10.2
 | 
			
		||||
	C6.7,10.1,6.7,10.2,6.8,10.2C6.7,10.2,6.7,10.2,6.8,10.2c-0.1,0-0.1,0-0.1,0c0,0,0,0,0,0s0,0,0,0s0,0,0,0c0,0,0,0,0,0s0,0,0,0
 | 
			
		||||
	s0,0,0,0s0,0,0,0s0,0,0,0c0,0,0,0,0,0C6.6,10.2,6.7,10.2,6.8,10.2C6.7,10.2,6.7,10.2,6.8,10.2c-0.1,0-0.1,0-0.1,0c0,0-0.1,0-0.1,0
 | 
			
		||||
	c0,0,0,0,0.1,0C6.7,10.2,6.7,10.2,6.8,10.2 M6.5,10.2C6.6,10.2,6.6,10.2,6.5,10.2C6.6,10.2,6.6,10.2,6.5,10.2
 | 
			
		||||
	C6.6,10.2,6.5,10.2,6.5,10.2C6.5,10.2,6.5,10.2,6.5,10.2c0,0,0.1,0,0.1-0.1c0,0,0,0,0,0C6.6,10.1,6.6,10.1,6.5,10.2
 | 
			
		||||
	c-0.1,0-0.1,0-0.1,0C6.4,10.2,6.4,10.2,6.5,10.2c-0.1,0-0.1,0-0.1,0C6.5,10.2,6.5,10.2,6.5,10.2c-0.1,0-0.1,0-0.1,0c0,0,0,0-0.1,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0-0.1,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0.1,0,0.1,0,0.1,0c0,0,0,0,0,0c0,0-0.1,0-0.1,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0.1,0,0.1,0c0.1,0,0.1,0,0.1,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0C6.5,10.2,6.5,10.2,6.5,10.2 M6.9,10.1C7,10.1,7,10.1,6.9,10.1C7,10.1,7,10.1,6.9,10.1C6.9,10.1,6.9,10.1,6.9,10.1
 | 
			
		||||
	C6.9,10.1,6.9,10.1,6.9,10.1L6.9,10.1L6.9,10.1L6.9,10.1C6.9,10.1,6.9,10.1,6.9,10.1L6.9,10.1L6.9,10.1L6.9,10.1L6.9,10.1
 | 
			
		||||
	C6.9,10.1,6.9,10.1,6.9,10.1C6.9,10.1,6.9,10.1,6.9,10.1L6.9,10.1L6.9,10.1L6.9,10.1L6.9,10.1L6.9,10.1L6.9,10.1L6.9,10.1
 | 
			
		||||
	C6.9,10.1,6.9,10.1,6.9,10.1L6.9,10.1C6.9,10.1,6.9,10.1,6.9,10.1 M6.6,10.1C6.7,10.1,6.7,10.1,6.6,10.1C6.7,10.1,6.7,10.1,6.6,10.1
 | 
			
		||||
	C6.7,10.1,6.6,10.1,6.6,10.1L6.6,10.1C6.6,10.1,6.6,10.1,6.6,10.1L6.6,10.1C6.6,10.1,6.6,10.1,6.6,10.1L6.6,10.1
 | 
			
		||||
	C6.6,10.1,6.6,10.1,6.6,10.1L6.6,10.1C6.6,10.1,6.6,10.1,6.6,10.1c-0.1,0-0.3,0.1-0.3,0.1c0,0,0,0,0,0c0,0,0,0-0.1,0
 | 
			
		||||
	c0,0,0.1,0,0.1,0c0,0,0.1,0,0.1,0c0,0-0.1,0-0.1,0c0,0,0.1,0,0.1,0C6.6,10.1,6.6,10.1,6.6,10.1 M6.8,10.1L6.8,10.1
 | 
			
		||||
	C6.8,10.1,6.8,10.1,6.8,10.1C6.8,10.1,6.8,10.1,6.8,10.1L6.8,10.1L6.8,10.1L6.8,10.1L6.8,10.1L6.8,10.1L6.8,10.1
 | 
			
		||||
	C6.8,10.1,6.8,10.1,6.8,10.1L6.8,10.1C6.8,10.1,6.8,10.1,6.8,10.1c-0.1,0-0.1,0-0.1,0c0,0,0,0,0,0C6.8,10.1,6.8,10.1,6.8,10.1
 | 
			
		||||
	 M7.1,10.1C7.1,10.1,7.1,10.1,7.1,10.1C7.1,10,7.1,10,7.1,10.1C7.1,10,7.1,10,7.1,10.1L7.1,10.1L7.1,10.1L7.1,10.1L7.1,10.1
 | 
			
		||||
	L7.1,10.1L7.1,10.1L7.1,10.1L7.1,10.1C7.1,10,7.1,10,7.1,10.1C7.1,10.1,7.1,10.1,7.1,10.1C7,10.1,7,10.1,7.1,10.1
 | 
			
		||||
	C7.1,10.1,7.1,10.1,7.1,10.1 M7,10.1c0,0,0.1,0,0.1,0c0,0,0,0,0,0c0,0,0,0,0.1,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	C7.1,10,7,10.1,7,10.1C7,10.1,7,10.1,7,10.1C7,10,7,10.1,7,10.1C7,10.1,7,10.1,7,10.1C7,10.1,7,10.1,7,10.1C7,10.1,7,10.1,7,10.1
 | 
			
		||||
	C7,10.1,7,10.1,7,10.1C7,10.1,7,10.1,7,10.1 M7.3,10C7.3,10,7.4,10,7.3,10C7.4,10,7.3,10,7.3,10C7.3,10,7.4,10,7.3,10
 | 
			
		||||
	C7.4,10,7.4,10,7.3,10C7.4,10,7.4,10,7.3,10C7.4,10,7.4,10,7.3,10C7.4,10,7.4,10,7.3,10C7.4,10,7.4,10,7.3,10C7.4,10,7.4,10,7.3,10
 | 
			
		||||
	C7.4,10,7.4,10,7.3,10C7.4,10,7.4,10,7.3,10C7.4,10,7.4,10,7.3,10C7.4,10,7.4,10,7.3,10C7.4,10,7.4,10,7.3,10c0.1,0,0.1,0,0.1,0
 | 
			
		||||
	C7.4,10,7.4,10,7.3,10c0.1,0,0.1,0,0.2,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0C7.5,10,7.4,10,7.3,10C7.4,10,7.4,10,7.3,10
 | 
			
		||||
	C7.3,10,7.3,10,7.3,10L7.3,10C7.3,10,7.3,10,7.3,10C7.3,10,7.3,10,7.3,10C7.3,10,7.3,10,7.3,10c-0.1,0-0.1,0-0.2,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0C7.2,10.1,7.2,10.1,7.3,10C7.3,10,7.3,10,7.3,10C7.3,10.1,7.3,10,7.3,10
 | 
			
		||||
	C7.3,10,7.3,10,7.3,10C7.3,10,7.3,10,7.3,10 M7.5,10C7.5,10,7.5,10,7.5,10C7.5,10,7.5,10,7.5,10C7.5,10,7.5,10,7.5,10
 | 
			
		||||
	C7.5,10,7.5,10,7.5,10C7.5,10,7.5,10,7.5,10 M7.5,10L7.5,10L7.5,10C7.5,10,7.5,10,7.5,10C7.5,10,7.5,10,7.5,10C7.5,10,7.5,10,7.5,10
 | 
			
		||||
	C7.5,10,7.5,10,7.5,10C7.5,10,7.5,10,7.5,10C7.5,10,7.5,10,7.5,10C7.5,10,7.5,10,7.5,10L7.5,10C7.5,10,7.5,10,7.5,10L7.5,10
 | 
			
		||||
	C7.5,10,7.5,10,7.5,10L7.5,10C7.5,10,7.5,10,7.5,10L7.5,10C7.5,10,7.5,10,7.5,10c-0.1,0-0.1,0-0.1,0C7.4,10,7.4,10,7.5,10
 | 
			
		||||
	C7.4,10,7.4,10,7.5,10L7.5,10L7.5,10L7.5,10L7.5,10L7.5,10L7.5,10L7.5,10c-0.1,0-0.1,0-0.1,0l0,0c0,0,0,0,0,0l0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0-0.1,0-0.1,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0l0,0h0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0-0.1,0-0.1,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0.1,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0.1,0,0.1,0c0,0,0,0,0,0l0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0c0,0-0.1,0-0.1,0c0,0,0,0,0,0c0,0,0.1,0,0.1,0
 | 
			
		||||
	C7.4,10.1,7.4,10.1,7.5,10C7.4,10.1,7.4,10.1,7.5,10C7.4,10.1,7.4,10.1,7.5,10C7.4,10.1,7.5,10,7.5,10C7.5,10,7.5,10,7.5,10
 | 
			
		||||
	C7.5,10,7.5,10,7.5,10L7.5,10C7.5,10,7.5,10,7.5,10C7.5,10,7.5,10,7.5,10C7.5,10,7.5,10,7.5,10C7.5,10,7.5,10,7.5,10
 | 
			
		||||
	C7.5,10,7.5,10,7.5,10C7.5,10,7.5,10,7.5,10 M9,11.2C8.6,10.5,8.1,10,7.5,10h0h0h0c0,0,0,0,0,0l0,0h0l0,0l0,0l0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0l0,0l0,0l0,0l0,0l0,0c0,0,0,0,0,0v0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0l0,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0h0c0,0,0,0,0.1,0c0,0,0,0,0.1,0
 | 
			
		||||
	c0,0,0,0,0,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0l0,0c0,0,0,0,0,0l0,0l0,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0
 | 
			
		||||
	l0,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0h0c0,0,0,0,0,0h0c0,0,0,0,0,0h0h0c0,0,0,0,0,0h0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0h0h0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0l0,0h0l0,0l0,0h0l0,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0h0c0,0,0,0,0.1,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0l0,0c-0.1,0-0.1,0-0.2,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0.1,0,0.1,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0l0,0l0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0l0,0l0,0l0,0l0,0l0,0l0,0l0,0l0,0l0,0l0,0h0c0,0,0,0,0,0c0,0,0,0,0,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0v0
 | 
			
		||||
	l0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0-0.1,0-0.1,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0-0.1,0-0.1,0c0,0,0,0,0,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0v0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c-0.1,0-0.1,0-0.1,0l0,0c0,0,0.1,0,0.1,0l0,0l0,0
 | 
			
		||||
	c0,0,0,0,0,0c-0.1,0-0.1,0-0.1,0c0,0,0,0,0,0l0,0l0,0c0,0,0,0,0,0c0,0,0,0-0.1,0h0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	l0,0c0,0,0.1,0,0.1,0c0,0,0,0,0.1-0.1l0,0l0,0c0,0,0,0-0.1,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0-0.1,0.1l0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0l0,0
 | 
			
		||||
	c0,0,0,0,0.1,0c0,0,0,0,0,0.1l0,0c0,0,0,0,0-0.1l0,0l0,0c0,0,0,0,0,0h0l0,0c0,0,0,0,0-0.1l0,0l0,0c0,0,0,0,0,0c0,0,0,0,0.1,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0-0.1-0.1-0.1,0l0,0l0,0c0,0,0,0,0,0c0,0,0.1,0,0.1,0c0,0,0,0,0,0c0,0-0.1,0-0.1,0c0,0,0,0,0,0l0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0.1,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0.1,0,0.1,0,0.1,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0l0,0c0,0,0,0,0,0C8.9,11.1,8.9,11.1,9,11.2C8.9,11.1,8.9,11.1,9,11.2L9,11.2C8.9,11.2,8.9,11.2,9,11.2
 | 
			
		||||
	C8.9,11.2,8.9,11.2,9,11.2C8.9,11.2,8.9,11.2,9,11.2C8.9,11.2,8.9,11.2,9,11.2C8.9,11.2,8.9,11.2,9,11.2C8.9,11.2,8.9,11.2,9,11.2
 | 
			
		||||
	C8.9,11.2,8.9,11.2,9,11.2C8.9,11.2,8.9,11.2,9,11.2C8.9,11.2,8.9,11.2,9,11.2C8.9,11.2,9,11.2,9,11.2C9,11.2,9,11.2,9,11.2"/>
 | 
			
		||||
<path style="fill:#B3E710;" d="M7.4,22.9L7.4,22.9C7.4,22.9,7.4,22.9,7.4,22.9L7.4,22.9C7.4,22.9,7.4,22.9,7.4,22.9L7.4,22.9
 | 
			
		||||
	C7.4,22.9,7.4,22.9,7.4,22.9C7.4,22.9,7.4,22.9,7.4,22.9 M9.4,20.1C9.4,20.1,9.4,20.1,9.4,20.1c-0.1,0-0.1,0-0.2,0c0,0,0,0.1,0,0.1
 | 
			
		||||
	C9.2,20.2,9.3,20.2,9.4,20.1 M8.8,22.1c0.3-0.5,0.6-1.2,0.8-2c0,0,0,0,0,0c0,0,0,0-0.1,0c-0.1,0-0.1,0.1-0.2,0.2c0,0,0,0,0,0
 | 
			
		||||
	c0,0-0.1,0-0.1,0c0,0,0-0.1,0-0.1c-0.1,0-0.1,0-0.1,0c0,0,0-0.1,0.1-0.1c-0.5,0-1.1,0-1.7,0c-0.5,0-1,0-1.4,0c0,0,0,0.1,0,0.1
 | 
			
		||||
	c0,0,0,0.1,0,0.2l0,0c0.1,0.1,0.1,0,0.1,0c0.1,0.1-0.1,0.1-0.1,0.2c0,0,0.1,0.1,0.1,0.1c0,0,0,0,0,0c0.1,0.1,0.2,0.1,0.6,0.7
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0.1,0.2,0.4,0.2,0.5,0.3
 | 
			
		||||
	c0.1,0.1-0.1,0.3-0.1,0.4c0,0-0.1,0-0.1,0c0,0,0,0,0,0s0,0,0,0c0,0,0,0,0,0s0,0,0,0s0,0,0,0s0,0,0,0c0,0,0,0,0,0s0,0,0,0l0.1,0
 | 
			
		||||
	c0,0.1,0.1,0.7,0.1,0.7c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0.1,0.1,0.1c0,0,0,0,0,0c0,0,0,0,0,0l0,0l0,0l0,0l0,0l0,0c0,0,0,0,0,0l0,0
 | 
			
		||||
	c0,0,0,0,0,0h0c0,0,0,0,0,0h0h0c0,0,0,0,0,0c0,0,0,0,0,0l0,0h0l0,0l0,0l0,0h0c0,0,0-0.1,0-0.1c0,0,0,0,0.1-0.1c0,0,0.1,0,0.2-0.1
 | 
			
		||||
	c0,0,0,0,0-0.1c0,0,0,0,0,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0.1,0,0.2,0,0.4-0.2c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0-0.1,0.1-0.1,0.1c0.2-0.1,0.2-0.1,0.4-0.3c0,0,0,0,0,0C8.7,22.1,8.8,22.1,8.8,22.1 M13.7,18.3C13.7,18.3,13.7,18.3,13.7,18.3
 | 
			
		||||
	C13.7,18.3,13.7,18.3,13.7,18.3C13.7,18.3,13.7,18.3,13.7,18.3C13.7,18.3,13.7,18.4,13.7,18.3L13.7,18.3
 | 
			
		||||
	C13.7,18.4,13.7,18.3,13.7,18.3 M14,15.4c0-0.2-0.1-0.5-0.1-0.7v0c0-0.1-0.1-0.1-0.2-0.2c0,0-0.1,0-0.3-0.6c0,0,0,0,0,0
 | 
			
		||||
	c0-0.1,0-0.1-0.1-0.1c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0.1c0,0,0,0-0.1,0c0,0,0,0-0.1,0.1c0,0,0,0,0,0.1c0,0,0,0.1-0.1,0.1
 | 
			
		||||
	c0,0,0,0-0.1,0c-0.1,0-0.1,0-0.1-0.1c0,0-0.1,0.5,0,0.7c0.1,0.3-0.2,0.4-0.2,0.7c0,0.2-0.1,0.4-0.1,0.7c0,0,0,0,0,0
 | 
			
		||||
	c0.1,0.1,0.1,0.7,0,0.8c0,0,0,0.1,0.1,0.1c0,0,0,0,0,0c0,0,0,0.1,0,0.1c0,0,0,0,0,0l0,0l0,0l0,0l0,0h0l0,0l0,0l0-0.1
 | 
			
		||||
	c0,0,0-0.1,0-0.1c0,0,0,0,0,0c0,0,0,0.2,0,0.2c0,0,0.1,0.1,0.1,0.1c0,0,0.1,0.1,0.1,0.1v0c0,0-0.1,0-0.1,0c0,0,0,0,0.1,0.2
 | 
			
		||||
	c0,0,0,0,0,0s0,0,0,0c0,0,0,0,0,0s0,0,0,0s0,0,0,0s0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0v0v0L12.8,18c0,0,0,0,0,0c0,0,0.2,0.3,0.3,0.3
 | 
			
		||||
	c0.1,0,0.1-0.2,0.2-0.2c0,0,0,0,0-0.1c0,0,0,0,0,0c0.1,0,0.3-0.2,0.3-0.2c0,0,0.2,0,0.2,0.1l0.2,0v0c0,0.1-0.2,0.1-0.2,0.1
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0.1,0,0.1,0c0,0,0.1,0.1,0.1,0.1v0l-0.1,0c0,0.1-0.1,0.2-0.2,0.3l0,0c0,0,0,0.1,0,0.1c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0.1-0.1,0.2l0,0l0,0c0,0,0,0,0,0.1c-0.1,0.4-0.5,1-0.5,1c0,0,0,0,0,0s0,0,0,0c0.1,0,0.2-0.3,0.3-0.6c0.2-0.3,0.3-0.6,0.4-0.9
 | 
			
		||||
	s0.2-0.6,0.2-1c0.1-0.3,0.1-0.7,0.1-1C14.1,16.1,14.1,15.8,14,15.4 M13.4,13.8C13.4,13.8,13.4,13.8,13.4,13.8
 | 
			
		||||
	C13.4,13.8,13.4,13.8,13.4,13.8C13.4,13.8,13.4,13.8,13.4,13.8C13.4,13.8,13.4,13.8,13.4,13.8C13.4,13.8,13.4,13.8,13.4,13.8
 | 
			
		||||
	c0,0.1,0,0.1,0.1,0.2C13.5,13.9,13.5,13.9,13.4,13.8 M13.2,13.5C13.2,13.5,13.2,13.5,13.2,13.5C13.2,13.4,13.2,13.4,13.2,13.5
 | 
			
		||||
	C13.2,13.5,13.2,13.5,13.2,13.5C13.2,13.5,13.2,13.5,13.2,13.5C13.2,13.5,13.2,13.5,13.2,13.5c0,0,0,0.1,0,0.1c0,0,0,0.1,0,0.1
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0C13.3,13.6,13.3,13.6,13.2,13.5 M13.1,13.3C13.1,13.3,13.1,13.2,13.1,13.3
 | 
			
		||||
	L13.1,13.3L13.1,13.3C13.1,13.3,13.1,13.3,13.1,13.3L13.1,13.3C13.1,13.3,13.1,13.3,13.1,13.3C13.1,13.3,13.1,13.3,13.1,13.3
 | 
			
		||||
	C13.1,13.3,13.1,13.3,13.1,13.3C13.1,13.3,13.1,13.3,13.1,13.3L13.1,13.3L13.1,13.3L13.1,13.3L13.1,13.3L13.1,13.3L13.1,13.3
 | 
			
		||||
	c0,0.1,0.1,0.1,0.1,0.1l0,0C13.2,13.4,13.1,13.3,13.1,13.3 M13.3,13.5C13.3,13.5,13.2,13.5,13.3,13.5l-0.1-0.1c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0-0.2-0.4-0.3-0.4l0,0c0,0-0.1,0-0.3,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0.1c0,0,0,0,0,0l0,0l0,0
 | 
			
		||||
	c0,0,0.1,0.1,0.1,0.2c0,0,0,0-0.1,0c0,0.1-0.1,0-0.1,0c0,0-0.1,0-0.1,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0.1c0,0,0.1,0,0.1,0.1
 | 
			
		||||
	c0.1,0.1,0.1,0.3,0.1,0.4c0,0,0,0,0,0c0,0,0,0,0,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0.1
 | 
			
		||||
	c0,0,0,0.1,0,0.1c0,0,0,0,0,0c0,0,0,0,0.1,0c0,0,0,0,0,0c0,0,0,0.1,0.1,0.1c0,0.1,0.1,0.1,0.1,0.1c0,0,0,0,0,0c0,0,0,0,0,0l0,0h0
 | 
			
		||||
	c0,0,0-0.1,0-0.1c0,0,0.1,0,0.1,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0-0.1,0-0.1c0,0,0,0,0,0c0,0,0-0.1,0-0.1c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0-0.1-0.1c0-0.1,0-0.1,0-0.2l0,0c0,0,0-0.1,0-0.1c0-0.1,0-0.1,0-0.1c-0.1-0.1-0.1-0.1-0.1-0.1c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0-0.1,0-0.1c0,0,0,0,0,0C13,13,13.3,13.5,13.3,13.5C13.3,13.5,13.3,13.5,13.3,13.5L13.3,13.5
 | 
			
		||||
	L13.3,13.5L13.3,13.5L13.3,13.5C13.3,13.5,13.3,13.5,13.3,13.5C13.3,13.5,13.3,13.5,13.3,13.5C13.3,13.5,13.3,13.6,13.3,13.5
 | 
			
		||||
	C13.3,13.6,13.3,13.6,13.3,13.5c0.1,0.1,0.1,0.2,0.1,0.2c0,0,0,0,0,0l0,0l0,0c0,0,0,0,0,0.1c0,0,0,0,0,0l0,0l0,0c0,0,0,0,0-0.1
 | 
			
		||||
	C13.4,13.7,13.3,13.6,13.3,13.5 M12.9,12.9C12.9,12.9,12.9,12.9,12.9,12.9C12.9,12.9,12.9,12.9,12.9,12.9C12.9,12.9,13,13,12.9,12.9
 | 
			
		||||
	C13,13,12.9,12.9,12.9,12.9 M13.1,13.1c0-0.1-0.1-0.1-0.1-0.2c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0C13,13,13,13,13.1,13.1
 | 
			
		||||
	C13,13,13,13,13.1,13.1C13,13.1,13.1,13.1,13.1,13.1C13.1,13.1,13.1,13.1,13.1,13.1 M9.2,12.9L9.2,12.9L9.2,12.9L9.2,12.9L9.2,12.9
 | 
			
		||||
	c-0.1,0-0.1,0-0.1,0.1C9.1,12.9,9.1,12.9,9.2,12.9C9.2,12.9,9.2,12.9,9.2,12.9C9.2,12.9,9.2,12.9,9.2,12.9 M2.7,12.5
 | 
			
		||||
	C2.8,12.5,2.7,12.5,2.7,12.5C2.7,12.5,2.7,12.5,2.7,12.5C2.7,12.6,2.7,12.6,2.7,12.5C2.7,12.6,2.7,12.6,2.7,12.5 M2.9,12.4
 | 
			
		||||
	C2.9,12.4,2.9,12.4,2.9,12.4C2.9,12.3,2.9,12.3,2.9,12.4c0.1-0.1,0.1-0.1,0.1-0.1c0,0,0,0,0,0C2.8,12.3,2.8,12.4,2.9,12.4
 | 
			
		||||
	C2.8,12.4,2.8,12.4,2.9,12.4C2.8,12.4,2.8,12.4,2.9,12.4C2.8,12.4,2.8,12.4,2.9,12.4C2.8,12.4,2.8,12.4,2.9,12.4
 | 
			
		||||
	C2.8,12.4,2.8,12.4,2.9,12.4C2.8,12.4,2.8,12.4,2.9,12.4C2.9,12.4,2.9,12.4,2.9,12.4 M2.9,12.2C3,12.1,3,12.1,2.9,12.2
 | 
			
		||||
	C3,12.1,3,12.1,2.9,12.2C3,12.1,3,12.1,2.9,12.2C2.9,12.1,2.9,12.2,2.9,12.2C2.9,12.2,2.9,12.2,2.9,12.2L2.9,12.2
 | 
			
		||||
	C2.9,12.2,2.9,12.2,2.9,12.2 M3,12.1C3,12,3,12,3,12.1C3.1,12,3.1,12,3,12.1C3.1,12,3.1,12,3,12.1C3,12,3,12,3,12.1
 | 
			
		||||
	C3,12.1,3,12.1,3,12.1C3,12,3,12.1,3,12.1 M9,13c0,0,0-0.2,0-0.2c0,0,0.1,0,0-0.1c0-0.2,0-0.1,0-0.1c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0-0.1,0-0.1,0c-0.1,0-0.1,0.1-0.2,0.1c0,0,0-0.1,0-0.1c0.1,0,0.2-0.1,0.2-0.1c-0.3-0.1-0.3-0.2-0.3-0.2
 | 
			
		||||
	c0-0.1,0-0.3-0.1-0.3c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0s0,0,0,0
 | 
			
		||||
	c0,0,0,0.1-0.1,0c0,0,0,0.1,0,0.1c0,0,0.1,0,0.1,0c0-0.1-0.1,0-0.1-0.1c0,0,0,0,0,0C8.4,12,8.4,12,8.3,12c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c-0.1-0.1-0.1-0.1-0.2-0.2c0,0,0,0-0.1,0.1c0,0,0,0,0,0.1C8,11.9,8,12,8,12c0,0,0,0,0,0c0,0,0-0.1-0.1-0.1C7.9,12,7.9,12,7.8,12
 | 
			
		||||
	c0,0,0,0,0,0c0-0.1-0.1,0-0.2,0c0,0,0,0,0,0c0,0,0-0.1,0-0.1c0,0,0,0.2,0,0.2c0,0,0-0.1,0-0.1c0,0,0,0,0,0c-0.1,0-0.2-0.2-0.2-0.2
 | 
			
		||||
	c0,0,0-0.1,0-0.1c0,0-0.1-0.1-0.1-0.1c-0.1,0-0.1,0-0.1,0c-0.1,0-0.1,0-0.2-0.1c0,0,0,0,0,0c0,0-0.1,0,0,0.1c0,0,0,0.1,0,0.1v0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0.1,0,0.1l-0.1,0.1C6.9,12,7.1,12,7,12.2c0,0.1-0.1,0.4-0.3,0.5c0,0,0.1,0.3,0.1,0.3c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0.3-0.4,0.3-0.4c0,0,0,0,0,0c0,0,0,0,0,0s0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0.1-0.1,0.1-0.1,0.2c0,0-0.1,0.1-0.1,0.1c0,0-0.1,0-0.1-0.1c0,0,0,0-0.1,0c0,0,0,0.1,0,0.1c0.3,0,0.5,0,0.8,0c0.5,0,1,0,1.5,0
 | 
			
		||||
	c0,0-0.1-0.1-0.1-0.1C9,12.9,9,12.9,9,12.8 M9,11.3C9,11.2,9,11.2,9,11.3L9,11.3L9,11.3 M6.8,11.3L6.8,11.3c0,0,0,0-0.1,0
 | 
			
		||||
	c0,0,0-0.1,0-0.1c0,0,0,0,0,0c0,0,0,0-0.1,0c0,0,0,0-0.1-0.1c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c-0.1,0.1-0.1,0.1-0.1,0.2c-0.1,0-0.1,0-0.1,0.1c0,0,0,0,0,0c0,0,0.1,0,0.1,0c0,0,0,0,0,0.1c0,0,0,0,0,0c0,0,0,0,0.1,0
 | 
			
		||||
	c0,0,0,0,0.1,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0C6.7,11.3,6.7,11.3,6.8,11.3C6.8,11.3,6.8,11.3,6.8,11.3 M7.3,10.8
 | 
			
		||||
	C7.3,10.8,7.3,10.8,7.3,10.8C7.3,10.8,7.2,10.8,7.3,10.8c-0.2,0-0.2,0.1-0.1,0.1C7.1,10.9,7.2,11,7.3,10.8"/>
 | 
			
		||||
<path style="fill:#B3E710;" d="M9.6,13.4C9.6,13.4,9.6,13.4,9.6,13.4C9.6,13.4,9.6,13.4,9.6,13.4C9.6,13.4,9.6,13.3,9.6,13.4
 | 
			
		||||
	L9.6,13.4C9.5,13.3,9.5,13.3,9.6,13.4C9.5,13.4,9.5,13.4,9.6,13.4c-0.1-0.1-0.1-0.1-0.1-0.1c0,0,0,0,0,0c0,0,0,0,0.1,0c0,0,0,0,0,0
 | 
			
		||||
	l0,0c0,0,0,0-0.1,0c0,0,0,0,0.1-0.1c0,0-0.1,0-0.1,0c0,0,0,0,0,0c0,0-0.1,0-0.1,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0-0.1,0c0,0,0,0-0.1,0.1c0-0.1,0-0.1,0.1-0.2c0,0-0.1,0-0.1,0C9,13,9,13,9,13.1c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0-0.1,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0.1,0c0,0-0.1,0-0.1,0.1c0,0,0,0,0,0.1c0.1,0,0.1,0,0.2,0c0,0,0.1,0,0.1,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0l0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0.1,0c0,0,0,0,0,0c0,0-0.1,0.1-0.1,0.1c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0.1-0.1,0.1-0.1c0,0,0,0,0,0l0,0c0,0,0,0,0,0.1
 | 
			
		||||
	c0,0,0,0,0,0l0,0L9.6,13.4C9.5,13.4,9.5,13.5,9.6,13.4C9.6,13.5,9.6,13.5,9.6,13.4C9.6,13.5,9.6,13.4,9.6,13.4 M12.6,12.9
 | 
			
		||||
	C12.6,12.9,12.6,12.9,12.6,12.9C12.6,12.9,12.6,12.9,12.6,12.9L12.6,12.9C12.6,12.9,12.6,12.9,12.6,12.9 M11.6,11.8
 | 
			
		||||
	c0,0-0.1-0.2-0.2-0.1c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0.1c0,0,0,0,0,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0s0,0,0,0s0,0,0,0s0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0s0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0-0.1,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0l0,0l0,0c0,0,0.1,0,0.1,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0-0.1c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0C11.7,11.9,11.7,11.9,11.6,11.8C11.6,11.9,11.6,11.9,11.6,11.8
 | 
			
		||||
	 M11.5,11.7L11.5,11.7C11.5,11.7,11.4,11.7,11.5,11.7C11.4,11.7,11.4,11.7,11.5,11.7C11.4,11.7,11.5,11.7,11.5,11.7 M11.8,11.7
 | 
			
		||||
	C11.8,11.6,11.8,11.6,11.8,11.7L11.8,11.7C11.8,11.7,11.8,11.7,11.8,11.7C11.8,11.7,11.8,11.7,11.8,11.7
 | 
			
		||||
	C11.8,11.7,11.8,11.7,11.8,11.7C11.9,11.7,11.9,11.7,11.8,11.7C11.9,11.7,11.9,11.7,11.8,11.7C11.9,11.7,11.9,11.7,11.8,11.7
 | 
			
		||||
	C11.9,11.7,11.9,11.7,11.8,11.7L11.8,11.7L11.8,11.7C11.9,11.7,11.8,11.7,11.8,11.7 M11.6,11.4C11.6,11.4,11.6,11.4,11.6,11.4
 | 
			
		||||
	L11.6,11.4C11.6,11.5,11.6,11.5,11.6,11.4C11.6,11.5,11.6,11.5,11.6,11.4c0.1,0.1,0.1,0.1,0.1,0.1C11.7,11.5,11.7,11.5,11.6,11.4
 | 
			
		||||
	C11.6,11.5,11.6,11.5,11.6,11.4C11.6,11.5,11.6,11.5,11.6,11.4 M9.4,11.4C9.4,11.4,9.4,11.4,9.4,11.4C9.4,11.4,9.4,11.4,9.4,11.4
 | 
			
		||||
	C9.4,11.4,9.4,11.4,9.4,11.4C9.4,11.4,9.3,11.4,9.4,11.4C9.3,11.4,9.4,11.4,9.4,11.4C9.4,11.4,9.4,11.4,9.4,11.4 M12.1,12.1
 | 
			
		||||
	c0,0-0.1-0.1-0.1-0.1C12,12,12,12,12.1,12.1C11.9,12,11.9,12,11.9,12c0,0-0.1-0.1-0.2-0.2c-0.2-0.1-0.2-0.2-0.2-0.2c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0-0.1-0.1-0.1-0.1c0,0-0.1,0-0.1,0c0,0,0-0.1-0.1-0.1c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0c0,0,0,0,0,0c0,0,0,0,0.1,0c0,0,0,0,0,0c0,0,0,0,0.1,0.1c0,0,0.1,0.1,0.1,0.1l0,0
 | 
			
		||||
	c0,0,0,0,0,0l0,0c0,0,0,0.1,0.1,0.1c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0c0.1,0.1,0.1,0.1,0.1,0.1c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0.1,0.1,0.1,0.1c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0.1c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0s0,0,0,0c0,0,0,0,0,0s0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0.1l0,0C11.9,12.1,11.9,12.1,12.1,12.1C12,12.2,12,12.2,12.1,12.1C12,12.2,12,12.2,12.1,12.1
 | 
			
		||||
	C12,12.2,12,12.1,12.1,12.1C12,12.2,12,12.2,12,12.2c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0.1,0,0.1,0,0.1
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0.1,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0C12.1,12.2,12.1,12.2,12.1,12.1
 | 
			
		||||
	C12.1,12.2,12.1,12.2,12.1,12.1C12.1,12.2,12.1,12.2,12.1,12.1C12.1,12.2,12.1,12.2,12.1,12.1C12.1,12.1,12.1,12.1,12.1,12.1
 | 
			
		||||
	 M9.9,10.7c0,0-0.1,0-0.1,0c0,0,0,0-0.1,0c0,0-0.1,0-0.1,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0s0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0-0.1,0c0,0,0,0,0,0c0,0,0,0-0.1,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0
 | 
			
		||||
	c0,0-0.1,0-0.1,0c0,0,0,0,0,0l0,0c0,0,0,0,0.1,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c-0.1,0-0.1-0.1-0.2-0.1c0,0,0,0,0,0l0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0.1,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0.1,0c0,0,0,0,0.1,0l0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0c0,0,0,0,0,0c0.1,0,0.1,0,0.1,0.1c0,0,0,0,0,0c0,0,0.1,0,0.1,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0.1,0,0.1,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0C10,10.8,10,10.8,9.9,10.7C10,10.8,10,10.8,9.9,10.7C10,10.8,10,10.7,9.9,10.7
 | 
			
		||||
	C10,10.7,10,10.7,9.9,10.7C10,10.7,9.9,10.7,9.9,10.7 M8.6,10.2C8.6,10.2,8.6,10.2,8.6,10.2C8.5,10.2,8.5,10.2,8.6,10.2
 | 
			
		||||
	C8.6,10.2,8.6,10.2,8.6,10.2L8.6,10.2C8.7,10.2,8.6,10.2,8.6,10.2L8.6,10.2L8.6,10.2L8.6,10.2L8.6,10.2C8.6,10.2,8.6,10.2,8.6,10.2
 | 
			
		||||
	 M12.1,11.9C12.1,11.9,12,11.8,12.1,11.9L12.1,11.9C12,11.9,12.1,11.9,12.1,11.9c0,0-0.1-0.1-0.1-0.1l0,0l0,0h0
 | 
			
		||||
	C12,11.8,12,11.9,12.1,11.9C12.1,11.9,12.1,11.9,12.1,11.9L12.1,11.9L12.1,11.9L12.1,11.9C12,11.8,12,11.8,12,11.8c0,0,0,0,0,0
 | 
			
		||||
	c0,0-0.1-0.1-0.1-0.1l0,0c0,0,0,0-0.1-0.1l0,0l0,0l0,0c0,0,0,0-0.1,0c0,0,0,0,0.1,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0.1,0.1,0.1,0.1l0,0c0.1,0,0.1,0.1,0.2,0.2l0,0L12.1,11.9L12.1,11.9L12.1,11.9C12,11.9,12,11.9,12.1,11.9
 | 
			
		||||
	C12,11.9,12,11.9,12.1,11.9C12.1,11.9,12,11.9,12.1,11.9C12.1,11.9,12.1,11.9,12.1,11.9L12.1,11.9L12.1,11.9L12.1,11.9
 | 
			
		||||
	C12.1,11.9,12,11.9,12.1,11.9C12,11.9,12,11.9,12.1,11.9C12,11.9,12,11.9,12.1,11.9C12.1,11.9,12.1,11.9,12.1,11.9
 | 
			
		||||
	C12.1,12,12.1,12,12.1,11.9L12.1,11.9C12,11.9,12,11.9,12.1,11.9C12,11.9,12,11.9,12.1,11.9C12,12,12.1,12,12.1,11.9
 | 
			
		||||
	c0,0.1,0,0.1,0,0.2c0,0,0,0,0,0c0,0,0,0,0,0l0,0C12.1,12,12.1,12,12.1,11.9c0,0.1,0,0.1,0.1,0.3c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0.1,0.1,0.1,0.1c0,0,0,0,0,0.1l0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0.1,0.1,0.1,0.1c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0.1,0,0.1,0c0,0,0,0,0,0l0,0l0,0
 | 
			
		||||
	c0,0,0,0,0.1,0.1c0.1,0.1,0.1,0.1,0.2,0.2c0.2,0,0.3,0,0.3,0l0,0c0,0,0,0,0,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c-0.1-0.1-0.1-0.2-0.2-0.3C12.6,12.4,12.3,12.1,12.1,11.9 M10.5,10.7c-0.2-0.1-0.4-0.2-0.6-0.3l0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0.1,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0.1,0,0.1,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0.1,0,0.1,0c0,0,0,0,0,0c0,0,0.1,0,0.1,0.1C10.4,10.7,10.5,10.7,10.5,10.7C10.5,10.8,10.5,10.8,10.5,10.7
 | 
			
		||||
	c0.1,0.1,0.2,0.1,0.2,0.1l0,0c0,0,0.1,0,0.1,0.1c0,0,0,0,0,0l0,0h0l0,0h0c0,0,0,0,0,0l0,0h0l0,0l0,0c0,0,0,0,0.1,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0.1,0.1c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0.1,0.1,0.1,0.1c0,0,0,0,0.1,0c0,0,0,0,0,0c0,0,0.1,0,0.1,0.1l0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0l0,0l0,0l0,0l0,0l0,0l0,0l0,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0l0,0c0,0,0.1,0.1,0.1,0.1l0,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c-0.1-0.1-0.1-0.1-0.2-0.2c0,0,0,0,0.1,0c0,0,0.1,0.1,0.1,0.1c0,0,0,0,0,0l0,0c0,0,0.1,0.1,0.1,0.1c0,0,0.1,0.1,0.1,0.1l0,0l0,0l0,0
 | 
			
		||||
	c0,0,0.1,0.1,0.1,0.1c0,0,0,0,0,0c0,0,0,0-0.1-0.1l0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0c0,0-0.1-0.1-0.1-0.1c-0.1-0.1-0.2-0.2-0.3-0.3
 | 
			
		||||
	C11.1,11.1,10.8,10.9,10.5,10.7 M8.3,10.1L8.3,10.1C8.3,10.1,8.3,10.1,8.3,10.1L8.3,10.1L8.3,10.1C8.3,10.1,8.2,10.1,8.3,10.1
 | 
			
		||||
	L8.3,10.1C8.2,10.1,8.3,10.1,8.3,10.1L8.3,10.1L8.3,10.1C8.3,10.1,8.3,10.1,8.3,10.1L8.3,10.1L8.3,10.1L8.3,10.1L8.3,10.1
 | 
			
		||||
	C8.3,10.1,8.3,10.1,8.3,10.1C8.3,10.1,8.3,10.1,8.3,10.1L8.3,10.1C8.3,10.1,8.3,10.1,8.3,10.1 M8.2,10.1L8.2,10.1L8.2,10.1
 | 
			
		||||
	C8.2,10.1,8.2,10.1,8.2,10.1C8.2,10.1,8.2,10.1,8.2,10.1C8.2,10.1,8.2,10.1,8.2,10.1C8.2,10.1,8.2,10.1,8.2,10.1L8.2,10.1
 | 
			
		||||
	C8.2,10.1,8.2,10.1,8.2,10.1C8.2,10.1,8.2,10.1,8.2,10.1 M8.2,10.1C8.2,10.1,8.2,10.1,8.2,10.1L8.2,10.1L8.2,10.1L8.2,10.1
 | 
			
		||||
	C8.1,10.1,8.1,10.1,8.2,10.1L8.2,10.1L8.2,10.1L8.2,10.1L8.2,10.1C8.2,10.1,8.2,10.1,8.2,10.1C8.2,10.1,8.2,10.1,8.2,10.1 M8,10
 | 
			
		||||
	L8,10C7.9,10,7.9,10,8,10C7.9,10,8,10,8,10C8,10,8,10,8,10 M7.9,10C7.8,10,7.8,10,7.9,10C7.8,10,7.8,10,7.9,10L7.9,10
 | 
			
		||||
	C7.8,10,7.9,10,7.9,10C7.9,10,7.9,10,7.9,10 M7.8,10L7.8,10C7.8,10,7.7,10,7.8,10C7.8,10,7.8,10,7.8,10C7.7,10,7.7,10,7.8,10
 | 
			
		||||
	C7.7,10,7.7,10,7.8,10C7.8,10,7.8,10,7.8,10L7.8,10C7.8,10,7.8,10,7.8,10 M7.7,10L7.7,10L7.7,10C7.6,10,7.6,10,7.7,10
 | 
			
		||||
	C7.6,10,7.6,10,7.7,10C7.6,10,7.6,10,7.7,10C7.6,10,7.7,10,7.7,10L7.7,10 M7.9,10c-0.1,0-0.2,0-0.2,0h0c0,0,0.1,0,0.1,0
 | 
			
		||||
	C7.8,10,7.8,10,7.9,10 M7.6,10C7.6,10,7.5,10,7.6,10L7.6,10C7.6,10,7.6,10,7.6,10C7.6,10,7.6,10,7.6,10 M8,10L8,10L8,10L8,10L8,10
 | 
			
		||||
	 M9,10H8.7c0,0,0,0.1,0,0.1c0,0,0,0.1,0,0.1c0,0,0,0,0,0c0,0,0.1,0,0.1,0c0,0,0.2,0,0.2,0.1C9,10.2,9,10,9,10H8.7l0,0.1c0,0,0,0,0,0
 | 
			
		||||
	l0-0.1c0,0,0-0.1,0-0.1c0,0,0,0,0,0c0,0,0.2,0.1,0.2,0.1c0,0,0.2,0.1,0.2,0.1v0c0,0-0.2-0.1-0.2-0.1c0,0-0.2-0.1-0.2-0.1h0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0s0,0,0,0C8.5,10,9,10.1,9,10v0.1c-1,0-0.3,0-0.3,0c0,0-0.1-0.1-0.1-0.1c0,0-0.1-0.1-0.1-0.1c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0.1,0,0.1,0l0,0h0l0,0.1c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0-0.1,0c0,0,0,0,0.1,0l0,0c0,0,0-0.1,0-0.1l0-0.1h0.2
 | 
			
		||||
	C8.8,10,9,10.1,9,10L9,10H8.3c0,0-0.3,0.1-0.3,0.1v-0.1c0,0,0.5-0.1,0.5-0.1h0l0,0.1c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0-0.4,0-0.4,0v0.1l0,0l0,0v0l0,0l0,0l0,0l0.4,0h0l0,0h0c0,0-0.4,0-0.4,0l0,0v0.1v0l0,0v0v0l0,0c0,0,0.2-0.1,0.2-0.1
 | 
			
		||||
	c0,0,0.2,0,0.2,0h0c0,0,0,0.1,0,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0-0.1,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0-0.1,0h0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0-0.1,0-0.1,0c0,0-0.1,0-0.1,0V10h0.1l0,0l0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0-0.2,0-0.2,0v0v0l0,0v0l0,0v0v0c0,0,0.1,0,0.1,0c0,0,0,0,0,0c0,0-0.1,0-0.1,0c0,0,0,0,0,0v0v0v0h0h0c0,0,0,0,0,0h0
 | 
			
		||||
	c0,0,0,0,0,0h0v0c0,0,0,0,0,0c0,0,0,0,0,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0-0.1,0-0.1,0c0,0,0,0,0,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0h0c0,0,0,0,0,0h0c0,0,0,0,0,0h0c0,0,0,0,0,0c0,0-0.1,0-0.1,0h0c0,0,0,0,0,0c0,0,0,0-0.1,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0h0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0h0h0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0.5,0,1,0.5,1.4,1.2c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0-0.2,0-0.1c0,0,0-0.2,0-0.2c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0.2,0,0.2
 | 
			
		||||
	c0,0,0,0.1,0,0.1c0,0,0.1,0,0.1,0.1c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0.1,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0l0,0l0,0h0c0,0,0,0,0,0h0c0,0,0,0,0,0c0,0-0.2,0-0.2,0c0,0-0.2,0-0.2,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0.2,0,0.2,0c0,0,0.1,0,0.1,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0l0,0l0,0c0,0-0.2,0-0.2,0c0,0-0.2,0-0.2,0c0,0,0-0.2,0-0.2
 | 
			
		||||
	c0,0,0.5-0.2,0.5-0.2h0c0,0,0,0.2,0,0.2c0,0,0,0.1,0,0.1c0,0,0,0,0,0c0,0,0,0,0,0.1c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0.1,0c0,0,0.1,0,0.1,0c0,0,0.2,0,0.2,0c0,0,0,0,0,0c0,0-0.1,0-0.2,0c0,0,0.1,0,0,0
 | 
			
		||||
	c0,0,0.1,0,0.1,0c0,0,0-0.4,0-0.4s-0.3,0-0.3,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0.2,0,0.2c0,0,0-0.1,0-0.1c0,0,0-0.1,0-0.1h0l0,0.2
 | 
			
		||||
	c0,0,0,0.1,0,0.1c0,0,0,0,0,0c0,0,0,0-0.1,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0-0.1-0.1
 | 
			
		||||
	c0,0,0,0,0.1,0l0,0c0,0,0,0-0.1,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0c0,0,0.2,0,0.2,0c0,0-0.7,0,0.3,0v0c-1,0-0.3,0-0.3,0H9.6c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0l0,0c0,0,0,0,0,0c0,0-0.5,0-0.5,0v0l0,0v0l0,0v0h0.2H9v0l0.3,0c0,0,0.1,0,0.1,0c0,0,0.1,0,0.1,0c0,0,0,0,0,0L9,11v0
 | 
			
		||||
	c0,0,0.4,0,0.4,0c0,0,0,0,0,0s0,0,0,0s0,0,0,0s0,0,0,0s0,0,0,0s0,0,0,0s0,0,0,0s0,0,0,0l0,0c0,0,0,0-0.1,0l0,0l0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0l0,0l0,0c0,0,0,0-0.1,0c0,0,0,0,0.1,0c0,0,0,0.1,0,0.1c0,0,0,0.1-0.1,0.1c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0-0.3-0.1-0.3-0.1v-0.1c0,0,0.1,0,0.1-0.1c0,0,0.1,0,0.1,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0-0.1,0-0.1,0c0,0-0.2,0-0.2,0c0,0,0,0,0,0c0,0,0.2,0.3,0.2,0.3h0c0,0,0-0.2,0-0.2c0,0,0.1,0.1,0.1,0.1c0,0,0,0.1,0,0.1
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0-0.2,0-0.2c0,0,0-0.1,0-0.1c0,0-0.1,0-0.1,0c0,0-0.2,0-0.2,0V11c0,0,0.2,0,0.2,0c0,0,0.1-0.2,0-0.2
 | 
			
		||||
	c0,0,0-0.1,0-0.1c0,0,0-0.1,0.1,0c0,0,0,0.2,0,0.2l0,0.2c0,0,0,0,0,0c0,0,0-0.3,0-0.3l0-0.1l0-0.1c0,0,0-0.3,0-0.3C9,10.2,9,10,9,10
 | 
			
		||||
	C9,10,9,10,9,10L9,10.2c0,0,0,0.1-0.1,0.1l0,0c0,0,0.1,0.1,0.1,0.1c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0l0,0l0,0l0,0c0,0-0.1,0-0.1,0c0,0,0.1,0,0.1,0c0,0,0-0.1,0-0.1V10H8.8H9v0.1
 | 
			
		||||
	l0,0V10c0,0-0.1,0-0.1,0l0,0H9L9,10v0.2l-0.1,0c0,0-0.1-0.1-0.1-0.1c0,0-0.1-0.1-0.1-0.1"/>
 | 
			
		||||
<path style="fill:#B3E710;" d="M10.4,20.8c0,0,0.1-0.2,0.1-0.2c0,0-0.1,0-0.1,0c-0.1-0.2-0.1-0.2-0.1-0.2c0,0-0.1,0-0.1,0
 | 
			
		||||
	c0,0-0.1-0.1-0.1-0.1c0,0-0.1,0-0.2,0.1c0,0,0.1-0.1,0-0.1c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0-0.1-0.1c-0.2,0.8-0.5,1.5-0.8,2
 | 
			
		||||
	c0.1,0,0.1,0,0.1,0c0,0,0.1,0,0.1-0.1c0,0,0.1,0,0.1,0c0,0,0.1-0.1,0.1-0.1c0-0.1,0.2-0.1,0.2-0.2c0.1-0.1,0.1-0.1,0.2-0.2
 | 
			
		||||
	c0-0.1,0.1-0.1,0.2-0.3c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0.1,0,0.1-0.1c0,0,0.1-0.1,0.1-0.1c0.1-0.1,0.1-0.1,0.2-0.1
 | 
			
		||||
	C10.3,20.8,10.4,20.8,10.4,20.8 M12.8,20.3c0.1-0.1,0.1-0.2,0.2-0.3c0,0,0,0,0,0s0,0,0,0c0,0,0,0,0,0c-0.6,0.9-1.1,1.3-1.3,1.5
 | 
			
		||||
	c0.2-0.2,0.4-0.3,0.6-0.5C12.3,20.9,12.6,20.6,12.8,20.3 M9.5,20.1C9.5,20.1,9.5,20.1,9.5,20.1C9.5,20.1,9.5,20.1,9.5,20.1
 | 
			
		||||
	C9.5,20.1,9.5,20.1,9.5,20.1 M9.4,20C9.4,20,9.4,20,9.4,20c0,0-0.1,0-0.2,0c0,0,0,0,0,0s0,0,0,0s0,0,0,0s0,0,0,0s0,0,0,0s0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0.1C9.3,20.1,9.3,20.1,9.4,20C9.4,20.1,9.4,20.1,9.4,20 M8.1,18.8C8.1,18.8,8.1,18.8,8.1,18.8
 | 
			
		||||
	C8.1,18.8,8.1,18.8,8.1,18.8C8.1,18.8,8.1,18.8,8.1,18.8C8.1,18.8,8.1,18.8,8.1,18.8C8.1,18.8,8.1,18.9,8.1,18.8
 | 
			
		||||
	C8.1,18.9,8.1,18.9,8.1,18.8C8.1,18.9,8,18.9,8,18.9c0,0,0,0,0,0C8.1,18.9,8.1,18.9,8.1,18.8C8.1,18.9,8.1,18.9,8.1,18.8 M6.8,17.4
 | 
			
		||||
	L6.8,17.4c0-0.1-0.2-0.1-0.2-0.1c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0h0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0h0l0,0
 | 
			
		||||
	c0,0,0.1,0.1,0.1,0.1C6.7,17.5,6.7,17.4,6.8,17.4C6.8,17.4,6.8,17.4,6.8,17.4C6.8,17.4,6.8,17.4,6.8,17.4
 | 
			
		||||
	C6.8,17.4,6.8,17.4,6.8,17.4L6.8,17.4 M8.2,17.4C8.2,17.4,8.2,17.4,8.2,17.4c0,0-0.1-0.1-0.2,0c0,0,0,0,0,0.1
 | 
			
		||||
	C8.1,17.4,8.2,17.4,8.2,17.4C8.2,17.4,8.2,17.4,8.2,17.4 M7.9,17.4C7.9,17.4,7.9,17.4,7.9,17.4c-0.1-0.1-0.2-0.1-0.2-0.1
 | 
			
		||||
	c-0.1,0-0.1,0-0.1,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0s0,0-0.1,0c0,0,0,0-0.1,0c-0.1,0-0.1,0-0.2,0l0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0.1,0,0.1,0c0,0,0,0,0,0c0,0,0,0,0,0.1c0,0,0,0,0,0c-0.1,0-0.2,0-0.3,0c0,0,0,0,0,0l0,0c0,0,0,0,0.1,0.1c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0.1,0c0.1,0,0.1,0,0.2,0c0,0,0,0.1,0.1,0.1c0-0.1,0-0.1,0.1-0.1c0,0,0,0,0,0c0,0,0.1,0,0.1,0c0,0,0,0,0,0
 | 
			
		||||
	C7.8,17.4,7.8,17.4,7.9,17.4C7.8,17.4,7.8,17.4,7.9,17.4C7.9,17.4,7.9,17.4,7.9,17.4 M7.1,17.1L7.1,17.1C7,17.1,7,17.1,6.9,17.1
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0h0h0h0h0h0h0l0,0l0,0c-0.1,0-0.2,0-0.2-0.1c0,0,0-0.1-0.1-0.1c0,0-0.1,0-0.1,0c-0.1,0-0.1,0-0.2-0.1
 | 
			
		||||
	c0,0,0,0,0,0c-0.1,0-0.1,0-0.2,0c0,0,0,0,0,0h0c0,0,0,0,0,0c0,0,0,0,0,0h0c0,0,0,0,0,0h0c0,0,0,0,0,0c-0.1,0-0.3,0-0.3,0.1
 | 
			
		||||
	c0,0,0,0,0,0l0,0c0,0,0.1,0,0.1,0c0.2-0.1,0.2-0.1,0.3-0.1c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0.1,0,0.1,0,0.2,0c0.1,0,0.1,0,0.1,0.1c0,0,0,0,0,0c0.1,0,0.1,0,0.1,0.1c0.1,0,0.1,0,0.2,0.1c0,0,0,0.1-0.1,0.1
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0.1,0,0.1,0c0.1,0,0.1,0,0.2,0C7,17.2,7.1,17.2,7.1,17.1 M7,15L7,15C7,15,6.9,14.9,7,15C6.9,14.9,6.9,14.9,7,15
 | 
			
		||||
	C6.9,15,7,15,7,15 M6.9,14.9C6.9,14.9,6.9,14.9,6.9,14.9L6.9,14.9L6.9,14.9 M6.9,14.9C6.9,14.9,6.9,14.9,6.9,14.9L6.9,14.9
 | 
			
		||||
	C6.9,14.9,6.9,14.9,6.9,14.9 M6.9,14.9C6.9,14.9,6.9,14.9,6.9,14.9L6.9,14.9C6.9,14.9,6.9,14.9,6.9,14.9 M6.9,14.9
 | 
			
		||||
	C6.9,14.9,6.9,14.9,6.9,14.9L6.9,14.9C6.9,14.9,6.9,14.9,6.9,14.9 M8.9,13.6C8.9,13.6,8.9,13.6,8.9,13.6C8.9,13.6,8.8,13.5,8.9,13.6
 | 
			
		||||
	c-0.1,0-0.1,0-0.1,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0-0.1,0-0.1c0,0,0,0-0.1,0c0,0,0,0,0,0c0,0,0,0.1-0.1,0.1c0,0,0,0,0,0.1
 | 
			
		||||
	C8.7,13.6,8.8,13.6,8.9,13.6C8.8,13.6,8.9,13.6,8.9,13.6 M2.7,13.3L2.7,13.3C2.7,13.2,2.7,13.2,2.7,13.3C2.7,13.2,2.7,13.2,2.7,13.3
 | 
			
		||||
	c0-0.1,0-0.1,0-0.1c0,0,0-0.1,0-0.1c0,0,0,0,0,0c0,0,0,0-0.1,0c0,0,0,0,0,0l0,0c0,0,0,0.1,0,0.1c0,0,0,0,0,0c0,0,0,0,0,0.1l0,0
 | 
			
		||||
	c0,0,0,0,0,0C2.6,13.2,2.6,13.2,2.7,13.3C2.6,13.2,2.6,13.2,2.7,13.3C2.7,13.2,2.6,13.2,2.7,13.3C2.6,13.2,2.6,13.2,2.7,13.3
 | 
			
		||||
	C2.6,13.2,2.7,13.3,2.7,13.3C2.7,13.3,2.7,13.3,2.7,13.3C2.7,13.3,2.7,13.3,2.7,13.3 M9.3,19.8C9.3,19.8,9.3,19.8,9.3,19.8
 | 
			
		||||
	c-0.1-0.1-0.1-0.1-0.1-0.3c0,0-0.4-0.3-0.5-0.2c0,0-0.1,0-0.1,0c0,0,0,0,0,0l0,0c-0.1-0.1-0.3-0.1-0.4-0.1c0,0-0.3,0-0.3,0
 | 
			
		||||
	c0,0,0,0,0,0c0-0.2,0.1-0.2-0.1-0.2c0,0,0.1,0,0.1,0C8,19,8.1,19,8.1,19c-0.1,0,0,0-0.1-0.1c0,0,0,0-0.1,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0-0.1c0,0,0,0,0,0c0,0,0.1,0,0.1,0l0,0l0,0c-0.2,0-0.3,0-0.5,0c-0.1,0-0.3-0.1-0.4-0.1c0,0,0-0.1,0-0.1c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0l0,0l0,0c0,0,0,0-0.2,0.1c0,0,0,0,0,0c0,0.1,0.1,0.1,0.1,0.2c0,0,0,0-0.1,0c-0.1-0.1-0.1-0.1,0-0.2c0-0.1,0-0.1,0-0.1
 | 
			
		||||
	c0,0,0.1,0,0.1,0c0,0,0.1,0,0.1,0c0,0-0.1,0-0.1,0c0,0,0,0,0,0c0,0-0.3,0.3-0.3,0.3c0,0,0,0,0,0c0,0,0-0.3-0.2-0.4
 | 
			
		||||
	c-0.1,0-0.2,0-0.3,0C6,18.5,5.8,18.2,5.9,18c0,0,0-0.1,0-0.2c0,0-0.1,0-0.1,0c0-0.1-0.2,0-0.2-0.1c-0.1,0-0.3,0-0.3,0c0,0,0,0,0,0
 | 
			
		||||
	l0,0l0,0c0.1-0.1,0.1-0.1,0.1-0.3c0,0,0,0,0-0.1l0,0c0,0,0,0,0-0.1l0,0c0,0,0,0,0,0.1c0-0.1,0-0.1,0-0.2c0,0,0,0,0,0l0,0l0-0.1
 | 
			
		||||
	C5.5,17,5.5,17,5.5,17c-0.2-0.1-0.3,0-0.4,0c-0.1,0,0,0-0.1,0c0,0.1,0,0-0.1,0c0,0,0,0,0,0c0,0,0,0.2,0,0.2c0,0,0,0.1,0,0.1
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c-0.1,0-0.3,0-0.4,0C4.2,17,4.2,17,4.2,16.9c0-0.3,0.1-0.8,0.1-0.8l0-0.1c0,0,0,0,0,0c0,0,0.1,0,0.1,0
 | 
			
		||||
	c0,0,0,0,0,0c0.1,0,0.1,0,0.2-0.1c0,0,0,0,0-0.1l0,0.1c0.1,0,0.2,0.1,0.3,0.1c0,0,0,0,0,0l0-0.1l0,0l0,0c0,0,0.1,0.1,0.1,0.1
 | 
			
		||||
	C5,16,5.1,16,5.1,16c0,0,0,0,0,0L5,15.9c0,0,0,0,0,0l0.1,0l0,0l0,0c0,0,0.1,0.1,0.1,0.1c0,0,0,0-0.1-0.1c0,0,0,0,0,0c0,0,0,0-0.1,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0.1,0c0.1,0,0.1,0,0.2,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0.1,0.1c0,0,0,0,0,0c0.1,0,0.1,0,0.1,0
 | 
			
		||||
	c0.2,0.1,0.2,0.1,0.2,0.1c0,0,0,0,0.2,0.1C6,16,6,16,6,16.1c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0.1,0,0.1
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0.1c0,0.1,0.1,0.1,0.1,0.2c0,0,0.1-0.3,0.1-0.3c0,0,0-0.2,0-0.2c0,0,0,0,0,0c0.1,0-0.2-0.3-0.1-0.6
 | 
			
		||||
	c0.1-0.2,0.4-0.2,0.6-0.4l0,0.1l0,0l0,0l0,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0.1,0,0.1,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0-0.1,0-0.1,0c0,0,0.1,0,0.1,0
 | 
			
		||||
	c0,0,0.1,0,0.1,0v0l-0.1,0l0,0l0,0l0.1,0l0,0v0l0,0c0,0,0,0.1,0,0.1c0,0,0,0,0,0c0,0,0-0.1,0-0.1c0,0,0,0,0,0l0,0l0,0l0,0.1
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0-0.1,0-0.1c0,0-0.1-0.1-0.1-0.1l0,0c0,0,0,0,0.1,0c0,0,0,0,0,0c0,0,0,0.1,0,0.1c0,0,0,0.1,0,0.1c0,0,0,0,0,0l0,0
 | 
			
		||||
	l0,0c0,0,0,0,0,0l0-0.1c0,0,0-0.1-0.1-0.1c0,0,0,0,0,0c0,0,0,0,0,0c0,0-0.1,0-0.1-0.1c0,0,0.1,0,0.1,0l0.1,0c0,0,0,0.4,0,0.4
 | 
			
		||||
	s-0.2,0-0.2,0c0,0,0-0.2,0-0.2c0,0,0-0.1,0-0.1c0,0,0,0.2,0.1,0.2c0,0,0,0.1,0,0.1c0,0,0,0,0,0h0c0,0,0,0,0,0c0,0,0-0.3,0-0.3
 | 
			
		||||
	c0,0,0-0.1,0-0.1l0,0.2l0,0.2c0,0,0,0,0,0l0-0.3l0-0.1l0-0.1l0,0.2l0,0.3h0l0-0.2l0-0.2c0,0,0,0.5,0,0.5s-0.1,0-0.1,0l0-0.2l0.1-0.1
 | 
			
		||||
	l0-0.1c0,0,0,0,0,0l0,0l0,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0l0,0l0,0l0,0l0,0l0,0l0,0c0,0,0,0.3,0,0.3s0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0-0.1,0,0l0-0.1c0,0,0-0.1,0-0.1C7,14.7,7,14.4,7,14.3c0,0,0-0.3,0-0.3h0l0,0.5C7,14.5,7,15,7,15c0,0-0.1,0-0.1,0
 | 
			
		||||
	s0-0.4,0-0.4l0,0c0,0,0.1-0.1,0.1-0.1c0-0.1,0.1-0.1,0-0.2l0,0c0,0,0,0,0,0c0.1,0,0.2-0.1,0.3-0.1c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0.2,0,0.2,0c0,0,0.1,0,0.1,0c0,0,0,0,0,0L7.7,14
 | 
			
		||||
	c0,0,0.3,0,0.3,0s0,0.2,0,0.2l-0.2,0l-0.1,0c0,0-0.1-0.1-0.1-0.1c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0.1,0,0.1,0c0-0.1,0.2-0.1,0.2-0.1
 | 
			
		||||
	c0,0,0,0,0,0l-0.1,0l-0.1,0.1L7.7,14c0,0,0,0,0,0l0-0.1l0,0l0,0l0,0l0,0c0,0,0,0.1,0,0c0,0,0,0.1,0,0.1h0l0-0.1l0,0l0,0l0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0.1,0.1,0.1c0,0,0,0.1,0,0.1c0,0,0,0,0,0l0-0.1l0-0.1l0,0c0,0,0.1,0,0.1,0
 | 
			
		||||
	c0,0,0,0,0,0l0,0c0,0,0.1,0,0.1-0.1l0,0.2l0,0.2c0,0,0,0,0,0c0,0,0-0.2-0.1-0.1c0,0,0-0.1,0-0.1c0.1,0,0.2-0.1,0.2-0.1
 | 
			
		||||
	c0,0-0.1,0-0.1,0c0,0-0.2,0-0.2,0l-0.2,0V14h0.2l0,0l0,0c0,0,0.1,0,0.1,0l0,0l0,0l0,0l0,0c0,0,0,0-0.1,0c0,0,0,0,0,0L8,13.8
 | 
			
		||||
	c0,0,0,0,0,0l0.1,0c0,0,0,0,0,0l0,0l0,0c0,0,0,0,0,0.1c0,0,0,0,0,0c0,0,0,0,0.1,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0
 | 
			
		||||
	c0,0,0.1-0.1,0.1-0.1c0,0,0,0.1,0,0.1l0,0.1c0,0,0,0,0,0l-0.2-0.1L8,13.8v0c0,0,0.3,0,0.3,0l0.1,0l0.1,0l0,0c0.1,0,0.1,0,0.2-0.1
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0l0,0l0,0c0,0,0,0-0.1,0l0,0l0,0l0,0c0,0-0.1,0-0.1,0c0,0,0,0.2-0.1,0.2c0,0,0,0.2,0,0.2
 | 
			
		||||
	c0,0,0,0,0,0l0-0.2c0,0-0.1-0.1-0.1-0.1l0,0.2c0,0,0,0.1,0,0.1c0,0,0,0,0,0S8,14,8,14s0-0.4,0-0.4l0.2,0l-0.1,0
 | 
			
		||||
	c-0.1,0-0.2,0-0.2-0.1c0,0,0,0,0,0c0,0,0.1,0,0.1,0c0,0,0.1-0.1,0.1-0.1l-0.1,0l-0.1,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0-0.1,0.3-0.1,0.3-0.1c-0.2-0.2-0.6,0.1-0.7,0.3C7.8,13,8.2,13,8.6,13c0.1,0,0.2,0,0.3,0c-0.5,0-1,0-1.5,0c-0.3,0-0.5,0-0.8,0
 | 
			
		||||
	c-1.7,0-3.1,0-3.9,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0.3-0.1,0.4c0,0,0,0,0,0c0,0,0,0,0-0.1c0,0,0,0,0,0
 | 
			
		||||
	c0,0-0.1,0-0.1,0c0,0,0,0.1,0,0.1c0,0,0,0,0,0l0,0l0,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0.1-0.1,0.2-0.2,0.4c0,0.1,0,0.1-0.2,0.6c0,0,0,0,0,0c0,0,0,0,0,0l0,0.1l0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0.1,0,0.1c0,0,0,0,0,0l0,0c0,0.1,0.1,0.3,0,0.4c0,0,0.1,0,0.1,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0
 | 
			
		||||
	c0,0,0.3,0,0.3,0c0,0.1-0.7,0.1,0.3,0.2c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c-1,0-1,0-1,0c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0c1,0,0.4,0.6,0.4,0.6c0,0,0.1,0,0.1,0c0,0,0.3,0.5,0.5,0.7c0,0,0.1-0.1,0.1-0.1c0,0,0,0,0-0.1c0,0,0,0,0,0
 | 
			
		||||
	c0-0.1-0.1-0.4-0.1-0.4c0,0,0,0,0,0c0,0,0-0.1,0-0.1c0,0,0,0,0,0c0-0.2-0.1-0.3-0.2-0.5c0,0,0-0.1,0-0.1c0,0,0.1,0,0.1,0l0,0
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0C2.8,16,2.8,16,3.1,16.3c0,0.1,0,0.1,0,0.1l0,0c0.1,0.2,0.2,0.3,0.3,0.5c0,0,0,0,0,0c0,0.1,0,0.1,0,0.2
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0,0,0,0C3.5,17.3,4,17.6,4,17.6c0.1,0,0.2,0.1,0.3,0.1c0.1,0,0.2-0.1,0.4,0c0.2,0.1,0.2,0.2,0.4,0.3L5.4,18
 | 
			
		||||
	c0,0,0,0,0-0.1c0,0,0,0,0.1,0c0,0,0,0,0,0c0,0,0,0,0,0c0.1,0.1,0.2,0.2,0.3,0.2l0,0l0,0l0,0c0,0,0,0,0,0c0,0,0,0.1,0.1,0.1l0.1,0
 | 
			
		||||
	c0,0,0.1,0,0.1-0.1c0,0,0,0,0,0c0,0,0.1,0.1,0.1,0.1c0,0-0.1,0-0.1,0c0,0,0,0,0,0l-0.1,0v0.2C6,18.8,6,19,6,19c0,0,0,0,0,0
 | 
			
		||||
	c0.1,0,0.1,0,0.2,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0-0.1,0-0.1c0,0,0.1-0.1,0.1-0.1c0,0-0.2-0.1-0.2-0.1c0,0-0.2-0.1-0.2-0.1
 | 
			
		||||
	c0,0,0,0,0,0c0,0,0.2,0,0.3,0c0,0,0.1,0,0.1,0c0.1,0,0.2,0,0.2,0.1c0,0,0.1,0.2,0.1,0.2c0,0,0,0.2,0.1,0.2c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0-0.1,0-0.1c0,0-0.1-0.1-0.2-0.1c0,0,0,0,0,0c0,0,0,0,0-0.1c-0.2,0-0.1,0.2-0.2,0.3c0,0,0,0,0,0c0,0,0,0,0,0l0,0
 | 
			
		||||
	c0,0,0,0-0.1-0.1c0,0,0,0,0,0c0,0,0,0.1,0,0.1c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0.1,0,0.1c0.1,0.1,0.1,0.3,0.1,0.3c0,0,0,0,0,0
 | 
			
		||||
	c0,0,0,0,0,0.1c0,0-0.1,0-0.1,0c0,0,0,0,0,0.1c0,0,0,0,0,0c0,0-0.1,0-0.1,0.1c0,0,0,0,0,0.1c0.4,0,0.9,0,1.4,0c0.6,0,1.2,0,1.7,0
 | 
			
		||||
	C9.1,20,9.3,19.9,9.3,19.8"/>
 | 
			
		||||
<linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="7.4653" y1="9.0342" x2="7.4653" y2="23.9663">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_8_);" d="M7.5,9C3.3,9,0,12.4,0,16.5C0,20.6,3.3,24,7.5,24c4.1,0,7.5-3.3,7.5-7.5
 | 
			
		||||
	C14.9,12.4,11.6,9,7.5,9z M13.7,18.6c-0.1,0.3-0.2,0.6-0.4,0.9s-0.3,0.6-0.5,0.9s-0.4,0.5-0.6,0.8s-0.5,0.5-0.8,0.6
 | 
			
		||||
	c-0.3,0.2-0.6,0.4-0.9,0.5c-0.3,0.2-0.6,0.3-0.9,0.4s-0.7,0.2-1,0.2C8.2,23,7.9,23,7.5,23C1,22.7-0.9,15.2,3.3,11.6
 | 
			
		||||
	C5,10.1,6.8,10,7.5,10c0.4,0,0.7,0,1.1,0.1c0.3,0.1,0.7,0.1,1,0.2s0.6,0.2,0.9,0.4c0.3,0.2,0.6,0.3,0.9,0.5c0.3,0.2,0.5,0.4,0.8,0.6
 | 
			
		||||
	c0.2,0.2,0.5,0.5,0.6,0.8c0.2,0.3,0.4,0.6,0.5,0.9s0.3,0.6,0.4,0.9s0.2,0.7,0.2,1c0.1,0.3,0.1,0.7,0.1,1.1c0,0.4,0,0.7-0.1,1.1
 | 
			
		||||
	C13.9,17.9,13.8,18.2,13.7,18.6z"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 52 KiB  | 
							
								
								
									
										228
									
								
								src/assets/img/mod/wiki.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,228 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In  -->
 | 
			
		||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
 | 
			
		||||
	<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
 | 
			
		||||
]>
 | 
			
		||||
<svg version="1.1"
 | 
			
		||||
	 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
 | 
			
		||||
	 x="0px" y="0px" width="24px" height="24px" viewBox="0 0 24 24" style="overflow:visible;enable-background:new 0 0 24 24;"
 | 
			
		||||
	 xml:space="preserve" preserveAspectRatio="xMinYMid meet">
 | 
			
		||||
<defs>
 | 
			
		||||
</defs>
 | 
			
		||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="18" y1="13" x2="18" y2="20.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#8E8E8E"/>
 | 
			
		||||
	<stop  offset="4.191053e-02" style="stop-color:#8A8A8A"/>
 | 
			
		||||
	<stop  offset="0.4613" style="stop-color:#626262"/>
 | 
			
		||||
	<stop  offset="0.7952" style="stop-color:#4A4A4A"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#414141"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#8E8E8E"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#8E8E8E"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#414141"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="12" y="13" style="fill:url(#SVGID_1_);" width="12" height="7"/>
 | 
			
		||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="17.5" y1="14" x2="17.5" y2="19">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#F7F7F7"/>
 | 
			
		||||
	<stop  offset="0.1044" style="stop-color:#FCFCFC"/>
 | 
			
		||||
	<stop  offset="0.3293" style="stop-color:#FFFFFF"/>
 | 
			
		||||
	<stop  offset="0.5692" style="stop-color:#E8E8E8"/>
 | 
			
		||||
	<stop  offset="0.8153" style="stop-color:#D7D7D7"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#D1D1D1"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#F7F7F7"/>
 | 
			
		||||
	<a:midPointStop  offset="0.2222" style="stop-color:#F7F7F7"/>
 | 
			
		||||
	<a:midPointStop  offset="0.3293" style="stop-color:#FFFFFF"/>
 | 
			
		||||
	<a:midPointStop  offset="0.3545" style="stop-color:#FFFFFF"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#D1D1D1"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="12" y="14" style="fill:url(#SVGID_2_);" width="11" height="5"/>
 | 
			
		||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="17" y1="15" x2="17" y2="18">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#8E8E8E"/>
 | 
			
		||||
	<stop  offset="4.191053e-02" style="stop-color:#8A8A8A"/>
 | 
			
		||||
	<stop  offset="0.4613" style="stop-color:#626262"/>
 | 
			
		||||
	<stop  offset="0.7952" style="stop-color:#4A4A4A"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#414141"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#8E8E8E"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#8E8E8E"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#414141"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="12" y="15" style="fill:url(#SVGID_3_);" width="10" height="3"/>
 | 
			
		||||
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="16.5" y1="0" x2="16.5" y2="24.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#8E8E8E"/>
 | 
			
		||||
	<stop  offset="4.191053e-02" style="stop-color:#8A8A8A"/>
 | 
			
		||||
	<stop  offset="0.4613" style="stop-color:#626262"/>
 | 
			
		||||
	<stop  offset="0.7952" style="stop-color:#4A4A4A"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#414141"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#8E8E8E"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#8E8E8E"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#414141"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="13" style="fill:url(#SVGID_4_);" width="7" height="24"/>
 | 
			
		||||
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="16.5" y1="1" x2="16.5" y2="23.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#F7F7F7"/>
 | 
			
		||||
	<stop  offset="0.1044" style="stop-color:#FCFCFC"/>
 | 
			
		||||
	<stop  offset="0.3293" style="stop-color:#FFFFFF"/>
 | 
			
		||||
	<stop  offset="0.5692" style="stop-color:#E8E8E8"/>
 | 
			
		||||
	<stop  offset="0.8153" style="stop-color:#D7D7D7"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#D1D1D1"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#F7F7F7"/>
 | 
			
		||||
	<a:midPointStop  offset="0.2222" style="stop-color:#F7F7F7"/>
 | 
			
		||||
	<a:midPointStop  offset="0.3293" style="stop-color:#FFFFFF"/>
 | 
			
		||||
	<a:midPointStop  offset="0.3545" style="stop-color:#FFFFFF"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#D1D1D1"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="14" y="1" style="fill:url(#SVGID_5_);" width="5" height="22"/>
 | 
			
		||||
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="16.5" y1="2" x2="16.5" y2="22.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#8E8E8E"/>
 | 
			
		||||
	<stop  offset="4.191053e-02" style="stop-color:#8A8A8A"/>
 | 
			
		||||
	<stop  offset="0.4613" style="stop-color:#626262"/>
 | 
			
		||||
	<stop  offset="0.7952" style="stop-color:#4A4A4A"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#414141"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#8E8E8E"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#8E8E8E"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#414141"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="15" y="2" style="fill:url(#SVGID_6_);" width="3" height="20"/>
 | 
			
		||||
<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="18" y1="4" x2="18" y2="11">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#8E8E8E"/>
 | 
			
		||||
	<stop  offset="4.191053e-02" style="stop-color:#8A8A8A"/>
 | 
			
		||||
	<stop  offset="0.4613" style="stop-color:#626262"/>
 | 
			
		||||
	<stop  offset="0.7952" style="stop-color:#4A4A4A"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#414141"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#8E8E8E"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#8E8E8E"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#414141"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="12" y="4" style="fill:url(#SVGID_7_);" width="12" height="7"/>
 | 
			
		||||
<linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="17.5" y1="5" x2="17.5" y2="10">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#F7F7F7"/>
 | 
			
		||||
	<stop  offset="0.1044" style="stop-color:#FCFCFC"/>
 | 
			
		||||
	<stop  offset="0.3293" style="stop-color:#FFFFFF"/>
 | 
			
		||||
	<stop  offset="0.5692" style="stop-color:#E8E8E8"/>
 | 
			
		||||
	<stop  offset="0.8153" style="stop-color:#D7D7D7"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#D1D1D1"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#F7F7F7"/>
 | 
			
		||||
	<a:midPointStop  offset="0.2222" style="stop-color:#F7F7F7"/>
 | 
			
		||||
	<a:midPointStop  offset="0.3293" style="stop-color:#FFFFFF"/>
 | 
			
		||||
	<a:midPointStop  offset="0.3545" style="stop-color:#FFFFFF"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#D1D1D1"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="12" y="5" style="fill:url(#SVGID_8_);" width="11" height="5"/>
 | 
			
		||||
<linearGradient id="SVGID_9_" gradientUnits="userSpaceOnUse" x1="17" y1="6" x2="17" y2="9">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#8E8E8E"/>
 | 
			
		||||
	<stop  offset="4.191053e-02" style="stop-color:#8A8A8A"/>
 | 
			
		||||
	<stop  offset="0.4613" style="stop-color:#626262"/>
 | 
			
		||||
	<stop  offset="0.7952" style="stop-color:#4A4A4A"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#414141"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#8E8E8E"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#8E8E8E"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#414141"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="12" y="6" style="fill:url(#SVGID_9_);" width="10" height="3"/>
 | 
			
		||||
<linearGradient id="SVGID_10_" gradientUnits="userSpaceOnUse" x1="6" y1="4" x2="6" y2="11">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#8E8E8E"/>
 | 
			
		||||
	<stop  offset="4.191053e-02" style="stop-color:#8A8A8A"/>
 | 
			
		||||
	<stop  offset="0.4613" style="stop-color:#626262"/>
 | 
			
		||||
	<stop  offset="0.7952" style="stop-color:#4A4A4A"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#414141"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#8E8E8E"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#8E8E8E"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#414141"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect y="4" style="fill:url(#SVGID_10_);" width="12" height="7"/>
 | 
			
		||||
<linearGradient id="SVGID_11_" gradientUnits="userSpaceOnUse" x1="6.5" y1="5" x2="6.5" y2="10">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#F7F7F7"/>
 | 
			
		||||
	<stop  offset="0.1044" style="stop-color:#FCFCFC"/>
 | 
			
		||||
	<stop  offset="0.3293" style="stop-color:#FFFFFF"/>
 | 
			
		||||
	<stop  offset="0.5692" style="stop-color:#E8E8E8"/>
 | 
			
		||||
	<stop  offset="0.8153" style="stop-color:#D7D7D7"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#D1D1D1"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#F7F7F7"/>
 | 
			
		||||
	<a:midPointStop  offset="0.2222" style="stop-color:#F7F7F7"/>
 | 
			
		||||
	<a:midPointStop  offset="0.3293" style="stop-color:#FFFFFF"/>
 | 
			
		||||
	<a:midPointStop  offset="0.3545" style="stop-color:#FFFFFF"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#D1D1D1"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="1" y="5" style="fill:url(#SVGID_11_);" width="11" height="5"/>
 | 
			
		||||
<linearGradient id="SVGID_12_" gradientUnits="userSpaceOnUse" x1="7" y1="6" x2="7" y2="9">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#8E8E8E"/>
 | 
			
		||||
	<stop  offset="4.191053e-02" style="stop-color:#8A8A8A"/>
 | 
			
		||||
	<stop  offset="0.4613" style="stop-color:#626262"/>
 | 
			
		||||
	<stop  offset="0.7952" style="stop-color:#4A4A4A"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#414141"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#8E8E8E"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#8E8E8E"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#414141"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="2" y="6" style="fill:url(#SVGID_12_);" width="10" height="3"/>
 | 
			
		||||
<linearGradient id="SVGID_13_" gradientUnits="userSpaceOnUse" x1="7.5" y1="0" x2="7.5" y2="24.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#8E8E8E"/>
 | 
			
		||||
	<stop  offset="4.191053e-02" style="stop-color:#8A8A8A"/>
 | 
			
		||||
	<stop  offset="0.4613" style="stop-color:#626262"/>
 | 
			
		||||
	<stop  offset="0.7952" style="stop-color:#4A4A4A"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#414141"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#8E8E8E"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#8E8E8E"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#414141"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="4" style="fill:url(#SVGID_13_);" width="7" height="24"/>
 | 
			
		||||
<linearGradient id="SVGID_14_" gradientUnits="userSpaceOnUse" x1="7.5" y1="1" x2="7.5" y2="23.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#F7F7F7"/>
 | 
			
		||||
	<stop  offset="0.1044" style="stop-color:#FCFCFC"/>
 | 
			
		||||
	<stop  offset="0.3293" style="stop-color:#FFFFFF"/>
 | 
			
		||||
	<stop  offset="0.5692" style="stop-color:#E8E8E8"/>
 | 
			
		||||
	<stop  offset="0.8153" style="stop-color:#D7D7D7"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#D1D1D1"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#F7F7F7"/>
 | 
			
		||||
	<a:midPointStop  offset="0.2222" style="stop-color:#F7F7F7"/>
 | 
			
		||||
	<a:midPointStop  offset="0.3293" style="stop-color:#FFFFFF"/>
 | 
			
		||||
	<a:midPointStop  offset="0.3545" style="stop-color:#FFFFFF"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#D1D1D1"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="5" y="1" style="fill:url(#SVGID_14_);" width="5" height="22"/>
 | 
			
		||||
<linearGradient id="SVGID_15_" gradientUnits="userSpaceOnUse" x1="7.5" y1="2" x2="7.5" y2="22.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#8E8E8E"/>
 | 
			
		||||
	<stop  offset="4.191053e-02" style="stop-color:#8A8A8A"/>
 | 
			
		||||
	<stop  offset="0.4613" style="stop-color:#626262"/>
 | 
			
		||||
	<stop  offset="0.7952" style="stop-color:#4A4A4A"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#414141"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#8E8E8E"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#8E8E8E"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#414141"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="6" y="2" style="fill:url(#SVGID_15_);" width="3" height="20"/>
 | 
			
		||||
<linearGradient id="SVGID_16_" gradientUnits="userSpaceOnUse" x1="6" y1="13" x2="6" y2="20.0005">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#8E8E8E"/>
 | 
			
		||||
	<stop  offset="4.191053e-02" style="stop-color:#8A8A8A"/>
 | 
			
		||||
	<stop  offset="0.4613" style="stop-color:#626262"/>
 | 
			
		||||
	<stop  offset="0.7952" style="stop-color:#4A4A4A"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#414141"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#8E8E8E"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#8E8E8E"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#414141"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect y="13" style="fill:url(#SVGID_16_);" width="12" height="7"/>
 | 
			
		||||
<linearGradient id="SVGID_17_" gradientUnits="userSpaceOnUse" x1="6.5" y1="14" x2="6.5" y2="19">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#F7F7F7"/>
 | 
			
		||||
	<stop  offset="0.1044" style="stop-color:#FCFCFC"/>
 | 
			
		||||
	<stop  offset="0.3293" style="stop-color:#FFFFFF"/>
 | 
			
		||||
	<stop  offset="0.5692" style="stop-color:#E8E8E8"/>
 | 
			
		||||
	<stop  offset="0.8153" style="stop-color:#D7D7D7"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#D1D1D1"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#F7F7F7"/>
 | 
			
		||||
	<a:midPointStop  offset="0.2222" style="stop-color:#F7F7F7"/>
 | 
			
		||||
	<a:midPointStop  offset="0.3293" style="stop-color:#FFFFFF"/>
 | 
			
		||||
	<a:midPointStop  offset="0.3545" style="stop-color:#FFFFFF"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#D1D1D1"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="1" y="14" style="fill:url(#SVGID_17_);" width="11" height="5"/>
 | 
			
		||||
<linearGradient id="SVGID_18_" gradientUnits="userSpaceOnUse" x1="7" y1="15" x2="7" y2="18">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#8E8E8E"/>
 | 
			
		||||
	<stop  offset="4.191053e-02" style="stop-color:#8A8A8A"/>
 | 
			
		||||
	<stop  offset="0.4613" style="stop-color:#626262"/>
 | 
			
		||||
	<stop  offset="0.7952" style="stop-color:#4A4A4A"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#414141"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#8E8E8E"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#8E8E8E"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#414141"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<rect x="2" y="15" style="fill:url(#SVGID_18_);" width="10" height="3"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 13 KiB  | 
							
								
								
									
										98
									
								
								src/assets/img/mod/workshop.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,98 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In  -->
 | 
			
		||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
 | 
			
		||||
	<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
 | 
			
		||||
]>
 | 
			
		||||
<svg version="1.1"
 | 
			
		||||
	 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
 | 
			
		||||
	 x="0px" y="0px" width="24px" height="24px" viewBox="0 0 24 24" style="overflow:visible;enable-background:new 0 0 24 24;"
 | 
			
		||||
	 xml:space="preserve" preserveAspectRatio="xMinYMid meet">
 | 
			
		||||
<defs>
 | 
			
		||||
</defs>
 | 
			
		||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="7.9995" y1="7.9868" x2="7.9995" y2="24.001">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#F0A829"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#C7671A"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#F0A829"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#F0A829"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#C7671A"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_1_);" d="M3,19.2l-3,1.6V24h16v-0.4c0-0.5-0.5-1.2-1-1.5l-6-3.1c-0.5-0.3-0.6-0.8-0.3-1.2
 | 
			
		||||
	c0,0,1.6-2,1.6-4.2C10.4,10.5,8.4,8,6,8c-2.4,0-4.4,2.6-4.4,5.7c0,2.1,1.6,4.2,1.6,4.2C3.6,18.3,3.5,18.9,3,19.2z"/>
 | 
			
		||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="7.7212" y1="8.9868" x2="7.7212" y2="23.001">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#FFEBA8"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#F8BE27"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#FFEBA8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#FFEBA8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#F8BE27"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_2_);" d="M1,23v-1.7L3.5,20c0.5-0.3,0.8-0.7,1-1.2c0.1-0.5,0-1.1-0.4-1.5c0,0-1.4-1.8-1.4-3.6
 | 
			
		||||
	C2.6,11.1,4.2,9,6,9s3.4,2.1,3.4,4.7c0,1.7-1.4,3.5-1.4,3.6c-0.3,0.4-0.5,1-0.4,1.5c0.1,0.5,0.5,1,1,1.2l5.9,3H1z"/>
 | 
			
		||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="6.1343" y1="9.9868" x2="6.1343" y2="22.001">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#F5AE0D"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#FFC30F"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#F5AE0D"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_3_);" d="M2,22L2,22l1.9-1.1c0.8-0.4,1.3-1.1,1.5-1.9c0.2-0.8,0-1.7-0.6-2.3c-0.3-0.4-1.2-1.8-1.2-3
 | 
			
		||||
	c0-2,1.1-3.7,2.4-3.7s2.4,1.7,2.4,3.7c0,1.1-0.9,2.5-1.2,3c-0.5,0.7-0.7,1.5-0.5,2.3c0.2,0.8,0.7,1.5,1.5,1.9l2.2,1.1H2z"/>
 | 
			
		||||
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="16" y1="7.9868" x2="16" y2="24.001">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#8D470D"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#7C3D09"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#8D470D"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#8D470D"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#7C3D09"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_4_);" d="M24,24v-3.4l-3-1.5c-0.5-0.3-0.6-0.8-0.3-1.2c0,0,1.6-2,1.6-4.2c0-3.2-1.9-5.7-4.4-5.7
 | 
			
		||||
	c-2.4,0-4.4,2.6-4.4,5.7c0,2.1,1.6,4.2,1.6,4.2c0.3,0.4,0.2,1-0.3,1.3l-6,3.2c-0.5,0.3-1,0.9-1,1.5V24H24z"/>
 | 
			
		||||
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="16.4121" y1="8.9868" x2="16.4121" y2="23.001">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#D58738"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#AB551F"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#D58738"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#D58738"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#AB551F"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_5_);" d="M9.8,23l5.7-3c0.5-0.3,0.8-0.7,1-1.2c0.1-0.5,0-1.1-0.4-1.5c0,0-1.4-1.8-1.4-3.6
 | 
			
		||||
	c0-2.6,1.5-4.7,3.4-4.7s3.4,2.1,3.4,4.7c0,1.8-1.4,3.6-1.4,3.6c-0.3,0.4-0.5,1-0.4,1.5s0.5,1,1,1.2l2.4,1.2V23H9.8z"/>
 | 
			
		||||
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="17.9424" y1="9.9868" x2="17.9424" y2="22.001">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#D0813A"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#AF551D"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#D0813A"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#D0813A"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#AF551D"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_6_);" d="M13.9,22l2.1-1.1c0.8-0.4,1.3-1.1,1.5-1.9c0.2-0.8,0-1.7-0.6-2.3c-0.3-0.4-1.2-1.8-1.2-3
 | 
			
		||||
	c0-2,1.1-3.7,2.4-3.7s2.4,1.7,2.4,3.7c0,1.2-0.9,2.5-1.2,3c-0.5,0.7-0.7,1.5-0.6,2.3c0.2,0.8,0.7,1.5,1.5,1.9l1.9,1V22H13.9z"/>
 | 
			
		||||
<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="7.4507" y1="0" x2="7.4507" y2="12.9043">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#76A1F0"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#6B90D5"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_7_);" d="M12.5,10.4c1.4-1.1,2.4-2.6,2.4-4.3c0.1-3.3-3.2-6-7.3-6.1C3.4-0.1,0.1,2.5,0,5.8
 | 
			
		||||
	c-0.1,3.3,3.2,6,7.3,6.1c1,0,2-0.1,2.9-0.4c0.8,1,2.8,1.4,2.8,1.4S12.2,11.7,12.5,10.4z"/>
 | 
			
		||||
<linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="7.4507" y1="1" x2="7.4507" y2="11.2168">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#82B4FB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<a:midPointStop  offset="0.5" style="stop-color:#BBE0F7"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#82B4FB"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_8_);" d="M11.4,11.2c-0.2-0.1-0.3-0.2-0.4-0.3l-0.4-0.6l-0.7,0.2c-0.8,0.2-1.6,0.4-2.4,0.4l-0.1,0
 | 
			
		||||
	C3.8,10.8,1,8.6,1,5.8C1,3.2,3.9,1,7.4,1l0.2,0c1.8,0,3.4,0.6,4.6,1.6c1.1,1,1.8,2.2,1.7,3.5c0,1.3-0.7,2.6-2,3.5l-0.3,0.2l-0.1,0.4
 | 
			
		||||
	C11.4,10.5,11.4,10.9,11.4,11.2z"/>
 | 
			
		||||
<linearGradient id="SVGID_9_" gradientUnits="userSpaceOnUse" x1="7.4507" y1="2" x2="7.4507" y2="9.9097">
 | 
			
		||||
	<stop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<stop  offset="0.5569" style="stop-color:#84ADEF"/>
 | 
			
		||||
	<stop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
	<a:midPointStop  offset="0" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="0.4" style="stop-color:#95BFF8"/>
 | 
			
		||||
	<a:midPointStop  offset="1" style="stop-color:#7CA4EB"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
<path style="fill:url(#SVGID_9_);" d="M7.4,9.9C4.4,9.9,2,8,2,5.9C2,3.8,4.5,2,7.4,2l0.1,0c1.5,0,2.9,0.5,4,1.4
 | 
			
		||||
	c0.9,0.8,1.4,1.7,1.4,2.7c0,1-0.6,2-1.6,2.7l-0.6,0.4l0,0.1L9.6,9.6C9,9.8,8.2,9.9,7.5,9.9L7.4,9.9z"/>
 | 
			
		||||
<path style="fill:#FFFFFF;" d="M5.5,4.5h1.9V6c0,0.5-0.1,1-0.3,1.3C6.9,7.6,6.5,7.9,5.9,8.1L5.5,7.3C5.9,7.2,6.1,7,6.2,6.9
 | 
			
		||||
	c0.1-0.2,0.2-0.3,0.2-0.6H5.5V4.5z M7.7,4.5h1.9V6c0,0.5-0.1,1-0.3,1.3C9.1,7.6,8.7,7.9,8.2,8.1L7.7,7.3C8.1,7.2,8.3,7,8.4,6.9
 | 
			
		||||
	c0.1-0.2,0.2-0.3,0.2-0.6H7.7V4.5z"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 6.6 KiB  | 
@ -12,8 +12,8 @@
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Component, OnInit } from '@angular/core';
 | 
			
		||||
import { NavParams, PopoverController } from '@ionic/angular';
 | 
			
		||||
import { Component, Input, OnInit } from '@angular/core';
 | 
			
		||||
import { PopoverController } from '@ionic/angular';
 | 
			
		||||
import { CoreCourses } from '../../services/courses';
 | 
			
		||||
import { CoreEnrolledCourseDataWithExtraInfoAndOptions } from '../../services/courses-helper';
 | 
			
		||||
import { CorePrefetchStatusInfo } from '@features/course/services/course-helper';
 | 
			
		||||
@ -27,18 +27,14 @@ import { CorePrefetchStatusInfo } from '@features/course/services/course-helper'
 | 
			
		||||
})
 | 
			
		||||
export class CoreCoursesCourseOptionsMenuComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
    course!: CoreEnrolledCourseDataWithExtraInfoAndOptions; // The course.
 | 
			
		||||
    prefetch!: CorePrefetchStatusInfo; // The prefecth info.
 | 
			
		||||
    @Input() course!: CoreEnrolledCourseDataWithExtraInfoAndOptions; // The course.
 | 
			
		||||
    @Input() prefetch!: CorePrefetchStatusInfo; // The prefecth info.
 | 
			
		||||
 | 
			
		||||
    downloadCourseEnabled = false;
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        navParams: NavParams,
 | 
			
		||||
        protected popoverController: PopoverController,
 | 
			
		||||
    ) {
 | 
			
		||||
        this.course = navParams.get('course') || {};
 | 
			
		||||
        this.prefetch = navParams.get('prefetch') || {};
 | 
			
		||||
    }
 | 
			
		||||
    ) { }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Component being initialized.
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,7 @@
 | 
			
		||||
    <form (ngSubmit)="submitPassword($event)" #enrolPasswordForm>
 | 
			
		||||
        <ion-item>
 | 
			
		||||
            <ion-label>
 | 
			
		||||
                <core-show-password [name]="'password'">
 | 
			
		||||
                <core-show-password name="password">
 | 
			
		||||
                    <ion-input
 | 
			
		||||
                        class="ion-text-wrap core-ioninput-password"
 | 
			
		||||
                        name="password"
 | 
			
		||||
 | 
			
		||||
@ -46,7 +46,7 @@ export class CoreCoursesAvailableCoursesPage implements OnInit {
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async loadCourses(): Promise<void> {
 | 
			
		||||
        const frontpageCourseId = CoreSites.instance.getCurrentSite()!.getSiteHomeId();
 | 
			
		||||
        const frontpageCourseId = CoreSites.instance.getCurrentSiteHomeId();
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const courses = await CoreCourses.instance.getCoursesByField();
 | 
			
		||||
 | 
			
		||||
@ -17,7 +17,7 @@ import { Injectable } from '@angular/core';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreCourses, CoreCourseSearchedData, CoreCourseUserAdminOrNavOptionIndexed, CoreEnrolledCourseData } from './courses';
 | 
			
		||||
import { makeSingleton } from '@singletons';
 | 
			
		||||
import { makeSingleton, Translate } from '@singletons';
 | 
			
		||||
import { CoreWSExternalFile } from '@services/ws';
 | 
			
		||||
import { AddonCourseCompletion } from '@/addons/coursecompletion/services/coursecompletion';
 | 
			
		||||
// import { CoreCoursePickerMenuPopoverComponent } from '@components/course-picker-menu/course-picker-menu-popover';
 | 
			
		||||
@ -34,8 +34,30 @@ export class CoreCoursesHelperProvider {
 | 
			
		||||
     * @param courseId Course ID to get the category.
 | 
			
		||||
     * @return Promise resolved with the list of courses and the category.
 | 
			
		||||
     */
 | 
			
		||||
    async getCoursesForPopover(): Promise<void> {
 | 
			
		||||
        // @todo params and logic
 | 
			
		||||
    async getCoursesForPopover(courseId?: number): Promise<{courses: Partial<CoreEnrolledCourseData>[]; categoryId?: number}> {
 | 
			
		||||
        const courses: Partial<CoreEnrolledCourseData>[] = await CoreCourses.instance.getUserCourses(false);
 | 
			
		||||
 | 
			
		||||
        // Add "All courses".
 | 
			
		||||
        courses.unshift({
 | 
			
		||||
            id: -1,
 | 
			
		||||
            fullname: Translate.instance.instant('core.fulllistofcourses'),
 | 
			
		||||
            categoryid: -1,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        let categoryId: number | undefined;
 | 
			
		||||
        if (courseId) {
 | 
			
		||||
            // Search the course to get the category.
 | 
			
		||||
            const course = courses.find((course) => course.id == courseId);
 | 
			
		||||
 | 
			
		||||
            if (course) {
 | 
			
		||||
                categoryId = course.categoryid;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            courses: courses,
 | 
			
		||||
            categoryId: categoryId,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -39,7 +39,7 @@
 | 
			
		||||
            </ion-item>
 | 
			
		||||
            <ion-item *ngIf="siteChecked && !isBrowserSSO" class="ion-margin-bottom">
 | 
			
		||||
                <ion-label>
 | 
			
		||||
                    <core-show-password [name]="'password'">
 | 
			
		||||
                    <core-show-password name="password">
 | 
			
		||||
                        <ion-input name="password" type="password" placeholder="{{ 'core.login.password' | translate }}"
 | 
			
		||||
                            formControlName="password" [clearOnEdit]="false" autocomplete="current-password" enterkeyhint="go"
 | 
			
		||||
                            required="true">
 | 
			
		||||
 | 
			
		||||
@ -106,7 +106,7 @@
 | 
			
		||||
                <ion-label position="stacked">
 | 
			
		||||
                    <span core-mark-required="true">{{ 'core.login.password' | translate }}</span>
 | 
			
		||||
                </ion-label>
 | 
			
		||||
                <core-show-password [name]="'password'">
 | 
			
		||||
                <core-show-password name="password">
 | 
			
		||||
                    <ion-input name="password" type="password" placeholder="{{ 'core.login.password' | translate }}"
 | 
			
		||||
                        formControlName="password" [clearOnEdit]="false" autocomplete="new-password" required="true">
 | 
			
		||||
                    </ion-input>
 | 
			
		||||
 | 
			
		||||
@ -40,7 +40,7 @@
 | 
			
		||||
        </ion-item>
 | 
			
		||||
        <ion-item class="ion-margin-bottom">
 | 
			
		||||
            <ion-label>
 | 
			
		||||
                <core-show-password [name]="'password'">
 | 
			
		||||
                <core-show-password name="password">
 | 
			
		||||
                    <ion-input class="core-ioninput-password" name="password" type="password"
 | 
			
		||||
                        placeholder="{{ 'core.login.password' | translate }}" formControlName="password" [clearOnEdit]="false"
 | 
			
		||||
                        autocomplete="current-password" enterkeyhint="go" required="true">
 | 
			
		||||
 | 
			
		||||
@ -81,7 +81,7 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
 | 
			
		||||
        }, CoreSites.instance.getCurrentSiteId());
 | 
			
		||||
 | 
			
		||||
        this.currentSite = CoreSites.instance.getCurrentSite()!;
 | 
			
		||||
        this.siteHomeId = this.currentSite?.getSiteHomeId() || 1;
 | 
			
		||||
        this.siteHomeId = CoreSites.instance.getCurrentSiteHomeId();
 | 
			
		||||
 | 
			
		||||
        const module = navParams['module'];
 | 
			
		||||
        if (module) {
 | 
			
		||||
 | 
			
		||||
@ -47,7 +47,7 @@ export class CoreSiteHomeProvider {
 | 
			
		||||
     */
 | 
			
		||||
    async getNewsForum(siteHomeId?: number): Promise<AddonModForumData> {
 | 
			
		||||
        if (!siteHomeId) {
 | 
			
		||||
            siteHomeId = CoreSites.instance.getCurrentSite()?.getSiteHomeId() || 1;
 | 
			
		||||
            siteHomeId = CoreSites.instance.getCurrentSiteHomeId();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const forums = await AddonModForum.instance.getCourseForums(siteHomeId);
 | 
			
		||||
 | 
			
		||||
@ -17,7 +17,7 @@ import { IonRefresher } from '@ionic/angular';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
// import { CoreSplitViewComponent } from '@components/split-view/split-view';
 | 
			
		||||
import { CoreTag } from '@features/tag/services/tag';
 | 
			
		||||
import { CoreTagAreaDelegate } from '@/core/features/tag/services/tag-area-delegate';
 | 
			
		||||
import { CoreTagAreaDelegate } from '@features/tag/services/tag-area-delegate';
 | 
			
		||||
import { ActivatedRoute, Router } from '@angular/router';
 | 
			
		||||
import { CoreTagFeedElement } from '../../services/tag-helper';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -17,9 +17,9 @@ import { Routes } from '@angular/router';
 | 
			
		||||
import { CoreMainMenuDelegate } from '@features/mainmenu/services/mainmenu-delegate';
 | 
			
		||||
import { CoreMainMenuRoutingModule } from '../mainmenu/mainmenu-routing.module';
 | 
			
		||||
import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate';
 | 
			
		||||
import { CoreTagMainMenuHandler, CoreTagMainMenuHandlerService } from './services/handlers/tag.mainmenu';
 | 
			
		||||
import { CoreTagIndexLinkHandler } from './services/handlers/index.link';
 | 
			
		||||
import { CoreTagSearchLinkHandler } from './services/handlers/search.link';
 | 
			
		||||
import { CoreTagMainMenuHandler, CoreTagMainMenuHandlerService } from './services/handlers/mainmenu';
 | 
			
		||||
import { CoreTagIndexLinkHandler } from './services/handlers/index-link';
 | 
			
		||||
import { CoreTagSearchLinkHandler } from './services/handlers/search-link';
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,7 @@
 | 
			
		||||
 | 
			
		||||
import { Component, Input } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
import { CoreUserTagFeedElement } from '@features/user/services/handlers/tag-area-handler';
 | 
			
		||||
import { CoreUserTagFeedElement } from '@features/user/services/handlers/tag-area';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Component to render the user tag area.
 | 
			
		||||
 | 
			
		||||
@ -58,8 +58,8 @@ export class CoreUserAboutPage implements OnInit {
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async ngOnInit(): Promise<void> {
 | 
			
		||||
        this.userId = this.route.snapshot.queryParams['userId'];
 | 
			
		||||
        this.courseId = this.route.snapshot.queryParams['courseId'];
 | 
			
		||||
        this.userId = parseInt(this.route.snapshot.queryParams['userId'], 10) || 0;
 | 
			
		||||
        this.courseId = parseInt(this.route.snapshot.queryParams['courseId'], 10) || 0;
 | 
			
		||||
 | 
			
		||||
        this.fetchUser().finally(() => {
 | 
			
		||||
            this.userLoaded = true;
 | 
			
		||||
 | 
			
		||||
@ -81,8 +81,8 @@ export class CoreUserProfilePage implements OnInit, OnDestroy {
 | 
			
		||||
     */
 | 
			
		||||
    async ngOnInit(): Promise<void> {
 | 
			
		||||
        this.site = CoreSites.instance.getCurrentSite();
 | 
			
		||||
        this.userId = this.route.snapshot.queryParams['userId'];
 | 
			
		||||
        this.courseId = this.route.snapshot.queryParams['courseId'];
 | 
			
		||||
        this.userId = parseInt(this.route.snapshot.queryParams['userId'], 10);
 | 
			
		||||
        this.courseId = parseInt(this.route.snapshot.queryParams['courseId'], 10);
 | 
			
		||||
 | 
			
		||||
        if (!this.site) {
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
@ -25,7 +25,7 @@ import { CoreContentLinksDelegate } from '@features/contentlinks/services/conten
 | 
			
		||||
import { CoreUserProfileLinkHandler } from './services/handlers/profile-link';
 | 
			
		||||
import { CoreCronDelegate } from '@services/cron';
 | 
			
		||||
import { CoreUserSyncCronHandler } from './services/handlers/sync-cron';
 | 
			
		||||
import { CoreUserTagAreaHandler } from './services/handlers/tag-area-handler';
 | 
			
		||||
import { CoreUserTagAreaHandler } from './services/handlers/tag-area';
 | 
			
		||||
import { CoreTagAreaDelegate } from '@features/tag/services/tag-area-delegate';
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										54
									
								
								src/core/pipes/duration.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,54 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Pipe, PipeTransform } from '@angular/core';
 | 
			
		||||
import { CoreLogger } from '@singletons/logger';
 | 
			
		||||
import { CoreTimeUtils } from '@services/utils/time';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Filter to turn a number of seconds to a duration. E.g. 60 -> 1 minute.
 | 
			
		||||
 */
 | 
			
		||||
@Pipe({
 | 
			
		||||
    name: 'coreDuration',
 | 
			
		||||
})
 | 
			
		||||
export class CoreDurationPipe implements PipeTransform {
 | 
			
		||||
 | 
			
		||||
    protected logger: CoreLogger;
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        this.logger = CoreLogger.getInstance('CoreBytesToSizePipe');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Turn a number of seconds to a duration. E.g. 60 -> 1 minute.
 | 
			
		||||
     *
 | 
			
		||||
     * @param seconds The number of seconds.
 | 
			
		||||
     * @return Formatted duration.
 | 
			
		||||
     */
 | 
			
		||||
    transform(seconds: string | number): string {
 | 
			
		||||
        if (typeof seconds == 'string') {
 | 
			
		||||
            // Convert the value to a number.
 | 
			
		||||
            const numberSeconds = parseInt(seconds, 10);
 | 
			
		||||
            if (isNaN(numberSeconds)) {
 | 
			
		||||
                this.logger.error('Invalid value received', seconds);
 | 
			
		||||
 | 
			
		||||
                return seconds;
 | 
			
		||||
            }
 | 
			
		||||
            seconds = numberSeconds;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return CoreTimeUtils.instance.formatTime(seconds);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -19,6 +19,7 @@ import { CoreNoTagsPipe } from './no-tags';
 | 
			
		||||
import { CoreSecondsToHMSPipe } from './seconds-to-hms';
 | 
			
		||||
import { CoreTimeAgoPipe } from './time-ago';
 | 
			
		||||
import { CoreBytesToSizePipe } from './bytes-to-size';
 | 
			
		||||
import { CoreDurationPipe } from './duration';
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    declarations: [
 | 
			
		||||
@ -28,6 +29,7 @@ import { CoreBytesToSizePipe } from './bytes-to-size';
 | 
			
		||||
        CoreFormatDatePipe,
 | 
			
		||||
        CoreBytesToSizePipe,
 | 
			
		||||
        CoreSecondsToHMSPipe,
 | 
			
		||||
        CoreDurationPipe,
 | 
			
		||||
    ],
 | 
			
		||||
    imports: [],
 | 
			
		||||
    exports: [
 | 
			
		||||
@ -37,6 +39,7 @@ import { CoreBytesToSizePipe } from './bytes-to-size';
 | 
			
		||||
        CoreFormatDatePipe,
 | 
			
		||||
        CoreBytesToSizePipe,
 | 
			
		||||
        CoreSecondsToHMSPipe,
 | 
			
		||||
        CoreDurationPipe,
 | 
			
		||||
    ],
 | 
			
		||||
})
 | 
			
		||||
export class CorePipesModule {}
 | 
			
		||||
 | 
			
		||||