+ Erlauben Sie der WalkApp bitte den Zugriff auf Ihre Positionsdaten (es werden keine persönlichen Daten erfasst).
+
+
+
+ Deaktivieren Sie bitte den Schlafmodus und nutzen Sie während des Spaziergangs keine anderen Funktionen ihres
+ Mobiltelefons (respektive bringen Sie die WalkApp nach Nutzung anderer Funktionen wieder als aktive App in den
+ Vordergrund).
+
+
+
+ Spazieren Sie anschliessend einfach entlang der Nord-Südachse der Reinacher Heide. Ihr Mobiltelefon versorgt die
+ App mit der aktuellen GPS Position, weshalb Ihnen Informationen im örtlichen Kontext Ihres Erlebnisses eingespielt
+ werden können. Dies geschieht in Form von Bildern, Texten und Tondokumente. Da sich die Dokumente auf den Ort
+ beziehen, kann es bei schnellem Gehen vorkommen, dass neue Informationen die aktuell abgespielten ausblenden. Bitte
+ regulieren Sie die Abspielung der Inhalte also mit Ihrer Gehgeschwindigkeit.
+
+
+
+
{{ hotspot.title }}
+
{{ hotspot.description }}
+
diff --git a/src/app/components/single-image/single-image.component.ts b/src/app/components/single-image/single-image.component.ts
new file mode 100644
index 0000000..5bc4828
--- /dev/null
+++ b/src/app/components/single-image/single-image.component.ts
@@ -0,0 +1,23 @@
+import { Component } from '@angular/core';
+import { filter } from 'rxjs';
+import { HotspotImageSingle, HotspotService } from 'src/app/services/hotspot.service';
+import { slideUpDownAnimation } from 'src/app/shared';
+
+@Component({
+ selector: 'app-single-image',
+ templateUrl: './single-image.component.html',
+ styleUrls: ['./single-image.component.css'],
+ animations: [slideUpDownAnimation]
+})
+export class SingleImageComponent {
+
+ hotspot?: HotspotImageSingle;
+
+ constructor(private hotspotService: HotspotService) {
+ this.hotspotService.trigger
+ .pipe(filter(h => h !== false && h.type === 1))
+ .subscribe(hotspot => {
+ if (hotspot && hotspot.type === 1) this.hotspot = hotspot;
+ })
+ }
+}
diff --git a/src/app/components/stack-fade/stack-fade.component.html b/src/app/components/stack-fade/stack-fade.component.html
index 0262c2d..338bd59 100644
--- a/src/app/components/stack-fade/stack-fade.component.html
+++ b/src/app/components/stack-fade/stack-fade.component.html
@@ -10,7 +10,7 @@
+
{{ textDisplay.title }}
{{ textDisplay.text }}
diff --git a/src/app/components/stack-fade/stack-fade.component.ts b/src/app/components/stack-fade/stack-fade.component.ts
index 92fee1a..f397746 100644
--- a/src/app/components/stack-fade/stack-fade.component.ts
+++ b/src/app/components/stack-fade/stack-fade.component.ts
@@ -1,26 +1,15 @@
import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core';
-import { trigger, transition, style, animate } from '@angular/animations';
import { distinctUntilChanged, Observable, Subject, Subscription, switchMap, takeUntil } from 'rxjs';
import { DataService, ParcoursService, StateService } from 'src/app/services';
-import { SectionText, StackImage } from 'src/app/shared';
+import { SectionText, StackImage, slideUpDownAnimation } from 'src/app/shared';
import { AudioService } from 'src/app/services/audio.service';
import { StackService } from 'src/app/services/stack.service';
-const fadeInOutAnimation = trigger('fadeInOut', [
- transition(':enter', [
- style({ bottom: '-33%', opacity: 0 }),
- animate('1s ease-in-out', style({ bottom: '0%', opacity: 1 }))
- ]),
- transition(':leave', [
- animate('1s ease-in-out', style({ opacity: 0 }))
- ])
-]);
-
@Component({
selector: 'app-stack-fade',
templateUrl: './stack-fade.component.html',
styleUrls: ['./stack-fade.component.css'],
- animations: [fadeInOutAnimation]
+ animations: [slideUpDownAnimation]
})
export class StackFadeComponent implements AfterViewInit, OnInit, OnDestroy {
diff --git a/src/app/components/trigger-hotspot-dialog.component.ts b/src/app/components/trigger-hotspot-dialog.component.ts
new file mode 100644
index 0000000..5d74444
--- /dev/null
+++ b/src/app/components/trigger-hotspot-dialog.component.ts
@@ -0,0 +1,42 @@
+import { Component } from '@angular/core';
+import { MatDialogRef } from '@angular/material/dialog';
+
+@Component({
+ selector: 'app-trigger-hotspot-dialog',
+ template: `
+
Trigger Hotspots
+
+
+ Bild
+ Bildserie
+ Infotext
+ Audiotext
+ Data
+ SWILD
+ close hotspot
+
+
+
+ Close
+
+ `,
+ styles: [`
+ ul {
+ margin: 0;
+ padding: 0;
+ list-style-type: none;
+ }
+ li {
+ margin: 8px 0;
+ }
+ `]
+})
+export class TriggerHotspotDialogComponent {
+ constructor(
+ public dialogRef: MatDialogRef
,
+ ) { }
+
+ select(type: number) {
+ this.dialogRef.close(type);
+ }
+}
diff --git a/src/app/components/walk/walk.component.css b/src/app/components/walk/walk.component.css
new file mode 100644
index 0000000..126ea48
--- /dev/null
+++ b/src/app/components/walk/walk.component.css
@@ -0,0 +1,19 @@
+:host {
+ position: absolute;
+ top: 48px;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ margin: 0;
+ padding: 0;
+ background-color: #fff;
+ background-image: url('/assets/mitwelten-logo.png');
+ background-size: 80%;
+ background-repeat: no-repeat;
+ background-position: 50% 50%;
+}
+
+.container {
+ height: 100%;
+ background-color: #ffffffcc;
+}
diff --git a/src/app/components/walk/walk.component.html b/src/app/components/walk/walk.component.html
new file mode 100644
index 0000000..b487ab4
--- /dev/null
+++ b/src/app/components/walk/walk.component.html
@@ -0,0 +1,11 @@
+
diff --git a/src/app/components/walk/walk.component.ts b/src/app/components/walk/walk.component.ts
new file mode 100644
index 0000000..73c1941
--- /dev/null
+++ b/src/app/components/walk/walk.component.ts
@@ -0,0 +1,25 @@
+import { Component, OnInit } from '@angular/core';
+import { HotspotService, HotspotType } from 'src/app/services/hotspot.service';
+import { hotspotCrossfadeAnimation } from 'src/app/shared';
+
+@Component({
+ selector: 'app-walk',
+ templateUrl: './walk.component.html',
+ styleUrls: ['./walk.component.css'],
+ animations: [hotspotCrossfadeAnimation],
+})
+export class WalkComponent implements OnInit {
+
+ hotspot?: HotspotType|false;
+
+ constructor(
+ public hotspotService: HotspotService
+ ) { }
+
+ ngOnInit(): void {
+ // "false" could be triggering a side effect to fade out audio, wait, then continue delivering "false"
+ this.hotspotService.trigger.subscribe(hotspot => this.hotspot = hotspot);
+ this.hotspotService.loadHotspots();
+ }
+
+}
diff --git a/src/app/services/audio-player.service.ts b/src/app/services/audio-player.service.ts
new file mode 100644
index 0000000..f4df475
--- /dev/null
+++ b/src/app/services/audio-player.service.ts
@@ -0,0 +1,145 @@
+import { Injectable } from '@angular/core';
+import { BehaviorSubject, Observable, Subject, takeUntil } from 'rxjs';
+import * as moment from 'moment';
+import { StreamState } from '../shared';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class AudioPlayerService {
+
+ private stop$ = new Subject();
+ private audioObj = new Audio();
+ private state: StreamState = {
+ playing: false,
+ readableCurrentTime: '',
+ readableDuration: '',
+ duration: undefined,
+ currentTime: undefined,
+ canplay: false,
+ error: false,
+ progress: 0,
+ };
+ private stateChange: BehaviorSubject = new BehaviorSubject(this.state);
+
+ private audioEvents = [
+ 'ended',
+ 'error',
+ 'play',
+ 'playing',
+ 'pause',
+ 'timeupdate',
+ 'canplay',
+ 'loadedmetadata',
+ 'loadstart'
+ ];
+
+ constructor() { }
+
+ playStream(url: string) {
+ return this.streamObservable(url).pipe(takeUntil(this.stop$));
+ }
+
+ getState(): Observable {
+ return this.stateChange.asObservable();
+ }
+
+ play() {
+ this.audioObj.play();
+ }
+
+ pause() {
+ this.audioObj.pause();
+ }
+
+ stop() {
+ this.stop$.next(null);
+ }
+
+ seekRelative(offset: number) {
+ this.audioObj.currentTime = this.audioObj.currentTime + offset;
+ }
+
+ seekTo(seconds: number) {
+ this.audioObj.currentTime = seconds;
+ }
+
+ formatTime(time: number, format: string = "HH:mm:ss") {
+ const momentTime = time * 1000;
+ return moment.utc(momentTime).format(format);
+ }
+
+ private updateStateEvents(event: Event): void {
+ switch (event.type) {
+ case 'canplay':
+ this.state.duration = this.audioObj.duration;
+ this.state.readableDuration = this.formatTime(this.state.duration);
+ this.state.canplay = true;
+ break;
+ case 'playing':
+ this.state.playing = true;
+ break;
+ case 'pause':
+ this.state.playing = false;
+ break;
+ case 'timeupdate':
+ this.state.currentTime = this.audioObj.currentTime;
+ this.state.progress = (this.audioObj.currentTime / this.audioObj.duration) * 100;
+ this.state.readableCurrentTime = this.formatTime(
+ this.state.currentTime
+ );
+ break;
+ case 'error':
+ this.resetState();
+ this.state.error = true;
+ break;
+ }
+ this.stateChange.next(this.state);
+ }
+
+ private streamObservable(url: string) {
+ return new Observable(observer => {
+ // Play audio
+ this.audioObj.src = url;
+ this.audioObj.load();
+ this.audioObj.play();
+
+ const handler = (event: Event) => {
+ this.updateStateEvents(event);
+ observer.next(event);
+ };
+
+ this.addEvents(this.audioObj, this.audioEvents, handler);
+ return () => {
+ this.audioObj.pause();
+ this.audioObj.currentTime = 0;
+ this.removeEvents(this.audioObj, this.audioEvents, handler);
+ };
+ });
+ }
+
+ private addEvents(obj: HTMLAudioElement, events: string[], handler: any) {
+ events.forEach(event => {
+ obj.addEventListener(event, handler);
+ });
+ }
+
+ private removeEvents(obj: HTMLAudioElement, events: string[], handler: any) {
+ events.forEach(event => {
+ obj.removeEventListener(event, handler);
+ });
+ }
+
+ private resetState() {
+ this.state = {
+ playing: false,
+ readableCurrentTime: '',
+ readableDuration: '',
+ duration: undefined,
+ currentTime: undefined,
+ canplay: false,
+ error: false,
+ progress: 0
+ };
+ }
+}
diff --git a/src/app/services/channel.service.ts b/src/app/services/channel.service.ts
new file mode 100644
index 0000000..139cad0
--- /dev/null
+++ b/src/app/services/channel.service.ts
@@ -0,0 +1,23 @@
+import { Injectable } from '@angular/core';
+import { MatDialog } from '@angular/material/dialog';
+import { ChooseChannelComponent } from '../components/choose-channel/choose-channel.component';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class ChannelService {
+
+ constructor(public dialog: MatDialog) { }
+
+ public chooseChannel() {
+ const dialogRef = this.dialog.open(ChooseChannelComponent, {
+ maxWidth: '90vw'
+ });
+ dialogRef.afterClosed().subscribe(v => {
+ console.log('ChooseChannelComponent closed', v);
+ if (v !== undefined && v.path !== undefined) {
+
+ }
+ })
+ }
+}
diff --git a/src/app/services/data.service.ts b/src/app/services/data.service.ts
index d16afe8..e24d2b4 100644
--- a/src/app/services/data.service.ts
+++ b/src/app/services/data.service.ts
@@ -1,7 +1,8 @@
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
-import { Observable } from 'rxjs';
+import { Observable, map } from 'rxjs';
import { Deployment, Note, ImageStack, SectionText, StackImage, WalkPath } from '../shared';
+import { HotspotType } from './hotspot.service';
@Injectable({
providedIn: 'root'
@@ -81,6 +82,21 @@ export class DataService {
return this.http.get(`${this.apiUrl}/walk/text/${walk_id}`)
}
+ public getWalkHotspots(walk_id: number) {
+ return this.http.get(`${this.apiUrl}/walk/hotspots/${walk_id}`)
+ .pipe(map(hotspots => {
+ return hotspots.map(h => {
+ if (h.type === 1) h.url = this.apiUrl+h.url
+ else if (h.type === 2) h.sequence.forEach(s => s.url = this.apiUrl+s.url)
+ else if (h.type === 4) {
+ h.portraitUrl = this.apiUrl+h.portraitUrl
+ h.audioUrl = this.apiUrl+h.audioUrl
+ }
+ return h;
+ })
+ }))
+ }
+
public getImageResource(url: string) {
return this.http.get(`${this.apiUrl}/files/walk/${url}`, {responseType: 'blob'});
}
diff --git a/src/app/services/hotspot.service.ts b/src/app/services/hotspot.service.ts
new file mode 100644
index 0000000..65a82f5
--- /dev/null
+++ b/src/app/services/hotspot.service.ts
@@ -0,0 +1,163 @@
+import { Component, Injectable } from '@angular/core';
+import { MatDialog } from '@angular/material/dialog';
+import { TriggerHotspotDialogComponent } from '../components/trigger-hotspot-dialog.component';
+import { CoordinatePoint } from '../shared';
+import { BehaviorSubject } from 'rxjs';
+import { ParcoursService } from './parcours.service';
+import { DataService } from './data.service';
+import { AudioService } from './audio.service';
+import distance from '@turf/distance';
+
+interface Hotspot {
+ location: CoordinatePoint;
+ subject?: string;
+ id: number;
+ type: number;
+}
+interface HotspotImage extends Hotspot {
+ title: string;
+ description: string;
+}
+export interface HotspotImageSingle extends HotspotImage {
+ type: 1;
+ url: string;
+ credits: string;
+}
+export interface HotspotImageSequence extends HotspotImage {
+ type: 2;
+ sequence: Array<{
+ url: string;
+ credits: string;
+ }>;
+}
+export interface HotspotInfotext extends Hotspot {
+ type: 3;
+ title: string;
+ text: string;
+}
+export interface HotspotAudiotext extends Hotspot {
+ type: 4;
+ portraitUrl: string;
+ audioUrl: string;
+}
+export interface HotspotCommunity extends Hotspot {
+ type: 5;
+ dataUrl: string;
+}
+export interface HotspotData extends Hotspot {
+ type: 6;
+ endpoint: string;
+ title: string;
+ text: string;
+}
+export type HotspotType = HotspotImageSingle|HotspotImageSequence|HotspotInfotext|HotspotAudiotext|HotspotData;
+
+@Injectable({
+ providedIn: 'root'
+})
+export class HotspotService {
+
+ /* hotspot types
+ 0 reset / no type
+ 1 single image
+ 2 image sequence
+ 3 info text
+ 4 audio text
+ 5 data
+ 6 community image record
+ */
+
+ private hotspots: Array = [];
+ private currentHotspot: HotspotType | null = null;
+
+ public trigger: BehaviorSubject;
+ public closeHotspots: BehaviorSubject>;
+
+ constructor(
+ private dialog: MatDialog,
+ private dataService: DataService,
+ private parcoursService: ParcoursService,
+ private audioService: AudioService,
+ ) {
+ this.trigger = new BehaviorSubject(false);
+ this.closeHotspots = new BehaviorSubject>([]);
+ this.parcoursService.location.subscribe(location => {
+ if (this.hotspots.length > 0) {
+ const c = this.hotspots.map(hotspot => Object.assign(hotspot, { distance: distance(
+ [hotspot.location.lon, hotspot.location.lat],
+ [location.coords.longitude, location.coords.latitude],
+ { units: 'meters' })
+ })).sort((a,b) => a.distance - b.distance).slice(0, 3);
+ if (c[0].distance <= 20.) {
+ if (this.currentHotspot?.id !== c[0].id) {
+ this.currentHotspot = c[0];
+ this.trigger.next(c[0]);
+ this.audioService.ping();
+ }
+ } else {
+ this.trigger.next(false);
+ this.currentHotspot = null;
+ }
+ this.closeHotspots.next(c);
+ }
+ })
+ }
+
+ loadHotspots() {
+ this.dataService.getWalkHotspots(1).subscribe(hotspots => this.hotspots = hotspots);
+ }
+
+ chooseHotspot() {
+ this.dialog.open(TriggerHotspotDialogComponent).afterClosed().subscribe(
+ (type: number) => {
+ switch (type) {
+ case 1:
+ this.trigger.next({
+ id: 43, type,
+ location: { lat: 1, lon: 4},
+ title: 'Single Image',
+ description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
+ url: '/assets/2990-0522_2023-05-15T11-00-08Z.jpg',
+ credits: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod'
+ })
+ break;
+ case 2:
+ this.trigger.next({
+ id: 42, type,
+ location: { lat: 1, lon: 4},
+ title: 'Image Sequence',
+ description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
+ sequence: [
+ { url: '/assets/img1.jpg', credits: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod' },
+ { url: '/assets/img2.jpg', credits: 'asdf' },
+ { url: '/assets/img3.jpg', credits: 'asdf' },
+ { url: '/assets/img4.jpg', credits: 'asdf' },
+ { url: '/assets/img5.jpg', credits: 'asdf' },
+ ]
+ })
+ break;
+ case 3:
+ this.trigger.next({
+ id: 44, type,
+ location: { lat: 1, lon: 4},
+ title: 'Aufbau eines Auenwald-Ufers',
+ text: 'Auenwälder befinden sich an Flussläufen und sind durch periodische Wasserstandschwankungen charakterisiert. Zudem kann man einen Auenwald in verschiedene Vegetationszonen einteilen - abhängig von der jeweiligen Entfernung zum Flussufer. Unmittelbar am Ufer befindet sich der Spülsaum mit sich kurzfristig ansiedelnden Pionierpflanzen. Landeinwärts folgt dann eine Zone mit niedrigen Weidengebüschen, die den mechanischen Belastungen des regelmässigen Hochwassers standhalten. Anschliessend beginnt der eigentliche Auenwald: Die Weichholz-Aue, welche regelmässig überschwemmt wird, beherbergt viele Weide- und Pappelarten. Die Hartholz-Aue, die nur noch selten überschwemmt wird, wird durch Baumarten mit hartem Holz charakterisiert, wie beispielsweise Ulmen, Eichen und Eschen. Zudem verleien viele Lianen der Hartholzaue eine Urwald-Charakter.',
+ })
+ break;
+ case 4:
+ this.trigger.next({
+ id: 45, type,
+ location: { lat: 1, lon: 4},
+ portraitUrl: '/assets/audiotext-portrait-ai.jpg',
+ audioUrl: '/assets/ice-crackling-loop-02.m4a',
+ })
+ break;
+
+ default:
+ this.trigger.next(false);
+ break;
+ }
+ }
+ )
+ }
+}
diff --git a/src/app/services/parcours.service.ts b/src/app/services/parcours.service.ts
index ae47e5a..1a4c85d 100644
--- a/src/app/services/parcours.service.ts
+++ b/src/app/services/parcours.service.ts
@@ -17,8 +17,11 @@ import { DataService } from './data.service';
})
export class ParcoursService {
+ /** GeolocationService, with error handling pipe */
private _geolocation: GeolocationService
- private trackerLocation: Position | undefined; /** position of device */
+
+ /** Position of device */
+ private trackerLocation: Position | undefined;
private toggleSource?: BehaviorSubject>;
public selectedPathID = 1;
@@ -76,6 +79,11 @@ export class ParcoursService {
})
}
+ /**
+ * Estimate length of path by summing length of all segments in `this.parcoursPath`
+ *
+ * Ouput to `this.parcoursLength`
+ */
private setParcours() {
// get length of path
this.parcoursLength = 0;
@@ -86,6 +94,13 @@ export class ParcoursService {
}
}
+ /**
+ * Create Observable that allows for the Location Service to be toggeled
+ * between GeolocationService and TrackRecorder playback
+ *
+ * - Output: Location (`this.location`)
+ * - Side-Effect: `this.updateProjection`, calculate progress along path
+ */
private initGeoLocation() {
this.toggleSource = new BehaviorSubject(this._geolocation);
this.toggleSource.pipe(
@@ -121,6 +136,18 @@ export class ParcoursService {
this.updateProjection(location);
}
+ /**
+ * Estimate parcours progress and distance to path by projecting the current
+ * geolocation of the device onto the path.
+ *
+ * Observable Outputs:
+ * - `this.distanceToPath`: Distance to path in meters
+ * - `this.progress`: Normalised progress
+ * - `this.closestPointOnParcours`: Progress in meters
+ * - `this.active`: Device geolocation in or out of focus of parcours
+ *
+ * @param location Current geolocation of device
+ */
private updateProjection(location: Position) {
// Find the closest point on the path to the object
let closestPoint: Position | undefined;
@@ -169,6 +196,13 @@ export class ParcoursService {
}
}
+ /**
+ * Euclidian (planar) distance between A and B
+ *
+ * @param a Geolocation A
+ * @param b Geolocation B
+ * @returns Planar distance between A and B
+ */
private distance(a: Position, b: Position): number {
const dx = a[0] - b[0];
const dy = a[1] - b[1];
diff --git a/src/app/shared/animations.ts b/src/app/shared/animations.ts
new file mode 100644
index 0000000..ad7f8a3
--- /dev/null
+++ b/src/app/shared/animations.ts
@@ -0,0 +1,28 @@
+import { trigger, style, transition, animate, state } from '@angular/animations';
+
+export const slideUpDownAnimation = trigger('slideUpDown', [
+ transition(':enter', [
+ style({ bottom: '-33%', opacity: 0 }),
+ animate('1s ease-in-out', style({ bottom: '0%', opacity: 1 }))
+ ]),
+ transition(':leave', [
+ animate('1s ease-in-out', style({ opacity: 0 }))
+ ])
+]);
+
+export const hotspotCrossfadeAnimation = trigger('hotspotCrossfade', [
+ transition(':enter', [
+ style({ opacity: 0 }),
+ animate('1s ease-in-out', style({ opacity: 1 }))
+ ]),
+ transition(':leave', [
+ animate('1s ease-in-out', style({ opacity: 0 }))
+ ])
+]);
+
+export const swipeAnimation = trigger('swipeAnimation', [
+ state('left', style({ transform: 'translateX(-100%)' })),
+ state('right', style({ transform: 'translateX(100%)' })),
+ state('center', style({ transform: 'translateX(0)' })),
+ transition('* => *', animate('300ms ease-in-out'))
+])
diff --git a/src/app/shared/index.ts b/src/app/shared/index.ts
index 26fa2d5..a2aed36 100644
--- a/src/app/shared/index.ts
+++ b/src/app/shared/index.ts
@@ -15,3 +15,7 @@ export * from './stack-image.type';
export * from './walk-path.type';
export * from './image-data.type';
export * from './image-stack.type';
+export * from './stream-state.type';
+
+// animations
+export * from './animations';
diff --git a/src/app/shared/stream-state.type.ts b/src/app/shared/stream-state.type.ts
new file mode 100644
index 0000000..00305c7
--- /dev/null
+++ b/src/app/shared/stream-state.type.ts
@@ -0,0 +1,10 @@
+export interface StreamState {
+ playing: boolean;
+ readableCurrentTime: string;
+ readableDuration: string;
+ duration: number | undefined;
+ currentTime: number | undefined;
+ canplay: boolean;
+ error: boolean;
+ progress: number;
+}
diff --git a/src/assets/2990-0522_2023-05-15T11-00-08Z.jpg b/src/assets/2990-0522_2023-05-15T11-00-08Z.jpg
new file mode 100644
index 0000000..50cfe3b
Binary files /dev/null and b/src/assets/2990-0522_2023-05-15T11-00-08Z.jpg differ
diff --git a/src/assets/audiotext-portrait-ai.jpg b/src/assets/audiotext-portrait-ai.jpg
new file mode 100644
index 0000000..464a72d
Binary files /dev/null and b/src/assets/audiotext-portrait-ai.jpg differ
diff --git a/src/assets/img1.jpg b/src/assets/img1.jpg
new file mode 100644
index 0000000..281de4f
Binary files /dev/null and b/src/assets/img1.jpg differ
diff --git a/src/assets/img2.jpg b/src/assets/img2.jpg
new file mode 100644
index 0000000..72f3f88
Binary files /dev/null and b/src/assets/img2.jpg differ
diff --git a/src/assets/img3.jpg b/src/assets/img3.jpg
new file mode 100644
index 0000000..a5bec08
Binary files /dev/null and b/src/assets/img3.jpg differ
diff --git a/src/assets/img4.jpg b/src/assets/img4.jpg
new file mode 100644
index 0000000..33ae0dd
Binary files /dev/null and b/src/assets/img4.jpg differ
diff --git a/src/assets/img5.jpg b/src/assets/img5.jpg
new file mode 100644
index 0000000..c2e2107
Binary files /dev/null and b/src/assets/img5.jpg differ
diff --git a/src/assets/mitwelten-logo.png b/src/assets/mitwelten-logo.png
new file mode 100644
index 0000000..877f735
Binary files /dev/null and b/src/assets/mitwelten-logo.png differ