refactor: use SwiperJS in schedule module

This commit is contained in:
Wieland Schöbl
2021-11-09 17:12:45 +00:00
parent d6cb7e1d3b
commit 523e34f6e4
27 changed files with 771 additions and 421 deletions

View File

@@ -12,23 +12,14 @@
* 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,
OnChanges,
OnDestroy,
OnInit,
SimpleChanges,
ViewChild,
} from '@angular/core';
import {Component, Input, OnDestroy, OnInit, ViewChild} from '@angular/core';
import {ActivatedRoute} from '@angular/router';
import {IonDatetime, Platform} from '@ionic/angular';
import {SCUuid} from '@openstapps/core';
import {Platform} from '@ionic/angular';
import {SCISO8601Date, SCUuid} from '@openstapps/core';
import {last} from 'lodash-es';
import moment, {Moment} from 'moment';
import {DateFormatPipe} from 'ngx-moment';
import {Subscription} from 'rxjs';
import {SharedAxisChoreographer} from '../../../animation/animation-choreographer';
import {
materialFade,
materialManualFade,
@@ -36,6 +27,7 @@ import {
} from '../../../animation/material-motion';
import {ScheduleProvider} from '../schedule.provider';
import {ScheduleEvent, ScheduleResponsiveBreakpoint} from './schema/schema';
import {SwiperComponent} from 'swiper/angular';
/**
* Component that displays the schedule
@@ -46,42 +38,21 @@ import {ScheduleEvent, ScheduleResponsiveBreakpoint} from './schema/schema';
styleUrls: ['calendar-view.scss'],
animations: [materialFade, materialSharedAxisX, materialManualFade],
})
export class CalendarViewComponent implements OnDestroy, OnInit, OnChanges {
export class CalendarViewComponent implements OnDestroy, OnInit {
/**
* UUID subscription
*/
private _uuidSubscription: Subscription;
/**
* The day that is routed to
* The day that the schedule started out on
*/
protected routeDate: Moment;
baselineDate: Moment;
/**
* @see {blockDateTimeChange}
* Oldest event to newest event
*/
anticipateDatetimeChangeBlocked = false;
/**
* @see {blockDateTimeChange}
*/
// tslint:disable-next-line:no-magic-numbers
readonly anticipateDatetimeChangeTimeoutMs: 100;
/**
* Animation state for cards
*/
cardsAnimationState: 'in' | 'out' = 'out';
/**
* The cursor
*/
@ViewChild('cursor', {read: HTMLElement}) cursor?: HTMLElement;
/**
* Choreographer
*/
dateLabelsChoreographer: SharedAxisChoreographer<Moment[]>;
dateRange: [Date, Date];
/**
* The date range to initially display
@@ -106,11 +77,17 @@ export class CalendarViewComponent implements OnDestroy, OnInit, OnChanges {
to: 22,
};
todaySlideIndex: number;
initialSlideIndex?: Promise<number>;
/**
* Layout of the schedule
*/
@Input() layout: ScheduleResponsiveBreakpoint;
@ViewChild('mainSwiper', {static: false}) swiperRef: SwiperComponent;
/**
* Get the date format for the date field
*/
@@ -128,15 +105,20 @@ export class CalendarViewComponent implements OnDestroy, OnInit, OnChanges {
scale = 60;
/**
* date -> (uid -> event)
* unix -> (uid -> event)
*/
testSchedule: Record<string, Record<SCUuid, ScheduleEvent>> = {};
testSchedule: Record<SCISO8601Date, Record<SCUuid, ScheduleEvent>> = {};
/**
* UUIDs
*/
uuids: SCUuid[];
/**
* For use in templates
*/
moment = moment;
constructor(
protected readonly scheduleProvider: ScheduleProvider,
protected readonly activatedRoute: ActivatedRoute,
@@ -152,88 +134,22 @@ export class CalendarViewComponent implements OnDestroy, OnInit, OnChanges {
this.hours = [...Array.from({length: this.hoursAmount}).keys()];
}
/**
* Because of some stupid Ionic implementation, there is no
* way to wait for the datetime picker to be dismissed without
* listening for (ionChange). Unfortunately that also includes
* changes caused by a page change, so whenever we do that,
* we have to block the event for a few milliseconds.
*/
blockDateTimeChange() {
this.anticipateDatetimeChangeBlocked = true;
setTimeout(() => {
this.anticipateDatetimeChangeBlocked = false;
}, this.anticipateDatetimeChangeTimeoutMs);
}
/**
* Determine displayed dates according to display size
*/
determineDisplayDates() {
// let's boldly assume that we at least display one day
const out = [moment(this.routeDate).startOf(this.layout.startOf)];
for (let i = 1; i < this.layout.days; i++) {
out.push(out[0].clone().add(i, 'day'));
}
this.displayDates = [
out.map(it => it.clone().subtract(this.layout.days, 'days')),
out,
out.map(it => it.clone().add(this.layout.days, 'days')),
];
this.dateLabelsChoreographer?.changeViewForState(this.getDateLabels(), 0);
// void this.mainSlides.slideTo(this.mode === 'schedule' ? 0 : 1, 0, false);
}
/**
* Get date labels
*/
getDateLabels(): Moment[] {
return (this.displayDates[1] ?? this.displayDates[0]).map(it => it.clone());
}
/**
* Jump to a date
*/
jumpToDate(alt: IonDatetime, offset = 0, date?: Moment) {
if (this.anticipateDatetimeChangeBlocked) {
return;
}
const newDate = (date ?? moment(alt.value)).subtract(offset, 'days');
const direction = this.routeDate.isBefore(newDate)
? 1
: this.routeDate.isAfter(newDate)
? -1
: 0;
this.blockDateTimeChange();
this.routeDate = newDate;
this.determineDisplayDates();
this.dateLabelsChoreographer.changeViewForState(
this.getDateLabels(),
direction,
);
}
/**
* Load events
*/
async loadEvents(): Promise<void> {
this.cardsAnimationState = 'out';
async loadEvents(): Promise<number> {
const dateSeries = await this.scheduleProvider.getDateSeries(this.uuids);
this.testSchedule = {};
for (const series of dateSeries) {
for (const series of dateSeries.dates) {
for (const date of series.dates) {
const parsedDate = moment(date).startOf('day').unix();
const index = moment(date)
.startOf('day')
.diff(this.baselineDate, 'days');
// fall back to default
(this.testSchedule[parsedDate] ?? (this.testSchedule[parsedDate] = {}))[
(this.testSchedule[index] ?? (this.testSchedule[index] = {}))[
series.uid
] = {
dateSeries: series,
@@ -245,20 +161,7 @@ export class CalendarViewComponent implements OnDestroy, OnInit, OnChanges {
}
}
this.cursor?.scrollIntoView();
this.cardsAnimationState = 'in';
}
/**
* On Changes
*/
ngOnChanges(changes: SimpleChanges) {
const layout = changes.layout?.currentValue as
| ScheduleResponsiveBreakpoint
| undefined;
if (layout) {
this.determineDisplayDates();
}
return this.todaySlideIndex;
}
/**
@@ -272,13 +175,6 @@ export class CalendarViewComponent implements OnDestroy, OnInit, OnChanges {
* Initialize
*/
ngOnInit() {
this._uuidSubscription = this.scheduleProvider.uuids$.subscribe(
async result => {
this.uuids = result;
await this.loadEvents();
},
);
let dayString: string | number | null =
this.activatedRoute.snapshot.paramMap.get('date');
if (dayString == undefined || dayString === 'now') {
@@ -288,37 +184,30 @@ export class CalendarViewComponent implements OnDestroy, OnInit, OnChanges {
? urlFragment
: moment.now();
}
this.routeDate = moment(dayString).startOf('day');
this.dateLabelsChoreographer = new SharedAxisChoreographer(
this.getDateLabels(),
);
this.determineDisplayDates();
this.baselineDate = moment(dayString).startOf('day');
this.initialSlideIndex = new Promise(resolve => {
this._uuidSubscription = this.scheduleProvider.uuids$.subscribe(
async result => {
this.uuids = result;
resolve(await this.loadEvents());
},
);
});
}
/**
* Change page
*/
async onPageChange(direction: number) {
this.blockDateTimeChange();
const amount = direction * this.displayDates[0].length;
this.routeDate.add(amount, 'days');
onPageChange(index: number) {
window.history.replaceState(
{},
'',
`#/${this.routeFragment}/${this.routeDate.format('YYYY-MM-DD')}`,
);
for (const slide of this.displayDates) {
for (const date of slide) {
date.add(amount, 'days');
}
}
this.dateLabelsChoreographer.changeViewForState(
this.getDateLabels(),
direction > 0 ? 1 : direction < 0 ? -1 : 0,
`${this.routeFragment}/${this.baselineDate
.clone()
.add(index, 'days')
.format('YYYY-MM-DD')}`,
);
}
}

View File

@@ -1,80 +1,89 @@
<div>
<ion-button fill="clear" class="left-button" (click)="mainSlides.prevPage()">
<!--
~ 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/>.
-->
<div class="header">
<ion-button
fill="clear"
class="left-button"
(click)="mainSwiper.pageBackwards()"
>
<ion-icon slot="icon-only" name="chevron-back-outline"></ion-icon>
</ion-button>
<ion-button fill="clear" class="right-button" (click)="mainSlides.nextPage()">
<ion-button
fill="clear"
class="right-button"
(click)="mainSwiper.pageForward()"
>
<ion-icon slot="icon-only" name="chevron-forward-outline"></ion-icon>
</ion-button>
<ion-item>
<ion-grid
class="day-labels"
[@materialSharedAxisX]="dateLabelsChoreographer.animationState"
(@materialSharedAxisX.done)="dateLabelsChoreographer.animationDone()"
<infinite-swiper
class="header-swiper"
#headerSwiper
[slidesPerView]="layout.days"
[controller]="mainSwiper"
>
<ion-row>
<ion-col
*ngFor="
let item of dateLabelsChoreographer.currentValue;
let idx = index
"
<ng-template let-index>
<div
*ngIf="index | dateFromIndex: baselineDate as date"
class="day-labels"
>
<ion-button expand="block" fill="clear" (click)="datetime.open()">
<ion-label>
{{
item
| amDateFormat: ((item | dateIsThis: 'week') ? 'dddd' : 'll')
date
| amDateFormat: ((date | dateIsThis: 'week') ? 'dddd' : 'll')
}}
</ion-label>
</ion-button>
<!-- This poor datetime element is a phantom element to provide us with a date picker -->
<ion-datetime
class="phantom"
#datetime
[displayFormat]="localDateFormat"
[value]="item.toISOString()"
(ionChange)="jumpToDate(datetime, idx)"
[value]="date.toISOString()"
(ionChange)="
mainSwiper.goToIndex(
moment($event.detail.value).diff(baselineDate, 'days')
)
"
>
</ion-datetime>
</ion-col>
</ion-row>
</ion-grid>
</div>
</ng-template>
</infinite-swiper>
</ion-item>
</div>
<ion-content>
<stapps-infinite-slides
#mainSlides
(pageChangeCallback)="onPageChange($event.direction)"
<infinite-swiper
#mainSwiper
[controller]="headerSwiper"
[slidesPerView]="layout.days"
(indexChange)="onPageChange($event)"
>
<ion-slide class="slide" *ngFor="let slide of displayDates">
<ion-grid>
<ion-row>
<ion-col *ngFor="let item of slide">
<div
class="vertical-line"
[style.height.px]="hoursAmount * scale"
></div>
<stapps-schedule-cursor
*ngIf="item | dateIsThis: 'date'"
[hoursRange]="hoursRange"
[scale]="scale"
#cursor
>
</stapps-schedule-cursor>
<div [@materialManualFade]="cardsAnimationState">
<stapps-schedule-card
*ngFor="let entry of testSchedule[item.unix()] | keyvalue"
[scheduleEvent]="entry.value"
[fromHour]="hoursRange.from"
[scale]="scale"
>
</stapps-schedule-card>
</div>
</ion-col>
</ion-row>
</ion-grid>
</ion-slide>
</stapps-infinite-slides>
<ng-template let-index>
<schedule-day
[day]="index | dateFromIndex: baselineDate"
[hoursRange]="hoursRange"
[scale]="scale"
[uuids]="uuids"
[dateSeries]="testSchedule[index]"
>
</schedule-day>
</ng-template>
</infinite-swiper>
<div
class="hour-lines"
*ngFor="let i of hours"

View File

@@ -1,4 +1,24 @@
div {
/*!
* 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/>.
*/
.header-swiper {
width: 100%;
height: 100%;
}
.header {
position: relative;
.left-button, .right-button {
@@ -18,64 +38,19 @@ div {
}
}
.day-labels {
// phantom element
.phantom {
position: absolute;
top: 0;
left: 0;
padding: 8px 20px 8px 30px;
width: 100%;
ion-row {
padding-right: 20px;
ion-col {
ion-button {
position: absolute;
top: -8px;
font-size: large;
text-align: center;
width: 100%;
}
// phantom element
ion-datetime {
position: absolute;
visibility: hidden;
height: 0 !important;
}
}
}
visibility: hidden;
height: 0 !important;
}
.slide {
ion-grid {
position: absolute;
top: 0;
height: fit-content;
width: 100%;
padding-top: 8px;
ion-row {
ion-col {
width: 100%;
.vertical-line {
position: absolute;
top: 0;
left: 0;
border-left: 1px solid #dbdbdb;
z-index: 1;
}
stapps-schedule-card {
z-index: 4;
position: absolute;
left: 0;
width: 100%;
}
}
}
}
.day-labels {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.hour-lines {

View File

@@ -1,107 +0,0 @@
/*
* 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

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

View File

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

View File

@@ -0,0 +1,256 @@
/*
* 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/>.
*/
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import {
AfterViewInit,
Component,
ContentChild,
ElementRef,
EventEmitter,
Input,
OnChanges,
OnDestroy,
OnInit,
Output,
QueryList,
SimpleChanges,
TemplateRef,
ViewChild,
ViewChildren,
ViewContainerRef,
} from '@angular/core';
import Swiper from 'swiper';
import {drop, dropRight, forEach, range, take, takeRight, zip} from 'lodash-es';
import {materialManualFade} from '../../../../animation/material-motion';
export interface SlideContext {
$implicit: number;
}
/**
* Wait for specified amount of time
*/
async function wait(ms?: number) {
await new Promise(resolve => setTimeout(resolve, ms));
}
/**
* This is an infinite version of the swiper
*
* The basic principle it works on is
* 1. The user can never swiper further than the amount of visible slides
* 2. Only out of view slides are re-initialized
*/
@Component({
selector: 'infinite-swiper',
templateUrl: 'infinite-swiper.html',
styleUrls: ['infinite-swiper.scss'],
animations: [materialManualFade],
})
export class InfiniteSwiperComponent
implements OnInit, AfterViewInit, OnDestroy, OnChanges
{
@Input() controller?: InfiniteSwiperComponent;
@Input() slidesPerView = 5;
virtualIndex = 0;
@ContentChild(TemplateRef) userSlideTemplateRef: TemplateRef<SlideContext>;
@Output() indexChange = new EventEmitter<number>();
slidesArray: number[];
@ViewChild('swiper', {static: true})
swiperElement: ElementRef<HTMLDivElement>;
@ViewChildren('slideContainers', {read: ViewContainerRef})
slideContainers: QueryList<ViewContainerRef>;
swiper: Swiper;
visibilityState: 'in' | 'out' = 'in';
private preventControllerCallback = false;
ngOnInit() {
this.createSwiper();
}
ngAfterViewInit() {
this.initSwiper();
}
ngOnDestroy() {
this.swiper.destroy();
this.clearSlides();
}
async ngOnChanges(changes: SimpleChanges) {
if ('slidesPerView' in changes) {
const change = changes.slidesPerView;
if (change.isFirstChange()) return;
// little bit of a cheesy trick just to reinitialize
// everything... But you know, it works just fine.
// And how often are you realistically going to
// resize your window.
this.visibilityState = 'out';
await wait(250);
this.ngOnDestroy();
this.createSwiper();
await wait();
this.initSwiper();
this.visibilityState = 'in';
}
}
createSwiper() {
this.resetSlides();
// I have absolutely no clue why two results are returned here.
// Probably a bug, so be on the lookout if you get odd errors
const [swiper] = new Swiper('.swiper', {
// TODO: evaluate if the controller has decent performance, some time in the future
// modules: [Controller],
slidesPerView: this.slidesPerView,
initialSlide: this.slidesPerView,
init: false,
}) as unknown as [Swiper, Swiper];
this.swiper = swiper;
}
initSwiper() {
this.swiper.init(this.swiperElement.nativeElement);
// SwiperJS controller still has some performance issues unfortunately...
// So unfortunately we are kind of forced to use a workaround :/
// TODO: evaluate if the controller has decent performance, some time in the future
/*setTimeout(() => {
this.swiper.controller.control = this.controller?.swiper;
});*/
this.shiftSlides();
this.swiper.on('activeIndexChange', () => {
if (!this.preventControllerCallback) {
this.controller?.controllerSlideTo(this.swiper.activeIndex);
}
});
this.swiper.on('slideChangeTransitionEnd', () => {
this.shiftSlides(this.swiper.activeIndex);
this.indexChange.emit(this.virtualIndex);
this.preventControllerCallback = false;
});
}
clearSlides() {
for (const container of this.slideContainers) {
while (container.length > 0) {
container.remove();
}
}
}
pageForward() {
this.swiper.slideTo(this.slidesPerView * 2);
}
pageBackwards() {
this.swiper.slideTo(0);
}
/**
* This method is require to not cause a callback loop
* when the controller slides
*/
private async controllerSlideTo(index: number) {
// TODO: prevent virtual index falling out of sync
this.preventControllerCallback = true;
this.swiper.slideTo(index);
await wait(400);
if (this.controller && this.virtualIndex !== this.controller.virtualIndex) {
console.warn(
`Virtual indices fell out of sync ${this.virtualIndex} : ${this.controller.virtualIndex}, correcting...`,
);
await this.controller.goToIndex(this.virtualIndex, false);
}
}
async goToIndex(index: number, runCallbacks = true) {
if (runCallbacks) {
this.controller?.goToIndex(index, false);
}
this.visibilityState = 'out';
await wait(250);
this.virtualIndex = index;
this.clearSlides();
this.shiftSlides();
this.visibilityState = 'in';
}
shiftSlides(activeIndex = this.slidesPerView) {
const delta = this.slidesPerView - activeIndex;
const deltaAmount = Math.abs(delta);
const direction = delta > 0;
this.virtualIndex -= delta;
const containers = this.slideContainers.toArray();
const slides = containers.map(it =>
it.length > 0 ? it.detach(0) : undefined,
);
// delete slides that are going to be dropped
for (const slide of (direction ? takeRight : take)(slides, deltaAmount)) {
slide?.destroy();
}
// reuse existing slides
const newElements: undefined[] = Array.from({length: deltaAmount});
const shiftedSlides = direction
? [...newElements, ...dropRight(slides, deltaAmount)]
: [...drop(slides, deltaAmount), ...newElements];
forEach(zip(containers, shiftedSlides), ([container, element], i) => {
// TODO: we should be able to skip this... In theory.
while (container!.length > 0) {
console.warn('Slide container is not empty after detach!');
container!.remove();
}
if (element) {
container!.insert(element);
} else {
container!.createEmbeddedView(this.userSlideTemplateRef, {
$implicit: this.virtualIndex + (i - this.slidesPerView),
});
}
});
this.swiper.slideTo(this.slidesPerView, 0, false);
}
resetSlides() {
this.slidesArray = range(0, this.slidesPerView * 3);
}
}

View File

@@ -0,0 +1,26 @@
<!--
~ 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/>.
-->
<div #swiper class="swiper">
<div class="swiper-wrapper">
<div
[@materialManualFade]="visibilityState"
class="swiper-slide"
*ngFor="let index of slidesArray"
>
<ng-container #slideContainers></ng-container>
</div>
</div>
</div>

View File

@@ -0,0 +1,19 @@
/*!
* 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/>.
*/
.swiper {
height: 100%;
width: 100%;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2018, 2019 StApps
* 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.
@@ -13,8 +13,8 @@
* 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';
import moment from 'moment';
/**
* Component that displays the schedule

View File

@@ -1,3 +1,18 @@
/*!
* 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/>.
*/
div {
padding: 0;
margin: 0;
@@ -5,7 +20,7 @@ div {
display: flex;
flex-direction: row;
width: 100%;
top: 0;
top: 4px;
z-index: 0;
div {
@@ -13,9 +28,8 @@ div {
height: fit-content;
hr {
width: calc(100% - 8px);
width: 100%;
position: absolute;
margin-left: 4px;
margin-right: 16px;
margin-top: 8px;
height: 2px;
@@ -29,7 +43,6 @@ div {
height: 8px;
position: absolute;
top: -3px;
left: -4px;
border-radius: 50% 0 50% 50%;
transform: rotateZ(45deg);
background-color: var(--ion-color-primary);

View File

@@ -0,0 +1,68 @@
/*
* 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, Input} from '@angular/core';
import moment from 'moment';
import {Range, ScheduleEvent} from '../schema/schema';
import {ScheduleProvider} from '../../schedule.provider';
import {SCISO8601Duration, SCUuid} from '@openstapps/core';
import {materialFade} from '../../../../animation/material-motion';
@Component({
selector: 'schedule-day',
templateUrl: 'schedule-day.html',
styleUrls: ['schedule-day.scss'],
animations: [materialFade],
})
export class ScheduleDayComponent {
@Input() day: moment.Moment;
@Input() hoursRange: Range<number>;
@Input() uuids: SCUuid[];
@Input() scale: number;
@Input() frequencies?: SCISO8601Duration[];
@Input() dateSeries?: Record<string, ScheduleEvent>;
constructor(protected readonly scheduleProvider: ScheduleProvider) {}
// ngOnInit() {
// this.dateSeries = this.fetchDateSeries();
// }
// TODO: backend bug results in the wrong date series being returned
/* async fetchDateSeries(): Promise<ScheduleEvent[]> {
const dateSeries = await this.scheduleProvider.getDateSeries(
this.uuids,
this.frequencies,
this.momentDay.clone().startOf('day').toISOString(),
this.momentDay.clone().endOf('day').toISOString(),
);
for (const series of dateSeries.dates) {
console.log(JSON.stringify(series.dates));
}
return dateSeries.dates.map(it => ({
dateSeries: it,
time: {
start: moment(it.dates.find(date => date === this.day)).hours(),
duration: it.duration,
},
}));
} */
}

View File

@@ -0,0 +1,35 @@
<!--
~ 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/>.
-->
<div>
<div class="vertical-line"></div>
<stapps-schedule-cursor
class="cursor"
*ngIf="day | dateIsThis: 'date'"
[hoursRange]="hoursRange"
[scale]="scale"
>
</stapps-schedule-cursor>
<div *ngIf="dateSeries as dateSeries">
<!-- TODO: entry/exit animation -->
<stapps-schedule-card
class="schedule-card"
*ngFor="let entry of dateSeries | entries"
[scheduleEvent]="entry"
[fromHour]="hoursRange.from"
[scale]="scale"
>
</stapps-schedule-card>
</div>
</div>

View File

@@ -0,0 +1,37 @@
/*!
* 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/>.
*/
.schedule-card {
position: absolute;
top: 13px;
left: 0;
z-index: 4;
width: 100%;
}
div {
height: 100%;
width: 100%;
}
.vertical-line {
position: absolute;
top: 0;
left: 0;
width: 1px;
height: 100%;
background-color: #dbdbdb;
}

View File

@@ -115,7 +115,7 @@ export class ScheduleSingleEventsComponent implements OnInit, OnDestroy {
// TODO: replace with filter
return ScheduleSingleEventsComponent.groupDateSeriesToDays(
dateSeries.filter(it => isNil(it.repeatFrequency)),
dateSeries.dates.filter(it => isNil(it.repeatFrequency)),
);
}

View File

@@ -71,8 +71,8 @@ export class ScheduleViewComponent extends CalendarViewComponent {
/**
* Load events
*/
// @Override
async loadEvents(): Promise<void> {
// TODO: @Override
/*async loadEvents(): Promise<void> {
this.cardsAnimationState = 'out';
const dateSeries = await this.scheduleProvider.getDateSeries(
this.uuids,
@@ -82,7 +82,7 @@ export class ScheduleViewComponent extends CalendarViewComponent {
this.testSchedule = {};
for (const series of dateSeries) {
for (const series of dateSeries.dates) {
if (series.dates.length > 0) {
const date = moment(moment.now())
.startOf('week')
@@ -104,5 +104,5 @@ export class ScheduleViewComponent extends CalendarViewComponent {
this.cursor?.scrollIntoView();
this.cardsAnimationState = 'in';
}
}*/
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 StApps
* 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.
@@ -21,6 +21,11 @@ interface DateRange {
start: number;
}
export interface Range<T> {
from: T;
to: T;
}
/**
* Minimal interface to provide information about a custom event
*/

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2018, 2019 StApps
* 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.
@@ -25,13 +25,16 @@ import {UtilModule} from '../../util/util.module';
import {DataModule} from '../data/data.module';
import {DataProvider} from '../data/data.provider';
import {CalendarViewComponent} from './page/calendar-view.component';
import {InfiniteSlidesComponent} from './page/grid/infinite-slides.component';
import {ScheduleCursorComponent} from './page/grid/schedule-cursor.component';
import {ModalEventCreatorComponent} from './page/modal/modal-event-creator.component';
import {SchedulePageComponent} from './page/schedule-page.component';
import {ScheduleSingleEventsComponent} from './page/schedule-single-events.component';
import {ScheduleViewComponent} from './page/schedule-view.component';
import {ScheduleProvider} from './schedule.provider';
import {SwiperModule} from 'swiper/angular';
import {ScheduleDayComponent} from './page/grid/schedule-day.component';
import {ThingTranslateModule} from '../../translation/thing-translate.module';
import {InfiniteSwiperComponent} from './page/grid/infinite-swiper.component';
const settingsRoutes: Routes = [
{path: 'schedule', redirectTo: 'schedule/calendar/now'},
@@ -48,23 +51,26 @@ const settingsRoutes: Routes = [
@NgModule({
declarations: [
CalendarViewComponent,
InfiniteSlidesComponent,
ModalEventCreatorComponent,
ScheduleCardComponent,
ScheduleCursorComponent,
SchedulePageComponent,
ScheduleSingleEventsComponent,
ScheduleDayComponent,
ScheduleViewComponent,
InfiniteSwiperComponent,
],
imports: [
CommonModule,
FormsModule,
UtilModule,
IonicModule.forRoot(),
TranslateModule.forChild(),
RouterModule.forChild(settingsRoutes),
DataModule,
FormsModule,
IonicModule.forRoot(),
MomentModule,
RouterModule.forChild(settingsRoutes),
SwiperModule,
TranslateModule.forChild(),
UtilModule,
ThingTranslateModule,
],
providers: [ScheduleProvider, DataProvider, DateFormatPipe],
})

View File

@@ -112,9 +112,17 @@ export class ScheduleProvider implements OnDestroy {
frequencies?: Array<SCISO8601Duration>,
from?: SCISO8601Date | 'now',
to?: SCISO8601Date | 'now',
): Promise<SCDateSeries[]> {
): Promise<{
dates: SCDateSeries[];
min: SCISO8601Date;
max: SCISO8601Date;
}> {
if (uuids.length === 0) {
return [];
return {
dates: [],
min: '',
max: '',
};
}
const filters: SCSearchFilter[] = [
@@ -159,12 +167,14 @@ export class ScheduleProvider implements OnDestroy {
if (from || to) {
const bounds: Bounds<string> = {};
if (from) {
console.log(from);
bounds.lowerBound = {
limit: from,
mode: 'inclusive',
};
}
if (to) {
console.log(to);
bounds.upperBound = {
limit: to,
mode: 'inclusive',
@@ -179,17 +189,22 @@ export class ScheduleProvider implements OnDestroy {
});
}
return (
await this.dataProvider.search({
filter: {
arguments: {
filters: filters,
operation: 'and',
},
type: 'boolean',
const result = await this.dataProvider.search({
filter: {
arguments: {
filters: filters,
operation: 'and',
},
})
).data as SCDateSeries[];
type: 'boolean',
},
});
return {
dates: result.data as SCDateSeries[],
// TODO: https://gitlab.com/openstapps/backend/-/issues/100
min: new Date(2021, 11, 1).toISOString(),
max: new Date(2022, 1, 24).toISOString(),
};
}
/**