guest mode

This commit is contained in:
2024-02-24 22:01:48 +01:00
parent 150e20ccfb
commit 4aeb452434
18 changed files with 601 additions and 55 deletions

View File

@@ -0,0 +1,23 @@
import {Injectable} from '@angular/core';
import {BehaviorSubject, Observable} from 'rxjs';
import {map} from 'rxjs/operators';
import {DbService} from 'src/app/services/db.service';
import {GuestShow} from './guest-show';
@Injectable({
providedIn: 'root',
})
export class GuestShowDataService {
public list$: BehaviorSubject<GuestShow[]> = new BehaviorSubject<GuestShow[]>([]);
private collection = 'guest';
public constructor(private dbService: DbService) {
this.dbService.col$<GuestShow>(this.collection).subscribe(_ => this.list$.next(_));
}
public read$: (id: string) => Observable<GuestShow | null> = (id: string): Observable<GuestShow | null> => this.list$.pipe(map(_ => _.find(s => s.id === id) || null));
public update$: (id: string, data: Partial<GuestShow>) => Promise<void> = async (id: string, data: Partial<GuestShow>): Promise<void> =>
await this.dbService.doc(this.collection + '/' + id).update(data);
public add: (data: Partial<GuestShow>) => Promise<string> = async (data: Partial<GuestShow>): Promise<string> => (await this.dbService.col(this.collection).add(data)).id;
public delete: (id: string) => Promise<void> = async (id: string): Promise<void> => await this.dbService.doc(this.collection + '/' + id).delete();
}

View File

@@ -0,0 +1,32 @@
import {Injectable} from '@angular/core';
import {Show} from '../shows/services/show';
import {Song} from '../songs/services/song';
import {GuestShowDataService} from './guest-show-data.service';
import {ShowService} from '../shows/services/show.service';
@Injectable({
providedIn: 'root',
})
export class GuestShowService {
public constructor(
private showService: ShowService,
private guestShowDataService: GuestShowDataService
) {}
public async share(show: Show, songs: Song[]): Promise<string> {
const data = {
showType: show.showType,
date: show.date,
songs: songs,
};
if (!show.shareId) {
const shareId = await this.guestShowDataService.add(data);
await this.showService.update$(show.id, {shareId});
} else {
await this.guestShowDataService.update$(show.shareId, data);
}
return window.location.protocol + '//' + window.location.host + '/guest/' + show.shareId;
}
}

View File

@@ -0,0 +1,10 @@
import firebase from 'firebase/compat/app';
import {Song} from '../songs/services/song';
import Timestamp = firebase.firestore.Timestamp;
export interface GuestShow {
id: string;
showType: string;
date: Timestamp;
songs: Song[];
}

View File

@@ -1,11 +1,25 @@
<div *ngIf="songs$ | async as songs" class="view">
<!-- <swiper>-->
<!-- <div *ngFor="let song of songs" class="song">-->
<!-- <app-song-text-->
<!-- [showSwitch]="false"-->
<!-- [text]="song"-->
<!-- chordMode="hide"-->
<!-- ></app-song-text>-->
<!-- </div>-->
<!-- </swiper>-->
<div *ngIf="show$|async as show" class="page">
<div class="title">
<div class="left">{{ show.showType|showType }}</div>
<div class="right">{{ show.date.toDate() | date: 'dd.MM.yyyy' }}</div>
</div>
<div class="view">
<swiper-container scrollbar="true">
<swiper-slide *ngFor="let song of show.songs; let i = index; trackBy: trackBy"
class="song-swipe">
<div class="song-title">{{ song.title }}</div>
<div class="legal">
<p *ngIf="song.artist">{{ song.artist }}</p>
</div>
<app-song-text
[text]="song.text"
></app-song-text>
</swiper-slide>
</swiper-container>
</div>
</div>

View File

@@ -1,12 +1,49 @@
.page {
background: #0009;
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
backdrop-filter: blur(8px);
--swiper-scrollbar-bg-color: #fff3;
--swiper-scrollbar-drag-bg-color: #fff9;
--swiper-scrollbar-sides-offset: 20px;
--swiper-scrollbar-top: 100px;
--swiper-scrollbar-bottom: auto;
}
.title {
color: white;
padding: 70px 20px 0;
display: flex;
justify-content: space-between;
align-items: center;
.left {
font-size: 1.8em;
color: #fff;
}
}
.song-title {
padding: 20px 20px 0;
font-size: 1.3em;
}
.legal {
padding: 0 20px;
font-size: 0.6em;
color: #fff9;
}
.view {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: #0003;
backdrop-filter: blur(10px);
z-index: 1;
color: white;
}
@@ -14,3 +51,9 @@ app-song-text {
margin: 20px;
display: block;
}
.song-swipe {
margin-top: 100px;
margin-bottom: 50px;
min-height: calc(100vh - 150px);
}

View File

@@ -1,11 +1,9 @@
import {Component} from '@angular/core';
import {SongService} from '../songs/services/song.service';
import {GlobalSettingsService} from '../../services/global-settings.service';
import {Observable} from 'rxjs';
import {filter, map, switchMap} from 'rxjs/operators';
import {ShowSongService} from '../shows/services/show-song.service';
import {GlobalSettings} from '../../services/global-settings';
import {ShowService} from '../shows/services/show.service';
import {GuestShowDataService} from './guest-show-data.service';
import {ActivatedRoute} from '@angular/router';
import {map, switchMap} from 'rxjs/operators';
import {Song} from '../songs/services/song';
import {ConfigService} from '../../services/config.service';
@Component({
selector: 'app-guest',
@@ -13,20 +11,17 @@ import {ShowService} from '../shows/services/show.service';
styleUrls: ['./guest.component.less'],
})
export class GuestComponent {
public songs$: Observable<string[]> = this.globalSettingsService.get$.pipe(
filter(_ => !!_),
map(_ => _ as GlobalSettings),
map(_ => _.currentShow),
switchMap(_ => this.showSongService.list$(_).pipe(map(l => ({showSongs: l, currentShow: _})))),
switchMap(_ => this.showService.read$(_.currentShow).pipe(map(l => ({showSongs: _.showSongs, show: l})))),
filter(_ => !!_.showSongs),
map(_ => (_?.show ? _.show.order.map(o => _.showSongs.find(f => f.id === o) ?? _.showSongs[0]).map(m => m.text) : []))
public show$ = this.currentRoute.params.pipe(
map(param => param.id as string),
switchMap(id => this.service.read$(id))
);
public config$ = this.configService.get$();
public constructor(
private songService: SongService,
private showService: ShowService,
private globalSettingsService: GlobalSettingsService,
private showSongService: ShowSongService
private currentRoute: ActivatedRoute,
private service: GuestShowDataService,
private configService: ConfigService
) {}
public trackBy = (index: number, show: Song) => show.id;
}

View File

@@ -1,11 +1,23 @@
import {NgModule} from '@angular/core';
import {CUSTOM_ELEMENTS_SCHEMA, NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {GuestComponent} from './guest.component';
import {RouterModule} from '@angular/router';
import {SongTextModule} from '../../widget-modules/components/song-text/song-text.module';
import {ShowTypeTranslaterModule} from '../../widget-modules/pipes/show-type-translater/show-type-translater.module';
@NgModule({
declarations: [GuestComponent],
imports: [CommonModule, RouterModule.forChild([{path: '', component: GuestComponent}]), SongTextModule],
imports: [
CommonModule,
RouterModule.forChild([
{
path: ':id',
component: GuestComponent,
},
]),
SongTextModule,
ShowTypeTranslaterModule,
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class GuestModule {}

View File

@@ -0,0 +1,13 @@
<div mat-dialog-content>
<a [href]="data.url">{{ data.url }}</a>
<div [style.background-image]="'url('+qrCode+')'" alt="qrcode" class="qrcode">
</div>
</div>
<div mat-dialog-actions>
<button [mat-dialog-close]="true" cdkFocusInitial mat-button>
Schließen
</button>
<button (click)="share()" mat-button>
Teilen
</button>
</div>

View File

@@ -0,0 +1,7 @@
.qrcode {
width: 300px;
height: 300px;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}

View File

@@ -0,0 +1,22 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {ShareDialogComponent} from './share-dialog.component';
describe('ShareDialogComponent', () => {
let component: ShareDialogComponent;
let fixture: ComponentFixture<ShareDialogComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ShareDialogComponent],
}).compileComponents();
fixture = TestBed.createComponent(ShareDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,49 @@
import {Component, Inject} from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogActions, MatDialogClose, MatDialogContent} from '@angular/material/dialog';
import {MatButton} from '@angular/material/button';
import QRCode from 'qrcode';
import {AsyncPipe} from '@angular/common';
import {ShowTypePipe} from '../../../../widget-modules/pipes/show-type-translater/show-type.pipe';
import {Show} from '../../services/show';
export interface ShareDialogData {
url: string;
show: Show;
}
@Component({
selector: 'app-share-dialog',
standalone: true,
imports: [MatButton, MatDialogActions, MatDialogContent, MatDialogClose, AsyncPipe],
templateUrl: './share-dialog.component.html',
styleUrl: './share-dialog.component.less',
})
export class ShareDialogComponent {
public qrCode: string;
public constructor(@Inject(MAT_DIALOG_DATA) public data: ShareDialogData) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment
QRCode.toDataURL(data.url, {
type: 'image/jpeg',
quality: 0.92,
width: 1280,
height: 1280,
color: {
dark: '#010414',
light: '#ffffff',
},
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-return
}).then(_ => (this.qrCode = _));
}
public async share() {
if (navigator.clipboard) await navigator.clipboard.writeText(this.data.url);
if (navigator.share)
await navigator.share({
title: new ShowTypePipe().transform(this.data.show.showType),
text: new ShowTypePipe().transform(this.data.show.showType) + ' am ' + this.data.show.date.toDate().toLocaleString('de'),
url: this.data.url,
});
}
}

View File

@@ -13,6 +13,7 @@ export interface Show {
published: boolean;
archived: boolean;
order: string[];
shareId: string;
presentationSongId: string;
presentationDynamicCaption: string;

View File

@@ -6,7 +6,7 @@
show.date.toDate() | date: 'dd.MM.yyyy'
}} - {{ getStatus(show) }}"
>
<p *ngIf="!useSwiper">{{show.public ? 'öffentliche' : 'geschlossene'}} Veranstaltung von
<p *ngIf="!useSwiper">{{ show.public ? 'öffentliche' : 'geschlossene' }} Veranstaltung von
<app-user-name [userId]="show.owner"></app-user-name>
</p>
<div class="head">
@@ -41,7 +41,8 @@
</div>
<swiper-container *ngIf="useSwiper" scrollbar="true">
<swiper-slide *ngFor="let song of orderedShowSongs(show); let i = index; trackBy: trackBy" [style.font-size]="textSize + 'em'"
<swiper-slide *ngFor="let song of orderedShowSongs(show); let i = index; trackBy: trackBy"
[style.font-size]="textSize + 'em'"
class="song-swipe">
<app-song
[fullscreen]="true"
@@ -76,6 +77,9 @@
<app-button (click)="onPublish(false)" *ngIf="show.published" [icon]="faUnpublish">
Veröffentlichung zurückziehen
</app-button>
<app-button (click)="onShare(show)" *ngIf="show.published" [icon]="faShare">
Teilen
</app-button>
<app-button (click)="onChange(show.id)" [icon]="faSliders" *ngIf="!show.published">
Ändern
</app-button>

View File

@@ -19,6 +19,7 @@ import {
faLock,
faMagnifyingGlassMinus,
faMagnifyingGlassPlus,
faShare,
faSliders,
faUser,
faUsers,
@@ -28,6 +29,8 @@ import {fade} from '../../../animations';
import {MatDialog} from '@angular/material/dialog';
import {ArchiveDialogComponent} from '../dialog/archive-dialog/archive-dialog.component';
import {closeFullscreen, openFullscreen} from '../../../services/fullscreen';
import {GuestShowService} from '../../guest/guest-show.service';
import {ShareDialogComponent} from '../dialog/share-dialog/share-dialog.component';
@Component({
selector: 'app-show',
@@ -46,14 +49,13 @@ export class ShowComponent implements OnInit, OnDestroy {
public faBoxOpen = faBoxOpen;
public faPublish = faExternalLinkAlt;
public faUnpublish = faLock;
public faShare = faShare;
public faDownload = faFileDownload;
public faSliders = faSliders;
public faUser = faUser;
public faUsers = faUsers;
public faZoomIn = faMagnifyingGlassPlus;
public faZoomOut = faMagnifyingGlassMinus;
public faPage = faFile;
public faLines = faFileLines;
private subs: Subscription[] = [];
public useSwiper = false;
@@ -65,7 +67,8 @@ export class ShowComponent implements OnInit, OnDestroy {
private docxService: DocxService,
private router: Router,
private cRef: ChangeDetectorRef,
public dialog: MatDialog
public dialog: MatDialog,
private guestShowService: GuestShowService
) {}
public ngOnInit(): void {
@@ -128,6 +131,11 @@ export class ShowComponent implements OnInit, OnDestroy {
if (this.showId != null) await this.showService.update$(this.showId, {published});
}
public onShare = async (show: Show): Promise<void> => {
const url = await this.guestShowService.share(show, this.orderedShowSongs(show));
this.dialog.open(ShareDialogComponent, {data: {url, show}});
};
public getStatus(show: Show): string {
if (show.published) {
return 'veröffentlicht';
@@ -162,7 +170,7 @@ export class ShowComponent implements OnInit, OnDestroy {
return show.order.map(_ => list.filter(f => f.id === _)[0]);
}
public trackBy = (index: number, show: ShowSong) => show.id;
public trackBy = (_: number, show: ShowSong) => show.id;
public async onChange(showId: string) {
await this.router.navigateByUrl('/shows/' + showId + '/edit');

View File

@@ -25,7 +25,10 @@ export class SongService {
// private list: Song[];
public constructor(private songDataService: SongDataService, private userService: UserService) {
public constructor(
private songDataService: SongDataService,
private userService: UserService
) {
// importCCLI = (songs: Song[]) => this.updateFromCLI(songs);
}