diff --git a/package-lock.json b/package-lock.json
index 43d78a3b..67262b70 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8789,6 +8789,14 @@
}
}
},
+ "dom7": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/dom7/-/dom7-4.0.0.tgz",
+ "integrity": "sha512-xOJ0LAHFwktyj8Xljz4R2wzRI+Y9mR0plkMP0WlqtwqAkqn/vbdAyRifiW/w8mXe17LGktntcAwsQ5fKVDBNYg==",
+ "requires": {
+ "ssr-window": "^4.0.0"
+ }
+ },
"domelementtype": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz",
@@ -19717,6 +19725,11 @@
"tweetnacl": "~0.14.0"
}
},
+ "ssr-window": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/ssr-window/-/ssr-window-4.0.0.tgz",
+ "integrity": "sha512-qCg6wJNeGNTVcPK2KFNfwtHU1gA3UZDZdxogu+Ys5+Ue5PMOENxUb7sscpAWWo4mWOBkJRCwQ50IlyA7qZ0hxw=="
+ },
"ssri": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz",
@@ -20306,6 +20319,15 @@
"stable": "^0.1.8"
}
},
+ "swiper": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/swiper/-/swiper-7.1.0.tgz",
+ "integrity": "sha512-uDsORU5ZS8q8Q0Mf4ml1FuOabjm3EWJOoFAUaENcIlgHhdr0sTPeX6BZZUaQ8qXWuBNEvz4XE2wJeVR7M03wIw==",
+ "requires": {
+ "dom7": "^4.0.0",
+ "ssr-window": "^4.0.0"
+ }
+ },
"symbol-observable": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz",
diff --git a/package.json b/package.json
index 12efc46f..dda07cb3 100644
--- a/package.json
+++ b/package.json
@@ -82,6 +82,7 @@
"core-js": "2.6.5",
"deepmerge": "3.3.0",
"form-data": "2.5.0",
+ "swiper": "7.1.0",
"geojson": "0.5.0",
"leaflet": "1.7.1",
"leaflet.markercluster": "1.5.1",
diff --git a/src/app/modules/schedule/page/calendar-view.component.ts b/src/app/modules/schedule/page/calendar-view.component.ts
index f7958e54..b16a74b2 100644
--- a/src/app/modules/schedule/page/calendar-view.component.ts
+++ b/src/app/modules/schedule/page/calendar-view.component.ts
@@ -12,23 +12,14 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see .
*/
-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;
+ 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;
+
/**
* 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> = {};
+ testSchedule: Record> = {};
/**
* 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 {
- this.cardsAnimationState = 'out';
+ async loadEvents(): Promise {
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')}`,
);
}
}
diff --git a/src/app/modules/schedule/page/calendar-view.html b/src/app/modules/schedule/page/calendar-view.html
index 011ba473..f03f6f50 100644
--- a/src/app/modules/schedule/page/calendar-view.html
+++ b/src/app/modules/schedule/page/calendar-view.html
@@ -1,80 +1,89 @@
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
.
+ */
+
+.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 {
diff --git a/src/app/modules/schedule/page/grid/infinite-slides.component.ts b/src/app/modules/schedule/page/grid/infinite-slides.component.ts
deleted file mode 100644
index 6c653694..00000000
--- a/src/app/modules/schedule/page/grid/infinite-slides.component.ts
+++ /dev/null
@@ -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 .
- */
-
-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);
- }
-}
diff --git a/src/app/modules/schedule/page/grid/infinite-slides.html b/src/app/modules/schedule/page/grid/infinite-slides.html
deleted file mode 100644
index 9ecd649f..00000000
--- a/src/app/modules/schedule/page/grid/infinite-slides.html
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/src/app/modules/schedule/page/grid/infinite-slides.scss b/src/app/modules/schedule/page/grid/infinite-slides.scss
deleted file mode 100644
index 0e8b596a..00000000
--- a/src/app/modules/schedule/page/grid/infinite-slides.scss
+++ /dev/null
@@ -1,4 +0,0 @@
-ion-slides {
- width: 100%;
- height: 1100px; // BIG TODO: This is completely bypasses the scale parameter
-}
diff --git a/src/app/modules/schedule/page/grid/infinite-swiper.component.ts b/src/app/modules/schedule/page/grid/infinite-swiper.component.ts
new file mode 100644
index 00000000..4765d6f8
--- /dev/null
+++ b/src/app/modules/schedule/page/grid/infinite-swiper.component.ts
@@ -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 .
+ */
+
+/* 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;
+
+ @Output() indexChange = new EventEmitter();
+
+ slidesArray: number[];
+
+ @ViewChild('swiper', {static: true})
+ swiperElement: ElementRef;
+
+ @ViewChildren('slideContainers', {read: ViewContainerRef})
+ slideContainers: QueryList;
+
+ 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);
+ }
+}
diff --git a/src/app/modules/schedule/page/grid/infinite-swiper.html b/src/app/modules/schedule/page/grid/infinite-swiper.html
new file mode 100644
index 00000000..15c37226
--- /dev/null
+++ b/src/app/modules/schedule/page/grid/infinite-swiper.html
@@ -0,0 +1,26 @@
+
+
+
diff --git a/src/app/modules/schedule/page/grid/infinite-swiper.scss b/src/app/modules/schedule/page/grid/infinite-swiper.scss
new file mode 100644
index 00000000..47cd69aa
--- /dev/null
+++ b/src/app/modules/schedule/page/grid/infinite-swiper.scss
@@ -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 .
+ */
+
+.swiper {
+ height: 100%;
+ width: 100%;
+}
diff --git a/src/app/modules/schedule/page/grid/schedule-cursor.component.ts b/src/app/modules/schedule/page/grid/schedule-cursor.component.ts
index 52cbb3d9..853674db 100644
--- a/src/app/modules/schedule/page/grid/schedule-cursor.component.ts
+++ b/src/app/modules/schedule/page/grid/schedule-cursor.component.ts
@@ -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 .
*/
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
diff --git a/src/app/modules/schedule/page/grid/schedule-cursor.scss b/src/app/modules/schedule/page/grid/schedule-cursor.scss
index 8654d09b..422c14d5 100644
--- a/src/app/modules/schedule/page/grid/schedule-cursor.scss
+++ b/src/app/modules/schedule/page/grid/schedule-cursor.scss
@@ -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 .
+ */
+
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);
diff --git a/src/app/modules/schedule/page/grid/schedule-day.component.ts b/src/app/modules/schedule/page/grid/schedule-day.component.ts
new file mode 100644
index 00000000..25bbde16
--- /dev/null
+++ b/src/app/modules/schedule/page/grid/schedule-day.component.ts
@@ -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 .
+ */
+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;
+
+ @Input() uuids: SCUuid[];
+
+ @Input() scale: number;
+
+ @Input() frequencies?: SCISO8601Duration[];
+
+ @Input() dateSeries?: Record;
+
+ constructor(protected readonly scheduleProvider: ScheduleProvider) {}
+
+ // ngOnInit() {
+ // this.dateSeries = this.fetchDateSeries();
+ // }
+
+ // TODO: backend bug results in the wrong date series being returned
+ /* async fetchDateSeries(): Promise {
+ 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,
+ },
+ }));
+ } */
+}
diff --git a/src/app/modules/schedule/page/grid/schedule-day.html b/src/app/modules/schedule/page/grid/schedule-day.html
new file mode 100644
index 00000000..7d78a1f2
--- /dev/null
+++ b/src/app/modules/schedule/page/grid/schedule-day.html
@@ -0,0 +1,35 @@
+
+
diff --git a/src/app/modules/schedule/page/grid/schedule-day.scss b/src/app/modules/schedule/page/grid/schedule-day.scss
new file mode 100644
index 00000000..7a7cd89a
--- /dev/null
+++ b/src/app/modules/schedule/page/grid/schedule-day.scss
@@ -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 .
+ */
+
+.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;
+}
diff --git a/src/app/modules/schedule/page/schedule-single-events.component.ts b/src/app/modules/schedule/page/schedule-single-events.component.ts
index 1cab65e8..c3b8901d 100644
--- a/src/app/modules/schedule/page/schedule-single-events.component.ts
+++ b/src/app/modules/schedule/page/schedule-single-events.component.ts
@@ -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)),
);
}
diff --git a/src/app/modules/schedule/page/schedule-view.component.ts b/src/app/modules/schedule/page/schedule-view.component.ts
index 1fda9ff7..2c11c506 100644
--- a/src/app/modules/schedule/page/schedule-view.component.ts
+++ b/src/app/modules/schedule/page/schedule-view.component.ts
@@ -71,8 +71,8 @@ export class ScheduleViewComponent extends CalendarViewComponent {
/**
* Load events
*/
- // @Override
- async loadEvents(): Promise {
+ // TODO: @Override
+ /*async loadEvents(): Promise {
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';
- }
+ }*/
}
diff --git a/src/app/modules/schedule/page/schema/schema.ts b/src/app/modules/schedule/page/schema/schema.ts
index 27089cec..92fd9488 100644
--- a/src/app/modules/schedule/page/schema/schema.ts
+++ b/src/app/modules/schedule/page/schema/schema.ts
@@ -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 {
+ from: T;
+ to: T;
+}
+
/**
* Minimal interface to provide information about a custom event
*/
diff --git a/src/app/modules/schedule/schedule.module.ts b/src/app/modules/schedule/schedule.module.ts
index b06c8fe4..032e8301 100644
--- a/src/app/modules/schedule/schedule.module.ts
+++ b/src/app/modules/schedule/schedule.module.ts
@@ -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],
})
diff --git a/src/app/modules/schedule/schedule.provider.ts b/src/app/modules/schedule/schedule.provider.ts
index fbcfe271..0c88da52 100644
--- a/src/app/modules/schedule/schedule.provider.ts
+++ b/src/app/modules/schedule/schedule.provider.ts
@@ -112,9 +112,17 @@ export class ScheduleProvider implements OnDestroy {
frequencies?: Array,
from?: SCISO8601Date | 'now',
to?: SCISO8601Date | 'now',
- ): Promise {
+ ): 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 = {};
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(),
+ };
}
/**
diff --git a/src/app/translation/common-string-pipes.ts b/src/app/translation/common-string-pipes.ts
index 303cb382..572b17d5 100644
--- a/src/app/translation/common-string-pipes.ts
+++ b/src/app/translation/common-string-pipes.ts
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2020-2021 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.
@@ -48,6 +48,28 @@ export class ArrayJoinPipe implements PipeTransform {
}
}
+@Injectable()
+@Pipe({
+ name: 'entries',
+ pure: true,
+})
+export class EntriesPipe implements PipeTransform {
+ transform(value: Record): T[] {
+ return Object.values(value);
+ }
+}
+
+@Injectable()
+@Pipe({
+ name: 'toUnix',
+ pure: true,
+})
+export class ToUnixPipe implements PipeTransform {
+ transform(value: string | number | Date | null | undefined): number {
+ return (value instanceof Date ? value : new Date(value ?? 0)).valueOf();
+ }
+}
+
@Injectable()
@Pipe({
name: 'sentencecase',
diff --git a/src/app/translation/thing-translate.module.ts b/src/app/translation/thing-translate.module.ts
index cbedf5c8..5f4c5ef2 100644
--- a/src/app/translation/thing-translate.module.ts
+++ b/src/app/translation/thing-translate.module.ts
@@ -23,6 +23,8 @@ import {
StringSplitPipe,
OpeningHoursPipe,
DurationLocalizedPipe,
+ ToUnixPipe,
+ EntriesPipe,
} from './common-string-pipes';
import {
ThingTranslateDefaultParser,
@@ -52,6 +54,8 @@ export interface ThingTranslateModuleConfig {
DateLocalizedFormatPipe,
OpeningHoursPipe,
SentenceCasePipe,
+ ToUnixPipe,
+ EntriesPipe,
],
exports: [
ArrayJoinPipe,
@@ -65,6 +69,8 @@ export interface ThingTranslateModuleConfig {
DateLocalizedFormatPipe,
OpeningHoursPipe,
SentenceCasePipe,
+ ToUnixPipe,
+ EntriesPipe,
],
})
export class ThingTranslateModule {
diff --git a/src/app/util/date-from-index.pipe.ts b/src/app/util/date-from-index.pipe.ts
new file mode 100644
index 00000000..7cd58f22
--- /dev/null
+++ b/src/app/util/date-from-index.pipe.ts
@@ -0,0 +1,27 @@
+/*
+ * 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 .
+ */
+
+import {Pipe, PipeTransform} from '@angular/core';
+import {Moment} from 'moment';
+
+@Pipe({
+ name: 'dateFromIndex',
+ pure: true,
+})
+export class DateFromIndexPipe implements PipeTransform {
+ transform(index: number, baseline: Moment): Moment {
+ return baseline.clone().add(index, 'days');
+ }
+}
diff --git a/src/app/util/date-is-today.pipe.ts b/src/app/util/date-is-today.pipe.ts
index 335af897..b621eb47 100644
--- a/src/app/util/date-is-today.pipe.ts
+++ b/src/app/util/date-is-today.pipe.ts
@@ -30,7 +30,16 @@ export class DateIsThisPipe implements PipeTransform {
* Transform
*/
// tslint:disable-next-line:prefer-function-over-method
- transform(value: Moment, granularity: unitOfTime.StartOf): boolean {
- return value.isSame(moment(moment.now()), granularity);
+ transform(
+ value: Moment | string | number,
+ granularity: unitOfTime.StartOf,
+ ): boolean {
+ return (
+ typeof value === 'string'
+ ? moment(value)
+ : typeof value === 'number'
+ ? moment.unix(value)
+ : value
+ ).isSame(moment(moment.now()), granularity);
}
}
diff --git a/src/app/util/util.module.ts b/src/app/util/util.module.ts
index 7e63af06..3dbbf5d5 100644
--- a/src/app/util/util.module.ts
+++ b/src/app/util/util.module.ts
@@ -17,9 +17,20 @@ import {NgModule} from '@angular/core';
import {ArrayLastPipe} from './array-last.pipe';
import {DateIsThisPipe} from './date-is-today.pipe';
import {NullishCoalescingPipe} from './nullish-coalecing.pipe';
+import {DateFromIndexPipe} from './date-from-index.pipe';
@NgModule({
- declarations: [ArrayLastPipe, DateIsThisPipe, NullishCoalescingPipe],
- exports: [ArrayLastPipe, DateIsThisPipe, NullishCoalescingPipe],
+ declarations: [
+ ArrayLastPipe,
+ DateIsThisPipe,
+ NullishCoalescingPipe,
+ DateFromIndexPipe,
+ ],
+ exports: [
+ ArrayLastPipe,
+ DateIsThisPipe,
+ NullishCoalescingPipe,
+ DateFromIndexPipe,
+ ],
})
export class UtilModule {}
diff --git a/src/global.scss b/src/global.scss
index 8498cc85..2c4376a9 100644
--- a/src/global.scss
+++ b/src/global.scss
@@ -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 .
+ */
+
// http://ionicframework.com/docs/theming/
@import "~@ionic/angular/css/normalize.css";
@import "~@ionic/angular/css/structure.css";
@@ -11,6 +26,10 @@
@import "~@ionic/angular/css/flex-utils.css";
@import "~@ionic/angular/css/display.css";
+// https://swiperjs.com/angular#styles
+@import "swiper/scss";
+@import "swiper/scss/controller";
+
/* StApps */
ion-item {