mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-06 21:42:49 +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
|
||||
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
|
||||
|
||||
## 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):
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 => [
|
||||
{
|
||||
|
||||
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/>.
|
||||
*/
|
||||
|
||||
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));
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user