| @ -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 {} | ||||
|  | ||||