feat: section dynamic slide buttons

This commit is contained in:
2023-10-09 15:57:19 +02:00
parent 1f62b5c5b0
commit e7607360e2
9 changed files with 152 additions and 30 deletions

View File

@@ -44,3 +44,5 @@ UserInterfaceState.xcuserstate
docs docs
bundle-info.html bundle-info.html
.browser-data/

View File

@@ -52,6 +52,44 @@ All the npm scripts are defined in `package.json` [file](package.json). It is re
## Most useful commands ## Most useful commands
## Editing the Ionic Database from the browser
Add the following function using the browser console
```js
function addToIonicDB(key, value) {
indexedDB.open('_ionicstorage').onsuccess = event => {
const db = event.target.result;
db.transaction('_ionickv', 'readwrite').objectStore('_ionickv').put(value, key);
};
}
```
You can then call the function in the browser to add values to the
ionic database in the IndexedDB.
For example, you can add a stored authorization like this:
```js
addToIonicDB(
'token_response',
JSON.stringify({
access_token: 'AT-123-abcdefghi',
refresh_token: 'RT-123-jklmnopqrs',
scope: '',
token_type: 'bearer',
issued_at: 1696852785,
expires_in: '28800',
}),
);
```
You'll need to run _Chromium_ using
```shell
pnpm chromium:no-cors
```
### Running the app ### Running the app
Install the npm packages needed for running the app (as for any other node project which uses npm): Install the npm packages needed for running the app (as for any other node project which uses npm):

View File

@@ -24,6 +24,7 @@
"check-icons": "ts-node-esm scripts/check-icon-correctness.ts", "check-icons": "ts-node-esm scripts/check-icon-correctness.ts",
"cypress:open": "cypress open", "cypress:open": "cypress open",
"cypress:run": "cypress run", "cypress:run": "cypress run",
"chromium:no-cors": "chromium --disable-web-security --user-data-dir=\".browser-data/chromium\"",
"docker:build": "sudo docker run -p 8100:8100 -p 35729:35729 -p 53703:53703 -v $PWD:/app -it registry.gitlab.com/openstapps/app bash -c \"npm install && npm run build\"", "docker:build": "sudo docker run -p 8100:8100 -p 35729:35729 -p 53703:53703 -v $PWD:/app -it registry.gitlab.com/openstapps/app bash -c \"npm install && npm run build\"",
"docker:build:android": "sudo docker run -p 8100:8100 -p 35729:35729 -p 53703:53703 -v $PWD:/app -it registry.gitlab.com/openstapps/app bash -c \"npm run build:android\"", "docker:build:android": "sudo docker run -p 8100:8100 -p 35729:35729 -p 53703:53703 -v $PWD:/app -it registry.gitlab.com/openstapps/app bash -c \"npm run build:android\"",
"docker:enter": "sudo docker run -p 8100:8100 -p 35729:35729 -p 53703:53703 -v $PWD:/app -it registry.gitlab.com/openstapps/app bash", "docker:enter": "sudo docker run -p 8100:8100 -p 35729:35729 -p 53703:53703 -v $PWD:/app -it registry.gitlab.com/openstapps/app bash",

View File

@@ -52,9 +52,9 @@ export class IdCardsProvider {
map(svg => { map(svg => {
let result = svg; let result = svg;
for (const key in user) { for (const key in user) {
result = result.replaceAll(`{{${key}}`, (user as unknown as Record<string, string>)[key]); result = result.replaceAll(`{{${key}}}`, (user as unknown as Record<string, string>)[key]);
} }
return `data:image/svg+xml;base64,${Buffer.from(result, 'base64').toString('base64')}`; return `data:image/svg+xml;utf8,${encodeURIComponent(result)}`;
}), }),
map(image => [ map(image => [
{ {

View File

@@ -0,0 +1,22 @@
import {Observable, shareReplay} from 'rxjs';
/**
* Create an observable from an intersection observer
*/
export function fromIntersectionObserver(
target: Element,
init?: IntersectionObserverInit,
): Observable<IntersectionObserverEntry[]> {
return new Observable<IntersectionObserverEntry[]>(observer => {
const intersectionObserver = new IntersectionObserver(item => {
observer.next(item);
}, init);
intersectionObserver.observe(target);
return {
unsubscribe() {
intersectionObserver.disconnect();
},
};
}).pipe(shareReplay(1));
}

View File

@@ -13,7 +13,7 @@
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {Observable} from 'rxjs'; import {Observable, shareReplay} from 'rxjs';
/** /**
* *
@@ -22,7 +22,7 @@ export function fromMutationObserver(
target: Node, target: Node,
options?: MutationObserverInit, options?: MutationObserverInit,
): Observable<MutationRecord[]> { ): Observable<MutationRecord[]> {
return new Observable(subscriber => { return new Observable<MutationRecord[]>(subscriber => {
const observer = new MutationObserver(mutations => { const observer = new MutationObserver(mutations => {
subscriber.next(mutations); subscriber.next(mutations);
}); });
@@ -30,5 +30,5 @@ export function fromMutationObserver(
return () => { return () => {
observer.disconnect(); observer.disconnect();
}; };
}); }).pipe(shareReplay(1));
} }

View File

@@ -14,17 +14,33 @@
*/ */
import {AfterContentInit, ChangeDetectionStrategy, Component, Input, ViewContainerRef} from '@angular/core'; import {AfterContentInit, ChangeDetectionStrategy, Component, Input, ViewContainerRef} from '@angular/core';
import {SCThings} from '@openstapps/core'; import {SCThings} from '@openstapps/core';
import {fromMutationObserver} from '../_helpers/rxjs/mutation-observer'; import {fromMutationObserver} from './rxjs/mutation-observer';
import {mergeMap, ReplaySubject, takeLast} from 'rxjs'; import {combineLatestWith, mergeMap, OperatorFunction, ReplaySubject, takeLast} from 'rxjs';
import {distinctUntilChanged, filter, map, startWith} from 'rxjs/operators'; import {distinctUntilChanged, filter, map, startWith} from 'rxjs/operators';
import {fromIntersectionObserver} from './rxjs/from-intersection-observer';
/**
* Operator function that checks if a slide is visible
*/
function isSlideVisible(
select: (slides: HTMLCollection) => Element | null,
): OperatorFunction<readonly [HTMLElement, HTMLCollection], boolean> {
return source =>
source.pipe(
map(([element, slides]) => [element, select(slides) as HTMLElement]),
filter(([, slide]) => slide !== null),
mergeMap(([element, slide]) => fromIntersectionObserver(slide, {threshold: 1, root: element})),
map(entry => entry.some(it => it.intersectionRatio === 1)),
);
}
/** /**
* Shows a horizontal list of action chips * Shows a horizontal list of action chips
*/ */
@Component({ @Component({
selector: 'stapps-section', selector: 'stapps-section',
templateUrl: 'section.component.html', templateUrl: 'section.html',
styleUrls: ['section.component.scss'], styleUrls: ['section.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class SectionComponent implements AfterContentInit { export class SectionComponent implements AfterContentInit {
@@ -36,6 +52,9 @@ export class SectionComponent implements AfterContentInit {
nativeElement = new ReplaySubject<HTMLElement>(1); nativeElement = new ReplaySubject<HTMLElement>(1);
/**
* The swiper child (may not emit at all)
*/
swiper = this.nativeElement.pipe( swiper = this.nativeElement.pipe(
takeLast(1), takeLast(1),
mergeMap(element => mergeMap(element =>
@@ -51,6 +70,44 @@ export class SectionComponent implements AfterContentInit {
), ),
); );
/**
* Emits the current list of all slides
*/
slides = this.swiper.pipe(
mergeMap(element =>
fromMutationObserver(element, {
childList: true,
}).pipe(
map(() => element.children),
startWith(element.children),
map(it => [element, it] as const),
),
),
);
/**
* Emits true when the first slide is fully visible,
* false when it's only partially or not visible
*/
firstSlideVisible = this.slides.pipe(isSlideVisible(slides => slides.item(0)));
/**
* Emits true when the last slide is fully visible,
* false when it's only partially or not visible
*/
lastSlideVisible = this.slides.pipe(isSlideVisible(slides => slides.item(slides.length - 1)));
/**
* If the nav should be shown
*
* Emits false if all slides are visible
*/
showNav = this.slides.pipe(
map(([, slides]) => slides.length > 1),
combineLatestWith(this.firstSlideVisible, this.lastSlideVisible),
map(([multipleSlides, firstVisible, lastVisible]) => multipleSlides && !(firstVisible && lastVisible)),
);
constructor(readonly viewContainerRef: ViewContainerRef) {} constructor(readonly viewContainerRef: ViewContainerRef) {}
ngAfterContentInit() { ngAfterContentInit() {

View File

@@ -25,27 +25,29 @@
</ng-template> </ng-template>
</ion-col> </ion-col>
<ng-container *ngIf="swiper | async as swiper"> <ng-container *ngIf="showNav | async">
<ion-col size="auto" class="swiper-button"> <ng-container *ngIf="swiper | async as swiper">
<ion-button <ion-col size="auto" class="swiper-button">
fill="clear" <ion-button
[color]="buttonColor" fill="clear"
(click)="swiper.scrollBy({left: -swiper.offsetWidth, behavior: 'smooth'})" [color]="buttonColor"
[disabled]="false" (click)="swiper.scrollBy({left: -swiper.offsetWidth, behavior: 'smooth'})"
> [disabled]="firstSlideVisible | async"
<ion-icon size="24" slot="icon-only" name="chevron_left"></ion-icon> >
</ion-button> <ion-icon size="24" slot="icon-only" name="chevron_left"></ion-icon>
</ion-col> </ion-button>
<ion-col size="auto" class="swiper-button"> </ion-col>
<ion-button <ion-col size="auto" class="swiper-button">
fill="clear" <ion-button
[color]="buttonColor" fill="clear"
(click)="swiper.scrollBy({left: swiper.offsetWidth, behavior: 'smooth'})" [color]="buttonColor"
[disabled]="false" (click)="swiper.scrollBy({left: swiper.offsetWidth, behavior: 'smooth'})"
> [disabled]="lastSlideVisible | async"
<ion-icon size="24" slot="icon-only" name="chevron_right"></ion-icon> >
</ion-button> <ion-icon size="24" slot="icon-only" name="chevron_right"></ion-icon>
</ion-col> </ion-button>
</ion-col>
</ng-container>
</ng-container> </ng-container>
<ion-col size="auto"> <ion-col size="auto">
<div> <div>