This commit is contained in:
@@ -1,5 +1,9 @@
|
|||||||
import {NgModule} from '@angular/core';
|
import {NgModule} from '@angular/core';
|
||||||
import {PreloadAllModules, RouterModule, Routes} from '@angular/router';
|
import {RouterModule, Routes} from '@angular/router';
|
||||||
|
import {FirebaseApp} from '@angular/fire/app';
|
||||||
|
import {provideStorage} from '@angular/fire/storage';
|
||||||
|
import {inject} from '@angular/core';
|
||||||
|
import {getStorage} from 'firebase/storage';
|
||||||
import {RoleGuard} from './widget-modules/guards/role.guard';
|
import {RoleGuard} from './widget-modules/guards/role.guard';
|
||||||
import {AuthGuard} from './widget-modules/guards/auth.guard';
|
import {AuthGuard} from './widget-modules/guards/auth.guard';
|
||||||
|
|
||||||
@@ -12,6 +16,7 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: 'songs',
|
path: 'songs',
|
||||||
loadChildren: () => import('./modules/songs/songs.module').then(m => m.SongsModule),
|
loadChildren: () => import('./modules/songs/songs.module').then(m => m.SongsModule),
|
||||||
|
providers: [provideStorage(() => getStorage(inject(FirebaseApp)))],
|
||||||
canActivate: [AuthGuard, RoleGuard],
|
canActivate: [AuthGuard, RoleGuard],
|
||||||
data: {
|
data: {
|
||||||
requiredRoles: ['user'],
|
requiredRoles: ['user'],
|
||||||
@@ -50,7 +55,6 @@ const routes: Routes = [
|
|||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forRoot(routes, {
|
RouterModule.forRoot(routes, {
|
||||||
preloadingStrategy: PreloadAllModules,
|
|
||||||
scrollPositionRestoration: 'enabled',
|
scrollPositionRestoration: 'enabled',
|
||||||
// relativeLinkResolution: 'legacy',
|
// relativeLinkResolution: 'legacy',
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import {ChangeDetectionStrategy, Component, OnInit} from '@angular/core';
|
import {ChangeDetectionStrategy, Component, OnInit} from '@angular/core';
|
||||||
import {fader} from './animations';
|
import {fader} from './animations';
|
||||||
import {register} from 'swiper/element/bundle';
|
|
||||||
import {RouterOutlet} from '@angular/router';
|
import {RouterOutlet} from '@angular/router';
|
||||||
import {NavigationComponent} from './widget-modules/components/application-frame/navigation/navigation.component';
|
import {NavigationComponent} from './widget-modules/components/application-frame/navigation/navigation.component';
|
||||||
|
|
||||||
@@ -13,10 +12,6 @@ import {NavigationComponent} from './widget-modules/components/application-frame
|
|||||||
imports: [RouterOutlet, NavigationComponent],
|
imports: [RouterOutlet, NavigationComponent],
|
||||||
})
|
})
|
||||||
export class AppComponent implements OnInit {
|
export class AppComponent implements OnInit {
|
||||||
public constructor() {
|
|
||||||
register();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ngOnInit(): void {
|
public ngOnInit(): void {
|
||||||
setTimeout(() => document.querySelector('#load-bg')?.classList.add('hidden'), 1000);
|
setTimeout(() => document.querySelector('#load-bg')?.classList.add('hidden'), 1000);
|
||||||
setTimeout(() => document.querySelector('#load-bg')?.remove(), 5000);
|
setTimeout(() => document.querySelector('#load-bg')?.remove(), 5000);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {SongTextComponent} from '../../widget-modules/components/song-text/song-
|
|||||||
import {ShowTypePipe} from '../../widget-modules/pipes/show-type-translater/show-type.pipe';
|
import {ShowTypePipe} from '../../widget-modules/pipes/show-type-translater/show-type.pipe';
|
||||||
import {concat, from, Observable, of} from 'rxjs';
|
import {concat, from, Observable, of} from 'rxjs';
|
||||||
import {GuestShow} from './guest-show';
|
import {GuestShow} from './guest-show';
|
||||||
|
import {ensureSwiperElement} from '../../services/swiper-element';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-guest',
|
selector: 'app-guest',
|
||||||
@@ -20,6 +21,10 @@ export class GuestComponent {
|
|||||||
private currentRoute = inject(ActivatedRoute);
|
private currentRoute = inject(ActivatedRoute);
|
||||||
private service = inject(GuestShowDataService);
|
private service = inject(GuestShowDataService);
|
||||||
|
|
||||||
|
public constructor() {
|
||||||
|
void ensureSwiperElement();
|
||||||
|
}
|
||||||
|
|
||||||
public showState$: Observable<GuestShowState> = this.currentRoute.params.pipe(
|
public showState$: Observable<GuestShowState> = this.currentRoute.params.pipe(
|
||||||
map(param => param.id as string),
|
map(param => param.id as string),
|
||||||
switchMap(id =>
|
switchMap(id =>
|
||||||
|
|||||||
@@ -1,21 +1,31 @@
|
|||||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {ShareDialogComponent} from './share-dialog.component';
|
import {ShareDialogComponent} from './share-dialog.component';
|
||||||
import QRCode from 'qrcode';
|
import {MAT_DIALOG_DATA} from '@angular/material/dialog';
|
||||||
|
|
||||||
describe('ShareDialogComponent', () => {
|
describe('ShareDialogComponent', () => {
|
||||||
let component: ShareDialogComponent;
|
let component: ShareDialogComponent;
|
||||||
let fixture: ComponentFixture<ShareDialogComponent>;
|
let fixture: ComponentFixture<ShareDialogComponent>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
spyOn(QRCode, 'toDataURL').and.resolveTo('data:image/jpeg;base64,test');
|
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [ShareDialogComponent],
|
imports: [ShareDialogComponent],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: MAT_DIALOG_DATA,
|
||||||
|
useValue: {
|
||||||
|
url: 'https://example.com/guest/1',
|
||||||
|
show: {
|
||||||
|
showType: 'service-worship',
|
||||||
|
date: {toDate: () => new Date('2026-03-20T00:00:00Z')},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
||||||
fixture = TestBed.createComponent(ShareDialogComponent);
|
fixture = TestBed.createComponent(ShareDialogComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
|
spyOn<any>(component, 'generateQrCode').and.resolveTo('data:image/jpeg;base64,test');
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import {Component, inject} from '@angular/core';
|
import {Component, inject} from '@angular/core';
|
||||||
import {MAT_DIALOG_DATA, MatDialogActions, MatDialogClose, MatDialogContent} from '@angular/material/dialog';
|
import {MAT_DIALOG_DATA, MatDialogActions, MatDialogClose, MatDialogContent} from '@angular/material/dialog';
|
||||||
import {MatButton} from '@angular/material/button';
|
import {MatButton} from '@angular/material/button';
|
||||||
import QRCode from 'qrcode';
|
|
||||||
import {ShowTypePipe} from '../../../../widget-modules/pipes/show-type-translater/show-type.pipe';
|
import {ShowTypePipe} from '../../../../widget-modules/pipes/show-type-translater/show-type.pipe';
|
||||||
import {Show} from '../../services/show';
|
import {Show} from '../../services/show';
|
||||||
|
|
||||||
@@ -24,18 +23,7 @@ export class ShareDialogComponent {
|
|||||||
public constructor() {
|
public constructor() {
|
||||||
const data = this.data;
|
const data = this.data;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment
|
void this.generateQrCode(data.url).then(qrCode => (this.qrCode = qrCode));
|
||||||
void 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((qrCode: string) => (this.qrCode = qrCode));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async share(): Promise<void> {
|
public async share(): Promise<void> {
|
||||||
@@ -48,4 +36,18 @@ export class ShareDialogComponent {
|
|||||||
url: this.data.url,
|
url: this.data.url,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async generateQrCode(url: string): Promise<string> {
|
||||||
|
const {default: QRCode} = await import('qrcode');
|
||||||
|
return await QRCode.toDataURL(url, {
|
||||||
|
type: 'image/jpeg',
|
||||||
|
quality: 0.92,
|
||||||
|
width: 1280,
|
||||||
|
height: 1280,
|
||||||
|
color: {
|
||||||
|
dark: '#010414',
|
||||||
|
light: '#ffffff',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import {TestBed} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
import {Packer} from 'docx';
|
|
||||||
import {DocxService} from './docx.service';
|
import {DocxService} from './docx.service';
|
||||||
|
|
||||||
describe('DocxService', () => {
|
describe('DocxService', () => {
|
||||||
let service: DocxService;
|
let service: DocxService;
|
||||||
type DocxServiceInternals = DocxService & {
|
type DocxServiceInternals = DocxService & {
|
||||||
prepareData: (showId: string) => Promise<unknown>;
|
prepareData: (showId: string) => Promise<unknown>;
|
||||||
prepareNewDocument: (data: unknown, options?: unknown) => unknown;
|
renderTitle: (docx: unknown, title: string) => unknown[];
|
||||||
|
renderSongs: (docx: unknown, songs: unknown[], options: unknown, config: unknown) => unknown[];
|
||||||
|
prepareNewDocument: (docx: unknown, data: unknown, options?: unknown, sections?: unknown) => unknown;
|
||||||
saveAs: (blob: Blob, name: string) => void;
|
saveAs: (blob: Blob, name: string) => void;
|
||||||
|
loadDocx: () => Promise<{Packer: {toBlob: (document: unknown) => Promise<Blob>}}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -32,6 +34,11 @@ describe('DocxService', () => {
|
|||||||
|
|
||||||
it('should build and save a docx file when all data is available', async () => {
|
it('should build and save a docx file when all data is available', async () => {
|
||||||
const blob = new Blob(['docx']);
|
const blob = new Blob(['docx']);
|
||||||
|
const docxModule = {
|
||||||
|
Packer: {
|
||||||
|
toBlob: jasmine.createSpy('toBlob').and.resolveTo(blob),
|
||||||
|
},
|
||||||
|
};
|
||||||
const serviceInternals = service as DocxServiceInternals;
|
const serviceInternals = service as DocxServiceInternals;
|
||||||
const prepareDataSpy = spyOn<any>(serviceInternals, 'prepareData').and.resolveTo({
|
const prepareDataSpy = spyOn<any>(serviceInternals, 'prepareData').and.resolveTo({
|
||||||
show: {
|
show: {
|
||||||
@@ -42,15 +49,17 @@ describe('DocxService', () => {
|
|||||||
user: {name: 'Benjamin'},
|
user: {name: 'Benjamin'},
|
||||||
config: {ccliLicenseId: '12345'},
|
config: {ccliLicenseId: '12345'},
|
||||||
});
|
});
|
||||||
|
spyOn<any>(serviceInternals, 'loadDocx').and.resolveTo(docxModule);
|
||||||
|
spyOn<any>(serviceInternals, 'renderTitle').and.returnValue([]);
|
||||||
|
spyOn<any>(serviceInternals, 'renderSongs').and.returnValue([]);
|
||||||
const prepareNewDocumentSpy = spyOn<any>(serviceInternals, 'prepareNewDocument').and.returnValue({doc: true});
|
const prepareNewDocumentSpy = spyOn<any>(serviceInternals, 'prepareNewDocument').and.returnValue({doc: true});
|
||||||
const saveAsSpy = spyOn<any>(serviceInternals, 'saveAs');
|
const saveAsSpy = spyOn<any>(serviceInternals, 'saveAs');
|
||||||
spyOn(Packer, 'toBlob').and.resolveTo(blob);
|
|
||||||
|
|
||||||
await service.create('show-1', {copyright: true});
|
await service.create('show-1', {copyright: true});
|
||||||
|
|
||||||
expect(prepareDataSpy).toHaveBeenCalledWith('show-1');
|
expect(prepareDataSpy).toHaveBeenCalledWith('show-1');
|
||||||
expect(prepareNewDocumentSpy).toHaveBeenCalled();
|
expect(prepareNewDocumentSpy).toHaveBeenCalled();
|
||||||
expect(Packer.toBlob).toHaveBeenCalledWith({doc: true} as never);
|
expect(docxModule.Packer.toBlob).toHaveBeenCalledWith({doc: true} as never);
|
||||||
expect(saveAsSpy).toHaveBeenCalledWith(blob, jasmine.stringMatching(/\.docx$/));
|
expect(saveAsSpy).toHaveBeenCalledWith(blob, jasmine.stringMatching(/\.docx$/));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import {Injectable, inject} from '@angular/core';
|
import {Injectable, inject} from '@angular/core';
|
||||||
import {Document, HeadingLevel, ISectionOptions, Packer, Paragraph} from 'docx';
|
|
||||||
import {ShowService} from './show.service';
|
import {ShowService} from './show.service';
|
||||||
import {ShowTypePipe} from '../../../widget-modules/pipes/show-type-translater/show-type.pipe';
|
import {ShowTypePipe} from '../../../widget-modules/pipes/show-type-translater/show-type.pipe';
|
||||||
import {ShowSongService} from './show-song.service';
|
import {ShowSongService} from './show-song.service';
|
||||||
@@ -17,6 +16,11 @@ import {LineType} from '../../songs/services/line-type';
|
|||||||
import {Line} from '../../songs/services/line';
|
import {Line} from '../../songs/services/line';
|
||||||
import {firstValueFrom} from 'rxjs';
|
import {firstValueFrom} from 'rxjs';
|
||||||
|
|
||||||
|
type DocxModule = typeof import('docx');
|
||||||
|
type DocxDocument = import('docx').Document;
|
||||||
|
type DocxParagraph = import('docx').Paragraph;
|
||||||
|
type DocxSectionOptions = import('docx').ISectionOptions;
|
||||||
|
|
||||||
export interface DownloadOptions {
|
export interface DownloadOptions {
|
||||||
copyright?: boolean;
|
copyright?: boolean;
|
||||||
chordMode?: ChordMode;
|
chordMode?: ChordMode;
|
||||||
@@ -35,13 +39,14 @@ export class DocxService {
|
|||||||
public async create(showId: string, options: DownloadOptions = {}): Promise<void> {
|
public async create(showId: string, options: DownloadOptions = {}): Promise<void> {
|
||||||
const data = await this.prepareData(showId);
|
const data = await this.prepareData(showId);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
const docx = await this.loadDocx();
|
||||||
const {show, songs, user, config} = data;
|
const {show, songs, user, config} = data;
|
||||||
const type = new ShowTypePipe().transform(show.showType);
|
const type = new ShowTypePipe().transform(show.showType);
|
||||||
const title = `${type} ${show.date.toDate().toLocaleDateString()}`;
|
const title = `${type} ${show.date.toDate().toLocaleDateString()}`;
|
||||||
|
|
||||||
const paragraphs = [...this.renderTitle(title), ...this.renderSongs(songs, options, config)];
|
const paragraphs = [...this.renderTitle(docx, title), ...this.renderSongs(docx, songs, options, config)];
|
||||||
|
|
||||||
const sections: ISectionOptions[] = [
|
const sections: DocxSectionOptions[] = [
|
||||||
{
|
{
|
||||||
properties: {
|
properties: {
|
||||||
page: {
|
page: {
|
||||||
@@ -51,16 +56,16 @@ export class DocxService {
|
|||||||
children: paragraphs,
|
children: paragraphs,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const document = this.prepareNewDocument(type, user.name, options, sections);
|
const document = this.prepareNewDocument(docx, type, user.name, options, sections);
|
||||||
|
|
||||||
const blob = await Packer.toBlob(document);
|
const blob = await docx.Packer.toBlob(document);
|
||||||
|
|
||||||
// saveAs from FileSaver will download the file
|
// saveAs from FileSaver will download the file
|
||||||
this.saveAs(blob, `${title}.docx`);
|
this.saveAs(blob, `${title}.docx`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private prepareNewDocument(type: string, name: string, options: DownloadOptions, sections: ISectionOptions[]): Document {
|
private prepareNewDocument(docx: DocxModule, type: string, name: string, options: DownloadOptions, sections: DocxSectionOptions[]): DocxDocument {
|
||||||
return new Document({
|
return new docx.Document({
|
||||||
creator: name,
|
creator: name,
|
||||||
title: type,
|
title: type,
|
||||||
description: '... mit Beschreibung',
|
description: '... mit Beschreibung',
|
||||||
@@ -91,32 +96,33 @@ export class DocxService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private renderSongs(
|
private renderSongs(
|
||||||
|
docx: DocxModule,
|
||||||
songs: {
|
songs: {
|
||||||
showSong: ShowSong;
|
showSong: ShowSong;
|
||||||
sections: Section[];
|
sections: Section[];
|
||||||
}[],
|
}[],
|
||||||
options: DownloadOptions,
|
options: DownloadOptions,
|
||||||
config: Config
|
config: Config
|
||||||
): Paragraph[] {
|
): DocxParagraph[] {
|
||||||
return songs.reduce((p: Paragraph[], song) => [...p, ...this.renderSong(song.showSong, song.showSong, song.sections, options, config)], []);
|
return songs.reduce((p: DocxParagraph[], song) => [...p, ...this.renderSong(docx, song.showSong, song.showSong, song.sections, options, config)], []);
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderSong(showSong: ShowSong, song: Song, sections: Section[], options: DownloadOptions, config: Config): Paragraph[] {
|
private renderSong(docx: DocxModule, showSong: ShowSong, song: Song, sections: Section[], options: DownloadOptions, config: Config): DocxParagraph[] {
|
||||||
const songTitle = this.renderSongTitle(song);
|
const songTitle = this.renderSongTitle(docx, song);
|
||||||
const copyright = this.renderCopyright(song, options, config);
|
const copyright = this.renderCopyright(docx, song, options, config);
|
||||||
const songText = this.renderSongText(sections, options?.chordMode ?? showSong.chordMode);
|
const songText = this.renderSongText(docx, sections, options?.chordMode ?? showSong.chordMode);
|
||||||
|
|
||||||
return copyright ? [songTitle, copyright, ...songText] : [songTitle, ...songText];
|
return copyright ? [songTitle, copyright, ...songText] : [songTitle, ...songText];
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderSongText(sections: Section[], chordMode: ChordMode): Paragraph[] {
|
private renderSongText(docx: DocxModule, sections: Section[], chordMode: ChordMode): DocxParagraph[] {
|
||||||
return sections.reduce((p: Paragraph[], section) => [...p, ...this.renderSection(section, chordMode)], []);
|
return sections.reduce((p: DocxParagraph[], section) => [...p, ...this.renderSection(docx, section, chordMode)], []);
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderSongTitle(song: Song): Paragraph {
|
private renderSongTitle(docx: DocxModule, song: Song): DocxParagraph {
|
||||||
return new Paragraph({
|
return new docx.Paragraph({
|
||||||
text: song.title,
|
text: song.title,
|
||||||
heading: HeadingLevel.HEADING_2,
|
heading: docx.HeadingLevel.HEADING_2,
|
||||||
thematicBreak: true,
|
thematicBreak: true,
|
||||||
spacing: {
|
spacing: {
|
||||||
before: 200,
|
before: 200,
|
||||||
@@ -124,7 +130,7 @@ export class DocxService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderCopyright(song: Song, options: DownloadOptions, config: Config): Paragraph | null {
|
private renderCopyright(docx: DocxModule, song: Song, options: DownloadOptions, config: Config): DocxParagraph | null {
|
||||||
if (!options?.copyright) {
|
if (!options?.copyright) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -135,13 +141,13 @@ export class DocxService {
|
|||||||
const origin = song.origin ? song.origin + ', ' : '';
|
const origin = song.origin ? song.origin + ', ' : '';
|
||||||
const licence = song.legalOwner === 'CCLI' ? 'CCLI-Liednummer: ' + song.legalOwnerId + ', CCLI-Lizenz: ' + config.ccliLicenseId : 'CCLI-Liednummer: ' + song.legalOwnerId;
|
const licence = song.legalOwner === 'CCLI' ? 'CCLI-Liednummer: ' + song.legalOwnerId + ', CCLI-Lizenz: ' + config.ccliLicenseId : 'CCLI-Liednummer: ' + song.legalOwnerId;
|
||||||
|
|
||||||
return new Paragraph({
|
return new docx.Paragraph({
|
||||||
text: artist + label + termsOfUse + origin + licence,
|
text: artist + label + termsOfUse + origin + licence,
|
||||||
style: 'licence',
|
style: 'licence',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderSection(section: Section, chordMode: ChordMode): Paragraph[] {
|
private renderSection(docx: DocxModule, section: Section, chordMode: ChordMode): DocxParagraph[] {
|
||||||
return section.lines
|
return section.lines
|
||||||
.filter(line => {
|
.filter(line => {
|
||||||
if (line.type === LineType.text) {
|
if (line.type === LineType.text) {
|
||||||
@@ -156,22 +162,22 @@ export class DocxService {
|
|||||||
return section.number === 0;
|
return section.number === 0;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.map((line, i) => this.renderLine(line, i === 0));
|
.map((line, i) => this.renderLine(docx, line, i === 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderLine(line: Line, isFirstLine: boolean): Paragraph {
|
private renderLine(docx: DocxModule, line: Line, isFirstLine: boolean): DocxParagraph {
|
||||||
const spacing = isFirstLine ? {before: 200} : {};
|
const spacing = isFirstLine ? {before: 200} : {};
|
||||||
return new Paragraph({
|
return new docx.Paragraph({
|
||||||
text: line.text,
|
text: line.text,
|
||||||
style: 'songtext',
|
style: 'songtext',
|
||||||
spacing,
|
spacing,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderTitle(type: string): Paragraph[] {
|
private renderTitle(docx: DocxModule, type: string): DocxParagraph[] {
|
||||||
const songTitle = new Paragraph({
|
const songTitle = new docx.Paragraph({
|
||||||
text: type,
|
text: type,
|
||||||
heading: HeadingLevel.HEADING_1,
|
heading: docx.HeadingLevel.HEADING_1,
|
||||||
thematicBreak: true,
|
thematicBreak: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -232,4 +238,8 @@ export class DocxService {
|
|||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private loadDocx(): Promise<DocxModule> {
|
||||||
|
return import('docx');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ import {ReportedTypePipe} from '../../../widget-modules/pipes/reported-type-tran
|
|||||||
import {BadgeComponent, BadgeType} from '../../../widget-modules/components/badge/badge.component';
|
import {BadgeComponent, BadgeType} from '../../../widget-modules/components/badge/badge.component';
|
||||||
import {ReportDialogComponent, ReportDialogSong} from '../dialog/report-dialog/report-dialog.component';
|
import {ReportDialogComponent, ReportDialogSong} from '../dialog/report-dialog/report-dialog.component';
|
||||||
import {PublishedTypePipe} from '../../../widget-modules/pipes/published-type-translator/published-type.pipe';
|
import {PublishedTypePipe} from '../../../widget-modules/pipes/published-type-translator/published-type.pipe';
|
||||||
|
import {ensureSwiperElement} from '../../../services/swiper-element';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-show',
|
selector: 'app-show',
|
||||||
@@ -126,6 +127,7 @@ export class ShowComponent implements OnInit, OnDestroy {
|
|||||||
private clockIntervalId: ReturnType<typeof setInterval> | null = null;
|
private clockIntervalId: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
public ngOnInit(): void {
|
public ngOnInit(): void {
|
||||||
|
void ensureSwiperElement();
|
||||||
this.currentTime = new Date();
|
this.currentTime = new Date();
|
||||||
this.clockIntervalId = setInterval(() => {
|
this.clockIntervalId = setInterval(() => {
|
||||||
this.currentTime = new Date();
|
this.currentTime = new Date();
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ import {deleteObject, getDownloadURL, ref, Storage} from '@angular/fire/storage'
|
|||||||
import {from, Observable} from 'rxjs';
|
import {from, Observable} from 'rxjs';
|
||||||
import {FileDataService} from './file-data.service';
|
import {FileDataService} from './file-data.service';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable()
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class FileService {
|
export class FileService {
|
||||||
private storage = inject(Storage);
|
private storage = inject(Storage);
|
||||||
private fileDataService = inject(FileDataService);
|
private fileDataService = inject(FileDataService);
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ import {ref, Storage, uploadBytesResumable} from '@angular/fire/storage';
|
|||||||
import {FileBase} from './fileBase';
|
import {FileBase} from './fileBase';
|
||||||
import {FileServer} from './fileServer';
|
import {FileServer} from './fileServer';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable()
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class UploadService extends FileBase {
|
export class UploadService extends FileBase {
|
||||||
private fileDataService = inject(FileDataService);
|
private fileDataService = inject(FileDataService);
|
||||||
private storage = inject(Storage);
|
private storage = inject(Storage);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {FileComponent} from './file/file.component';
|
|||||||
templateUrl: './edit-file.component.html',
|
templateUrl: './edit-file.component.html',
|
||||||
styleUrls: ['./edit-file.component.less'],
|
styleUrls: ['./edit-file.component.less'],
|
||||||
imports: [CardComponent, NgStyle, MatIconButton, MatIcon, FileComponent, AsyncPipe],
|
imports: [CardComponent, NgStyle, MatIconButton, MatIcon, FileComponent, AsyncPipe],
|
||||||
|
providers: [UploadService],
|
||||||
})
|
})
|
||||||
export class EditFileComponent {
|
export class EditFileComponent {
|
||||||
private activatedRoute = inject(ActivatedRoute);
|
private activatedRoute = inject(ActivatedRoute);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {AsyncPipe} from '@angular/common';
|
|||||||
templateUrl: './file.component.html',
|
templateUrl: './file.component.html',
|
||||||
styleUrls: ['./file.component.less'],
|
styleUrls: ['./file.component.less'],
|
||||||
imports: [MatIconButton, FaIconComponent, AsyncPipe],
|
imports: [MatIconButton, FaIconComponent, AsyncPipe],
|
||||||
|
providers: [FileService],
|
||||||
})
|
})
|
||||||
export class FileComponent {
|
export class FileComponent {
|
||||||
private fileService = inject(FileService);
|
private fileService = inject(FileService);
|
||||||
|
|||||||
@@ -9,12 +9,12 @@
|
|||||||
<link href="apple-touch-icon.png" rel="apple-touch-icon" sizes="180x180" />
|
<link href="apple-touch-icon.png" rel="apple-touch-icon" sizes="180x180" />
|
||||||
<link href="favicon-32x32.png" rel="icon" sizes="32x32" type="image/png" />
|
<link href="favicon-32x32.png" rel="icon" sizes="32x32" type="image/png" />
|
||||||
<link href="favicon-16x16.png" rel="icon" sizes="16x16" type="image/png" />
|
<link href="favicon-16x16.png" rel="icon" sizes="16x16" type="image/png" />
|
||||||
<link color="#4286f4" href="safari-pinned-tab.svg" rel="mask-icon" />
|
<link color="#6f8f95" href="safari-pinned-tab.svg" rel="mask-icon" />
|
||||||
<meta content="#4286f4" name="msapplication-TileColor" />
|
<meta content="#6f8f95" name="msapplication-TileColor" />
|
||||||
<meta content="#4286f4" name="theme-color" />
|
<meta content="#6f8f95" name="theme-color" />
|
||||||
|
|
||||||
<link href="manifest.webmanifest" rel="manifest" />
|
<link href="manifest.webmanifest" rel="manifest" />
|
||||||
<meta content="#4286f4" name="theme-color" />
|
<meta content="#6f8f95" name="theme-color" />
|
||||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap" rel="stylesheet" />
|
||||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
|
||||||
<link href="https://fonts.googleapis.com/css?family=Ubuntu+Mono&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css?family=Ubuntu+Mono&display=swap" rel="stylesheet" />
|
||||||
|
|||||||
@@ -6,15 +6,12 @@ import {bootstrapApplication, BrowserModule} from '@angular/platform-browser';
|
|||||||
import {provideAnimations} from '@angular/platform-browser/animations';
|
import {provideAnimations} from '@angular/platform-browser/animations';
|
||||||
import {AppRoutingModule} from './app/app-routing.module';
|
import {AppRoutingModule} from './app/app-routing.module';
|
||||||
import {ServiceWorkerModule} from '@angular/service-worker';
|
import {ServiceWorkerModule} from '@angular/service-worker';
|
||||||
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
|
|
||||||
import {AppComponent} from './app/app.component';
|
import {AppComponent} from './app/app.component';
|
||||||
import {FirebaseApp, provideFirebaseApp} from '@angular/fire/app';
|
import {FirebaseApp, provideFirebaseApp} from '@angular/fire/app';
|
||||||
import {provideFirestore} from '@angular/fire/firestore';
|
import {provideFirestore} from '@angular/fire/firestore';
|
||||||
import {provideAuth} from '@angular/fire/auth';
|
import {provideAuth} from '@angular/fire/auth';
|
||||||
import {provideStorage} from '@angular/fire/storage';
|
|
||||||
import {initializeApp} from 'firebase/app';
|
import {initializeApp} from 'firebase/app';
|
||||||
import {getAuth} from 'firebase/auth';
|
import {getAuth} from 'firebase/auth';
|
||||||
import {getStorage} from 'firebase/storage';
|
|
||||||
import {initializeFirestore, persistentLocalCache, persistentMultipleTabManager} from 'firebase/firestore';
|
import {initializeFirestore, persistentLocalCache, persistentMultipleTabManager} from 'firebase/firestore';
|
||||||
import {UserService} from './app/services/user/user.service';
|
import {UserService} from './app/services/user/user.service';
|
||||||
|
|
||||||
@@ -39,8 +36,7 @@ bootstrapApplication(AppComponent, {
|
|||||||
AppRoutingModule,
|
AppRoutingModule,
|
||||||
ServiceWorkerModule.register('ngsw-worker.js', {
|
ServiceWorkerModule.register('ngsw-worker.js', {
|
||||||
enabled: environment.production,
|
enabled: environment.production,
|
||||||
}),
|
})
|
||||||
FontAwesomeModule
|
|
||||||
),
|
),
|
||||||
provideFirebaseApp(() => initializeApp(environment.firebase)),
|
provideFirebaseApp(() => initializeApp(environment.firebase)),
|
||||||
provideAuth(() => getAuth(inject(FirebaseApp))),
|
provideAuth(() => getAuth(inject(FirebaseApp))),
|
||||||
@@ -49,7 +45,6 @@ bootstrapApplication(AppComponent, {
|
|||||||
localCache: persistentLocalCache({tabManager: persistentMultipleTabManager()}),
|
localCache: persistentLocalCache({tabManager: persistentMultipleTabManager()}),
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
provideStorage(() => getStorage(inject(FirebaseApp))),
|
|
||||||
{provide: MAT_DATE_LOCALE, useValue: 'de-DE'},
|
{provide: MAT_DATE_LOCALE, useValue: 'de-DE'},
|
||||||
provideAnimations(),
|
provideAnimations(),
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user