refactor: display id-cards in their own modal

This commit is contained in:
Rainer Killinger
2024-06-05 11:07:30 +00:00
committed by Thea Schöbl
parent a6b88d3534
commit 341b209092
12 changed files with 273 additions and 74 deletions

View File

@@ -1,10 +1,14 @@
import {ChangeDetectionStrategy, Component, Input} from '@angular/core';
import {ChangeDetectionStrategy, Component, ElementRef, Input} from '@angular/core';
import {SCIdCard} from '@openstapps/core';
import {FullScreenImageDirective} from '../../util/full-screen-image.directive';
import {ThingTranslateModule} from '../../translation/thing-translate.module';
import {AsyncPipe, TitleCasePipe} from '@angular/common';
import {InRangeNowPipe, ToDateRangePipe} from '../../util/in-range.pipe';
import {TranslateModule} from '@ngx-translate/core';
import {AnimationController, ModalController} from '@ionic/angular';
import {ScreenBrightness} from '@capacitor-community/screen-brightness';
import {ScreenOrientation} from '@capacitor/screen-orientation';
import {Capacitor} from '@capacitor/core';
import {iosDuration, iosEasing, mdDuration, mdEasing} from 'src/app/animation/easings';
@Component({
selector: 'stapps-id-card',
@@ -12,17 +16,130 @@ import {TranslateModule} from '@ngx-translate/core';
styleUrls: ['id-card.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
hostDirectives: [FullScreenImageDirective],
imports: [
FullScreenImageDirective,
ThingTranslateModule,
InRangeNowPipe,
ToDateRangePipe,
AsyncPipe,
TranslateModule,
TitleCasePipe,
],
imports: [ThingTranslateModule, InRangeNowPipe, ToDateRangePipe, AsyncPipe, TranslateModule, TitleCasePipe],
})
export class IdCardComponent {
@Input({required: true}) item: SCIdCard;
constructor(
private modalController: ModalController,
private animationController: AnimationController,
private elementRef: ElementRef,
) {}
async presentFullscreen() {
const top = await this.modalController.getTop();
const mode = document.querySelector('ion-app')!.getAttribute('mode');
if (top) return;
if (window.innerWidth >= 768) return;
if (Capacitor.isNativePlatform()) {
const orientation = await ScreenOrientation.orientation();
ScreenOrientation.lock({orientation: 'portrait'});
if (orientation.type.startsWith('landscape')) {
await new Promise(resolve => setTimeout(resolve, 500));
}
}
const img: HTMLImageElement = this.elementRef.nativeElement.querySelector('img')!;
const safeArea = (location: string) =>
Number(img.computedStyleMap().get(`--ion-safe-area-${location}`)!.toString().replace(/px$/, ''));
const safeAreaTop = safeArea('top');
const safeAreaBottom = safeArea('bottom');
const safeAreaLeft = safeArea('left');
const safeAreaRight = safeArea('right');
const windowWidth = window.innerWidth - safeAreaLeft - safeAreaRight;
const windowHeight = window.innerHeight - safeAreaTop - safeAreaBottom;
const isLandscape = windowWidth > windowHeight;
const nativeBounds = img.getBoundingClientRect();
const imageAspect = nativeBounds.width / nativeBounds.height;
const imgWidth = Math.min(windowHeight, windowWidth * imageAspect);
const imgHeight = imgWidth / imageAspect;
const fullscreenWidth = isLandscape ? imgHeight : imgWidth;
const fullscreenHeight = isLandscape ? imgWidth : imgHeight;
const scale = fullscreenWidth / windowWidth;
const animation = (modal: HTMLElement, leave = false) => {
const root = modal.shadowRoot!;
const sourceAnimation = this.animationController
.create()
.addElement(this.elementRef.nativeElement)
.fromTo('opacity', 0, 0);
const backdrop =
'linear-gradient(to bottom,' +
'transparent 16px,' +
'rgba(0, 0, 0, 0.3) 20%,' +
'rgba(0, 0, 0, 0.3) 80%,' +
'transparent 100%)';
const wrapperAnimation = this.animationController
.create()
.beforeStyles({'--background': 'transparent', 'margin': '0'})
.addElement(root.querySelector('.modal-wrapper')!)
.fromTo('transform', 'scale(1)', 'scale(1)')
.fromTo('opacity', '1', '1');
const backdropAnimation = this.animationController
.create()
.beforeStyles({background: backdrop, filter: 'blur(16px)'})
.addElement(root.querySelector('ion-backdrop')!)
.fromTo('opacity', leave ? 1 : 0, leave ? 0 : 1);
const small =
`translate(${nativeBounds.left - safeAreaLeft}px, ${nativeBounds.top - safeAreaTop}px) ` +
`scale(${nativeBounds.width / windowWidth}) ` +
`rotate(0) `;
const large =
`translate(${windowWidth - (windowWidth - fullscreenHeight) / 2}px, ${
(windowHeight - fullscreenWidth) / 2
}px)` +
`scale(${scale}) ` +
`rotate(${isLandscape ? 0 : 90}deg) `;
const cardAnimation = this.animationController
.create()
.addElement(modal.querySelector('stapps-id-card')!)
.beforeStyles({
'transform-origin': 'top left',
'filter': 'drop-shadow(0 0 16px rgba(0, 0, 0, 0.1))',
})
.fromTo('transform', leave ? large : small, leave ? small : large);
return this.animationController
.create()
.addElement(modal)
.easing(mode === 'ios' ? iosEasing : mdEasing)
.duration(2 * (mode === 'ios' ? iosDuration : mdDuration))
.addAnimation([wrapperAnimation, backdropAnimation, cardAnimation, sourceAnimation]);
};
const modal = await this.modalController.create({
component: IdCardComponent,
backdropDismiss: true,
mode: 'md',
componentProps: {
item: this.item,
},
presentingElement: this.elementRef.nativeElement,
enterAnimation: base => animation(base),
leaveAnimation: base => animation(base, true),
});
const dismiss = () => modal.dismiss();
window.addEventListener('click', dismiss);
modal.addEventListener('didDismiss', () => window.removeEventListener('click', dismiss));
if (Capacitor.isNativePlatform()) {
const brightness = (await ScreenBrightness.getBrightness()).brightness;
ScreenBrightness.setBrightness({brightness: 1});
modal.addEventListener('didDismiss', () => {
ScreenOrientation.unlock();
ScreenBrightness.setBrightness({brightness: brightness === 1 ? 0.5 : brightness});
});
}
await modal.present();
}
}

View File

@@ -1,4 +1,9 @@
<img [src]="item.image" [alt]="'name' | thingTranslate: item" draggable="false" />
<img
[src]="item.image"
[alt]="'name' | thingTranslate: item"
draggable="false"
(click)="presentFullscreen()"
/>
@if (item.validity && (item.validity | toDateRange | isInRangeNow | async) === false) {
<div class="expired">{{ 'profile.userInfo.expired' | translate | titlecase }}</div>
}

View File

@@ -3,11 +3,6 @@
overflow: hidden;
}
:host:fullscreen {
margin: 0;
padding: 0;
}
img {
border-radius: 3mm;
}

View File

@@ -19,7 +19,6 @@ import {IonicModule} from '@ionic/angular';
import {AsyncPipe, TitleCasePipe} from '@angular/common';
import {ThingTranslateModule} from '../../translation/thing-translate.module';
import {UtilModule} from '../../util/util.module';
import {FullScreenImageDirective} from '../../util/full-screen-image.directive';
import {IdCardComponent} from './id-card.component';
import {TranslateModule} from '@ngx-translate/core';
import {Observable} from 'rxjs';
@@ -36,7 +35,6 @@ import {Observable} from 'rxjs';
AsyncPipe,
ThingTranslateModule,
UtilModule,
FullScreenImageDirective,
IdCardComponent,
TranslateModule,
TitleCasePipe,