feat: timetable module - schedule and calendar

This commit is contained in:
Wieland Schöbl
2021-08-13 12:27:40 +00:00
parent e81b2e161d
commit d8ede006df
59 changed files with 3287 additions and 555 deletions

View File

@@ -0,0 +1,107 @@
/*
* Copyright (C) 2021 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component, EventEmitter, Input, Output, ViewChild} from '@angular/core';
import {IonSlides} from '@ionic/angular';
/**
* Component that can display infinite slides
*/
@Component({
selector: 'stapps-infinite-slides',
templateUrl: 'infinite-slides.html',
styleUrls: ['infinite-slides.scss'],
})
export class InfiniteSlidesComponent {
/**
* If the view was initialized
*/
initialized = false;
/**
* Callback for when the page has changed
*
* The caller needs to replace the component here
*/
@Output() pageChangeCallback: EventEmitter<{
/**
* The current page
*/
currentPage: number;
/**
* The direction that was scrolled
*/
direction: number;
}> = new EventEmitter();
/**
* The virtual page we are currently on
*/
page = 0;
/**
* Options for IonSlides
*/
@Input() slideOpts = {
initialSlide: 1,
speed: 200,
loop: false,
};
/**
* Slider element
*/
@ViewChild('slides') slides: IonSlides;
/**
* Slide to next page
*/
async nextPage() {
await this.slides.slideNext(this.slideOpts.speed);
}
/**
* Change page
*/
async onPageChange(direction: number) {
if (!this.initialized) {
// setting the initial page to 1 causes a page change to
// be emitted initially, which intern would cause the
// page to actually change one to far, so we listen for
// that first page change and skip it
this.initialized = true;
return;
}
this.page += direction;
this.pageChangeCallback.emit({
currentPage: this.page,
direction: direction,
});
// tslint:disable-next-line:no-magic-numbers
this.slides.slideTo(1, 0, false).then();
}
/**
* Slide to previous page
*/
async prevPage() {
await this.slides.slidePrev(this.slideOpts.speed);
}
}

View File

@@ -0,0 +1,9 @@
<ion-slides
#slides
pager="false"
[options]="slideOpts"
(ionSlideNextEnd)="onPageChange(1)"
(ionSlidePrevEnd)="onPageChange(-1)"
>
<ng-content></ng-content>
</ion-slides>

View File

@@ -0,0 +1,4 @@
ion-slides {
width: 100%;
height: 1100px; // BIG TODO: This is completely bypasses the scale parameter
}

View File

@@ -0,0 +1,100 @@
/*
* Copyright (C) 2020 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component, Input, OnInit} from '@angular/core';
import moment from 'moment';
import {ScheduleProvider} from '../../schedule.provider';
import {ScheduleEvent} from '../schema/schema';
/**
* Component that can display a schedule event
*/
@Component({
selector: 'stapps-schedule-card',
templateUrl: 'schedule-card.html',
styleUrls: ['../../../data/list/data-list-item.scss', 'schedule-card.scss'],
})
export class ScheduleCardComponent implements OnInit {
/**
* The hour from which on the schedule is displayed
*/
@Input() fromHour = 0;
/**
* Card Y start position
*/
fromY = 0;
/**
* Card Y end position
*/
height = 0;
/**
* Show the card without a top offset
*/
@Input() noOffset = false;
/**
* The scale of the schedule
*/
@Input() scale = 1;
/**
* The event
*/
@Input() scheduleEvent: ScheduleEvent;
/**
* The title of the event
*/
title: string;
constructor(private readonly scheduleProvider: ScheduleProvider) {}
/**
* Get the note text
*/
getNote(): string | undefined {
return 'categories' in this.scheduleEvent.dateSeries.event
? this.scheduleEvent.dateSeries.event.categories?.join(', ')
: undefined;
}
/**
* Initializer
*/
ngOnInit() {
this.fromY = this.noOffset ? 0 : this.scheduleEvent.time.start;
this.height = moment.duration(this.scheduleEvent.time.duration).asHours();
this.title = this.scheduleEvent.dateSeries.event.name;
}
/**
* Remove the event
*/
removeEvent(): false {
if (confirm('Remove event?')) {
this.scheduleProvider.uuids$.next(
this.scheduleProvider.uuids$.value.filter(
it => it !== this.scheduleEvent.dateSeries.uid,
),
);
}
// to prevent event propagation
return false;
}
}

View File

@@ -0,0 +1,30 @@
<ion-card
[style.height.px]="height * scale"
[style.marginTop.px]="(fromY - fromHour) * scale - 5"
[routerLink]="['/data-detail', scheduleEvent.dateSeries.event.uid]"
>
<ion-card-header mode="md">
<ion-card-title>
{{
this.scheduleEvent.dateSeries.event.name
| nullishCoalesce: this.scheduleEvent.dateSeries.name
}}
</ion-card-title>
<ion-card-subtitle>
<ion-icon name="calendar"></ion-icon>
<span class="repetitions">
{{ scheduleEvent.dateSeries.frequency }}
until
{{
scheduleEvent.dateSeries.dates | last | amDateFormat: 'DD. MMM YYYY'
}}
</span>
</ion-card-subtitle>
</ion-card-header>
<ion-card-content>
<ion-note>
{{ getNote() }}
</ion-note>
</ion-card-content>
<div></div>
</ion-card>

View File

@@ -0,0 +1,37 @@
ion-card {
z-index: 2;
ion-grid {
padding: 0;
margin: 0;
ion-row {
ion-col {
height: 5px;
padding: 0;
margin: 0;
}
}
}
ion-card-header {
height: available;
width: 100%;
ion-card-title {
height: 50px;
width: 100%;
}
}
div {
position: absolute;
bottom: 0;
height: 20px;
width: 100%;
background: linear-gradient(
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 255) 100%
);
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright (C) 2018, 2019 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component, Input, OnInit} from '@angular/core';
import moment from 'moment';
import {HoursRange} from '../schema/schema';
/**
* Component that displays the schedule
*/
@Component({
selector: 'stapps-schedule-cursor',
templateUrl: 'schedule-cursor.html',
styleUrls: ['schedule-cursor.scss'],
})
export class ScheduleCursorComponent implements OnInit {
/**
* Cursor update
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore unused
private cursorInterval: NodeJS.Timeout;
/**
* Range of hours to display
*/
@Input() readonly hoursRange: HoursRange;
/**
* Cursor
*/
now = ScheduleCursorComponent.getCursorTime();
/**
* Vertical scale of the schedule (distance between hour lines)
*/
@Input() readonly scale: number;
/**
* Get a floating point time 0..24
*/
static getCursorTime(): number {
const mnt = moment(moment.now());
const hh = mnt.hours();
const mm = mnt.minutes();
// tslint:disable-next-line:no-magic-numbers
return hh + mm / 60;
}
/**
* Initialize
*/
ngOnInit() {
this.cursorInterval = setInterval(async () => {
this.now = ScheduleCursorComponent.getCursorTime();
// tslint:disable-next-line:no-magic-numbers
}, 1000 * 60 /*1 Minute*/);
}
}

View File

@@ -0,0 +1,6 @@
<div [style.marginTop.px]="(now - hoursRange.from) * scale">
<div>
<hr />
<div></div>
</div>
</div>

View File

@@ -0,0 +1,38 @@
div {
padding: 0;
margin: 0;
position: absolute;
display: flex;
flex-direction: row;
width: 100%;
top: 0;
z-index: 0;
div {
width: 100%;
height: fit-content;
hr {
width: calc(100% - 8px);
position: absolute;
margin-left: 4px;
margin-right: 16px;
margin-top: 8px;
height: 2px;
border-top: 2px solid var(--ion-color-primary);
margin-block-start: 0;
margin-block-end: 0;
}
div {
width: 8px;
height: 8px;
position: absolute;
top: -3px;
left: -4px;
border-radius: 50% 0 50% 50%;
transform: rotateZ(45deg);
background-color: var(--ion-color-primary);
}
}
}