refactor: replace timetable event modal with route

This commit is contained in:
2023-02-17 16:51:52 +01:00
committed by Rainer Killinger
parent 22e70ae92b
commit 3e5724d9be
12 changed files with 176 additions and 145 deletions

View File

@@ -1,5 +1,5 @@
/*! /*
* Copyright (C) 2022 StApps * Copyright (C) 2023 StApps
* This program is free software: you can redistribute it and/or modify it * 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 * under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3. * Software Foundation, version 3.
@@ -13,23 +13,8 @@
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
:host { // these are the ionic values
height: 100%; export const iosEasing = 'cubic-bezier(0.32,0.72,0,1)';
display: flex; export const iosDuration = 540;
flex-direction: column; export const mdEasing = 'cubic-bezier(0.36,0.66,0.04,1)';
flex: 1 1 20%; export const mdDuration = 280;
}
ion-button {
ion-label {
color: var(--ion-color-light);
}
}
ion-card-content {
height: 100%;
padding: 0;
stapps-data-list {
height: 100%;
}
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright (C) 2023 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 {AnimationBuilder, AnimationController} from '@ionic/angular';
import {AnimationOptions} from '@ionic/angular/providers/nav-controller';
import {iosDuration, iosEasing, mdDuration, mdEasing} from './easings';
/**
*
*/
export function fabExpand(animationController: AnimationController): AnimationBuilder {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (_baseElement: HTMLElement, options: AnimationOptions | any) => {
const rootTransition = animationController
.create()
.duration(options.duration ?? (options.mode === 'ios' ? iosDuration : mdDuration * 1.4))
.easing(options.mode === 'ios' ? iosEasing : mdEasing);
const back = options.direction === 'back';
const fabView = back ? options.enteringEl! : options.leavingEl!;
const otherView = back ? options.leavingEl! : options.enteringEl!;
const fab = fabView.querySelector('ion-fab-button').shadowRoot.querySelector('.button-native');
const fabBounds = fab.getBoundingClientRect();
const viewBounds = otherView.getBoundingClientRect();
const useReducedMotion = viewBounds.width > 500;
const reducedMotionTransform = `${Math.min(viewBounds.width * 0.3, 200)}px`;
const reducedMotionViewBorderRadius = '128px';
const reducedMotionFabGrow = '2';
const reducedMotionViewShrink = '0.9';
const viewCenterX = (viewBounds.width - viewBounds.x) / 2;
const viewCenterY = (viewBounds.height - viewBounds.y) / 2;
const viewOnFab = useReducedMotion
? `translate(${reducedMotionTransform}, ${reducedMotionTransform}) scale(${reducedMotionViewShrink})`
: `translate(${(fabBounds.x - viewBounds.x) / 2}px, ${(fabBounds.y - viewBounds.y) / 2}px) scale(${
fabBounds.width / viewBounds.width
}, ${fabBounds.height / viewBounds.height})`;
const fabOnView = useReducedMotion
? `translate(-${reducedMotionTransform}, -${reducedMotionTransform}) scale(${reducedMotionFabGrow})`
: `translate(${viewCenterX - fabBounds.x}px, ${viewCenterY - fabBounds.y}px) scale(${
viewBounds.width / fabBounds.width
}, ${viewBounds.height / fabBounds.height})`;
const transformNormal = `translate(0px, 0px) scale(1, 1)`;
const viewBorderRadius = useReducedMotion ? reducedMotionViewBorderRadius : '50%';
const fabViewFade = animationController
.create()
.beforeStyles({zIndex: -1})
.fromTo('opacity', '1', '1')
.addElement(fabView);
const fabGrow = animationController
.create()
.beforeStyles({transformOrigin: 'center'})
.fromTo('transform', back ? fabOnView : transformNormal, back ? transformNormal : fabOnView)
.fromTo('opacity', back ? '0' : '1', back ? '1' : '0')
.fromTo('borderRadius', back ? '0' : '50%', back ? '50%' : '0')
.addElement(fab);
const viewGrow = animationController
.create()
.beforeStyles({zIndex: 200, overflow: 'hidden', transformOrigin: 'center'})
.fromTo('transform', back ? transformNormal : viewOnFab, back ? viewOnFab : transformNormal)
.fromTo('opacity', back ? '1' : '0', back ? '0' : '1')
.fromTo('borderRadius', back ? '0' : viewBorderRadius, back ? viewBorderRadius : '0')
.addElement(otherView);
return rootTransition.addAnimation(fabGrow).addAnimation(viewGrow).addAnimation(fabViewFade);
};
}

View File

@@ -46,7 +46,13 @@ import {searchPageSwitchAnimation} from './search-page-switch-animation';
providers: [ContextMenuService], providers: [ContextMenuService],
}) })
export class SearchPageComponent implements OnInit, OnDestroy { export class SearchPageComponent implements OnInit, OnDestroy {
title = 'search.title'; @Input() title = 'search.title';
@Input() placeholder = 'search.search_bar.placeholder';
@Input() searchInstruction = 'search.instruction';
@Input() backUrl?: string;
isHebisAvailable = false; isHebisAvailable = false;

View File

@@ -15,9 +15,9 @@
<stapps-context contentId="data-list"></stapps-context> <stapps-context contentId="data-list"></stapps-context>
<ion-header> <ion-header>
<ion-toolbar color="primary" mode="ios" *ngIf="showDrawer"> <ion-toolbar color="primary" mode="ios" *ngIf="showDrawer && showTopToolbar">
<ion-buttons slot="start"> <ion-buttons slot="start">
<ion-back-button></ion-back-button> <ion-back-button [defaultHref]="backUrl"></ion-back-button>
</ion-buttons> </ion-buttons>
<ion-title>{{ title | translate }}</ion-title> <ion-title>{{ title | translate }}</ion-title>
</ion-toolbar> </ion-toolbar>
@@ -28,7 +28,7 @@
(search)="hideKeyboard()" (search)="hideKeyboard()"
[(ngModel)]="queryText" [(ngModel)]="queryText"
showClearButton="always" showClearButton="always"
placeholder="{{ 'search.search_bar.placeholder' | translate }}" placeholder="{{ placeholder | translate }}"
mode="md" mode="md"
type="search" type="search"
enterkeyhint="search" enterkeyhint="search"
@@ -61,7 +61,7 @@
[style.display]="!showDefaultData && !items && !loading ? 'block' : 'none'" [style.display]="!showDefaultData && !items && !loading ? 'block' : 'none'"
> >
<ion-label class="centeredMessageContainer"> <ion-label class="centeredMessageContainer">
{{ 'search.instruction' | translate }} {{ searchInstruction | translate }}
</ion-label> </ion-label>
</div> </div>
<stapps-data-list <stapps-data-list

View File

@@ -0,0 +1,30 @@
/*
* Copyright (C) 2023 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} from '@angular/core';
import {SCSearchFilter, SCThingType} from '@openstapps/core';
@Component({
selector: 'stapps-choose-events-page',
templateUrl: 'choose-events-page.html',
})
export class ChooseEventsPageComponent {
forcedFilter: SCSearchFilter = {
arguments: {
field: 'type',
value: SCThingType.AcademicEvent,
},
type: 'value',
};
}

View File

@@ -1,5 +1,5 @@
<!-- <!--
~ Copyright (C) 2022 StApps ~ Copyright (C) 2023 StApps
~ This program is free software: you can redistribute it and/or modify it ~ 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 ~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3. ~ Software Foundation, version 3.
@@ -12,22 +12,12 @@
~ You should have received a copy of the GNU General Public License along with ~ You should have received a copy of the GNU General Public License along with
~ this program. If not, see <https://www.gnu.org/licenses/>. ~ this program. If not, see <https://www.gnu.org/licenses/>.
--> -->
<stapps-search-page
<ion-toolbar color="primary" mode="ios"> [showNavigation]="false"
<ion-title>{{ 'schedule.addEventModal.addEvent' | translate | titlecase }}</ion-title> [showDefaultData]="false"
<ion-buttons slot="end"> [forcedFilter]="forcedFilter"
<ion-button fill="clear" (click)="modalController.dismiss()"> [backUrl]="'..'"
<ion-label>{{ 'modal.DISMISS' | translate }}</ion-label> [title]="'schedule.addEventPage.TITLE'"
</ion-button> [placeholder]="'schedule.addEventPage.PLACEHOLDER'"
</ion-buttons> [searchInstruction]="'schedule.addEventPage.SEARCH_INSTRUCTION'"
</ion-toolbar> ></stapps-search-page>
<ion-card-content>
<stapps-search-page
[showDrawer]="false"
[forcedFilter]="filter"
[itemRouting]="false"
[showTopToolbar]="false"
[showNavigation]="false"
></stapps-search-page>
</ion-card-content>

View File

@@ -1,65 +0,0 @@
/*
* Copyright (C) 2022 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, OnDestroy, OnInit} from '@angular/core';
import {SCSearchFilter, SCThingType} from '@openstapps/core';
import {ModalController} from '@ionic/angular';
import {DataRoutingService} from '../../../data/data-routing.service';
import {DataDetailComponent} from '../../../data/detail/data-detail.component';
import {Subscription} from 'rxjs';
/**
* TODO
*/
@Component({
selector: 'modal-event-creator',
templateUrl: 'modal-event-creator.html',
styleUrls: ['modal-event-creator.scss'],
})
export class ModalEventCreatorComponent implements OnInit, OnDestroy {
subscriptions: Subscription[] = [];
constructor(readonly modalController: ModalController, readonly dataRoutingService: DataRoutingService) {}
ngOnInit() {
this.subscriptions.push(
this.dataRoutingService.itemSelectListener().subscribe(async item => {
const modal = await this.modalController.create({
component: DataDetailComponent,
componentProps: {
isModal: true,
inputItem: item,
},
canDismiss: true,
});
return modal.present();
}),
);
}
ngOnDestroy() {
for (const subscription of this.subscriptions) subscription.unsubscribe();
}
/**
* Forced filter
*/
filter: SCSearchFilter = {
arguments: {
field: 'type',
value: SCThingType.AcademicEvent,
},
type: 'value',
};
}

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2022 StApps * Copyright (C) 2023 StApps
* This program is free software: you can redistribute it and/or modify it * 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 * under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3. * Software Foundation, version 3.
@@ -15,12 +15,13 @@
import {AfterViewInit, Component, HostListener, Input, OnInit, ViewChild} from '@angular/core'; import {AfterViewInit, Component, HostListener, Input, OnInit, ViewChild} from '@angular/core';
import {Location} from '@angular/common'; import {Location} from '@angular/common';
import {ActivatedRoute, Router} from '@angular/router'; import {ActivatedRoute, Router} from '@angular/router';
import {IonRouterOutlet} from '@ionic/angular'; import {AnimationController, IonRouterOutlet} from '@ionic/angular';
import {SharedAxisChoreographer} from '../../../animation/animation-choreographer'; import {SharedAxisChoreographer} from '../../../animation/animation-choreographer';
import {materialSharedAxisX} from '../../../animation/material-motion'; import {materialSharedAxisX} from '../../../animation/material-motion';
import {ScheduleResponsiveBreakpoint} from './schema/schema'; import {ScheduleResponsiveBreakpoint} from './schema/schema';
import {CalendarService} from '../../calendar/calendar.service'; import {CalendarService} from '../../calendar/calendar.service';
import moment from 'moment'; import moment from 'moment';
import {fabExpand} from '../../../animation/fab-expand';
/** /**
* This needs to be sorted by break point low -> high * This needs to be sorted by break point low -> high
@@ -93,6 +94,8 @@ export class SchedulePageComponent implements OnInit, AfterViewInit {
isModalOpen = false; isModalOpen = false;
fabAnimation = fabExpand(this.animationController);
/** /**
* Amount of days that should be shown according to current display width * Amount of days that should be shown according to current display width
*/ */
@@ -111,6 +114,7 @@ export class SchedulePageComponent implements OnInit, AfterViewInit {
private calendarService: CalendarService, private calendarService: CalendarService,
readonly routerOutlet: IonRouterOutlet, readonly routerOutlet: IonRouterOutlet,
private router: Router, private router: Router,
private animationController: AnimationController,
private location: Location, private location: Location,
) {} ) {}
@@ -168,12 +172,4 @@ export class SchedulePageComponent implements OnInit, AfterViewInit {
onTodayClick() { onTodayClick() {
this.calendarService.emitGoToDate(moment().startOf('day')); this.calendarService.emitGoToDate(moment().startOf('day'));
} }
onFABClick() {
this.isModalOpen = true;
}
onModalDismiss() {
this.isModalOpen = false;
}
} }

View File

@@ -63,15 +63,15 @@
<stapps-single-events *ngSwitchCase="'single'"></stapps-single-events> <stapps-single-events *ngSwitchCase="'single'"></stapps-single-events>
</div> </div>
<ion-fab vertical="bottom" horizontal="end" slot="fixed" (click)="onFABClick()"> <ion-fab
vertical="bottom"
horizontal="end"
slot="fixed"
[routerLink]="['./event-picker']"
[routerAnimation]="fabAnimation"
>
<ion-fab-button> <ion-fab-button>
<ion-icon name="add"></ion-icon> <ion-icon name="add"></ion-icon>
</ion-fab-button> </ion-fab-button>
</ion-fab> </ion-fab>
<ion-modal canDismiss="true" [isOpen]="isModalOpen" (ionModalWillDismiss)="onModalDismiss()">
<ng-template>
<modal-event-creator></modal-event-creator>
</ng-template>
</ion-modal>
</ion-content> </ion-content>

View File

@@ -1,16 +1,16 @@
/* /*
* Copyright (C) 2022 StApps * Copyright (C) 2023 StApps
* This program is free software: you can redistribute it and/or modify it * 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 * under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3. * Software Foundation, version 3.
* *
* This program is distributed in the hope that it will be useful, but WITHOUT * This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details. * more details.
* *
* You should have received a copy of the GNU General Public License along with * You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';
import {NgModule} from '@angular/core'; import {NgModule} from '@angular/core';
@@ -26,7 +26,6 @@ import {DataModule} from '../data/data.module';
import {DataProvider} from '../data/data.provider'; import {DataProvider} from '../data/data.provider';
import {CalendarViewComponent} from './page/calendar-view.component'; import {CalendarViewComponent} from './page/calendar-view.component';
import {ScheduleCursorComponent} from './page/grid/schedule-cursor.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 {SchedulePageComponent} from './page/schedule-page.component';
import {ScheduleSingleEventsComponent} from './page/schedule-single-events.component'; import {ScheduleSingleEventsComponent} from './page/schedule-single-events.component';
import {ScheduleViewComponent} from './page/schedule-view.component'; import {ScheduleViewComponent} from './page/schedule-view.component';
@@ -37,6 +36,7 @@ import {ThingTranslateModule} from '../../translation/thing-translate.module';
import {InfiniteSwiperComponent} from './page/grid/infinite-swiper.component'; import {InfiniteSwiperComponent} from './page/grid/infinite-swiper.component';
import {CalendarComponent} from './page/components/calendar.component'; import {CalendarComponent} from './page/components/calendar.component';
import {IonIconModule} from '../../util/ion-icon/ion-icon.module'; import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
import {ChooseEventsPageComponent} from './page/choose-events-page.component';
const settingsRoutes: Routes = [ const settingsRoutes: Routes = [
{path: 'schedule', redirectTo: 'schedule/calendar/now'}, {path: 'schedule', redirectTo: 'schedule/calendar/now'},
@@ -45,6 +45,8 @@ const settingsRoutes: Routes = [
{path: 'schedule/single', redirectTo: 'schedule/single/now'}, {path: 'schedule/single', redirectTo: 'schedule/single/now'},
// calendar | recurring | single // calendar | recurring | single
{path: 'schedule/:mode/:date', component: SchedulePageComponent}, {path: 'schedule/:mode/:date', component: SchedulePageComponent},
// TODO: this is temporary until the new generalized search page is finished
{path: 'schedule/:mode/:date/event-picker', component: ChooseEventsPageComponent},
]; ];
/** /**
@@ -54,7 +56,7 @@ const settingsRoutes: Routes = [
declarations: [ declarations: [
CalendarComponent, CalendarComponent,
CalendarViewComponent, CalendarViewComponent,
ModalEventCreatorComponent, ChooseEventsPageComponent,
ScheduleCardComponent, ScheduleCardComponent,
ScheduleCursorComponent, ScheduleCursorComponent,
SchedulePageComponent, SchedulePageComponent,

View File

@@ -454,8 +454,10 @@
"recurring": "Stundenplan", "recurring": "Stundenplan",
"calendar": "Kalender", "calendar": "Kalender",
"single": "Einzeltermine", "single": "Einzeltermine",
"addEventModal": { "addEventPage": {
"addEvent": "Events Hinzufügen" "TITLE": "Termine Hinzufügen",
"PLACEHOLDER": "Termine",
"SEARCH_INSTRUCTION": "Termine finden"
}, },
"card": { "card": {
"forEach": "Alle", "forEach": "Alle",

View File

@@ -454,8 +454,10 @@
"recurring": "Recurring", "recurring": "Recurring",
"calendar": "Calendar", "calendar": "Calendar",
"single": "Single Events", "single": "Single Events",
"addEventModal": { "addEventPage": {
"addEvent": "Add Events" "TITLE": "Add Events",
"PLACEHOLDER": "Events",
"SEARCH_INSTRUCTION": "Find events"
}, },
"card": { "card": {
"forEach": "Every", "forEach": "Every",