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
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
## 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
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",
"cypress:open": "cypress open",
"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: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",

View File

@@ -52,9 +52,9 @@ export class IdCardsProvider {
map(svg => {
let result = svg;
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 => [
{

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/>.
*/
import {Observable} from 'rxjs';
import {Observable, shareReplay} from 'rxjs';
/**
*
@@ -22,7 +22,7 @@ export function fromMutationObserver(
target: Node,
options?: MutationObserverInit,
): Observable<MutationRecord[]> {
return new Observable(subscriber => {
return new Observable<MutationRecord[]>(subscriber => {
const observer = new MutationObserver(mutations => {
subscriber.next(mutations);
});
@@ -30,5 +30,5 @@ export function fromMutationObserver(
return () => {
observer.disconnect();
};
});
}).pipe(shareReplay(1));
}

View File

@@ -14,17 +14,33 @@
*/
import {AfterContentInit, ChangeDetectionStrategy, Component, Input, ViewContainerRef} from '@angular/core';
import {SCThings} from '@openstapps/core';
import {fromMutationObserver} from '../_helpers/rxjs/mutation-observer';
import {mergeMap, ReplaySubject, takeLast} from 'rxjs';
import {fromMutationObserver} from './rxjs/mutation-observer';
import {combineLatestWith, mergeMap, OperatorFunction, ReplaySubject, takeLast} from 'rxjs';
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
*/
@Component({
selector: 'stapps-section',
templateUrl: 'section.component.html',
styleUrls: ['section.component.scss'],
templateUrl: 'section.html',
styleUrls: ['section.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SectionComponent implements AfterContentInit {
@@ -36,6 +52,9 @@ export class SectionComponent implements AfterContentInit {
nativeElement = new ReplaySubject<HTMLElement>(1);
/**
* The swiper child (may not emit at all)
*/
swiper = this.nativeElement.pipe(
takeLast(1),
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) {}
ngAfterContentInit() {

View File

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