mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-21 17:12:43 +00:00
feat: section dynamic slide buttons
This commit is contained in:
2
frontend/app/.gitignore
vendored
2
frontend/app/.gitignore
vendored
@@ -44,3 +44,5 @@ UserInterfaceState.xcuserstate
|
|||||||
|
|
||||||
docs
|
docs
|
||||||
bundle-info.html
|
bundle-info.html
|
||||||
|
|
||||||
|
.browser-data/
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 => [
|
||||||
{
|
{
|
||||||
|
|||||||
22
frontend/app/src/app/util/rxjs/from-intersection-observer.ts
Normal file
22
frontend/app/src/app/util/rxjs/from-intersection-observer.ts
Normal 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));
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
}
|
}
|
||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user