From 523e34f6e46dad6f812e9a819b5d4a4e220485a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wieland=20Sch=C3=B6bl?= Date: Tue, 9 Nov 2021 17:12:45 +0000 Subject: [PATCH] refactor: use SwiperJS in schedule module --- package-lock.json | 22 ++ package.json | 1 + .../schedule/page/calendar-view.component.ts | 199 +++----------- .../modules/schedule/page/calendar-view.html | 115 ++++---- .../modules/schedule/page/calendar-view.scss | 87 +++--- .../page/grid/infinite-slides.component.ts | 107 -------- .../schedule/page/grid/infinite-slides.html | 9 - .../schedule/page/grid/infinite-slides.scss | 4 - .../page/grid/infinite-swiper.component.ts | 256 ++++++++++++++++++ .../schedule/page/grid/infinite-swiper.html | 26 ++ .../schedule/page/grid/infinite-swiper.scss | 19 ++ .../page/grid/schedule-cursor.component.ts | 4 +- .../schedule/page/grid/schedule-cursor.scss | 21 +- .../page/grid/schedule-day.component.ts | 68 +++++ .../schedule/page/grid/schedule-day.html | 35 +++ .../schedule/page/grid/schedule-day.scss | 37 +++ .../page/schedule-single-events.component.ts | 2 +- .../schedule/page/schedule-view.component.ts | 8 +- .../modules/schedule/page/schema/schema.ts | 7 +- src/app/modules/schedule/schedule.module.ts | 22 +- src/app/modules/schedule/schedule.provider.ts | 39 ++- src/app/translation/common-string-pipes.ts | 24 +- src/app/translation/thing-translate.module.ts | 6 + src/app/util/date-from-index.pipe.ts | 27 ++ src/app/util/date-is-today.pipe.ts | 13 +- src/app/util/util.module.ts | 15 +- src/global.scss | 19 ++ 27 files changed, 771 insertions(+), 421 deletions(-) delete mode 100644 src/app/modules/schedule/page/grid/infinite-slides.component.ts delete mode 100644 src/app/modules/schedule/page/grid/infinite-slides.html delete mode 100644 src/app/modules/schedule/page/grid/infinite-slides.scss create mode 100644 src/app/modules/schedule/page/grid/infinite-swiper.component.ts create mode 100644 src/app/modules/schedule/page/grid/infinite-swiper.html create mode 100644 src/app/modules/schedule/page/grid/infinite-swiper.scss create mode 100644 src/app/modules/schedule/page/grid/schedule-day.component.ts create mode 100644 src/app/modules/schedule/page/grid/schedule-day.html create mode 100644 src/app/modules/schedule/page/grid/schedule-day.scss create mode 100644 src/app/util/date-from-index.pipe.ts 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 @@ -
- + +
+ - + - - - +
{{ - item - | amDateFormat: ((item | dateIsThis: 'week') ? 'dddd' : 'll') + date + | amDateFormat: ((date | dateIsThis: 'week') ? 'dddd' : 'll') }} - - - +
+ +
- - - - - -
- - -
- - -
-
-
-
-
-
- + + + + +
. + */ + +.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 {