feat: tab navigation bar animations and state

This commit is contained in:
Rainer Killinger
2022-09-23 16:34:07 +02:00
parent b2cc1fd91f
commit 7ecba0b781
32 changed files with 615 additions and 182 deletions

View File

@@ -1,16 +1,16 @@
/*
* 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 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.
* 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/>.
* 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 {CommonModule} from '@angular/common';
import {NgModule} from '@angular/core';
@@ -22,16 +22,14 @@ import {TranslateModule} from '@ngx-translate/core';
import {SettingsModule} from '../settings/settings.module';
import {ContextMenuComponent} from './context/context-menu.component';
import {ContextMenuService} from './context/context-menu.service';
import {NavigationComponent} from './navigation/navigation.component';
import {TabsModule} from './tabs/tabs.module';
import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
/**
* Menu module
*/
@NgModule({
declarations: [NavigationComponent, ContextMenuComponent],
exports: [NavigationComponent, ContextMenuComponent],
declarations: [ContextMenuComponent],
exports: [ContextMenuComponent],
imports: [
CommonModule,
IonIconModule,
@@ -40,7 +38,6 @@ import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
RouterModule,
SettingsModule,
TranslateModule.forChild(),
TabsModule,
LayoutModule,
],
providers: [ContextMenuService],

View File

@@ -1,3 +1,18 @@
<!--
~ 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/>.
-->
<ion-split-pane contentId="main" when="md">
<ion-menu
menuId="main"
@@ -9,11 +24,7 @@
<ion-header>
<ion-toolbar color="primary" mode="ios">
<ion-buttons slot="start"></ion-buttons>
<ion-title
class="clickable"
[routerLink]="['/']"
[routerDirection]="'root'"
>
<ion-title class="clickable" rootLink="/">
<ion-img src="assets/imgs/header.svg" class="logo"></ion-img>
</ion-title>
</ion-toolbar>
@@ -25,17 +36,12 @@
{{ category.translations[language].name | titlecase }}
</ion-label>
</ion-list-header>
<ion-menu-toggle auto-hide="false" *ngFor="let item of category.items">
<ion-item
[routerDirection]="'root'"
[routerLink]="['/' + item.route]"
>
<ion-icon slot="end" [name]="item.icon"></ion-icon>
<ion-label>
{{ item.translations[language].title | titlecase }}
</ion-label>
</ion-item>
</ion-menu-toggle>
<ion-item *ngFor="let item of category.items" [rootLink]="item.route">
<ion-icon slot="end" [name]="item.icon"></ion-icon>
<ion-label>
{{ item.translations[language].title | titlecase }}
</ion-label>
</ion-item>
</ion-list>
</ion-content>
</ion-menu>

View File

@@ -0,0 +1,36 @@
/*
* 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 {NgModule} from '@angular/core';
import {RootLinkDirective} from './root-link.directive';
import {NavigationComponent} from './navigation.component';
import {TabsComponent} from './tabs.component';
import {CommonModule} from '@angular/common';
import {IonicModule} from '@ionic/angular';
import {IonIconModule} from '../../../util/ion-icon/ion-icon.module';
import {TranslateModule} from '@ngx-translate/core';
import {RouterModule} from '@angular/router';
@NgModule({
declarations: [RootLinkDirective, NavigationComponent, TabsComponent],
imports: [
CommonModule,
IonicModule,
IonIconModule,
TranslateModule,
RouterModule,
],
exports: [TabsComponent, RootLinkDirective, NavigationComponent],
})
export class NavigationModule {}

View File

@@ -1,7 +1,21 @@
/*!
* 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 '../../../../theme/util/mixins';
:host {
ion-split-pane {
margin-bottom: calc(var(--ion-tabbar-height) + env(safe-area-inset-bottom));
@@ -32,3 +46,16 @@
}
}
}
ion-router-outlet {
background: white;
}
.link-active > * {
color: var(--ion-color-primary);
::ng-deep stapps-icon {
--fill: 1;
}
}

View File

@@ -0,0 +1,106 @@
/*
* 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 {
Directive,
ElementRef,
Input,
OnDestroy,
OnInit,
Renderer2,
} from '@angular/core';
import {AnimationController, NavController} from '@ionic/angular';
import {Router, RouterEvent} from '@angular/router';
import {tabsTransition} from './tabs-transition';
import {Subscription} from 'rxjs';
@Directive({
selector: '[rootLink]',
})
export class RootLinkDirective implements OnInit, OnDestroy {
@Input() rootLink: string;
@Input() redirectedFrom: string;
dispose: () => void;
subscriptions: Subscription[] = [];
private readonly classNames = ['tab-selected', 'link-active'];
private needsInit = true;
constructor(
private element: ElementRef,
private renderer: Renderer2,
private navController: NavController,
private router: Router,
private animationController: AnimationController,
) {}
ngOnInit() {
const animation = tabsTransition(this.animationController);
this.renderer.setAttribute(this.element.nativeElement, 'button', '');
this.subscriptions.push(
this.router.events.subscribe(event => {
if (
event instanceof RouterEvent &&
// @ts-expect-error access private member
(this.navController.direction === 'root' || this.needsInit)
) {
if (
event.url === this.rootLink ||
(this.redirectedFrom && event.url === this.redirectedFrom)
) {
this.setActive();
} else {
this.setInactive();
}
this.needsInit = false;
}
}),
);
this.dispose = this.renderer.listen(
this.element.nativeElement,
'click',
() => {
this.setActive();
this.navController.setDirection('root', true, 'back', animation);
void this.router.navigate([this.rootLink]);
},
);
}
setActive() {
for (const className of this.classNames) {
this.renderer.addClass(this.element.nativeElement, className);
}
}
setInactive() {
for (const className of this.classNames) {
this.renderer.removeClass(this.element.nativeElement, className);
}
}
ngOnDestroy() {
this.dispose();
for (const subscription of this.subscriptions) {
subscription.unsubscribe();
}
}
}

View File

@@ -0,0 +1,31 @@
/*
* 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 {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
const routes: Routes = [
{
path: '',
redirectTo: '/dashboard',
pathMatch: 'full',
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class TabsRoutingModule {}

View File

@@ -0,0 +1,76 @@
/*
* 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 type {AnimationBuilder} from '@ionic/angular';
import {AnimationController} from '@ionic/angular';
import type {AnimationOptions} from '@ionic/angular/providers/nav-controller';
/**
*
*/
export function tabsTransition(
animationController: AnimationController,
): AnimationBuilder {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (_baseElement: HTMLElement, options: AnimationOptions | any) => {
const duration = options.duration || 350;
const contentExitDuration = options.contentExitDuration || 100;
const rootTransition = animationController.create().duration(duration);
const enterTransition = animationController
.create()
.fromTo('opacity', '1', '1')
.addElement(options.enteringEl);
const exitZIndex = animationController
.create()
.beforeStyles({zIndex: 0})
.afterClearStyles(['zIndex'])
.addElement(options.leavingEl);
const exitTransition = animationController
.create()
.duration(contentExitDuration * 2)
.easing('cubic-bezier(0.87, 0, 0.13, 1)')
.fromTo('opacity', '1', '0')
.addElement(options.leavingEl.querySelector('ion-header'));
const contentExit = animationController
.create()
.easing('linear')
.duration(contentExitDuration)
.fromTo('opacity', '1', '0')
.addElement(
options.leavingEl.querySelectorAll(':scope > *:not(ion-header)'),
);
const contentEnter = animationController
.create()
.delay(contentExitDuration)
.duration(duration - contentExitDuration)
.easing('cubic-bezier(0.16, 1, 0.3, 1)')
.fromTo('transform', 'scale(1.025)', 'scale(1)')
.fromTo('opacity', '0', '1')
.addElement(
options.enteringEl.querySelectorAll(':scope > *:not(ion-header)'),
);
rootTransition.addAnimation([
enterTransition,
contentExit,
contentEnter,
exitTransition,
exitZIndex,
]);
return rootTransition;
};
}

View File

@@ -1,19 +1,31 @@
/*!
* 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 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.
* 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/>.
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
:host {
position: absolute;
bottom: 0;
left: 0;
right: 0;
z-index: 100;
}
.tab-selected ::ng-deep stapps-icon {
--fill: 1;
}
/*:host {
display: flex;
flex-direction: row;
background: var(--ion-color-primary-contrast);
@@ -36,8 +48,7 @@
background: var(--ion-color-primary-contrast);
text-decoration: none;
&:focus,
&.active{
&.active-root-link {
color: var(--ion-color-medium-contrast);
ion-icon ::ng-deep stapps-icon {
@@ -48,10 +59,11 @@
ion-icon {
font-size: 28px;
}
ion-label {
text-transform: uppercase;
font-size: var(--font-size-xs);
font-weight: var(--font-weight-semi-bold);
}
}
}
}*/

View File

@@ -1,3 +1,18 @@
/*
* 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} from '@angular/core';
import {
SCAppConfigurationMenuCategory,

View File

@@ -1,6 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/*
* Copyright (C) 2018, 2019 StApps
* 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.
@@ -13,6 +12,8 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
import {TestBed, waitForAsync} from '@angular/core/testing';

View File

@@ -0,0 +1,37 @@
<!--
~ 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/>.
-->
<ion-tab-bar slot="bottom">
<ion-tab-button rootLink="/dashboard" redirectedFrom="/" tab="dashboard">
<ion-icon name="home"></ion-icon>
<ion-label>{{ 'tabs.home' | translate }}</ion-label>
</ion-tab-button>
<ion-tab-button rootLink="/canteen">
<ion-icon name="local_cafe"></ion-icon>
<ion-label>{{ 'tabs.canteens' | translate }}</ion-label>
</ion-tab-button>
<ion-tab-button rootLink="/schedule">
<ion-icon name="school"></ion-icon>
<ion-label>{{ 'tabs.schedule' | translate }}</ion-label>
</ion-tab-button>
<ion-tab-button rootLink="/map">
<ion-icon name="map"></ion-icon>
<ion-label>{{ 'tabs.map' | translate }}</ion-label>
</ion-tab-button>
<ion-tab-button rootLink="/profile">
<ion-icon name="account_circle"></ion-icon>
<ion-label>{{ 'tabs.profile' | translate }}</ion-label>
</ion-tab-button>
</ion-tab-bar>

View File

@@ -1,16 +0,0 @@
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
const routes: Routes = [
{
path: '',
redirectTo: '/dashboard',
pathMatch: 'full',
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class TabsRoutingModule {}

View File

@@ -1,37 +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 {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {RouterModule} from '@angular/router';
import {IonicModule} from '@ionic/angular';
import {TabsComponent} from './tabs.component';
import {TranslateModule} from '@ngx-translate/core';
import {IonIconModule} from '../../../util/ion-icon/ion-icon.module';
@NgModule({
imports: [
CommonModule,
IonicModule,
IonIconModule,
TranslateModule,
RouterModule,
],
declarations: [TabsComponent],
exports: [TabsComponent],
})
export class TabsModule {}

View File

@@ -1,51 +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/>.
-->
<a
[routerDirection]="'root'"
[routerLink]="['/dashboard']"
routerLinkActive="active"
>
<ion-icon [name]="'home'"></ion-icon>
<ion-label>{{ 'tabs.home' | translate }}</ion-label>
</a>
<a
[routerDirection]="'root'"
[routerLink]="['/canteen']"
routerLinkActive="active"
>
<ion-icon [name]="'local_cafe'"></ion-icon>
<ion-label>{{ 'tabs.canteens' | translate }}</ion-label>
</a>
<a
[routerDirection]="'root'"
[routerLink]="['/schedule']"
routerLinkActive="active"
>
<ion-icon [name]="'school'"></ion-icon>
<ion-label>{{ 'tabs.schedule' | translate }}</ion-label>
</a>
<a [routerDirection]="'root'" [routerLink]="['/map']" routerLinkActive="active">
<ion-icon [name]="'map'"></ion-icon>
<ion-label>{{ 'tabs.map' | translate }}</ion-label>
</a>
<a
[routerDirection]="'root'"
[routerLink]="['/profile']"
routerLinkActive="active"
>
<ion-icon [name]="'account_circle'"></ion-icon>
<ion-label>{{ 'tabs.profile' | translate }}</ion-label>
</a>