diff --git a/frontend/app/.gitignore b/frontend/app/.gitignore index 03c48788..3286ad6f 100644 --- a/frontend/app/.gitignore +++ b/frontend/app/.gitignore @@ -44,3 +44,5 @@ UserInterfaceState.xcuserstate docs bundle-info.html + +.browser-data/ diff --git a/frontend/app/README.md b/frontend/app/README.md index 117ae404..7f489316 100644 --- a/frontend/app/README.md +++ b/frontend/app/README.md @@ -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): diff --git a/frontend/app/package.json b/frontend/app/package.json index f0a8c01c..701b0d03 100644 --- a/frontend/app/package.json +++ b/frontend/app/package.json @@ -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", diff --git a/frontend/app/src/app/modules/profile/id-cards.provider.ts b/frontend/app/src/app/modules/profile/id-cards.provider.ts index 84e90073..2e560822 100644 --- a/frontend/app/src/app/modules/profile/id-cards.provider.ts +++ b/frontend/app/src/app/modules/profile/id-cards.provider.ts @@ -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)[key]); + result = result.replaceAll(`{{${key}}}`, (user as unknown as Record)[key]); } - return `data:image/svg+xml;base64,${Buffer.from(result, 'base64').toString('base64')}`; + return `data:image/svg+xml;utf8,${encodeURIComponent(result)}`; }), map(image => [ { diff --git a/frontend/app/src/app/util/rxjs/from-intersection-observer.ts b/frontend/app/src/app/util/rxjs/from-intersection-observer.ts new file mode 100644 index 00000000..96f9737f --- /dev/null +++ b/frontend/app/src/app/util/rxjs/from-intersection-observer.ts @@ -0,0 +1,22 @@ +import {Observable, shareReplay} from 'rxjs'; + +/** + * Create an observable from an intersection observer + */ +export function fromIntersectionObserver( + target: Element, + init?: IntersectionObserverInit, +): Observable { + return new Observable(observer => { + const intersectionObserver = new IntersectionObserver(item => { + observer.next(item); + }, init); + intersectionObserver.observe(target); + + return { + unsubscribe() { + intersectionObserver.disconnect(); + }, + }; + }).pipe(shareReplay(1)); +} diff --git a/frontend/app/src/app/_helpers/rxjs/mutation-observer.ts b/frontend/app/src/app/util/rxjs/mutation-observer.ts similarity index 87% rename from frontend/app/src/app/_helpers/rxjs/mutation-observer.ts rename to frontend/app/src/app/util/rxjs/mutation-observer.ts index 590081f1..d6b3b311 100644 --- a/frontend/app/src/app/_helpers/rxjs/mutation-observer.ts +++ b/frontend/app/src/app/util/rxjs/mutation-observer.ts @@ -13,7 +13,7 @@ * this program. If not, see . */ -import {Observable} from 'rxjs'; +import {Observable, shareReplay} from 'rxjs'; /** * @@ -22,7 +22,7 @@ export function fromMutationObserver( target: Node, options?: MutationObserverInit, ): Observable { - return new Observable(subscriber => { + return new Observable(subscriber => { const observer = new MutationObserver(mutations => { subscriber.next(mutations); }); @@ -30,5 +30,5 @@ export function fromMutationObserver( return () => { observer.disconnect(); }; - }); + }).pipe(shareReplay(1)); } diff --git a/frontend/app/src/app/util/section.component.ts b/frontend/app/src/app/util/section.component.ts index ca8e080e..d4eb7b40 100644 --- a/frontend/app/src/app/util/section.component.ts +++ b/frontend/app/src/app/util/section.component.ts @@ -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 { + 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(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() { diff --git a/frontend/app/src/app/util/section.component.html b/frontend/app/src/app/util/section.html similarity index 58% rename from frontend/app/src/app/util/section.component.html rename to frontend/app/src/app/util/section.html index c72ca019..4316cd15 100644 --- a/frontend/app/src/app/util/section.component.html +++ b/frontend/app/src/app/util/section.html @@ -25,27 +25,29 @@ - - - - - - - - - - - + + + + + + + + + + + + +
diff --git a/frontend/app/src/app/util/section.component.scss b/frontend/app/src/app/util/section.scss similarity index 100% rename from frontend/app/src/app/util/section.component.scss rename to frontend/app/src/app/util/section.scss